Index: src/assets/css/guardian.css
===================================================================
--- src/assets/css/guardian.css	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
+++ src/assets/css/guardian.css	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
@@ -118,2 +118,18 @@
 .g-note .tl-media-video, .g-note .tl-media-audio { width: 100%; margin: 8px 0 0; border-radius: 10px; display: block; }
 .g-note .tl-media-video { max-height: 320px; background: #000; }
+
+/* ── Paneel per ward ───────────────────────────────────────────────────────
+   Een guardian denkt per kind, niet per functie: achter de regel van een kind
+   zit alles over dat kind. De regel zelf draagt de tellers, zodat een dicht
+   paneel nooit iets verbergt dat een antwoord nodig heeft. */
+.g-card.ward > .row { gap: 8px; }
+.g-panel { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
+.g-panel[hidden] { display: none; }
+.g-panel-sec + .g-panel-sec { margin-top: 16px; }
+.g-panel-sec h3 { margin: 0 0 6px; font-size: .82rem; font-weight: 600; letter-spacing: .03em;
+  text-transform: uppercase; color: var(--sub); }
+.g-panel-sec .g-card { background: transparent; border-color: var(--line); }
+.g-panel-sec .g-card + .g-card { margin-top: 6px; }
+.g-panel-sec .row { flex-wrap: wrap; }
+.g-empty.small { margin: 0; font-size: .85rem; }
+.tag.help { color: var(--accent); border-color: var(--accent); }
Index: src/assets/js/guardian.js
===================================================================
--- src/assets/js/guardian.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
+++ src/assets/js/guardian.js	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
@@ -43,32 +43,44 @@
 
   // ── 1. Help requests ───────────────────────────────────────────────────
+  // A call for help is not an alarm: it may well be settled quietly between a
+  // guardian and the child. So the card carries no siren, it just has to be
+  // impossible to miss. It shows up twice on purpose (Robins keuze): the recent
+  // ones across all children at the top, the full history of one child in that
+  // child's panel.
+  var HELP_TOP = 5;
+
+  function helpCard(h) {
+    var card = el('div', 'g-card help');
+    var row = el('div', 'row');
+    var who = el('span', 'who grow');
+    // name_html carries the custom emojis (FEP-9098) of the display name, the
+    // same way de Krant renders a byline. Falls back to the plain name.
+    if (h.name_html) who.innerHTML = h.name_html;
+    else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle);
+    row.appendChild(who);
+    row.appendChild(el('span', 'when', when(h, h.published || h.created_at)));
+    card.appendChild(row);
+    var body = el('div', 'body g-note');
+    // body_html is the shared note-body partial, rendered server-side: the
+    // content with its emojis, the quote / link-preview card and the media.
+    // Falls back to the bare content for rows stored before that existed.
+    body.innerHTML = h.body_html || h.content || '';   // sanitized server-side on ingest
+    card.appendChild(body);
+    if (h.note_url) {
+      var a = el('a', 'g-link', T.open || 'open');
+      a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener';
+      card.appendChild(a);
+    }
+    return card;
+  }
+
   function renderHelp() {
     var list = document.getElementById('help-list');
     list.textContent = '';
     var help = S.help || [];
-    help.forEach(function (h) {
-      var card = el('div', 'g-card help');
-      var row = el('div', 'row');
-      var who = el('span', 'who grow');
-      // name_html carries the custom emojis (FEP-9098) of the display name, the
-      // same way de Krant renders a byline. Falls back to the plain name.
-      if (h.name_html) who.innerHTML = h.name_html;
-      else who.textContent = h.actor_name || handleOf(h.actor_uri, h.actor_handle);
-      row.appendChild(who);
-      row.appendChild(el('span', 'when', when(h, h.published || h.created_at)));
-      card.appendChild(row);
-      var body = el('div', 'body g-note');
-      // body_html is the shared note-body partial, rendered server-side: the
-      // content with its emojis, the quote / link-preview card and the media.
-      // Falls back to the bare content for rows stored before that existed.
-      body.innerHTML = h.body_html || h.content || '';   // sanitized server-side on ingest
-      card.appendChild(body);
-      if (h.note_url) {
-        var a = el('a', 'g-link', T.open || 'open');
-        a.href = h.note_url; a.target = '_blank'; a.rel = 'noopener';
-        card.appendChild(a);
-      }
-      list.appendChild(card);
-    });
+    help.slice(0, HELP_TOP).forEach(function (h) { list.appendChild(helpCard(h)); });
+    if (help.length > HELP_TOP) {
+      list.appendChild(el('p', 'g-sec-sub', '+ ' + (help.length - HELP_TOP) + ' — ' + (T.panel_help || '')));
+    }
     var badge = document.getElementById('help-count');
     badge.textContent = help.length; badge.hidden = help.length === 0;
@@ -119,5 +131,101 @@
   }
 
-  // ── 4. Accepted wards ──────────────────────────────────────────────────
+  // ── 4. Accepted wards: one panel per child ─────────────────────────────
+  // A guardian thinks per child, not per function, so everything about one
+  // child sits behind that child's row: the gated settings, the follow requests
+  // waiting on them, their calls for help, their recent posts. The row itself
+  // carries counts, so nothing that needs an answer hides inside a closed
+  // panel.
+  var openPanels = {};   // ward uri -> open, so a refresh does not close it
+  // The follow requests and the wards' posts arrive from their own endpoints
+  // and are grouped into the panels by ward, so they are cached here rather
+  // than rendered into a section of their own.
+  var FEED = [], FOLLOWS = [];
+
+  function sectionInto(panel, title, items, empty, build) {
+    var h = el('div', 'g-panel-sec');
+    h.appendChild(el('h3', null, title));
+    if (!items.length) h.appendChild(el('p', 'g-empty small', empty));
+    else items.forEach(function (it) { h.appendChild(build(it)); });
+    panel.appendChild(h);
+    return h;
+  }
+
+  function embedsButton(w) {
+    // Gated feature: external (non-fediverse) embeds. Off by default for a
+    // ward; only a guardian can open it, and the gate is enforced server-side
+    // when the feed is built, so this button is the only thing that moves it.
+    // Shown for EVERY ward, including one on another server. There the value is
+    // unknown (it lives on the ward's server), but proposing is exactly as
+    // possible: the proposal travels, the ward's server tallies the guardians
+    // and enforces. A guardian next door must not have more say than one far
+    // away.
+    var known = w.embeds === true || w.embeds === false;
+    var emb = el('button', 'quiet small',
+      (known ? (w.embeds ? T.embeds_on : T.embeds_off) : T.embeds_propose) || 'Link previews');
+    emb.addEventListener('click', function () {
+      emb.disabled = true;
+      fetch('/guardian/wards/embeds', {
+        method: 'POST', headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ uri: w.other_uri, allow: known ? !w.embeds : true }),
+      }).then(function (r) { return r.json(); })
+        .then(function (j) {
+          // Not settled yet: the other guardians still have to answer.
+          if (j && j.state === 'open') {
+            emb.textContent = (T.embeds_waiting || 'waiting for the other guardians');
+            emb.disabled = true;
+            return;
+          }
+          refresh();
+        })
+        .catch(function () { emb.disabled = false; });
+    });
+    return emb;
+  }
+
+  function wardPanel(w) {
+    var uri = w.other_uri;
+    var panel = el('div', 'g-panel');
+    panel.hidden = !openPanels[uri];
+
+    var set = el('div', 'g-panel-sec');
+    set.appendChild(el('h3', null, T.settings_title || 'Settings'));
+    var setRow = el('div', 'row');
+    setRow.appendChild(embedsButton(w));
+    set.appendChild(setRow);
+    panel.appendChild(set);
+
+    sectionInto(panel, T.panel_follow || 'Follow requests',
+      FOLLOWS.filter(function (f) { return f.wardUri === uri; }),
+      T.panel_follow_empty || '', followCard);
+
+    sectionInto(panel, T.panel_help || 'Calls for help',
+      (S.help || []).filter(function (h) { return h.actor_uri === uri; }),
+      T.panel_help_empty || '', helpCard);
+
+    sectionInto(panel, T.panel_posts || 'Recent posts',
+      FEED.filter(function (p) { return p.authorUri === uri; }),
+      T.panel_posts_empty || '', feedCard);
+
+    var act = el('div', 'g-panel-sec');
+    act.appendChild(el('h3', null, T.panel_actions || 'Actions'));
+    var actRow = el('div', 'row');
+    var wave = el('button', 'small', T.wave || '👋 Wave');
+    wave.addEventListener('click', function () { sendWave(uri, wave); });
+    actRow.appendChild(wave);
+    var rel = el('button', 'quiet small', T.release);
+    // Releasing a ward is heavy and hard to undo (coming back needs a fresh
+    // offer the ward accepts), so it asks first and spells out what changes.
+    rel.addEventListener('click', function () {
+      var who = handleOf(uri, w.other_handle);
+      var msg = (T.release_confirm || 'Release {who}?').replace('{who}', who);
+      if (window.confirm(msg)) remove(uri, rel);
+    });
+    actRow.appendChild(rel);
+    act.appendChild(actRow);
+    panel.appendChild(act);
+    return panel;
+  }
+
   function renderWards() {
     var list = document.getElementById('wards-list');
@@ -125,50 +233,24 @@
     var wards = S.wards || [];
     wards.forEach(function (w) {
-      var card = el('div', 'g-card');
+      var uri = w.other_uri;
+      var card = el('div', 'g-card ward');
       var row = el('div', 'row');
-      row.appendChild(el('span', 'who grow', handleOf(w.other_uri, w.other_handle)));
+      row.appendChild(el('span', 'who grow', handleOf(uri, w.other_handle)));
+      // Counts on the row: whatever is waiting must be visible with the panel shut.
+      var nHelp = (S.help || []).filter(function (h) { return h.actor_uri === uri; }).length;
+      var nFollow = FOLLOWS.filter(function (f) { return f.wardUri === uri; }).length;
+      if (nHelp) row.appendChild(el('span', 'tag help', '🛟 ' + nHelp));
+      if (nFollow) row.appendChild(el('span', 'tag co', nFollow + ' ' + (nFollow === 1 ? (T.badge_follow_one || '') : (T.badge_follow || ''))));
       row.appendChild(el('span', 'tag ok', T.active));
-      var wave = el('button', 'small', T.wave || '👋 Wave');
-      wave.addEventListener('click', function () { sendWave(w.other_uri, wave); });
-      row.appendChild(wave);
-      // Gated feature: external (non-fediverse) embeds. Off by default for a
-      // ward; only a guardian can open it, and the gate is enforced server-side
-      // when the feed is built, so this button is the only thing that moves it.
-      // Shown for EVERY ward, including one on another server. There the value
-      // is unknown (it lives on the ward's server), but proposing is exactly as
-      // possible: the proposal travels, the ward's server tallies the guardians
-      // and enforces. A guardian next door must not have more say than one far
-      // away.
-      var known = w.embeds === true || w.embeds === false;
-      var emb = el('button', 'quiet small',
-        (known ? (w.embeds ? T.embeds_on : T.embeds_off) : T.embeds_propose) || 'Link previews');
-      emb.addEventListener('click', function () {
-        emb.disabled = true;
-        fetch('/guardian/wards/embeds', {
-          method: 'POST', headers: { 'Content-Type': 'application/json' },
-          body: JSON.stringify({ uri: w.other_uri, allow: known ? !w.embeds : true }),
-        }).then(function (r) { return r.json(); })
-          .then(function (j) {
-            // Not settled yet: the other guardians still have to answer.
-            if (j && j.state === 'open') {
-              emb.textContent = (T.embeds_waiting || 'waiting for the other guardians');
-              emb.disabled = true;
-              return;
-            }
-            refresh();
-          })
-          .catch(function () { emb.disabled = false; });
+      var toggle = el('button', 'quiet small', openPanels[uri] ? T.panel_close : T.panel_open);
+      row.appendChild(toggle);
+      card.appendChild(row);
+      var panel = wardPanel(w);
+      card.appendChild(panel);
+      toggle.addEventListener('click', function () {
+        openPanels[uri] = !openPanels[uri];
+        panel.hidden = !openPanels[uri];
+        toggle.textContent = openPanels[uri] ? T.panel_close : T.panel_open;
       });
-      row.appendChild(emb);
-      var btn = el('button', 'quiet small', T.release);
-      // Releasing a ward is heavy and hard to undo (coming back needs a fresh
-      // offer the ward accepts), so it asks first and spells out what changes.
-      btn.addEventListener('click', function () {
-        var who = handleOf(w.other_uri, w.other_handle);
-        var msg = (T.release_confirm || 'Release {who}?').replace('{who}', who);
-        if (window.confirm(msg)) remove(w.other_uri, btn);
-      });
-      row.appendChild(btn);
-      card.appendChild(row);
       list.appendChild(card);
     });
@@ -197,26 +279,23 @@
 
   // ── 0. Wards' corner: read-only feed of your wards' posts ───────────────
-  function renderFeed(items) {
-    var list = document.getElementById('feed-list');
-    list.textContent = '';
-    (items || []).forEach(function (p) {
-      var card = el('div', 'g-card feed');
-      var head = el('div', 'row');
-      head.appendChild(el('span', 'who grow', p.author));
-      if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
-      card.appendChild(head);
-      var body = el('div', 'feed-body');
-      if (p.cw) {
-        var d = document.createElement('details');
-        var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
-        var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
-        body.appendChild(d);
-      } else {
-        body.innerHTML = p.content || '';   // server-sanitized HTML (same as Berichten)
-      }
-      card.appendChild(body);
-      list.appendChild(card);
-    });
-    show('feed-section', (items || []).length > 0);
+  // Lives inside each child's panel now, so the fetches only fill a cache and
+  // ask the ward list to redraw. A guardian watches, it does not publish.
+  function feedCard(p) {
+    var card = el('div', 'g-card feed');
+    var head = el('div', 'row');
+    head.appendChild(el('span', 'who grow', p.author));
+    if (p.published) head.appendChild(el('span', 'g-when', when(p, p.published)));
+    card.appendChild(head);
+    var body = el('div', 'feed-body');
+    if (p.cw) {
+      var d = document.createElement('details');
+      var sum = document.createElement('summary'); sum.textContent = p.cw; d.appendChild(sum);
+      var inner = el('div'); inner.innerHTML = p.content || ''; d.appendChild(inner);
+      body.appendChild(d);
+    } else {
+      body.innerHTML = p.content || '';   // server-sanitized HTML (same as Berichten)
+    }
+    card.appendChild(body);
+    return card;
   }
 
@@ -224,6 +303,6 @@
     return fetch('/guardian/api/feed?site=' + encodeURIComponent(S.site))
       .then(function (r) { return r.json(); })
-      .then(function (f) { if (f && !f.error) renderFeed(f.items); })
-      .catch(function () { /* corner just stays hidden */ });
+      .then(function (f) { if (f && !f.error) { FEED = f.items || []; renderWards(); } })
+      .catch(function () { /* panels just show "nothing yet" */ });
   }
 
@@ -236,26 +315,23 @@
     }).then(loadFollowReqs);
   }
