Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 88d7c8fc087ab49a2fd3ee63bf184d15d1f0fa31)
+++ src/routes/guardian.js	(revision e27b8db4e316874f7498fbe36e20270bc39b1d7f)
@@ -54,5 +54,6 @@
     'away_title', 'away_sub', 'away_week', 'away_month', 'away_done',
     // A gated-setting proposal from a fellow guardian (5.6).
-    'gated_title', 'gated_line_on', 'gated_line_off', 'gated_agree', 'gated_disagree'];
+    'gated_title', 'gated_line_on', 'gated_line_off', 'gated_agree', 'gated_disagree',
+    'play_propose', 'play_on', 'play_off'];
   const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
   s.wave = i18nT(L, 'guardian.wave');
@@ -90,4 +91,5 @@
       ...w,
       embeds: wardEmbedSetting(w.other_uri),
+      playback: wardPlaybackSetting(w.other_uri),
       guardians: wardGuardianStatuses(w.other_uri),
     })),
@@ -466,11 +468,14 @@
  * the ward lives elsewhere and the setting is not ours to show.
  */
-function wardEmbedSetting(uri) {
+function wardEmbedSetting(uri) { return wardGateSetting(uri, 'external_embeds'); }
+/** The playback gate of a ward we host (5.6): the heavier sibling. */
+function wardPlaybackSetting(uri) { return wardGateSetting(uri, 'external_playback'); }
+function wardGateSetting(uri, column) {
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   if (!base || !String(uri || '').startsWith(`${base}/`)) return null;
   const slug = String(uri).trim().replace(/\/+$/, '').split('/').pop();
-  const row = slug ? db.prepare('SELECT external_embeds FROM sites WHERE slug = ?').get(slug) : null;
+  const row = slug ? db.prepare(`SELECT ${column === 'external_playback' ? 'external_playback' : 'external_embeds'} AS v FROM sites WHERE slug = ?`).get(slug) : null;
   if (!row) return null;
-  return row.external_embeds === null || row.external_embeds === undefined ? false : row.external_embeds === 1;
+  return row.v === null || row.v === undefined ? false : row.v === 1;
 }
 
@@ -480,4 +485,8 @@
 // can move, and only a committed guardian of THAT ward may move it.
 router.post('/wards/embeds', requireAuth, express.json({ limit: '4kb' }), (req, res) => {
+  req.body = { ...req.body, feature: req.body?.feature === 'shaer:externalPlayback' ? 'shaer:externalPlayback' : 'shaer:externalEmbeds' };
+  return proposeGated(req, res);
+});
+function proposeGated(req, res) {
   const site = siteForUser(req);
   if (!site) return res.status(404).json({ error: 'no_site' });
@@ -496,5 +505,5 @@
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   const me = AP.actorId(base, site.slug);
-  const feature = 'shaer:externalEmbeds';
+  const feature = req.body.feature;   // normalised by the route above
   const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
   const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
@@ -504,4 +513,12 @@
     Guardianship.gated.rememberGatedOffer(offerId, localWard.slug, feature, allow);
     const r = Guardianship.gated.recordGatedVote(localWard.slug, feature, me, allow);
+    // Same forward as the S2S path: without it the other guardians never learn
+    // the proposal exists and a threshold of two can never be met.
+    if (r.state === 'open') {
+      for (const g of Guardianship.listGuardians(localWard.slug).map((x) => x.other_uri)) {
+        if (g === me) continue;
+        AP.deliverToActor(site, g, { ...offer, to: [g] }).catch(() => { /* queued */ });
+      }
+    }
     return res.json({ ok: true, allow, state: r.state, need: r.need, of: r.of });
   }
