Index: src/services/guardianship/handshake.js
===================================================================
--- src/services/guardianship/handshake.js	(revision 6c152a5a2b3de8369604ab7ceccccad68732ae88)
+++ src/services/guardianship/handshake.js	(revision 439f09574e5643c1bfc9e6787974389afdb0802b)
@@ -20,4 +20,5 @@
 import * as relations from './relations.js';
 import * as gated from './gated.js';
+import * as availability from './availability.js';
 
 let deps = null;
@@ -194,4 +195,9 @@
   if (!['Offer', 'Accept', 'Reject', 'Undo'].includes(type)) return null;
   const me = deps.selfId(site.slug);
+  // One answer restores everything (§3.6): any C2S activity from this actor
+  // is that answer, for every local ward it guards. Runs before anything is
+  // even looked at, so the target of a running lapse cancels it by doing
+  // anything at all — including trying to vote on it.
+  try { availability.oneAnswer(me, Date.now()); } catch { /* never load-bearing */ }
 
   // ── Undo: a guardian ends its own guardianship (§3.2). Same path as the
@@ -206,4 +212,24 @@
   // ── Offer: the local site is the guardian-candidate. ───────────────────
   if (type === 'Offer') {
+    // §3.6.3 over C2S: a guardian here proposes releasing a dormant
+    // co-guardian. A ward we host opens locally; a remote ward gets the
+    // proposal delivered, because the ward's server is the one that tallies
+    // and enforces (the §5.6 line: a guardian next door must not have more
+    // say than one far away).
+    const lp = availability.parseLapse(activity.object);
+    if (lp) {
+      const id = `${me}/lapses/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
+      const wardSlug = deps.localSlug(lp.ward);
+      if (wardSlug) {
+        const r = availability.openLapse({ id, wardSlug, wardUri: lp.ward, target: lp.target, openedBy: me, now: Date.now() });
+        if (r.error) return { status: r.error === 'not_in_available_set' ? 403 : 409, error: r.error };
+        deps.deliverTo(site, lp.target, { id, type: 'Offer', actor: me, to: [lp.target], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } }).catch(() => { /* best-effort */ });
+        notify(wardSlug, { kind: 'lapse_opened', lapse: id, target: lp.target, set: r.set });
+        return { status: 202, id, url: id, 'shaer:set': r.set, 'shaer:threshold': r.threshold };
+      }
+      const offer = { id, type: 'Offer', actor: me, to: [lp.ward], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } };
+      const delivered = await fanout(site, [lp.ward], offer);
+      return { status: 202, id, url: id, delivered };
+    }
     const rel = parseRelationship(activity.object);
     if (!rel) return null;
@@ -231,4 +257,12 @@
   const offerId = idOf(activity.object);
   if (!offerId) return { status: 400, error: 'missing_offer' };
+  // A lapse vote over C2S (§3.6.3): the same Accept/Reject wire the offers
+  // and gated follows use, which is exactly why the Shaer clients need no
+  // new verbs for it.
+  if (availability.getLapse(offerId)) {
+    const r = availability.lapseVote(offerId, me, type === 'Accept', Date.now());
+    if (r && r.error) return { status: r.error === 'not_in_set' ? 403 : 409, error: r.error };
+    return { status: 202, id: offerId, url: offerId, 'shaer:outcome': 'open', 'shaer:accepts': r.accepts, 'shaer:threshold': r.threshold };
+  }
   let offer = offers.getOffer(site.slug, offerId);
   if (!offer) return { status: 404, error: 'no_such_offer' };
@@ -273,4 +307,24 @@
       const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value);
       notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state });
+      return true;
+    }
+    // §3.6.3: a co-guardian proposes releasing a dormant guardian of THIS
+    // ward. The ward's server opens, tallies and (after the full window)
+    // executes, exactly as it does for the gated settings above.
+    const lp = availability.parseLapse(activity.object);
+    if (lp) {
+      if (lp.ward !== me) return false;   // not our ward
+      const id = idOf(activity) || `${me}/lapses/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
+      const r = availability.openLapse({ id, wardSlug: site.slug, wardUri: me, target: lp.target, openedBy: actor, now: Date.now() });
+      if (r.error) {
+        notify(site.slug, { kind: 'lapse_refused', reason: r.error, target: lp.target });
+        return true;   // consumed: the refusal is the answer
+      }
+      // The target is notified like any dormancy marking (§3.6.2): in
+      // protocol (a copy of the Offer, so one answer can cancel it) AND the
+      // §6 handle, which for a committed guardian is its inbox — the same
+      // door this delivery knocks on.
+      deps.deliverTo(site, lp.target, activity).catch(() => { /* best-effort */ });
+      notify(site.slug, { kind: 'lapse_opened', lapse: id, target: lp.target, set: r.set });
       return true;
     }
@@ -304,4 +358,12 @@
     return true;
   }
+  // §3.6.3: a set member answering a running lapse. Irreversible, so even a
+  // full tally leaves it open until the window closes (§3.5); the completion
+  // happens lazily on reads (queues) once the window has run.
+  if (availability.getLapse(offerId)) {
+    const r = availability.lapseVote(offerId, actor, type === 'Accept', Date.now());
+    notify(site.slug, { kind: 'lapse_vote', lapse: offerId, by: actor, state: r && !r.error ? 'recorded' : (r && r.error) || 'refused' });
+    return true;
+  }
   let offer = offers.getOffer(site.slug, offerId);
   if (!offer) return false;
