Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ src/routes/activitypub.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
@@ -195,9 +195,9 @@
   if (audience === 'friend') {
     // The owner's app builds its feed from this leg, and every note here is
-    // by the site itself: give it the same `shaer:author` byline the timeline
-    // entries carry, so your own cards get a header too (avatar + name).
+    // by the site itself, so give it a byline too (avatar + name): de
+    // ingesloten actor in attributedTo, net als de tijdlijn.
     const me = AP.selfAuthor(baseUrl(req), site);
-    // De kaart op je eigen post (shaer-k3f): dezelfde shaer:quote/shaer:embed
-    // die de tijdlijn voor andermans posts draagt, uit de snapshots die
+    // De kaart op je eigen post (shaer-k3f): dezelfde quote/preview die de
+    // tijdlijn voor andermans posts draagt, uit de snapshots die
     // deliverCreate bij het publiceren opsloeg. Op note-id gekoppeld, want
     // buildOutbox sorteert en mengt tracks erdoorheen. De embed alleen voor de
@@ -213,7 +213,4 @@
     for (const it of ob.orderedItems) {
       if (it && it.object && typeof it.object === 'object') {
-        it.object['shaer:author'] = me;
-        // En de standaardvorm ernaast (shaer-nmw): de ingesloten actor, zodat
-        // ook een generieke lezer de byline heeft.
         it.object.attributedTo = AP.actorObject(
           (typeof it.object.attributedTo === 'string' ? it.object.attributedTo : undefined) || AP.actorId(baseUrl(req), site.slug),
@@ -222,8 +219,6 @@
         const row = byNote.get(it.object.id);
         if (row) {
-          it.object['shaer:quote'] = AP.timelineQuote(row.quote_json);
           it.object.quote = AP.quoteObject(row.quote_json);
           if (bearerEmbeds) {
-            it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback });
             it.object.preview = AP.previewObject(row.embed_json, { playback: bearerEmbeds.playback });
           }
@@ -501,35 +496,13 @@
         return tags.length ? tags : undefined;
       })(),
-      // FEP-044f: the resolved quoted post (author + content), so the client
-      // renders an embedded quote card instead of a bare link. Omitted when the
-      // note has no quote or the quoted post could not be resolved.
-      'shaer:quote': quotesAllowed ? AP.timelineQuote(t.quote_json) : undefined,
-      // The post author's display info (name / @handle / avatar), so every card
-      // gets a byline header like the quote card. attributedTo stays the bare
-      // actor URI; this is the resolved presentation Klonkt already stored.
-      'shaer:author': gateAuthor((t.author_name || t.author_handle || t.author_icon) ? {
-        name: t.author_name || undefined, handle: t.author_handle || undefined,
-        icon: t.author_icon || undefined, url: t.author_url || undefined,
-        // FEP-9098: emojis in the display name (":shortcode:"), if any.
-        emojis: (() => { try { return t.author_emoji_json ? JSON.parse(t.author_emoji_json) : undefined; } catch { return undefined; } })(),
-      } : undefined),
-      // When a followed account boosted this, who did ("X boosted"). Omitted for
-      // ordinary posts.
-      'shaer:booster': gateAuthor((t.reblog_name || t.reblog_handle || t.reblog_icon) ? {
-        name: t.reblog_name || undefined, handle: t.reblog_handle || undefined,
-        icon: t.reblog_icon || undefined,
-        // FEP-9098: emojis in the booster's display name (":shortcode:"), if any.
-        emojis: (() => { try { return t.reblog_emoji_json ? JSON.parse(t.reblog_emoji_json) : undefined; } catch { return undefined; } })(),
-      } : undefined),
       // Whether THIS account already liked/boosted the note, so the app's
       // detail-view buttons show the current state (and can toggle/undo).
       'shaer:liked': !!(reacties.get(t.id) || {}).liked,
       'shaer:boosted': !!(reacties.get(t.id) || {}).boosted,
-      // An external (non-fediverse) embed, thumbnail-only and never an iframe.
-      // Omitted entirely when the gate is closed (see above).
-      // Carries shaer:playerUrl only when the playback gate is open too.
-      'shaer:embed': embedsAllowed ? AP.timelineEmbed(t.embed_json, { playback: playbackAllowed }) : undefined,
-      // De standaardvormen (shaer-nmw): FEP-044f quote als object, AS2 preview
-      // als kaart. Dezelfde poorten als hun shaer:-tegenhangers hierboven.
+      // FEP-044f: de geciteerde post als object, zodat de client een kaart
+      // rendert in plaats van een kale link. AS2 preview is diezelfde kaart
+      // voor een EXTERNE link: thumbnail, nooit de iframe van de aanbieder.
+      // Allebei weg zodra hun poort dicht staat; de speler in preview hangt
+      // aan de playback-poort.
       quote: quotesAllowed ? AP.quoteObject(t.quote_json) : undefined,
       preview: embedsAllowed ? AP.previewObject(t.embed_json, { playback: playbackAllowed }) : undefined,
@@ -578,11 +551,4 @@
       'shaer:wave': m.wave ? true : undefined,
       'shaer:helpRequest': m.help_request ? true : undefined,
-      'shaer:quote': quotesAllowed ? AP.timelineQuote(m.quote_json) : undefined,
-      'shaer:author': gateAuthor((m.actor_name || m.actor_handle || m.actor_icon) ? {
-        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
-        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
-        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
-      } : undefined),
-      'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,
       quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
       preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined,
@@ -612,11 +578,4 @@
       tag: [{ type: 'Mention', href: me, name: myHandle }, ...(AP.timelineEmojis(m.emoji_json) || [])],
       attachment: AP.timelineAttachments(m.media_json),
-      'shaer:quote': AP.timelineQuote(m.quote_json),
-      'shaer:author': (m.actor_name || m.actor_handle || m.actor_icon) ? {
-        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
-        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
-        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
-      } : undefined,
-      'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,
       quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
       preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined,
@@ -637,5 +596,4 @@
     object: {
       ...n, content: AP.stripLeadingMentions(n.content),
-      'shaer:author': mine,
       attributedTo: AP.actorObject(typeof n.attributedTo === 'string' ? n.attributedTo : me, mine),
     },
@@ -957,6 +915,4 @@
   AP.sendAP(res, {
     '@context': AP.AP_CONTEXT,
-    'shaer:quote': AP.timelineQuote(uit.quoteJson),
-    'shaer:embed': embedsAllowed ? AP.timelineEmbed(uit.embedJson, { playback }) : undefined,
     quote: AP.quoteObject(uit.quoteJson),
     preview: embedsAllowed ? AP.previewObject(uit.embedJson, { playback }) : undefined,
@@ -996,5 +952,8 @@
     attachment: AP.gateAttachments(n.attachment, { images: imagesOk, audio: musicOk }),
     tag: emojiOk ? n.tag : AP.stripEmojiTags(n.tag),
-    'shaer:author': (n['shaer:author'] && !emojiOk) ? { ...n['shaer:author'], emojis: undefined } : n['shaer:author'],
+    // De emoji-poort knipt in de byline zelf: FEP-9098 zit in de tag van de
+    // ingesloten actor, niet meer in een eigen emoji-kaart ernaast.
+    attributedTo: (!emojiOk && n.attributedTo && typeof n.attributedTo === 'object')
+      ? { ...n.attributedTo, tag: undefined } : n.attributedTo,
   }));
   uit.hidden = kring.hidden;
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ src/services/ActivityPubService.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
@@ -3751,5 +3751,7 @@
     id: o.id,
     type: 'Note',
