Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision ccaa5301c212451f2e6b7c28c49282055278e961)
+++ 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 ccaa5301c212451f2e6b7c28c49282055278e961)
+++ 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',