-  function renderFollowReqs(items) {
-    var list = document.getElementById('follow-list');
-    list.textContent = '';
-    (items || []).forEach(function (f) {
-      var card = el('div', 'g-card');
-      var row = el('div', 'row');
-      row.appendChild(el('span', 'who grow', f.follower + '  →  ' + f.ward));
-      var ok = el('button', 'small', T.accept || 'Accept');
-      ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); });
-      var no = el('button', 'quiet small', T.reject || 'Deny');
-      no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); });
-      row.appendChild(ok); row.appendChild(no);
-      card.appendChild(row);
-      list.appendChild(card);
-    });
-    show('follow-section', (items || []).length > 0);
+  function followCard(f) {
+    var card = el('div', 'g-card');
+    var row = el('div', 'row');
+    // Inside the child's own panel the ward name is a given, so only the
+    // person asking is named here.
+    row.appendChild(el('span', 'who grow', f.follower));
+    var ok = el('button', 'small', T.accept || 'Accept');
+    ok.addEventListener('click', function () { answerFollow(f.id, 'approve', ok); });
+    var no = el('button', 'quiet small', T.reject || 'Deny');
+    no.addEventListener('click', function () { answerFollow(f.id, 'reject', no); });
+    row.appendChild(ok); row.appendChild(no);
+    card.appendChild(row);
+    return card;
   }
   function loadFollowReqs() {
     return fetch('/guardian/api/follow-requests?site=' + encodeURIComponent(S.site))
       .then(function (r) { return r.json(); })
-      .then(function (f) { if (f && !f.error) renderFollowReqs(f.items); })
-      .catch(function () { /* stays hidden */ });
+      .then(function (f) { if (f && !f.error) { FOLLOWS = f.items || []; renderWards(); } })
+      .catch(function () { /* panels just show "none waiting" */ });
   }
 
Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
+++ src/routes/guardian.js	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
@@ -41,5 +41,9 @@
     'pending', 'active', 'retract', 'release', 'release_confirm', 'open', 'push_unavailable',
     'embeds_on', 'embeds_off', 'embeds_propose', 'embeds_waiting',
-    'accept', 'reject', 'complete', 'awaiting_others', 'coguard'];
+    'accept', 'reject', 'complete', 'awaiting_others', 'coguard',
+    // The per-ward panel: everything about one child in one place.
+    'settings_title', 'panel_open', 'panel_close', 'panel_help', 'panel_help_empty',
+    'panel_follow', 'panel_follow_empty', 'panel_posts', 'panel_posts_empty',
+    'panel_actions', 'badge_help', 'badge_follow', 'badge_follow_one', 'help_empty'];
   const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
   s.wave = i18nT(L, 'guardian.wave');
@@ -139,4 +143,5 @@
       id: p.id,
       author: p.author_handle || p.author_name || p.author_uri,
+      authorUri: p.author_uri,   // the grouping key: which child's panel this belongs in
       authorName: p.author_name,
       authorIcon: p.author_icon,
