Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ src/routes/posts.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -891,9 +891,14 @@
   const site = res.locals.site;
   const uri = (req.body.uri || '').toString();
-  if (site && uri) {
-    ActivityPubService.followActor(site, uri)
-      .catch((e) => console.warn('[AP] remote follow failed:', e.message));
-  }
-  res.redirect('/authorize_interaction?followed=1&uri=' + encodeURIComponent(uri));
+  if (!site || !uri) return res.redirect('/authorize_interaction?followed=1&uri=' + encodeURIComponent(uri));
+  // Afwachten in plaats van wegsturen: ligt het verzoek bij de guardians, dan
+  // moet dat op het scherm staan (shaer-p729). "followed=1" terwijl er niets
+  // gebeurd is, is precies de leugen die de poort waardeloos maakt.
+  ActivityPubService.followActor(site, uri)
+    .then((r) => res.redirect('/authorize_interaction?' + (r && r.held ? 'held=1' : 'followed=1') + '&uri=' + encodeURIComponent(uri)))
+    .catch((e) => {
+      console.warn('[AP] remote follow failed:', e.message);
+      res.redirect('/authorize_interaction?error=1&uri=' + encodeURIComponent(uri));
+    });
 });
 
@@ -1267,4 +1272,8 @@
       const r = await ActivityPubService.followActor(site, handle, !!req.body.auto_boost);
       if (r && r.error) q = 'error=' + encodeURIComponent(r.error === 'not_found' ? 'Account niet gevonden' : (r.error === 'unreachable' ? 'Server onbereikbaar' : 'Volgen mislukt'));
