Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 5462bab0a30abfb9338812cb154a11cebda17b67)
+++ src/config/database.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
@@ -147,4 +147,5 @@
   // of its followers to this account — the claim must be visible on OUR side.
   ensureColumn('sites', 'ap_aliases', 'TEXT');
+  ensureColumn('sites', 'moved_to', 'TEXT');   // FEP-7628 slice 2: waarheen dit account vertrok
 
   // Per-post noindex + type
Index: src/routes/admin-sites.js
===================================================================
--- src/routes/admin-sites.js	(revision 5462bab0a30abfb9338812cb154a11cebda17b67)
+++ src/routes/admin-sites.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
@@ -296,4 +296,25 @@
     error: req.query.error || null,
   });
+});
+
+// ==================== MOVE (FEP-7628, slice 2) ====================
+// The explicit departure: announce to every follower that this account now
+// lives elsewhere. Deliberately its own POST with its own button, never a
+// side effect of Save: a Move is a door you close behind you.
+router.post('/:slug/move', requireSiteManagerBySlug, async (req, res) => {
+  const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(req.params.slug);
+  if (!site) return res.redirect('/admin/sites?error=Not+found');
+  const r = await AP.moveAccount(site, req.body.move_target || '');
+  if (r && r.ok) {
+    return res.redirect(`/admin/sites/${req.params.slug}/edit?success=` + encodeURIComponent(`Verhuizing aangekondigd naar ${r.target} (${r.inboxes} inboxen).`));
+  }
+  const msg = {
+    guarded_account: 'Dit account heeft guardians; verhuizen kan pas als de guardianship mee kan (shaer-tge).',
+    no_backreference: 'Het nieuwe profiel claimt dit account niet in zijn aliassen. Zet daar eerst dit adres als alias.',
+    not_found: 'Nieuw adres niet gevonden. Gebruik @naam@server of een actor-URL.',
+    unreachable: 'Het nieuwe profiel is niet bereikbaar.',
+    self: 'Dat is dit account zelf.',
+  }[r && r.error] || 'Verhuizen mislukte; probeer het opnieuw.';
+  res.redirect(`/admin/sites/${req.params.slug}/edit?error=` + encodeURIComponent(msg));
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 5462bab0a30abfb9338812cb154a11cebda17b67)
+++ src/services/ActivityPubService.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
@@ -3843,4 +3843,61 @@
 }
 
