Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision c26cc18be55079e31ab7f92b27f94d8de391003e)
+++ src/routes/guardian.js	(revision 780a7c655af23759b03e276bc56d8be0bc5d44ff)
@@ -32,10 +32,12 @@
 function uiStrings(L) {
   const keys = ['sent', 'sent_retry', 'sending', 'not_found', 'failed', 'network',
-    'pending', 'active', 'retract', 'release', 'open', 'push_unavailable'];
+    'pending', 'active', 'retract', 'release', 'open', 'push_unavailable',
+    'accept', 'reject', 'complete', 'awaiting_others', 'coguard'];
   return Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
 }
 
 function dashboardState(site, L) {
-  const wards = Guardianship.listWards(site.slug);
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const me = AP.actorId(base, site.slug);
   const help = db.prepare(
     `SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, content, published, created_at
@@ -44,6 +46,7 @@
   return {
     site: site.slug,
-    wards: wards.filter((w) => w.status === 'accepted'),
-    pendingOffers: wards.filter((w) => w.status === 'offered'),
+    me,
+    wards: Guardianship.listWards(site.slug),               // committed wards
+    offers: Guardianship.offersCollection(`${me}/queues/offers`, site.slug, me).orderedItems,
     help,
     strings: uiStrings(L),
@@ -94,5 +97,19 @@
 });
 
-// ── Manage: retract a pending offer / release a ward ─────────────────────
+// ── Answer an offer (co-guardian accept/reject, or the candidate's final
+//    "complete"). All three are a C2S Accept/Reject on the offer id; the
+//    handshake module decides when it commits (§3.1).
+router.post('/offer', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
+  const site = siteForUser(req);
+  if (!site) return res.status(404).json({ error: 'no_site' });
+  const offerId = String(req.body?.offer || '').trim();
+  const answer = req.body?.answer === 'reject' ? 'Reject' : 'Accept';
+  if (!offerId) return res.status(400).json({ error: 'empty_offer' });
+  const r = await AP.ingestOutboxActivity(site, req.session.user, { type: answer, object: offerId });
+  if (!r || r.status >= 400) return res.status(r?.status || 500).json({ error: r?.error || 'answer_failed' });
+  res.json({ ok: true, committed: !!r.committed, readyToCommit: !!r.readyToCommit });
+});
+
+// ── Manage: release a committed ward (local Undo; federation is Fase 4). ──
 router.post('/wards/remove', requireAuth, express.json({ limit: '4kb' }), (req, res) => {
   const site = siteForUser(req);
Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision c26cc18be55079e31ab7f92b27f94d8de391003e)
+++ src/routes/posts.js	(revision 780a7c655af23759b03e276bc56d8be0bc5d44ff)
@@ -931,11 +931,12 @@
     return renderPage(req, res, 'partials/messages-append', { items, seen: seenAt, hasMore, nextOffset: offset + FEED_PAGE, moreBase });
   }
-  // FEP-633c: pending guardianship offers TO this account (ward side) show
-  // as a special message with an accept button (Robins besluit: the kid
-  // answers in their own Klonkt; safety is handled out-of-band by the
-  // guardians themselves).
-  const guardianOffers = site
-    ? Guardianship.listOffers(site.slug).filter((o) => o.role === 'ward')
-    : [];
+  // FEP-633c: pending guardianship offers TO this account (I am the ward)
+  // show as a special message with an accept button (Robins besluit: the kid
+  // answers in its own Klonkt; safety is out-of-band by the guardians).
+  const gBase = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const gMe = site ? ActivityPubService.actorId(gBase, site.slug) : null;
+  const guardianOffers = (site
+    ? Guardianship.offersCollection(`${gMe}/queues/offers`, site.slug, gMe).orderedItems
+    : []).filter((o) => o['shaer:ward'] === gMe && o['shaer:needsMyAccept']);
   renderPage(req, res, 'pages/messages', {
     pageTitleKey: 'msg.title', bodyClass: 'on-special', items, seenAt,
@@ -951,15 +952,10 @@
   const back = `${res.locals.siteUrlBase || ''}/messages`;
   const answer = req.body.answer === 'accept' ? 'Accept' : (req.body.answer === 'reject' ? 'Reject' : null);
-  const guardian = String(req.body.guardian || '').trim();
-  if (!site || !answer || !guardian) return res.redirect(back + '?error=guardianship');
-  const row = Guardianship.getRelation(site.slug, 'ward', guardian);
-  if (!row || row.status !== 'offered') return res.redirect(back + '?error=guardianship');
+  const offer = String(req.body.offer || '').trim();
+  if (!site || !answer || !offer) return res.redirect(back + '?error=guardianship');
   try {
-    const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
-    const me = ActivityPubService.actorId(base, site.slug);
-    const r = await Guardianship.handleGuardianshipOutbox(site, {
-      type: answer,
-      object: row.offer_id || { type: 'Relationship', subject: me, relationship: 'shaer:Guardian', object: guardian },
-    });
+    // Same C2S Accept/Reject the apps use; the handshake module records the
+    // ward's accept and (once the candidate returns the handle) commits.
+    const r = await ActivityPubService.ingestOutboxActivity(site, req.session.user, { type: answer, object: offer });
     if (r && r.status < 400) return res.redirect(back + '?success=' + (answer === 'Accept' ? 'guardian_accepted' : 'guardian_rejected'));
   } catch { /* fall through */ }
