Index: src/assets/css/style.css
===================================================================
--- src/assets/css/style.css	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
+++ src/assets/css/style.css	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
@@ -559,4 +559,24 @@
 
 .empty { text-align: center; padding: 4rem 1rem; }
+/* Moved account (FEP-7628 slice 3): the signpost above the feed. Loud on
+   purpose — a visitor must not read a stale timeline thinking it is current. */
+.moved-banner {
+    text-align: center;
+    margin: 1.5rem 0 2rem;
+    padding: 1.75rem 1.25rem;
+    border: 2px solid var(--accent, #e8b04b);
+    border-radius: var(--radius, 10px);
+    background: color-mix(in srgb, var(--accent, #e8b04b) 10%, var(--paper-2, var(--paper)));
+}
+.moved-banner-lead { margin: 0 0 .5rem; font-size: 1rem; opacity: .85; }
+.moved-banner-link {
+    display: inline-block;
+    font-family: var(--font-display);
+    font-size: clamp(1.5rem, 5vw, 2.4rem);
+    font-weight: 700;
+    line-height: 1.15;
+    overflow-wrap: anywhere;
+}
+.moved-banner-hint { margin: .75rem 0 0; font-size: .85rem; color: var(--ink-soft); }
 .empty h1 { font-size: 2rem; }
 .empty p { color: var(--ink-muted); }
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
+++ src/routes/posts.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
@@ -232,8 +232,13 @@
   recordPageview(site.id, req);
 
+  // FEP-7628 slice 3: this account moved. A visitor who lands here deserves
+  // the same signpost the fediverse gets — one big link to the new address.
+  const movedTo = site.moved_to && /^https?:\/\//i.test(String(site.moved_to)) ? String(site.moved_to) : null;
   renderPage(req, res, 'pages/home', {
     pinnedPosts,
     posts,
     hasMore, nextOffset: offset + FEED_PAGE, moreBase,
+    movedTo,
+    movedToLabel: movedTo ? (ActivityPubService.actorDisplay(site.slug, movedTo).handle || movedTo) : null,
     pageTitle: site.title,
     socialDescr: site.description || site.tagline || '',
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
+++ src/services/ActivityPubService.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
@@ -234,4 +234,10 @@
     }
   } catch { /* skip malformed ap_aliases */ }
+  // FEP-7628 slice 3: this account moved. The old actor stays online AS A
+  // SIGNPOST — that is the whole point of keeping it: whoever missed the Move
+  // activity (offline server, later visitor) still learns where we went by
+  // fetching us. Per the FEP the moved actor "should be considered inactive",
+  // and publishers should stop delivering here.
+  if (site.moved_to && /^https?:\/\//i.test(String(site.moved_to))) actor.movedTo = String(site.moved_to);
   // Profile links → PropertyValue rows: Mastodon/PeerTube/WordPress-ActivityPub render these as
   // profile metadata (rel=me enables link-back verification). Additive; ignored by simpler receivers.
@@ -3892,7 +3898,20 @@
     to: [`${me}/followers`],
   };
+  // FEP-7628: after setting movedTo, notify the followers with an Update of
+  // the actor, so their servers hold the signpost even if the Move itself is
+  // lost. Built from the FRESH row: `site` still carries the pre-move values.
+  const movedSite = db.prepare('SELECT * FROM sites WHERE slug = ?').get(site.slug) || { ...site, moved_to: target.id };
+  const update = {
+    '@context': AP_CONTEXT,
+    id: `${me}#update-${Date.now()}-${rid()}`,
+    type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
+    object: buildActor(base, movedSite),
+    published: new Date().toISOString(),
+  };
   const inboxes = [...new Set(fStmts().list.all(site.slug).map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
+  const send = deliverFn || deliverWithRetry;
   for (const inbox of inboxes) {
-    await (deliverFn || deliverWithRetry)(site.slug, inbox, move, `${me}#main-key`, keys.private_pem);
+    await send(site.slug, inbox, update, `${me}#main-key`, keys.private_pem);
+    await send(site.slug, inbox, move, `${me}#main-key`, keys.private_pem);
   }
   console.log('[AP] MOVE announced:', site.slug, '→', target.id, 'naar', inboxes.length, 'inbox(en)');
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
+++ src/services/i18n.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
@@ -912,4 +912,6 @@
     'vblk.back': 'Terug',
     'vblk.to_home': 'Naar de hoofdpagina',
+    'phome.moved_lead': 'Dit account is verhuisd. Je vindt me nu hier:',
+    'phome.moved_hint': 'Volg je me al? Dan verhuist je server je waarschijnlijk vanzelf mee. Zo niet: volg het nieuwe adres.',
     'phome.empty_title': 'Hier is het nog stil.',
     'phome.empty_sub': 'Nog geen posts. Spannend.',
@@ -1854,4 +1856,6 @@
     'vblk.back': 'Back',
     'vblk.to_home': 'Go to the main page',
+    'phome.moved_lead': 'This account has moved. You can find me here now:',
+    'phome.moved_hint': 'Already following me? Your server most likely moves you along by itself. If not: follow the new address.',
     'phome.empty_title': "It's quiet here for now.",
     'phome.empty_sub': 'No posts yet. Exciting.',
@@ -2796,4 +2800,6 @@
     'vblk.back': 'Zurück',
     'vblk.to_home': 'Zur Hauptseite',
+    'phome.moved_lead': 'Dieses Konto ist umgezogen. Du findest mich jetzt hier:',
+    'phome.moved_hint': 'Folgst du mir schon? Dann zieht dein Server dich wahrscheinlich automatisch mit. Falls nicht: folge der neuen Adresse.',
     'phome.empty_title': 'Hier ist es noch still.',
     'phome.empty_sub': 'Noch keine Beiträge. Spannend.',
Index: src/views/pages/home.ejs
===================================================================
--- src/views/pages/home.ejs	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
+++ src/views/pages/home.ejs	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
@@ -17,4 +17,18 @@
     #pcms-main) so it survives HTMX swaps and is visible on every
     non-admin page. %>
+
+<%# FEP-7628 slice 3: this account moved. The signpost comes FIRST, before
+    any post: a visitor should not scroll a dead feed to find out we left.
+    typeof-guard because an old route may render this template for one
+    request right after a deploy (view cache fills on first hit). %>
+<% if (typeof movedTo !== 'undefined' && movedTo) { %>
+  <div class="container">
+    <aside class="moved-banner">
+      <p class="moved-banner-lead"><%= t('phome.moved_lead') %></p>
+      <a class="moved-banner-link" href="<%= movedTo %>" rel="me noopener"><%= movedToLabel || movedTo %></a>
+      <p class="moved-banner-hint"><%= t('phome.moved_hint') %></p>
+    </aside>
+  </div>
+<% } %>
 
 <!-- ========== TIMELINE ========== -->
Index: test/move-actor.test.js
===================================================================
--- test/move-actor.test.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
+++ test/move-actor.test.js	(revision bfa6fa1e058aa9b3c2821d56a16842937aeed22a)
@@ -163,10 +163,42 @@
   assert.equal(r.target, 'https://elders.example/users/nieuw');
   assert.equal(db.prepare("SELECT moved_to FROM sites WHERE slug = 'radio'").get().moved_to, 'https://elders.example/users/nieuw');
-  assert.equal(delivered.length, 2, 'both follower inboxes');
-  for (const d of delivered) {
-    assert.equal(d.activity.type, 'Move');
+  // Slice 3 added the Update(actor) alongside it, so count the Moves.
+  const moves = delivered.filter((d) => d.activity.type === 'Move');
+  assert.equal(moves.length, 2, 'one Move per follower inbox');
+  for (const d of moves) {
     assert.equal(d.activity.object, ME_RADIO);
     assert.equal(d.activity.target, 'https://elders.example/users/nieuw');
   }
-  assert.ok(delivered.some((d) => d.inbox === 'https://b.example/shared'), 'shared inbox preferred');
-});
+  assert.ok(moves.some((d) => d.inbox === 'https://b.example/shared'), 'shared inbox preferred');
+});
+
+// ── Slice 3: the signpost ──────────────────────────────────────────
+// The old actor stays online and SAYS where it went (FEP-7628 movedTo), and
+// the followers are told with an Update alongside the Move.
+
+test('a moved account carries movedTo on its actor; an unmoved one does not', () => {
+  const fresh = db.prepare("SELECT * FROM sites WHERE slug = 'blog'").get();
+  assert.equal('movedTo' in buildActor('https://test.example', fresh), false, 'no move, no signpost');
+  const moved = { ...fresh, moved_to: 'https://elders.example/users/nieuw' };
+  assert.equal(buildActor('https://test.example', moved).movedTo, 'https://elders.example/users/nieuw');
+  // Junk in the column never reaches the wire.
+  assert.equal('movedTo' in buildActor('https://test.example', { ...fresh, moved_to: 'niet-een-url' }), false);
+});
+
+test('the move tells the followers twice: an Update carrying movedTo, and the Move', async () => {
+  db.prepare("DELETE FROM ap_followers WHERE slug = 'radio'").run();
+  db.prepare("UPDATE sites SET moved_to = NULL WHERE slug = 'radio'").run();
+  db.prepare("INSERT INTO ap_followers (slug, actor_uri, inbox) VALUES ('radio', 'https://c.example/u/c', 'https://c.example/inbox')").run();
+  const sent = [];
+  const r = await moveAccount(radioSite(), 'https://elders.example/users/nieuw', {
+    fetchActorFn: async () => ({ id: 'https://elders.example/users/nieuw', inbox: 'https://elders.example/inbox', alsoKnownAs: [ME_RADIO] }),
+    deliverFn: async (slug, inbox, activity) => { sent.push(activity); },
+  });
+  assert.equal(r.ok, true);
+  const update = sent.find((a) => a.type === 'Update');
+  const move = sent.find((a) => a.type === 'Move');
+  assert.ok(update, 'an Update is sent');
+  assert.ok(move, 'the Move is still sent');
+  assert.equal(update.object.movedTo, 'https://elders.example/users/nieuw', 'the Update carries the signpost');
+  assert.equal(update.object.id, ME_RADIO, 'and it is OUR actor being updated');
+});