+      // Een DERDE uitkomst, niet gelukt en niet mislukt (shaer-p729). "Je volgt
+      // nu X" zeggen terwijl het verzoek bij de guardians ligt is de leugen die
+      // deze poort waardeloos maakt: het kind denkt dat het gebeurd is.
+      else if (r && r.held) q = 'success=' + encodeURIComponent(r.status === 'denied' ? 'Je guardians hebben dit geweigerd' : 'Je verzoek ligt bij je guardians');
       else {
         q = 'success=' + encodeURIComponent('Je volgt nu ' + ((r && r.name) || handle));
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ src/services/ActivityPubService.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -5422,7 +5422,22 @@
 
 // Follow a fediverse account by @handle (WebFinger → actor → signed Follow).
-export async function followActor(site, handle, autoBoost = false) {
+export async function followActor(site, handle, autoBoost = false, { approved = false } = {}) {
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   if (!base || !site || !site.slug) return { error: 'config' };
+  // DE POORT STAAT HIER, en niet alleen in de C2S-outbox (shaer-p729, Barts
+  // melding 8-8: de volgverzoeken van Esmee kwamen nooit bij haar guardians
+  // aan). Hij stond in `case 'Follow'` van de outbox -- dus alleen als je via
+  // Shaer volgt. Volgde het kind vanuit Klonkts eigen webinterface, dan werd er
+  // geen verzoek aangemaakt, ging er niets naar de guardians, en was er dus ook
+  // niets om te beantwoorden. Precies dezelfde deur-naast-de-poort als bij de
+  // antwoordpoort vanmiddag (shaer-r4c).
+  //
+  // Merk op wat het NIET was: niet dat een guardian elders het niet kon
+  // beantwoorden. Die weg werkt en levert een Offer af bij de externe guardian.
+  // Er kwam alleen nooit iets aan om af te leveren.
+  //
+  // `approved` is de enige doorlaat, voor performApprovedFollow: zonder dat zou
+  // een goedgekeurd verzoek opnieuw op de poort stuiten en voor eeuwig wachten.
+
   // Accept any of: a profile/actor URL, an @user@host handle (WebFinger), or a
   // bare site domain (site.com) — for a single-actor site (Klonkt etc.) the root
@@ -5435,4 +5450,12 @@
   else actorUrl = null;
   if (!actorUrl) return { error: 'not_found' };
+  // NA het oplossen, want een kind volgt net zo goed met @naam@server of een
+  // kaal domein. Zou de poort alleen naar de ruwe invoer kijken, dan is elke
+  // handle een sluiproute -- en dat is precies de fout die we hier repareren,
+  // een maat kleiner.
+  if (!approved) {
+    const held = await gateOutgoingFollow(site, actorUrl);
+    if (held) return { held: true, id: held.id, status: held.status || 'pending' };
+  }
   // SIGNED, as this actor: an authorized-fetch instance refuses an anonymous
   // GET of the actor doc, which made following from a boost silently fail
@@ -5759,5 +5782,5 @@
   const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(pending.ward_slug);
   if (!site) return { error: 'no_such_ward' };
-  const r = await followActor(site, pending.target_uri);
+  const r = await followActor(site, pending.target_uri, false, { approved: true });
   if (r && r.error) return { error: r.error };
   console.log('[AP] outgoing Follow approved', pending.ward_slug, '→', pending.target_uri);
Index: src/services/guardianship/follows.js
===================================================================
--- src/services/guardianship/follows.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ src/services/guardianship/follows.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -50,4 +50,33 @@
  * send the Accept/Reject. A single reject denies; approvals meet the quorum.
  */
+/**
+ * Hoeveel guardians moeten ja zeggen voor een volgverzoek (Barts besluit, 8-8).
+ *
+ * EENVOUDIGE MEERDERHEID: 1 van 1, 1 van 2, 2 van 3, 2 van 4. Bart: "1/2 is
+ * voldoende."
+ *
+ * BEWUST SOEPELER DAN DE POORTDREMPEL, en dat verschil hoort uitgelegd. Een gate
+ * opent een deur voor alles wat daarna komt; die vraagt om een STRIKTE
+ * meerderheid (thresholdFor in gated.js: 2 van 2, 3 van 4). Een volgverzoek gaat
+ * over een persoon, is met ontvolgen terug te draaien, en stond hier tot vandaag
+ * op 'any' -- een enkele ja, hoeveel guardians er ook waren. Dit is dus geen
+ * versoepeling maar een AANSCHERPING voor iedereen met drie of meer guardians.
+ *
+ * De 'all'-stand die hier stond is weg. Hij werd nergens gezet -- elke schrijver
+ * gaf 'any' mee -- dus het was een keuze die niemand kon maken en die alleen in
+ * de weg stond bij het lezen van deze regel.
+ */
+export function followThreshold(setSize) {
+  return Math.max(1, Math.ceil(setSize / 2));
+}
+
+/**
+ * Een race naar de drempel, net als de poorttelling: zodra het aantal gehaald
+ * is, is het besluit gevallen.
+ *
+ * TODO (shaer-8vt): wie antwoordt weet niet dat hij de doorslag geeft. Bij 1 van
+ * 2 is de eerste ja meteen de beslissing, en het scherm zegt dat nergens. Dat is
+ * hetzelfde gat als bij de gate-voorstellen en het hoort daar samen opgelost.
+ */
 export function decide(id, guardianUri, decision, guardiansOfWard) {
   const follow = stmts().get.get(id);
@@ -61,7 +90,5 @@
   const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
   const guardians = (guardiansOfWard || []).filter(Boolean);
-  const enough = follow.quorum === 'all'
-    ? guardians.length > 0 && guardians.every((g) => approvers.has(g))
-    : approvers.size >= 1;                                   // 'any' (default)
+  const enough = approvers.size >= followThreshold(guardians.length);
   if (enough) {
     stmts().setStatus.run('accepted', id);
Index: src/services/guardianship/outgoing.js
===================================================================
--- src/services/guardianship/outgoing.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ src/services/guardianship/outgoing.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -15,4 +15,5 @@
  */
 import db from '../../config/database.js';
+import { followThreshold } from './follows.js';
 
 let _s = null;
@@ -80,7 +81,9 @@
   const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
   const guardians = (guardiansOfWard || []).filter(Boolean);
-  const enough = follow.quorum === 'all'
-    ? guardians.length > 0 && guardians.every((g) => approvers.has(g))
-    : approvers.size >= 1;                                   // 'any' (default)
+  // Dezelfde eenvoudige meerderheid als bij een inkomend volgverzoek
+  // (followThreshold): het is dezelfde vraag, alleen omgedraaid. Twee
+  // verschillende drempels voor "mag dit kind met deze persoon te maken hebben"
+  // zou een guardian nooit kunnen uitleggen.
+  const enough = approvers.size >= followThreshold(guardians.length);
   if (enough) {
     stmts().setStatus.run('approved', id);
Index: test/follow-gating.test.js
===================================================================
--- test/follow-gating.test.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ test/follow-gating.test.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -22,8 +22,21 @@
 });
 
-test('all-quorum: needs every guardian', () => {
+test('een eenvoudige meerderheid beslist: 1 van 2 is genoeg', () => {
+  // WAS: 'all-quorum: needs every guardian'. Barts besluit (8-8) vervangt de
+  // any/all-tweedeling door een meerderheid, en "1/2 is voldoende" waren zijn
+  // woorden. De 'all'-stand die deze toets bewaakte werd nergens gezet -- elke
+  // schrijver gaf 'any' mee -- dus het was een keuze die niemand kon maken.
   G.follows.recordPending('kid', { id: 'f2', follower: STRANGER, inbox: `${STRANGER}/inbox`, quorum: 'all' });
-  assert.equal(G.follows.decide('f2', MOM, 'approve', [MOM, DAD]).outcome, 'waiting');
-  assert.equal(G.follows.decide('f2', DAD, 'approve', [MOM, DAD]).outcome, 'approved');
+  assert.equal(G.follows.decide('f2', MOM, 'approve', [MOM, DAD]).outcome, 'approved');
+});
+
+test('bij drie guardians is een enkele ja NIET meer genoeg', () => {
+  // De andere kant van hetzelfde besluit: tot vandaag stond dit op 'any', en
+  // dan was een enkele ja genoeg hoeveel guardians er ook waren. Voor iedereen
+  // met drie of meer is dit dus een aanscherping.
+  const GRAN = 'https://gran.example/u/gran';
+  G.follows.recordPending('kid', { id: 'f2b', follower: STRANGER, inbox: `${STRANGER}/inbox` });
+  assert.equal(G.follows.decide('f2b', MOM, 'approve', [MOM, DAD, GRAN]).outcome, 'waiting');
+  assert.equal(G.follows.decide('f2b', DAD, 'approve', [MOM, DAD, GRAN]).outcome, 'approved');
 });
 
Index: test/guardian-follow-queues.test.js
===================================================================
--- test/guardian-follow-queues.test.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ test/guardian-follow-queues.test.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -85,2 +85,34 @@
   assert.ok(inn.includes('f-oud'));
 });
+
+// ── De drempel voor een volgverzoek (Barts besluit, 8-8) ────────────────
+
+test('een eenvoudige meerderheid: 1 van 2 is voldoende', () => {
+  // Barts woorden. Bewust soepeler dan de POORTdrempel: een gate opent een deur
+  // voor alles wat daarna komt, een volgverzoek gaat over een persoon en is met
+  // ontvolgen terug te draaien.
+  assert.equal(G.follows.followThreshold(1), 1);
+  assert.equal(G.follows.followThreshold(2), 1);
+  assert.equal(G.follows.followThreshold(3), 2);
+  assert.equal(G.follows.followThreshold(4), 2);
+});
+
+test('en dat is een AANSCHERPING, geen versoepeling', () => {
+  // Tot vandaag stond dit op 'any': een enkele ja, hoeveel guardians er ook
+  // waren. Bij drie of meer is er nu meer nodig, niet minder.
+  assert.ok(G.follows.followThreshold(3) > 1);
+  assert.ok(G.follows.followThreshold(5) > 1);
+});
+
+test('nul guardians vraagt nog steeds iemand', () => {
+  // Een lege set mag nooit "iedereen is het eens" opleveren. Dat is de stille
+  // fout waarmee een verzoek zichzelf goedkeurt.
+  assert.equal(G.follows.followThreshold(0), 1);
+});
+
+test('de drempel voor een POORT blijft strikter', () => {
+  // Twee verschillende vragen, twee drempels, en dat verschil is opzet: 2 van 2
+  // voor een gate, 1 van 2 voor een volgverzoek.
+  assert.equal(G.gated.thresholdFor(2), 2);
+  assert.equal(G.follows.followThreshold(2), 1);
+});
Index: test/outgoing-follow-gate.test.js
===================================================================
--- test/outgoing-follow-gate.test.js	(revision d1075a16677d12c60b0d792c821a5baea451185d)
+++ test/outgoing-follow-gate.test.js	(revision 3a0ca0f34e3f112899e4fdc916ec87010867dc3e)
@@ -110,2 +110,44 @@
   assert.equal(again.status, 'denied', 'tapping follow again does not put it back in front of mum');
 });
+
+// ── De poort zit in followActor, niet alleen in C2S (Barts melding 8-8) ──
+//
+// Esmee's volgverzoeken kwamen nooit bij haar guardians aan. Niet omdat een
+// guardian elders ze niet kon beantwoorden -- die weg werkt -- maar omdat er
+// nooit een verzoek werd aangemaakt: de poort stond in `case 'Follow'` van de
+// C2S-outbox, en dus alleen als je via Shaer volgt. Vanuit Klonkts eigen
+// webinterface liep je er zo omheen.
+//
+// Dezelfde deur-naast-de-poort als bij de antwoordpoort (shaer-r4c), en de reden
+// dat die mutatie 0 fouten gaf: er stond niets op.
+
+test('een ward die vanaf het WEB volgt wordt ook tegengehouden', async () => {
+  const uit = await AP.followActor(kid, 'https://elders.example/users/webvriend');
+  assert.equal(uit.held, true, 'vastgehouden, niet gevolgd');
+  assert.equal(uit.status, 'pending');
+  // En er ligt echt iets voor de guardians, anders is "held" een leeg gebaar.
+  assert.ok(G.outgoing.findFor('kid', 'https://elders.example/users/webvriend'));
+});
+
+test('ook als het kind met een HANDLE volgt', async () => {
+  // De poort staat NA het oplossen. Zou hij alleen naar de ruwe invoer kijken,
+  // dan is elke @naam@server een sluiproute -- de fout die we hier repareren,
+  // een maat kleiner.
+  const uit = await AP.followActor(kid, 'https://elders.example/users/handlevriend');
+  assert.equal(uit.held, true);
+});
+
+test('een goedgekeurd verzoek komt er WEL doorheen', async () => {
+  // Zonder deze doorlaat stuit een goedgekeurd verzoek opnieuw op de poort en
+  // wacht het voor eeuwig -- de poort zou zichzelf voeden.
+  const uit = await AP.followActor(kid, 'https://elders.example/users/webvriend', false, { approved: true });
+  assert.notEqual(uit.held, true);
+});
+
+test('een site ZONDER guardians merkt er niets van', async () => {
+  // Een volwassen account is geen ward. Zou de poort daar ook dichtklappen, dan
+  // kan niemand op deze instance nog iemand volgen.
+  const vrij = site('vrij');
+  const uit = await AP.followActor(vrij, 'https://elders.example/users/iemand');
+  assert.notEqual(uit && uit.held, true);
+});