-    attributedTo: actorUri,
+    // De ingesloten actor (shaer-nmw): de byline hoort in attributedTo, waar
+    // elke AP-lezer hem zoekt, en niet in een eigen property ernaast.
+    attributedTo: actorObject(actorUri, actorInfo(authors.get(actorUri), actorUri)),
     inReplyTo: (typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) || objectUri,
     content: HtmlSanitizerService.sanitize(String(o.content || '').slice(0, 50_000)),
@@ -3777,5 +3779,4 @@
       return out.length ? out : undefined;
     })(),
-    'shaer:author': actorInfo(authors.get(actorUri), actorUri),
   })).sort((a, b) => String(a.published || '').localeCompare(String(b.published || '')));
 
@@ -3802,5 +3803,8 @@
   const kept = [], out = { hidden: 0 };
   for (const n of notes) {
-    if (circle.has(n.attributedTo)) kept.push(n);
+    // actorUriOf, niet n.attributedTo: sinds de byline ingesloten meegaat is
+    // dat een OBJECT en zou een kale vergelijking hier stil alles wegfilteren
+    // -- een ward met een lege thread en nergens een foutmelding.
+    if (circle.has(actorUriOf(n.attributedTo))) kept.push(n);
     else out.hidden += 1;
   }
Index: test/c2s-messages.test.js
===================================================================
--- test/c2s-messages.test.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ test/c2s-messages.test.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
@@ -94,5 +94,5 @@
   assert.ok((wave.tag || []).some((x) => x.type === 'Mention' && x.href === 'https://test.example/ap/users/kid'),
     'with a Mention the client recognises itself in');
