Index: src/routes/ward.js
===================================================================
--- src/routes/ward.js	(revision 084b046d930ee79f779d73d05b08e90d00fd684f)
+++ src/routes/ward.js	(revision 4bb93aa4a060a71f9d58edb59da3c88711d16844)
@@ -101,4 +101,13 @@
       toCards: i18nT(L, 'ward.to_cards'),
       skyEmpty: i18nT(L, 'ward.sky_empty'),
+      handHint: i18nT(L, 'ward.hand_hint'),
+      close: i18nT(L, 'ward.close'),
+      writeHere: i18nT(L, 'ward.write_here'),
+      send: i18nT(L, 'ward.send'),
+      wave: i18nT(L, 'ward.wave'),
+      waveCall: i18nT(L, 'ward.wave_call'),
+      waveThanks: i18nT(L, 'ward.wave_thanks'),
+      notNow: i18nT(L, 'ward.not_now'),
+      waveTo: i18nT(L, 'ward.wave_to'),
     },
   };
@@ -254,4 +263,82 @@
 });
 
+/**
+ * Eén gesprek, beide kanten, nieuwste eerst, met een cursor voor "meer".
+ *
+ * Dezelfde bron als de C2S-lezing die Shaer gebruikt (conversationHistory):
+ * inkomend zijn de ap_mentions, uitgaand de eigen directe notes. De body komt
+ * af van de server, net als in de feed; de client hoeft alleen bubbels te
+ * tekenen en te weten welke kant elk bericht op wijst.
+ */
+router.get('/api/thread', requireAuth, (req, res) => {
+  const site = siteForUser(req);
+  if (!site) return res.status(404).json({ error: 'no_site' });
+  const other = String(req.query.with || '');
+  if (!/^https?:\/\//i.test(other)) return res.status(400).json({ error: 'with_must_be_actor_uri' });
+  const L = resolveLang(req);
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+
+  const page = AP.conversationHistory(site.slug, other, {
+    before: req.query.before ? String(req.query.before) : null,
+    limit: req.query.limit,
+  });
+  const inRefs = page.rows.filter((r) => r.direction === 'in').map((r) => r.ref);
+  const inMap = new Map(AP.messageRowsByUri(site.slug, inRefs).map((m) => [m.object_uri, m]));
+
+  const items = page.rows.map((r) => {
+    if (r.direction === 'in') {
+      const m = inMap.get(r.ref);
+      if (!m) return null;
+      return {
+        dir: 'in', id: m.object_uri,
+        whenText: formatDateTime(m.published || m.created_at),
+        bodyHtml: renderNoteBody(m, L),
+        wave: !!m.wave, help: !!m.help_request,
+      };
+    }
+    const n = AP.getOutboxNote(base, r.ref);
+    if (!n) return null;
+    return {
+      dir: 'out', id: n.id,
+      whenText: formatDateTime(n.published),
+      // Eigen notes zijn door onszelf gebouwd en gesaniteerd bij het maken.
+      bodyHtml: n.content || '',
+      wave: n['shaer:wave'] === true,
+    };
+  }).filter(Boolean);
+
+  // Openen is lezen. De markering is optellend (maximum), dus een oud toestel
+  // kan nooit iets weer ongelezen maken.
+  const nieuwsteIn = items.find((i) => i.dir === 'in');
+  if (nieuwsteIn) { try { AP.markRead(site.slug, nieuwsteIn.id); } catch { /* lezen faalt stil */ } }
+
+  res.json({ items, more: page.more, oldest: page.oldest });
+});
+
+/**
+ * Een bericht in een gesprek, of een zwaai (§5, shaer:wave).
+ *
+ * Dezelfde draad als de boei (deliverDirectNote): een direct bericht aan één
+ * persoon, nooit een post, nooit boostbaar. De wave-vlag reist mee zodat de
+ * andere kant hem als zwaai toont en niet als kale tekst.
+ */
+router.post('/api/message', requireAuth, express.json({ limit: '8kb' }), async (req, res) => {
+  const site = siteForUser(req);
+  if (!site) return res.status(404).json({ error: 'no_site' });
+  const to = String(req.body?.to || '').trim();
+  const text = String(req.body?.text || '').trim().slice(0, 2000);
+  if (!/^https?:\/\//i.test(to)) return res.status(400).json({ error: 'to_must_be_actor_uri' });
+  if (!text) return res.status(400).json({ error: 'empty' });
+
+  const sent = await Guardianship.deliverDirectNote(site, {
+    recipients: [to],
+    text,
+    language: site.language || 'nl',
+    wave: req.body?.wave === true,
+  }).catch(() => null);
+  if (!sent) return res.status(502).json({ error: 'not_delivered' });
+  res.json({ ok: true });
+});
+
 function pwaAsset(rel, type) {
   return (req, res) => {