+/**
+ * Slice 2 van shaer-0j2 (FEP-7628, DRAFT): de UITGAANDE helft — deze Klonkt
+ * is het oude huis en kondigt het vertrek aan. Twee eisen voordat er iets
+ * de deur uit gaat:
+ *  1. Geen guardians: een warded account verhuizen zonder de guardianship
+ *     te hertargeten zou het vangnet van het kind stil breken; dat is
+ *     shaer-tge's gated beslissing, dus tot die er is weigert een bewaakt
+ *     account de verhuizing.
+ *  2. De NIEUWE actor claimt ons in alsoKnownAs — dezelfde back-reference
+ *     die elke ontvangende server (onze eigen slice 1 incluis) eist. Zonder
+ *     die claim is de Move overal dood bij aankomst.
+ * De Move gaat duurzaam naar elke volger-inbox; hun servers doen de
+ * re-follow. `moved_to` wordt hier vastgelegd; het SERVEREN ervan op de
+ * actor (en het beleid van de oude site) is slice 3.
+ * Deps injecteerbaar voor tests (geen netwerk in node:test).
+ */
+export async function moveAccount(site, targetRaw, { fetchActorFn = null, deliverFn = null } = {}) {
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  if (!base || !site || !site.slug) return { error: 'config' };
+  try {
+    const guardians = Guardianship.listGuardians(site.slug);
+    if (guardians.length) {
+      console.warn('[AP] move refused: guarded account (shaer-tge):', site.slug, '→', String(targetRaw || ''));
+      return { error: 'guarded_account' };
+    }
+  } catch { /* geen guardianship-tabellen = geen guardians */ }
+  const s = String(targetRaw || '').trim();
+  let targetUri = null;
+  if (/^https?:\/\//i.test(s)) targetUri = safeUrl(s);
+  else if (s.includes('@')) targetUri = await webfingerResolve(s);
+  if (!targetUri) return { error: 'not_found' };
+  const me = actorId(base, site.slug);
+  if (targetUri === me) return { error: 'self' };
+  const target = await (fetchActorFn ? fetchActorFn(targetUri) : signedGetJson(site.slug, targetUri));
+  if (!target || !target.id || !target.inbox) return { error: 'unreachable' };
+  const aka = [].concat(target.alsoKnownAs || [])
+    .map((a) => (typeof a === 'string' ? a : (a && a.id))).filter(Boolean);
+  if (!aka.includes(me)) return { error: 'no_backreference' };
+  db.prepare('UPDATE sites SET moved_to = ? WHERE slug = ?').run(target.id, site.slug);
+  const keys = getOrCreateKeys(site.slug);
+  const move = {
+    '@context': AP_CONTEXT,
+    id: `${me}#move-${Date.now()}-${rid()}`,
+    type: 'Move',
+    actor: me,
+    object: me,
+    target: target.id,
+    to: [`${me}/followers`],
+  };
+  const inboxes = [...new Set(fStmts().list.all(site.slug).map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
+  for (const inbox of inboxes) {
+    await (deliverFn || deliverWithRetry)(site.slug, inbox, move, `${me}#main-key`, keys.private_pem);
+  }
+  console.log('[AP] MOVE announced:', site.slug, '→', target.id, 'naar', inboxes.length, 'inbox(en)');
+  return { ok: true, target: target.id, inboxes: inboxes.length };
+}
+
 // FEP-633c §5.3 note (authorized fetch): true when `actorUri` is a committed
 /**
@@ -4359,5 +4416,5 @@
   getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote,
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
-  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
+  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
   acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 5462bab0a30abfb9338812cb154a11cebda17b67)
+++ src/services/i18n.js	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
@@ -253,4 +253,9 @@
     'asite.links_hint': 'Getoond als merk-iconen op de profielkop. Voeg er zoveel toe als je wilt.',
     'asite.aliases': 'Fediverse-aliassen',
+    'asite.move': 'Verhuizen (fediverse)',
+    'asite.move_hint': 'Kondig aan je volgers aan dat dit account ergens anders verder gaat. Het nieuwe profiel moet dit adres eerst als alias claimen; volgers verhuizen dan vanzelf mee. Een account met guardians kan nog niet verhuizen.',
+    'asite.move_confirm': 'Weet je het zeker? Je volgers krijgen te horen dat dit account is verhuisd.',
+    'asite.move_btn': 'Kondig verhuizing aan',
+    'asite.moved_to': 'Verhuisd naar',
     'asite.aliases_hint': 'Eén per regel: je oude account als @naam@server of als actor-URL. Nodig om volgers van een oud account hierheen te verhuizen; de oude server controleert of dit profiel het oude claimt.',
     'asite.link_add': '+ Link toevoegen',
@@ -1191,4 +1196,9 @@
     'asite.links_hint': 'Shown as brand icons in the profile header. Add as many as you like.',
     'asite.aliases': 'Fediverse aliases',
+    'asite.move': 'Move (fediverse)',
+    'asite.move_hint': 'Announce to your followers that this account continues elsewhere. The new profile must claim this address as an alias first; followers then move along automatically. An account with guardians cannot move yet.',
+    'asite.move_confirm': 'Are you sure? Your followers will be told this account has moved.',
+    'asite.move_btn': 'Announce move',
+    'asite.moved_to': 'Moved to',
     'asite.aliases_hint': 'One per line: your old account as @name@server or as an actor URL. Needed to move followers from an old account to this one; the old server checks that this profile claims the old one.',
     'asite.link_add': '+ Add link',
@@ -2128,4 +2138,9 @@
     'asite.links_hint': 'Werden als Marken-Icons im Profilkopf gezeigt. Füge so viele hinzu, wie du möchtest.',
     'asite.aliases': 'Fediverse-Aliasse',
+    'asite.move': 'Umzug (Fediverse)',
+    'asite.move_hint': 'Kündige deinen Followern an, dass dieses Konto woanders weitergeht. Das neue Profil muss diese Adresse zuerst als Alias beanspruchen; Follower ziehen dann automatisch mit. Ein Konto mit Guardians kann noch nicht umziehen.',
+    'asite.move_confirm': 'Bist du sicher? Deine Follower erfahren, dass dieses Konto umgezogen ist.',
+    'asite.move_btn': 'Umzug ankündigen',
+    'asite.moved_to': 'Umgezogen nach',
     'asite.aliases_hint': 'Einer pro Zeile: dein altes Konto als @name@server oder als Actor-URL. Nötig, um Follower eines alten Kontos hierher umzuziehen; der alte Server prüft, ob dieses Profil das alte beansprucht.',
     'asite.link_add': '+ Link hinzufügen',
Index: src/views/pages/admin-site-edit.ejs
===================================================================
--- src/views/pages/admin-site-edit.ejs	(revision 5462bab0a30abfb9338812cb154a11cebda17b67)
+++ src/views/pages/admin-site-edit.ejs	(revision 0ca7e9a4accafc47292602b2067055a4cc416408)
@@ -270,4 +270,21 @@
     <button type="submit" class="btn btn-primary"><%= isNew ? t('asite.submit_create') : t('asite.submit_save') %></button>
   </form>
+
+<% if (!isNew) { %>
+  <form method="post" action="/admin/sites/<%= site.slug %>/move" class="admin-form"
+        onsubmit="return confirm('<%= t('asite.move_confirm') %>');">
+    <fieldset>
+      <legend><%= t('asite.move') %></legend>
+      <p class="form-hint"><%= t('asite.move_hint') %></p>
+      <% if (typeof site.moved_to === 'string' && site.moved_to) { %>
+        <p class="form-hint"><strong><%= t('asite.moved_to') %>:</strong> <%= site.moved_to %></p>
+      <% } %>
+      <label>
+        <input type="text" name="move_target" placeholder="@nieuw@andere-server.example" required>
+      </label>
+      <button type="submit" class="btn btn-danger"><%= t('asite.move_btn') %></button>
+    </fieldset>
+  </form>
+<% } %>
 </div>
 