-  assert.equal(wave['shaer:author'].name, 'Oma', 'and a byline to show');
+  assert.equal(wave.attributedTo.name, 'Oma', 'and a byline to show');
   assert.ok(!/@kid@test\.example/.test(wave.content), 'the leading @mention is stripped, like Berichten on the web');
 
Index: test/own-card.test.js
===================================================================
--- test/own-card.test.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ test/own-card.test.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
@@ -120,5 +120,5 @@
   globalThis.fetch = vorige;
   assert.equal(uit.notes.length, 1);
-  const auteur = uit.notes[0]['shaer:author'];
+  const auteur = uit.notes[0].attributedTo;
   assert.equal(auteur.url, `${HOST}/`, 'de html-Link, niet de array gestringd');
   assert.ok(!String(auteur.url).includes('object'), 'nergens [object Object]');
Index: test/standard-shapes.test.js
===================================================================
--- test/standard-shapes.test.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ test/standard-shapes.test.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
@@ -83,2 +83,54 @@
   assert.equal(AP.quoteObject('rommel'), undefined);
 });
+
+// ── En dan het weghalen (shaer-nmw, tweede helft) ────────────────────
+//
+// Bewijzen dat de standaardvorm ERBIJ staat is de helft; de andere helft is dat
+// het dialect er niet meer NAAST staat. Zonder deze wacht sluipt een
+// shaer:author zo weer een nieuwe leg in en spreken we stilletjes weer onze
+// eigen taal. Daarom door de echte route, want daar wordt de vorm beslist.
+test('de C2S-leg spreekt geen dialect meer waar de standaard het regelt', async (t) => {
+  const crypto = await import('crypto');
+  const express = (await import('express')).default;
+  const db = (await import('../src/config/database.js')).default;
+  const routes = (await import('../src/routes/activitypub.js')).default;
+
+  db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+    .run('u1', 'u1', 'u1@t', 'x', 'god');
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)')
+    .run('s1', 'kind', 'kind', 'u1');
+  db.prepare(`INSERT INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon,
+              reblog_name, reblog_handle, content)
+              VALUES ('https://elders/n1', 'kind', 'https://elders/u/til', 'Tante Til', '@til@elders',
+                      'https://elders/til.png', 'Esmee', '@es@elders', '<p>hoi</p>')`).run();
+
+  const bearer = 'test-token-' + 'b'.repeat(24);
+  db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
+    .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
+
+  const app = express();
+  app.use(routes);
+  const server = app.listen(0);
+  t.after(() => server.close());
+  await new Promise((r) => server.once('listening', r));
+  const doc = await (await fetch(`http://127.0.0.1:${server.address().port}/ap/users/kind/inbox`,
+    { headers: { Authorization: `Bearer ${bearer}` } })).json();
+
+  const tekst = JSON.stringify(doc);
+  // Op de SLEUTEL, niet op een stuk tekst: 'shaer:quote' zit ook in
+  // shaer:quoteCards, en dat is een poortnaam die blijft. Een te grove zeef
+  // zou hier rood staan om iets dat klopt -- of erger, groen om iets dat niet
+  // klopt.
+  for (const weg of ['shaer:author', 'shaer:booster', 'shaer:quote', 'shaer:embed']) {
+    assert.ok(!tekst.includes(`"${weg}":`), `${weg} hoort weg te zijn`);
+  }
+  // Wat WEL mag blijven, en waarom: per-lezer-interactiestatus heeft in AS2
+  // geen tegenhanger (Mastodon lost het in zijn eigen REST-API op), en de
+  // FEP-633c-termen zijn bewust van ons en in standaardisatie.
+  assert.ok(tekst.includes('shaer:liked'), 'interactiestatus blijft, die heeft geen standaard');
+
+  const item = doc.orderedItems.find((i) => i.object.id === 'https://elders/n1');
+  assert.equal(item.type, 'Announce', 'een boost is de wrapper, niet een zijkanaal');
+  assert.equal(item.object.attributedTo.name, 'Tante Til', 'en de byline zit in attributedTo');
+  assert.equal(item.object.attributedTo.preferredUsername, 'til');
+});
Index: test/thread.test.js
===================================================================
--- test/thread.test.js	(revision 0db3c72bed823ab28b6f74eedfed3a1cf11faf98)
+++ test/thread.test.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
@@ -52,4 +52,9 @@
 ];
 