@@ -157,5 +162,5 @@
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
   return Guardianship.listWards(site.slug)
-    .map((w) => (w.other_uri.startsWith(base) ? w.other_uri.split('/').pop() : null))
+    .map((w) => (w.other_uri.startsWith(base) ? { slug: w.other_uri.split('/').pop(), uri: w.other_uri } : null))
     .filter(Boolean);
 }
@@ -166,8 +171,10 @@
   const items = [];
   const host = (() => { try { return new URL(process.env.PUBLIC_BASE_URL || '').host; } catch { return ''; } })();
+  // wardUri is the grouping key for the per-ward panel: the handle is for
+  // reading, the URI is what identifies the child across both cases below.
   // Local wards (guardian co-located): read the pending follows directly.
-  for (const wardSlug of wardSlugsOf(site)) {
-    for (const f of Guardianship.follows.listForWard(wardSlug)) {
-      items.push({ id: f.id, ward: `@${wardSlug}@${host}`, follower: f.follower_handle || f.follower_name || f.follower_uri, followerIcon: f.follower_icon, remote: false, created: f.created_at });
+  for (const w of wardSlugsOf(site)) {
+    for (const f of Guardianship.follows.listForWard(w.slug)) {
+      items.push({ id: f.id, ward: `@${w.slug}@${host}`, wardUri: w.uri, follower: f.follower_handle || f.follower_name || f.follower_uri, followerIcon: f.follower_icon, remote: false, created: f.created_at });
     }
   }
@@ -175,5 +182,5 @@
   for (const rev of Guardianship.follows.listReviews(site.slug)) {
     const wardName = (() => { try { const u = new URL(rev.ward_uri); return `@${u.pathname.split('/').pop()}@${u.host}`; } catch { return rev.ward_uri; } })();
-    items.push({ id: rev.id, ward: wardName, follower: rev.follower_handle || rev.follower_uri, followerIcon: rev.follower_icon, remote: true, created: rev.created_at });
+    items.push({ id: rev.id, ward: wardName, wardUri: rev.ward_uri, follower: rev.follower_handle || rev.follower_uri, followerIcon: rev.follower_icon, remote: true, created: rev.created_at });
   }
   res.json({ items });
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
+++ src/services/i18n.js	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
@@ -67,5 +67,5 @@
     'admin.b_paid': 'Betaalde posts', 'admin.b_push': 'Notificaties', 'admin.back': 'Terug naar Beheer',
     'push.t': 'Notificaties', 'push.intro': 'Krijg een melding op dit apparaat bij nieuwe volgers, reacties en berichten, ook als de site niet open staat. Versleuteld tot in je browser; wij sturen zo min mogelijk inhoud mee.', 'push.unavailable': 'Push is op deze server niet beschikbaar (sleutel kon niet worden aangemaakt of de dependency ontbreekt).', 'push.unsupported': 'Deze browser ondersteunt geen push-notificaties.', 'push.ios_hint': 'Op iPhone/iPad werkt dit alleen als de site op je beginscherm staat: deel-knop, dan "Zet op beginscherm", en open de site daarna vanaf daar.', 'push.this_device': 'Dit apparaat:', 'push.checking': 'controleren…', 'push.state_on': 'meldingen staan aan', 'push.state_off': 'meldingen staan uit', 'push.state_denied': 'geblokkeerd in de browserinstellingen', 'push.state_unknown': 'status onbekend', 'push.state_unsupported': 'niet ondersteund', 'push.enable': 'Zet aan op dit apparaat', 'push.disable': 'Zet uit', 'push.test': 'Stuur testmelding', 'push.what': 'Waarvoor wil je een melding?', 'push.a_follow': 'Nieuwe volger', 'push.a_reply': 'Reactie of vermelding', 'push.a_like': 'Waardering (ster)', 'push.a_boost': 'Boost', 'push.a_dm': 'Privébericht', 'push.saved': 'Opgeslagen.', 'push.devices': 'Gekoppelde apparaten', 'push.device': 'Apparaat', 'push.since': 'sinds', 'push.remove': 'Verwijder', 'push.enable_failed': 'aanzetten mislukt', 'push.on_short': 'Word supporter',
-    'push.n_follow_t': 'Nieuwe volger', 'push.n_follow_b': '{who} volgt je nu', 'push.n_reply_t': 'Reactie op "{title}"', 'push.n_mention_t': 'Vermelding', 'push.n_dm_t': 'Privébericht', 'push.n_dm_b': 'Nieuw bericht van {who}', 'push.n_like_t': 'Nieuwe waardering', 'push.n_like_b': '{who} waardeerde "{title}"', 'push.n_boost_t': 'Geboost', 'push.n_boost_b': '{who} boostte "{title}"', 'msg.guard_offer': 'wil je guardian worden. Bespreek dit met je ouders of verzorgers voordat je beslist.', 'msg.guard_accept': 'Accepteer', 'msg.guard_reject': 'Weiger', 'msg.guard_accepted': 'Guardian geaccepteerd. Jullie zijn nu verbonden.', 'msg.guard_rejected': 'Aanvraag geweigerd.', 'msg.guard_failed': 'Dat lukte niet; probeer het opnieuw.', 'msg.guardians_label': 'Jouw guardians', 'msg.waved_at_you': 'zwaaide naar je', 'msg.help_request': 'vroeg om hulp', 'msg.wave_r1': 'Wat leuk!', 'msg.wave_r2': 'Bel me even', 'msg.wave_back': '👋 Terug', 'msg.wave_sent': 'Zwaai verstuurd.', 'guardian.feed_title': 'Van je wards', 'guardian.feed_sub': 'Meelezen met wat je wards plaatsen. Alleen kijken.', 'guardian.follow_title': 'Volgverzoeken', 'guardian.follow_sub': 'Iemand wil een van je wards volgen. Jij beslist.', 'guardian.wave': '👋 Zwaai', 'guardian.waved': '👋 verstuurd', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Wards beheren en hulpverzoeken opvangen.', 'guardian.acting_as': 'Je handelt als', 'guardian.help_title': 'Hulpverzoeken', 'guardian.help_sub': 'Als een ward de reddingsboei gebruikt, verschijnt het hier.', 'guardian.help_empty': 'Geen hulpverzoeken. Mooi zo.', 'guardian.adopt_title': 'Ward adopteren', 'guardian.adopt_sub': 'Vul de handle van het kind in (@kind@server.eu). Ze krijgen een aanvraag in hun Klonkt die ze accepteren.', 'guardian.adopt_label': 'Handle van de ward', 'guardian.adopt_btn': 'Verstuur aanvraag', 'guardian.pending_title': 'Verzonden aanvragen', 'guardian.pending_sub': 'Wacht tot de ward accepteert.', 'guardian.wards_title': 'Mijn wards', 'guardian.wards_empty': 'Nog geen wards. Adopteer er hierboven een.', 'guardian.push_title': 'Meldingen', 'guardian.push_sub': 'Ontvang een melding bij een hulpverzoek of voogdij-antwoord, ook als de app dicht is.', 'guardian.push_on': 'Zet meldingen aan', 'guardian.push_off': 'Meldingen staan aan; tik om uit te zetten', 'guardian.sent': 'Aanvraag verstuurd. Zie hieronder bij Verzonden aanvragen.', 'guardian.sent_retry': 'Aanvraag opgeslagen; we blijven proberen te bezorgen.', 'guardian.sending': 'Versturen…', 'guardian.not_found': 'Die handle konden we niet vinden.', 'guardian.failed': 'Mislukt', 'guardian.network': 'Netwerkfout.', 'guardian.pending': 'wacht op antwoord', 'guardian.active': 'actief', 'guardian.retract': 'Intrekken', 'guardian.release': 'Loslaten', 'guardian.embeds_on': 'Linkvoorbeelden: aan', 'guardian.embeds_off': 'Linkvoorbeelden: uit', 'guardian.embeds_propose': 'Linkvoorbeelden voorstellen', 'guardian.embeds_waiting': 'wacht op de andere guardians', 'guardian.release_confirm': '{who} loslaten?\n\nJe stopt dan als guardian. Je ziet hun berichten niet meer, je krijgt geen hulpverzoeken meer van ze, en je kunt volgverzoeken niet meer voor ze beoordelen.\n\nTerugkomen kan alleen met een nieuwe aanvraag die zij accepteren.', 'guardian.open': 'open', 'guardian.accept': 'Accepteer', 'guardian.reject': 'Weiger', 'guardian.complete': 'Voltooien', 'guardian.awaiting_others': 'wacht op de andere partijen', 'guardian.coguard': 'mede-voogdij-aanvraag', 'guardian.push_unavailable': 'Push niet beschikbaar', 'push.n_help_t': 'Hulpvraag', 'push.n_help_b': '{who} vraagt om je hulp', 'push.n_guard_offer_t': 'Voogdij-aanvraag', 'push.n_guard_offer_b': '{who} wil je guardian worden', 'push.n_guard_ward_t': 'Ward geaccepteerd', 'push.n_guard_ward_b': '{who} accepteerde je als guardian', 'push.n_guard_cog_t': 'Mede-voogdij gevraagd', 'push.n_guard_cog_b': 'Er is een guardian-aanvraag voor {who}', 'push.n_test_t': 'Klonkt-testnotificatie', 'push.n_test_b': 'Werkt. Zo komen meldingen binnen op dit apparaat.',
+    'push.n_follow_t': 'Nieuwe volger', 'push.n_follow_b': '{who} volgt je nu', 'push.n_reply_t': 'Reactie op "{title}"', 'push.n_mention_t': 'Vermelding', 'push.n_dm_t': 'Privébericht', 'push.n_dm_b': 'Nieuw bericht van {who}', 'push.n_like_t': 'Nieuwe waardering', 'push.n_like_b': '{who} waardeerde "{title}"', 'push.n_boost_t': 'Geboost', 'push.n_boost_b': '{who} boostte "{title}"', 'msg.guard_offer': 'wil je guardian worden. Bespreek dit met je ouders of verzorgers voordat je beslist.', 'msg.guard_accept': 'Accepteer', 'msg.guard_reject': 'Weiger', 'msg.guard_accepted': 'Guardian geaccepteerd. Jullie zijn nu verbonden.', 'msg.guard_rejected': 'Aanvraag geweigerd.', 'msg.guard_failed': 'Dat lukte niet; probeer het opnieuw.', 'msg.guardians_label': 'Jouw guardians', 'msg.waved_at_you': 'zwaaide naar je', 'msg.help_request': 'vroeg om hulp', 'msg.wave_r1': 'Wat leuk!', 'msg.wave_r2': 'Bel me even', 'msg.wave_back': '👋 Terug', 'msg.wave_sent': 'Zwaai verstuurd.', 'guardian.feed_title': 'Van je wards', 'guardian.feed_sub': 'Meelezen met wat je wards plaatsen. Alleen kijken.', 'guardian.follow_title': 'Volgverzoeken', 'guardian.follow_sub': 'Iemand wil een van je wards volgen. Jij beslist.', 'guardian.wave': '👋 Zwaai', 'guardian.waved': '👋 verstuurd', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Wards beheren en hulpverzoeken opvangen.', 'guardian.acting_as': 'Je handelt als', 'guardian.help_title': 'Hulpverzoeken', 'guardian.help_sub': 'Als een ward de reddingsboei gebruikt, verschijnt het hier.', 'guardian.help_empty': 'Geen hulpverzoeken. Mooi zo.', 'guardian.adopt_title': 'Ward adopteren', 'guardian.adopt_sub': 'Vul de handle van het kind in (@kind@server.eu). Ze krijgen een aanvraag in hun Klonkt die ze accepteren.', 'guardian.adopt_label': 'Handle van de ward', 'guardian.adopt_btn': 'Verstuur aanvraag', 'guardian.pending_title': 'Verzonden aanvragen', 'guardian.pending_sub': 'Wacht tot de ward accepteert.', 'guardian.wards_title': 'Mijn wards', 'guardian.settings_title': 'Instellingen', 'guardian.panel_open': 'Bekijken', 'guardian.panel_close': 'Sluiten', 'guardian.panel_help': 'Hulpvragen van dit kind', 'guardian.panel_help_empty': 'Nog geen hulpvragen.', 'guardian.panel_follow': 'Volgverzoeken', 'guardian.panel_follow_empty': 'Geen openstaande volgverzoeken.', 'guardian.panel_posts': 'Recente berichten', 'guardian.panel_posts_empty': 'Nog niets te zien.', 'guardian.panel_actions': 'Acties', 'guardian.badge_help': 'hulpvragen', 'guardian.badge_follow': 'volgverzoeken', 'guardian.badge_follow_one': 'volgverzoek', 'guardian.wards_empty': 'Nog geen wards. Adopteer er hierboven een.', 'guardian.push_title': 'Meldingen', 'guardian.push_sub': 'Ontvang een melding bij een hulpverzoek of voogdij-antwoord, ook als de app dicht is.', 'guardian.push_on': 'Zet meldingen aan', 'guardian.push_off': 'Meldingen staan aan; tik om uit te zetten', 'guardian.sent': 'Aanvraag verstuurd. Zie hieronder bij Verzonden aanvragen.', 'guardian.sent_retry': 'Aanvraag opgeslagen; we blijven proberen te bezorgen.', 'guardian.sending': 'Versturen…', 'guardian.not_found': 'Die handle konden we niet vinden.', 'guardian.failed': 'Mislukt', 'guardian.network': 'Netwerkfout.', 'guardian.pending': 'wacht op antwoord', 'guardian.active': 'actief', 'guardian.retract': 'Intrekken', 'guardian.release': 'Loslaten', 'guardian.embeds_on': 'Linkvoorbeelden: aan', 'guardian.embeds_off': 'Linkvoorbeelden: uit', 'guardian.embeds_propose': 'Linkvoorbeelden voorstellen', 'guardian.embeds_waiting': 'wacht op de andere guardians', 'guardian.release_confirm': '{who} loslaten?\n\nJe stopt dan als guardian. Je ziet hun berichten niet meer, je krijgt geen hulpverzoeken meer van ze, en je kunt volgverzoeken niet meer voor ze beoordelen.\n\nTerugkomen kan alleen met een nieuwe aanvraag die zij accepteren.', 'guardian.open': 'open', 'guardian.accept': 'Accepteer', 'guardian.reject': 'Weiger', 'guardian.complete': 'Voltooien', 'guardian.awaiting_others': 'wacht op de andere partijen', 'guardian.coguard': 'mede-voogdij-aanvraag', 'guardian.push_unavailable': 'Push niet beschikbaar', 'push.n_help_t': 'Hulpvraag', 'push.n_help_b': '{who} vraagt om je hulp', 'push.n_guard_offer_t': 'Voogdij-aanvraag', 'push.n_guard_offer_b': '{who} wil je guardian worden', 'push.n_guard_ward_t': 'Ward geaccepteerd', 'push.n_guard_ward_b': '{who} accepteerde je als guardian', 'push.n_guard_cog_t': 'Mede-voogdij gevraagd', 'push.n_guard_cog_b': 'Er is een guardian-aanvraag voor {who}', 'push.n_test_t': 'Klonkt-testnotificatie', 'push.n_test_b': 'Werkt. Zo komen meldingen binnen op dit apparaat.',
     'apaid.t': 'Betaalde posts', 'apaid.intro': 'Koppel je eigen Patreon-campagne. Supporters ontgrendelen betaalde posts met een passkey, zonder account en zonder cookie. Wij bewaren geen namen of e-mailadressen van supporters, alleen het versleutelde token van jouw campagne.', 'apaid.saved': 'Opgeslagen.', 'apaid.nokey': 'Let op: de encryptiesleutel kon niet worden aangemaakt of gelezen (schrijfrechten op de opslagmap?). Zonder sleutel kunnen secrets niet veilig worden opgeslagen.', 'apaid.status': 'Status:', 'apaid.connected': 'verbonden', 'apaid.campaign': 'campagne', 'apaid.configured': 'ingesteld, nog niet verbonden (vul een token in)', 'apaid.notyet': 'nog niet ingesteld', 'apaid.redirect_h': 'Zet deze redirect-URI in je Patreon-client', 'apaid.redirect_p': 'Bij je Patreon API-client, onder Redirect URIs, moet exact deze regel staan. Klopt hij niet, dan geeft Patreon een foutmelding in plaats van je supporters terug te sturen.', 'apaid.copy': 'Kopieer', 'apaid.copied': 'Gekopieerd', 'apaid.client_id': 'Patreon client id', 'apaid.client_secret': 'Patreon client secret', 'apaid.keep': 'Leeg laten = huidige waarde behouden.', 'apaid.campaign_id': 'Campagne-id', 'apaid.public_page': 'Openbare Patreon-pagina', 'apaid.public_help': 'De link waar bezoekers supporter kunnen worden. Getoond als "Word supporter" wanneer iemand nog niet doneert.', 'apaid.access': 'Creator access token', 'apaid.refresh': 'Creator refresh token', 'apaid.token_help': 'De access + refresh token krijg je op je Patreon API-clientpagina. Wij versleutelen ze en verversen automatisch.', 'apaid.min_eur': 'Standaard-steunbedrag voor een betaalde post (euro)', 'apaid.save': 'Opslaan', 'apaid.disconnect': 'Koppeling verwijderen', 'apaid.disconnect_confirm': 'Patreon-koppeling verwijderen?', 'apaid.unchanged': 'blijft ongewijzigd',
     'pgate.h': 'Voor supporters', 'pgate.sub': 'Deze post is voor supporters van deze site. Word supporter en ontgrendel hem daarna met een passkey. Geen account op deze site, geen cookie.', 'pgate.sub_cents': 'Deze post is voor supporters van deze site (vanaf €{eur} per maand op Patreon). Word supporter en ontgrendel hem daarna met een passkey. Geen account op deze site, geen cookie.', 'pgate.join': 'Word supporter op Patreon', 'pgate.unlock_have': 'Al supporter? Ontgrendelen', 'pgate.unlock': 'Ontgrendelen met Patreon', 'pgate.join_short': 'Word supporter', 'pgate.confirm': 'Bevestig met je passkey…', 'pgate.failed': 'Ontgrendelen mislukt. Probeer opnieuw.', 'pgate.error': 'Er ging iets mis. Probeer opnieuw.',
@@ -1008,5 +1008,5 @@
     'admin.b_paid': 'Paid posts', 'admin.b_push': 'Notifications', 'admin.back': 'Back to Admin',
     'push.t': 'Notifications', 'push.intro': 'Get a notification on this device for new followers, replies and messages, even when the site is closed. Encrypted all the way to your browser; we send as little content as possible.', 'push.unavailable': 'Push is unavailable on this server (the key could not be created or the dependency is missing).', 'push.unsupported': 'This browser does not support push notifications.', 'push.ios_hint': 'On iPhone/iPad this only works when the site is on your home screen: share button, then "Add to Home Screen", and open it from there.', 'push.this_device': 'This device:', 'push.checking': 'checking…', 'push.state_on': 'notifications are on', 'push.state_off': 'notifications are off', 'push.state_denied': 'blocked in the browser settings', 'push.state_unknown': 'status unknown', 'push.state_unsupported': 'not supported', 'push.enable': 'Turn on for this device', 'push.disable': 'Turn off', 'push.test': 'Send a test notification', 'push.what': 'What do you want to be notified about?', 'push.a_follow': 'New follower', 'push.a_reply': 'Reply or mention', 'push.a_like': 'Like (star)', 'push.a_boost': 'Boost', 'push.a_dm': 'Private message', 'push.saved': 'Saved.', 'push.devices': 'Linked devices', 'push.device': 'Device', 'push.since': 'since', 'push.remove': 'Remove', 'push.enable_failed': 'turning on failed',
-    'push.n_follow_t': 'New follower', 'push.n_follow_b': '{who} now follows you', 'push.n_reply_t': 'Reply to "{title}"', 'push.n_mention_t': 'Mention', 'push.n_dm_t': 'Private message', 'push.n_dm_b': 'New message from {who}', 'push.n_like_t': 'New like', 'push.n_like_b': '{who} liked "{title}"', 'push.n_boost_t': 'Boosted', 'push.n_boost_b': '{who} boosted "{title}"', 'msg.guard_offer': 'wants to become your guardian. Talk this over with your parents or carers before you decide.', 'msg.guard_accept': 'Accept', 'msg.guard_reject': 'Reject', 'msg.guard_accepted': 'Guardian accepted. You are now connected.', 'msg.guard_rejected': 'Offer rejected.', 'msg.guard_failed': 'That did not work; try again.', 'msg.guardians_label': 'Your guardians', 'msg.waved_at_you': 'waved at you', 'msg.help_request': 'asked for help', 'msg.wave_r1': 'Lovely!', 'msg.wave_r2': 'Call me', 'msg.wave_back': '👋 Back', 'msg.wave_sent': 'Wave sent.', 'guardian.feed_title': 'Your wards', 'guardian.feed_sub': 'Read along with what your wards post. Watch only.', 'guardian.follow_title': 'Follow requests', 'guardian.follow_sub': 'Someone wants to follow one of your wards. You decide.', 'guardian.wave': '👋 Wave', 'guardian.waved': '👋 sent', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Manage wards and catch calls for help.', 'guardian.acting_as': 'You act as', 'guardian.help_title': 'Help requests', 'guardian.help_sub': 'When a ward uses the help buoy, it shows up here.', 'guardian.help_empty': 'No help requests. Good.', 'guardian.adopt_title': 'Adopt a ward', 'guardian.adopt_sub': 'Enter the child handle (@kid@server.eu). They get an offer in their Klonkt to accept.', 'guardian.adopt_label': 'Ward handle', 'guardian.adopt_btn': 'Send offer', 'guardian.pending_title': 'Sent offers', 'guardian.pending_sub': 'Waiting for the ward to accept.', 'guardian.wards_title': 'My wards', 'guardian.wards_empty': 'No wards yet. Adopt one above.', 'guardian.push_title': 'Notifications', 'guardian.push_sub': 'Get notified on a call for help or a guardianship answer, even with the app closed.', 'guardian.push_on': 'Turn on notifications', 'guardian.push_off': 'Notifications are on; tap to turn off', 'guardian.sent': 'Offer sent. See it below under Sent offers.', 'guardian.sent_retry': 'Offer saved; we keep trying to deliver it.', 'guardian.sending': 'Sending…', 'guardian.not_found': 'We could not find that handle.', 'guardian.failed': 'Failed', 'guardian.network': 'Network error.', 'guardian.pending': 'awaiting answer', 'guardian.active': 'active', 'guardian.retract': 'Retract', 'guardian.release': 'Release', 'guardian.embeds_on': 'Link previews: on', 'guardian.embeds_off': 'Link previews: off', 'guardian.embeds_propose': 'Propose link previews', 'guardian.embeds_waiting': 'waiting for the other guardians', 'guardian.release_confirm': 'Release {who}?\n\nYou stop being their guardian. You will no longer see their posts, no longer receive their calls for help, and no longer decide on follow requests for them.\n\nComing back means a fresh offer that they accept.', 'guardian.open': 'open', 'guardian.accept': 'Accept', 'guardian.reject': 'Reject', 'guardian.complete': 'Complete', 'guardian.awaiting_others': 'awaiting the other parties', 'guardian.coguard': 'co-guardianship offer', 'guardian.push_unavailable': 'Push unavailable', 'push.n_help_t': 'Call for help', 'push.n_help_b': '{who} is asking for your help', 'push.n_guard_offer_t': 'Guardianship offer', 'push.n_guard_offer_b': '{who} wants you as their guardian', 'push.n_guard_ward_t': 'Ward accepted', 'push.n_guard_ward_b': '{who} accepted you as guardian', 'push.n_guard_cog_t': 'Co-guardianship asked', 'push.n_guard_cog_b': 'A guardian offer for {who} needs you', 'push.n_test_t': 'Klonkt test notification', 'push.n_test_b': 'It works. This is how notifications arrive on this device.',
+    'push.n_follow_t': 'New follower', 'push.n_follow_b': '{who} now follows you', 'push.n_reply_t': 'Reply to "{title}"', 'push.n_mention_t': 'Mention', 'push.n_dm_t': 'Private message', 'push.n_dm_b': 'New message from {who}', 'push.n_like_t': 'New like', 'push.n_like_b': '{who} liked "{title}"', 'push.n_boost_t': 'Boosted', 'push.n_boost_b': '{who} boosted "{title}"', 'msg.guard_offer': 'wants to become your guardian. Talk this over with your parents or carers before you decide.', 'msg.guard_accept': 'Accept', 'msg.guard_reject': 'Reject', 'msg.guard_accepted': 'Guardian accepted. You are now connected.', 'msg.guard_rejected': 'Offer rejected.', 'msg.guard_failed': 'That did not work; try again.', 'msg.guardians_label': 'Your guardians', 'msg.waved_at_you': 'waved at you', 'msg.help_request': 'asked for help', 'msg.wave_r1': 'Lovely!', 'msg.wave_r2': 'Call me', 'msg.wave_back': '👋 Back', 'msg.wave_sent': 'Wave sent.', 'guardian.feed_title': 'Your wards', 'guardian.feed_sub': 'Read along with what your wards post. Watch only.', 'guardian.follow_title': 'Follow requests', 'guardian.follow_sub': 'Someone wants to follow one of your wards. You decide.', 'guardian.wave': '👋 Wave', 'guardian.waved': '👋 sent', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Manage wards and catch calls for help.', 'guardian.acting_as': 'You act as', 'guardian.help_title': 'Help requests', 'guardian.help_sub': 'When a ward uses the help buoy, it shows up here.', 'guardian.help_empty': 'No help requests. Good.', 'guardian.adopt_title': 'Adopt a ward', 'guardian.adopt_sub': 'Enter the child handle (@kid@server.eu). They get an offer in their Klonkt to accept.', 'guardian.adopt_label': 'Ward handle', 'guardian.adopt_btn': 'Send offer', 'guardian.pending_title': 'Sent offers', 'guardian.pending_sub': 'Waiting for the ward to accept.', 'guardian.wards_title': 'My wards', 'guardian.settings_title': 'Settings', 'guardian.panel_open': 'Open', 'guardian.panel_close': 'Close', 'guardian.panel_help': 'Calls for help from this child', 'guardian.panel_help_empty': 'No calls for help yet.', 'guardian.panel_follow': 'Follow requests', 'guardian.panel_follow_empty': 'No follow requests waiting.', 'guardian.panel_posts': 'Recent posts', 'guardian.panel_posts_empty': 'Nothing here yet.', 'guardian.panel_actions': 'Actions', 'guardian.badge_help': 'calls for help', 'guardian.badge_follow': 'follow requests', 'guardian.badge_follow_one': 'follow request', 'guardian.wards_empty': 'No wards yet. Adopt one above.', 'guardian.push_title': 'Notifications', 'guardian.push_sub': 'Get notified on a call for help or a guardianship answer, even with the app closed.', 'guardian.push_on': 'Turn on notifications', 'guardian.push_off': 'Notifications are on; tap to turn off', 'guardian.sent': 'Offer sent. See it below under Sent offers.', 'guardian.sent_retry': 'Offer saved; we keep trying to deliver it.', 'guardian.sending': 'Sending…', 'guardian.not_found': 'We could not find that handle.', 'guardian.failed': 'Failed', 'guardian.network': 'Network error.', 'guardian.pending': 'awaiting answer', 'guardian.active': 'active', 'guardian.retract': 'Retract', 'guardian.release': 'Release', 'guardian.embeds_on': 'Link previews: on', 'guardian.embeds_off': 'Link previews: off', 'guardian.embeds_propose': 'Propose link previews', 'guardian.embeds_waiting': 'waiting for the other guardians', 'guardian.release_confirm': 'Release {who}?\n\nYou stop being their guardian. You will no longer see their posts, no longer receive their calls for help, and no longer decide on follow requests for them.\n\nComing back means a fresh offer that they accept.', 'guardian.open': 'open', 'guardian.accept': 'Accept', 'guardian.reject': 'Reject', 'guardian.complete': 'Complete', 'guardian.awaiting_others': 'awaiting the other parties', 'guardian.coguard': 'co-guardianship offer', 'guardian.push_unavailable': 'Push unavailable', 'push.n_help_t': 'Call for help', 'push.n_help_b': '{who} is asking for your help', 'push.n_guard_offer_t': 'Guardianship offer', 'push.n_guard_offer_b': '{who} wants you as their guardian', 'push.n_guard_ward_t': 'Ward accepted', 'push.n_guard_ward_b': '{who} accepted you as guardian', 'push.n_guard_cog_t': 'Co-guardianship asked', 'push.n_guard_cog_b': 'A guardian offer for {who} needs you', 'push.n_test_t': 'Klonkt test notification', 'push.n_test_b': 'It works. This is how notifications arrive on this device.',
     'apaid.t': 'Paid posts', 'apaid.intro': 'Connect your own Patreon campaign. Supporters unlock paid posts with a passkey, no account and no cookie. We store no supporter names or email addresses, only the encrypted token of your campaign.', 'apaid.saved': 'Saved.', 'apaid.nokey': 'Note: the encryption key could not be created or read (write permissions on the storage directory?). Without a key, secrets cannot be stored safely.', 'apaid.status': 'Status:', 'apaid.connected': 'connected', 'apaid.campaign': 'campaign', 'apaid.configured': 'configured, not connected yet (enter a token)', 'apaid.notyet': 'not configured yet', 'apaid.redirect_h': 'Put this redirect URI in your Patreon client', 'apaid.redirect_p': 'In your Patreon API client, under Redirect URIs, exactly this line must be present. If it does not match, Patreon shows an error instead of sending your supporters back.', 'apaid.copy': 'Copy', 'apaid.copied': 'Copied', 'apaid.client_id': 'Patreon client id', 'apaid.client_secret': 'Patreon client secret', 'apaid.keep': 'Leave empty = keep the current value.', 'apaid.campaign_id': 'Campaign id', 'apaid.public_page': 'Public Patreon page', 'apaid.public_help': 'The link where visitors can become a supporter. Shown as "Become a supporter" when someone does not pledge yet.', 'apaid.access': 'Creator access token', 'apaid.refresh': 'Creator refresh token', 'apaid.token_help': 'You get the access + refresh token on your Patreon API client page. We encrypt them and refresh automatically.', 'apaid.min_eur': 'Default support amount for a paid post (euro)', 'apaid.save': 'Save', 'apaid.disconnect': 'Remove connection', 'apaid.disconnect_confirm': 'Remove the Patreon connection?', 'apaid.unchanged': 'stays unchanged',
     'pgate.h': 'For supporters', 'pgate.sub': 'This post is for supporters of this site. Become a supporter and then unlock it with a passkey. No account on this site, no cookie.', 'pgate.sub_cents': 'This post is for supporters of this site (from €{eur} per month on Patreon). Become a supporter and then unlock it with a passkey. No account on this site, no cookie.', 'pgate.join': 'Become a supporter on Patreon', 'pgate.unlock_have': 'Already a supporter? Unlock', 'pgate.unlock': 'Unlock with Patreon', 'pgate.join_short': 'Become a supporter', 'pgate.confirm': 'Confirm with your passkey…', 'pgate.failed': 'Unlocking failed. Try again.', 'pgate.error': 'Something went wrong. Try again.',
@@ -1943,5 +1943,5 @@
     'admin.b_paid': 'Bezahlte Beiträge', 'admin.b_push': 'Benachrichtigungen', 'admin.back': 'Zurück zur Verwaltung',
     'push.t': 'Benachrichtigungen', 'push.intro': 'Erhalte auf diesem Gerät eine Meldung bei neuen Followern, Antworten und Nachrichten, auch wenn die Seite geschlossen ist. Verschlüsselt bis in deinen Browser; wir senden so wenig Inhalt wie möglich mit.', 'push.unavailable': 'Push ist auf diesem Server nicht verfügbar (Schlüssel konnte nicht erstellt werden oder die Abhängigkeit fehlt).', 'push.unsupported': 'Dieser Browser unterstützt keine Push-Benachrichtigungen.', 'push.ios_hint': 'Auf iPhone/iPad funktioniert das nur, wenn die Seite auf deinem Home-Bildschirm liegt: Teilen-Knopf, dann "Zum Home-Bildschirm", und öffne sie danach von dort.', 'push.this_device': 'Dieses Gerät:', 'push.checking': 'prüfen…', 'push.state_on': 'Benachrichtigungen sind an', 'push.state_off': 'Benachrichtigungen sind aus', 'push.state_denied': 'in den Browser-Einstellungen blockiert', 'push.state_unknown': 'Status unbekannt', 'push.state_unsupported': 'nicht unterstützt', 'push.enable': 'Auf diesem Gerät einschalten', 'push.disable': 'Ausschalten', 'push.test': 'Testmeldung senden', 'push.what': 'Wofür möchtest du eine Meldung?', 'push.a_follow': 'Neuer Follower', 'push.a_reply': 'Antwort oder Erwähnung', 'push.a_like': 'Like (Stern)', 'push.a_boost': 'Boost', 'push.a_dm': 'Private Nachricht', 'push.saved': 'Gespeichert.', 'push.devices': 'Verbundene Geräte', 'push.device': 'Gerät', 'push.since': 'seit', 'push.remove': 'Entfernen', 'push.enable_failed': 'Einschalten fehlgeschlagen',
-    'push.n_follow_t': 'Neuer Follower', 'push.n_follow_b': '{who} folgt dir jetzt', 'push.n_reply_t': 'Antwort auf "{title}"', 'push.n_mention_t': 'Erwähnung', 'push.n_dm_t': 'Private Nachricht', 'push.n_dm_b': 'Neue Nachricht von {who}', 'push.n_like_t': 'Neues Like', 'push.n_like_b': '{who} gefällt "{title}"', 'push.n_boost_t': 'Geboostet', 'push.n_boost_b': '{who} hat "{title}" geboostet', 'msg.guard_offer': 'möchte dein Guardian werden. Besprich das mit deinen Eltern oder Betreuern, bevor du entscheidest.', 'msg.guard_accept': 'Annehmen', 'msg.guard_reject': 'Ablehnen', 'msg.guard_accepted': 'Guardian angenommen. Ihr seid jetzt verbunden.', 'msg.guard_rejected': 'Angebot abgelehnt.', 'msg.guard_failed': 'Das hat nicht geklappt; versuch es erneut.', 'msg.guardians_label': 'Deine Guardians', 'msg.waved_at_you': 'hat dir zugewinkt', 'msg.help_request': 'hat um Hilfe gebeten', 'msg.wave_r1': 'Wie schön!', 'msg.wave_r2': 'Ruf mich an', 'msg.wave_back': '👋 Zurück', 'msg.wave_sent': 'Winken gesendet.', 'guardian.feed_title': 'Deine Wards', 'guardian.feed_sub': 'Lies mit, was deine Wards posten. Nur schauen.', 'guardian.follow_title': 'Follow-Anfragen', 'guardian.follow_sub': 'Jemand möchte einem deiner Wards folgen. Du entscheidest.', 'guardian.wave': '👋 Winken', 'guardian.waved': '👋 gesendet', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Wards verwalten und Hilferufe auffangen.', 'guardian.acting_as': 'Du handelst als', 'guardian.help_title': 'Hilferufe', 'guardian.help_sub': 'Wenn ein Ward die Rettungsboje nutzt, erscheint es hier.', 'guardian.help_empty': 'Keine Hilferufe. Gut so.', 'guardian.adopt_title': 'Ward adoptieren', 'guardian.adopt_sub': 'Gib das Handle des Kindes ein (@kind@server.eu). Es bekommt ein Angebot in seinem Klonkt zum Annehmen.', 'guardian.adopt_label': 'Ward-Handle', 'guardian.adopt_btn': 'Angebot senden', 'guardian.pending_title': 'Gesendete Angebote', 'guardian.pending_sub': 'Warten, bis der Ward annimmt.', 'guardian.wards_title': 'Meine Wards', 'guardian.wards_empty': 'Noch keine Wards. Adoptiere oben eins.', 'guardian.push_title': 'Meldungen', 'guardian.push_sub': 'Erhalte eine Meldung bei einem Hilferuf oder einer Vormundschafts-Antwort, auch bei geschlossener App.', 'guardian.push_on': 'Meldungen einschalten', 'guardian.push_off': 'Meldungen sind an; tippen zum Ausschalten', 'guardian.sent': 'Angebot gesendet. Siehe unten bei Gesendete Angebote.', 'guardian.sent_retry': 'Angebot gespeichert; wir versuchen weiter zuzustellen.', 'guardian.sending': 'Senden…', 'guardian.not_found': 'Dieses Handle konnten wir nicht finden.', 'guardian.failed': 'Fehlgeschlagen', 'guardian.network': 'Netzwerkfehler.', 'guardian.pending': 'wartet auf Antwort', 'guardian.active': 'aktiv', 'guardian.retract': 'Zurückziehen', 'guardian.release': 'Loslassen', 'guardian.embeds_on': 'Linkvorschauen: an', 'guardian.embeds_off': 'Linkvorschauen: aus', 'guardian.embeds_propose': 'Linkvorschauen vorschlagen', 'guardian.embeds_waiting': 'wartet auf die anderen Guardians', 'guardian.release_confirm': '{who} loslassen?\n\nDu bist dann nicht mehr Guardian. Du siehst ihre Beitraege nicht mehr, erhaeltst keine Hilferufe mehr von ihnen und entscheidest nicht mehr ueber Follow-Anfragen fuer sie.\n\nZurueck geht nur mit einem neuen Angebot, das sie annehmen.', 'guardian.open': 'öffnen', 'guardian.accept': 'Annehmen', 'guardian.reject': 'Ablehnen', 'guardian.complete': 'Abschließen', 'guardian.awaiting_others': 'wartet auf die anderen Parteien', 'guardian.coguard': 'Mit-Vormundschaftsangebot', 'guardian.push_unavailable': 'Push nicht verfügbar', 'push.n_help_t': 'Hilferuf', 'push.n_help_b': '{who} bittet um deine Hilfe', 'push.n_guard_offer_t': 'Vormundschaftsangebot', 'push.n_guard_offer_b': '{who} möchte dich als Guardian', 'push.n_guard_ward_t': 'Ward akzeptiert', 'push.n_guard_ward_b': '{who} hat dich als Guardian akzeptiert', 'push.n_guard_cog_t': 'Mit-Vormundschaft gefragt', 'push.n_guard_cog_b': 'Ein Guardian-Angebot für {who} braucht dich', 'push.n_test_t': 'Klonkt-Testmeldung', 'push.n_test_b': 'Funktioniert. So kommen Meldungen auf diesem Gerät an.',
+    'push.n_follow_t': 'Neuer Follower', 'push.n_follow_b': '{who} folgt dir jetzt', 'push.n_reply_t': 'Antwort auf "{title}"', 'push.n_mention_t': 'Erwähnung', 'push.n_dm_t': 'Private Nachricht', 'push.n_dm_b': 'Neue Nachricht von {who}', 'push.n_like_t': 'Neues Like', 'push.n_like_b': '{who} gefällt "{title}"', 'push.n_boost_t': 'Geboostet', 'push.n_boost_b': '{who} hat "{title}" geboostet', 'msg.guard_offer': 'möchte dein Guardian werden. Besprich das mit deinen Eltern oder Betreuern, bevor du entscheidest.', 'msg.guard_accept': 'Annehmen', 'msg.guard_reject': 'Ablehnen', 'msg.guard_accepted': 'Guardian angenommen. Ihr seid jetzt verbunden.', 'msg.guard_rejected': 'Angebot abgelehnt.', 'msg.guard_failed': 'Das hat nicht geklappt; versuch es erneut.', 'msg.guardians_label': 'Deine Guardians', 'msg.waved_at_you': 'hat dir zugewinkt', 'msg.help_request': 'hat um Hilfe gebeten', 'msg.wave_r1': 'Wie schön!', 'msg.wave_r2': 'Ruf mich an', 'msg.wave_back': '👋 Zurück', 'msg.wave_sent': 'Winken gesendet.', 'guardian.feed_title': 'Deine Wards', 'guardian.feed_sub': 'Lies mit, was deine Wards posten. Nur schauen.', 'guardian.follow_title': 'Follow-Anfragen', 'guardian.follow_sub': 'Jemand möchte einem deiner Wards folgen. Du entscheidest.', 'guardian.wave': '👋 Winken', 'guardian.waved': '👋 gesendet', 'guardian.app_name': 'Klonkt Guardian', 'guardian.tagline': 'Wards verwalten und Hilferufe auffangen.', 'guardian.acting_as': 'Du handelst als', 'guardian.help_title': 'Hilferufe', 'guardian.help_sub': 'Wenn ein Ward die Rettungsboje nutzt, erscheint es hier.', 'guardian.help_empty': 'Keine Hilferufe. Gut so.', 'guardian.adopt_title': 'Ward adoptieren', 'guardian.adopt_sub': 'Gib das Handle des Kindes ein (@kind@server.eu). Es bekommt ein Angebot in seinem Klonkt zum Annehmen.', 'guardian.adopt_label': 'Ward-Handle', 'guardian.adopt_btn': 'Angebot senden', 'guardian.pending_title': 'Gesendete Angebote', 'guardian.pending_sub': 'Warten, bis der Ward annimmt.', 'guardian.wards_title': 'Meine Wards', 'guardian.settings_title': 'Einstellungen', 'guardian.panel_open': 'Ansehen', 'guardian.panel_close': 'Schliessen', 'guardian.panel_help': 'Hilferufe dieses Kindes', 'guardian.panel_help_empty': 'Noch keine Hilferufe.', 'guardian.panel_follow': 'Folgeanfragen', 'guardian.panel_follow_empty': 'Keine offenen Folgeanfragen.', 'guardian.panel_posts': 'Neueste Beitraege', 'guardian.panel_posts_empty': 'Noch nichts zu sehen.', 'guardian.panel_actions': 'Aktionen', 'guardian.badge_help': 'Hilferufe', 'guardian.badge_follow': 'Folgeanfragen', 'guardian.badge_follow_one': 'Folgeanfrage', 'guardian.wards_empty': 'Noch keine Wards. Adoptiere oben eins.', 'guardian.push_title': 'Meldungen', 'guardian.push_sub': 'Erhalte eine Meldung bei einem Hilferuf oder einer Vormundschafts-Antwort, auch bei geschlossener App.', 'guardian.push_on': 'Meldungen einschalten', 'guardian.push_off': 'Meldungen sind an; tippen zum Ausschalten', 'guardian.sent': 'Angebot gesendet. Siehe unten bei Gesendete Angebote.', 'guardian.sent_retry': 'Angebot gespeichert; wir versuchen weiter zuzustellen.', 'guardian.sending': 'Senden…', 'guardian.not_found': 'Dieses Handle konnten wir nicht finden.', 'guardian.failed': 'Fehlgeschlagen', 'guardian.network': 'Netzwerkfehler.', 'guardian.pending': 'wartet auf Antwort', 'guardian.active': 'aktiv', 'guardian.retract': 'Zurückziehen', 'guardian.release': 'Loslassen', 'guardian.embeds_on': 'Linkvorschauen: an', 'guardian.embeds_off': 'Linkvorschauen: aus', 'guardian.embeds_propose': 'Linkvorschauen vorschlagen', 'guardian.embeds_waiting': 'wartet auf die anderen Guardians', 'guardian.release_confirm': '{who} loslassen?\n\nDu bist dann nicht mehr Guardian. Du siehst ihre Beitraege nicht mehr, erhaeltst keine Hilferufe mehr von ihnen und entscheidest nicht mehr ueber Follow-Anfragen fuer sie.\n\nZurueck geht nur mit einem neuen Angebot, das sie annehmen.', 'guardian.open': 'öffnen', 'guardian.accept': 'Annehmen', 'guardian.reject': 'Ablehnen', 'guardian.complete': 'Abschließen', 'guardian.awaiting_others': 'wartet auf die anderen Parteien', 'guardian.coguard': 'Mit-Vormundschaftsangebot', 'guardian.push_unavailable': 'Push nicht verfügbar', 'push.n_help_t': 'Hilferuf', 'push.n_help_b': '{who} bittet um deine Hilfe', 'push.n_guard_offer_t': 'Vormundschaftsangebot', 'push.n_guard_offer_b': '{who} möchte dich als Guardian', 'push.n_guard_ward_t': 'Ward akzeptiert', 'push.n_guard_ward_b': '{who} hat dich als Guardian akzeptiert', 'push.n_guard_cog_t': 'Mit-Vormundschaft gefragt', 'push.n_guard_cog_b': 'Ein Guardian-Angebot für {who} braucht dich', 'push.n_test_t': 'Klonkt-Testmeldung', 'push.n_test_b': 'Funktioniert. So kommen Meldungen auf diesem Gerät an.',
     'apaid.t': 'Bezahlte Beiträge', 'apaid.intro': 'Verbinde deine eigene Patreon-Kampagne. Unterstützer entsperren bezahlte Beiträge mit einem Passkey, ohne Konto und ohne Cookie. Wir speichern keine Namen oder E-Mail-Adressen von Unterstützern, nur das verschlüsselte Token deiner Kampagne.', 'apaid.saved': 'Gespeichert.', 'apaid.nokey': 'Achtung: der Verschlüsselungsschlüssel konnte nicht erstellt oder gelesen werden (Schreibrechte auf dem Speicherordner?). Ohne Schlüssel können Secrets nicht sicher gespeichert werden.', 'apaid.status': 'Status:', 'apaid.connected': 'verbunden', 'apaid.campaign': 'Kampagne', 'apaid.configured': 'eingerichtet, noch nicht verbunden (Token eintragen)', 'apaid.notyet': 'noch nicht eingerichtet', 'apaid.redirect_h': 'Trage diese Redirect-URI in deinen Patreon-Client ein', 'apaid.redirect_p': 'In deinem Patreon-API-Client muss unter Redirect URIs genau diese Zeile stehen. Stimmt sie nicht, zeigt Patreon eine Fehlermeldung statt deine Unterstützer zurückzuschicken.', 'apaid.copy': 'Kopieren', 'apaid.copied': 'Kopiert', 'apaid.client_id': 'Patreon Client-ID', 'apaid.client_secret': 'Patreon Client-Secret', 'apaid.keep': 'Leer lassen = aktuellen Wert behalten.', 'apaid.campaign_id': 'Kampagnen-ID', 'apaid.public_page': 'Öffentliche Patreon-Seite', 'apaid.public_help': 'Der Link, unter dem Besucher Unterstützer werden können. Wird als "Unterstützer werden" gezeigt, wenn jemand noch nicht spendet.', 'apaid.access': 'Creator Access-Token', 'apaid.refresh': 'Creator Refresh-Token', 'apaid.token_help': 'Access- und Refresh-Token bekommst du auf deiner Patreon-API-Client-Seite. Wir verschlüsseln sie und erneuern automatisch.', 'apaid.min_eur': 'Standard-Unterstützungsbetrag für einen bezahlten Beitrag (Euro)', 'apaid.save': 'Speichern', 'apaid.disconnect': 'Verbindung entfernen', 'apaid.disconnect_confirm': 'Patreon-Verbindung entfernen?', 'apaid.unchanged': 'bleibt unverändert',
     'pgate.h': 'Für Unterstützer', 'pgate.sub': 'Dieser Beitrag ist für Unterstützer dieser Seite. Werde Unterstützer und entsperre ihn danach mit einem Passkey. Kein Konto auf dieser Seite, kein Cookie.', 'pgate.sub_cents': 'Dieser Beitrag ist für Unterstützer dieser Seite (ab €{eur} pro Monat auf Patreon). Werde Unterstützer und entsperre ihn danach mit einem Passkey. Kein Konto auf dieser Seite, kein Cookie.', 'pgate.join': 'Unterstützer werden auf Patreon', 'pgate.unlock_have': 'Schon Unterstützer? Entsperren', 'pgate.unlock': 'Mit Patreon entsperren', 'pgate.join_short': 'Unterstützer werden', 'pgate.confirm': 'Bestätige mit deinem Passkey…', 'pgate.failed': 'Entsperren fehlgeschlagen. Versuch es erneut.', 'pgate.error': 'Etwas ist schiefgegangen. Versuch es erneut.',
Index: src/views/pages/guardian.ejs
===================================================================
--- src/views/pages/guardian.ejs	(revision a7bcf669de39441a2bbc7c41545fc9e17db4b233)
+++ src/views/pages/guardian.ejs	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
@@ -32,19 +32,7 @@
 
   <main>
-    <!-- 0. Wards' corner: meelezen met je wards (read-only). -->
-    <section id="feed-section" hidden>
-      <div class="g-sec-head"><h2><%= t('guardian.feed_title') %></h2></div>
-      <p class="g-sec-sub"><%= t('guardian.feed_sub') %></p>
-      <div id="feed-list" class="g-list"></div>
-    </section>
-
-    <!-- 0b. Follow-verzoeken op je wards (FEP-633c §5.3). -->
-    <section id="follow-section" hidden>
-      <div class="g-sec-head"><h2><%= t('guardian.follow_title') %></h2></div>
-      <p class="g-sec-sub"><%= t('guardian.follow_sub') %></p>
-      <div id="follow-list" class="g-list"></div>
-    </section>
-
-    <!-- 1. Berichtencentrum: hulpverzoeken. Dat is waar dit voor bestaat. -->
+    <!-- 1. Hulpverzoeken, uitgelicht over alle wards heen. Dat is waar dit voor
+            bestaat, en het moet opvallen zonder dat je eerst een kind opent.
+            De volledige geschiedenis per kind staat in dat kind zijn paneel. -->
     <section id="help-section">
       <div class="g-sec-head">
@@ -76,5 +64,7 @@
     </section>
 
-    <!-- 4. Mijn wards (geaccepteerd). -->
+    <!-- 4. Mijn wards. Elk kind is een regel die opent naar een paneel met
+            alles over dat kind: instellingen, volgverzoeken, hulpvragen en
+            recente berichten. Een guardian denkt per kind, niet per functie. -->
     <section id="wards-section">
       <div class="g-sec-head"><h2><%= t('guardian.wards_title') %></h2></div>
Index: test/guardian-panel.test.js
===================================================================
--- test/guardian-panel.test.js	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
+++ test/guardian-panel.test.js	(revision 70677e962e64de43ac4076f9b87a4ccb49017561)
@@ -0,0 +1,53 @@
+// The Guardian PWA is built in the browser from a state blob, so the usual
+// tests cannot reach it. These two failure modes bit during the rebuild and are
+// cheap to guard: a renderer writing into a section that no longer exists, and
+// a grouping key quietly dropped from a route so every panel comes up empty.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'fs';
+
+const read = (p) => fs.readFileSync(new URL(p, import.meta.url), 'utf8');
+const client = read('../src/assets/js/guardian.js');
+const page = read('../src/views/pages/guardian.ejs');
+const route = read('../src/routes/guardian.js');
+
+test('the client only touches element ids the page actually has', () => {
+  // g-fatal is the exception: the crash banner is created by the client itself
+  // (getElementById || createElement), precisely because the page may be too
+  // broken to have it.
+  const ids = new Set([...page.matchAll(/id="([^"]+)"/g)].map((m) => m[1])).add('g-fatal');
+  const used = [...client.matchAll(/getElementById\('([^']+)'\)/g)].map((m) => m[1]);
+  const missing = [...new Set(used)].filter((id) => !ids.has(id));
+  assert.deepEqual(missing, [], `guardian.js writes into ${missing.join(', ')}, which the page does not have`);
+});
+
+test('the sections that moved into the panels are gone from the page', () => {
+  for (const id of ['feed-section', 'follow-section', 'feed-list', 'follow-list']) {
+    assert.ok(!page.includes(`id="${id}"`), `${id} moved into the per-ward panel and must not be a section of its own`);
+  }
+});
+
+test('every panel section has something to group by', () => {
+  // A child's panel is filled by matching these against the ward's actor URI.
+  // Lose one and that section silently shows "nothing yet" for every child.
+  assert.match(route, /authorUri: p\.author_uri/, "the wards' posts");
+  assert.match(route, /wardUri: w\.uri/, 'follow requests on a local ward');
+  assert.match(route, /wardUri: rev\.ward_uri/, 'follow requests forwarded from a remote ward');
+  for (const key of ['authorUri', 'wardUri', 'actor_uri']) {
+    assert.ok(client.includes(key), `the panel groups on ${key}`);
+  }
+});
+
+test('the labels the panel renders are all served to it', () => {
+  // uiStrings() picks the keys by hand, so a label used in the client but not
+  // listed there renders as an empty string with no error anywhere. Two shapes
+  // count as served: an entry in the keys array, and a direct s.foo = ...
+  // assignment underneath it (wave/waved arrived that way).
+  const served = new Set([
+    ...[...route.matchAll(/'([a-z_]+)'/g)].map((m) => m[1]),
+    ...[...route.matchAll(/\bs\.([a-z_]+)\s*=/g)].map((m) => m[1]),
+  ]);
+  const used = [...new Set([...client.matchAll(/T\.([a-z_]+)/g)].map((m) => m[1]))];
+  const missing = used.filter((k) => !served.has(k));
+  assert.deepEqual(missing, [], `uiStrings() does not serve: ${missing.join(', ')}`);
+});