+// De echte fetch, voordat de stub hem overneemt: de route-test onderaan doet
+// een ECHT verzoek aan een testserver op localhost, en dat mag de stub niet
+// beantwoorden met zijn 404.
+const echteFetch = globalThis.fetch;
+
 globalThis.fetch = async (url) => {
   const u = String(url);
@@ -69,6 +74,6 @@
   assert.equal(uit.notes.length, 2);
   // Oudste eerst: een gesprek lees je van boven naar beneden.
-  assert.equal(uit.notes[0]['shaer:author'].name, 'tante');
-  assert.equal(uit.notes[1]['shaer:author'].name, 'vreemde');
+  assert.equal(uit.notes[0].attributedTo.name, 'tante');
+  assert.equal(uit.notes[1].attributedTo.name, 'vreemde');
   // De sanitizer heeft het script eruit gehaald, de tekst mag blijven.
   assert.ok(!uit.notes[1].content.includes('<script'), 'script weggeschoond');
@@ -96,5 +101,7 @@
   const kring = AP.filterThreadToCircle('pupil', uit.notes);
   assert.equal(kring.notes.length, 1);
-  assert.equal(kring.notes[0]['shaer:author'].name, 'tante');
+  // Ook de zeef zelf leest de ingesloten actor: een kale vergelijking op
+  // attributedTo zou hier stil ALLES wegfilteren.
+  assert.equal(kring.notes[0].attributedTo.name, 'tante');
   // De vreemde is er, en dat mag gezegd: geteld, niet stil weggelaten.
   assert.equal(kring.hidden, 1);
@@ -143,2 +150,45 @@
   globalThis.fetch = vorige;
 });
+
+// ── Door de route heen, want daar wordt de vorm beslist ──────────────
+//
+// De byline van een antwoord verhuisde naar de ingesloten attributedTo
+// (shaer-nmw). Deze leg had geen route-dekking, en juist hier zit de
+// emoji-poort die IN die byline knipt -- knipt hij in het oude veld, dan
+// gebeurt er stil niets meer.
+test('de thread-route geeft de byline in attributedTo, en de emoji-poort knipt daarin', async (t) => {
+  const crypto = await import('crypto');
+  const express = (await import('express')).default;
+  const routes = (await import('../src/routes/activitypub.js')).default;
+
+  const bearer = 'test-token-' + 'c'.repeat(24);
+  db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
+    .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
+
+  const app = express();
+  app.use(routes);
+  const server = app.listen(0);
+  t.after(() => server.close());
+  await new Promise((r) => server.once('listening', r));
+  const haal = async () => (await (await echteFetch(
+    `http://127.0.0.1:${server.address().port}/ap/users/kind/thread?object=${encodeURIComponent(BRON)}`,
+    { headers: { Authorization: `Bearer ${bearer}` } })).json());
+
+  const doc = await haal();
+  assert.ok(!JSON.stringify(doc).includes('"shaer:author":'), 'geen dialect meer op deze leg');
+  const vanTante = doc.orderedItems.find((n) => n.id.endsWith('/replies/2'));
+  assert.equal(vanTante.attributedTo.name, 'tante', 'de byline zit in attributedTo');
+  assert.equal(vanTante.attributedTo.preferredUsername, 'tante');
+  assert.ok((vanTante.tag || []).some((x) => x.type === 'Emoji'), 'open poort: de emoji van het antwoord blijft');
+
+  // Poort dicht: FEP-9098 zit in de tag van de ingesloten actor, dus daar
+  // hoort geknipt te worden -- en het antwoord zelf houdt zijn eigen emoji
+  // niet meer over.
+  db.prepare("UPDATE sites SET gate_custom_emoji = 0 WHERE slug = 'kind'").run();
+  db.prepare("INSERT INTO ap_guardianships (slug, other_uri, role, status) VALUES ('kind', ?, 'guardian', 'accepted')")
+    .run('https://203.0.113.20/users/tante');
+  const dicht = await haal();
+  const na = dicht.orderedItems.find((n) => n.id.endsWith('/replies/2'));
+  assert.ok(!(na.tag || []).some((x) => x.type === 'Emoji'), 'dichte poort: geen emoji meer');
+  assert.ok(!na.attributedTo.tag, 'ook niet in de byline');
+});
