Index: src/routes/ward.js
===================================================================
--- src/routes/ward.js	(revision a5a62490bec7673afe4e4c5c788f48466eb96b8b)
+++ src/routes/ward.js	(revision 084b046d930ee79f779d73d05b08e90d00fd684f)
@@ -98,4 +98,7 @@
       helpSent: i18nT(L, 'ward.help_sent'),
       cancel: i18nT(L, 'ward.cancel'),
+      toSky: i18nT(L, 'ward.to_sky'),
+      toCards: i18nT(L, 'ward.to_cards'),
+      skyEmpty: i18nT(L, 'ward.sky_empty'),
     },
   };
@@ -217,4 +220,38 @@
 });
 
+/**
+ * De gesprekken voor de hemel: iedereen met wie dit kind persoonlijk berichten
+ * heeft gewisseld, met hoe lang geleden dat voor het laatst was.
+ *
+ * De hemel toont GESPREKKEN, geen posts. Directe berichten leven in
+ * ap_mentions en niet in de tijdlijn, precies omdat een bericht aan genoemde
+ * mensen geen post is. Per tegenpartij houden we het jongste bericht: dat is
+ * wat de afstand tot het midden bepaalt.
+ */
+router.get('/api/conversations', requireAuth, (req, res) => {
+  const site = siteForUser(req);
+  if (!site) return res.status(404).json({ error: 'no_site' });
+  const now = Date.now();
+  const byUri = new Map();
+  for (const m of AP.getDirectMessages(site.slug, 200)) {
+    if (!m.actor_uri) continue;
+    const when = Date.parse(m.published || m.created_at || '') || now;
+    const seen = byUri.get(m.actor_uri);
+    if (!seen || when > seen.when) {
+      byUri.set(m.actor_uri, {
+        uri: m.actor_uri,
+        name: m.actor_name || m.actor_handle || m.actor_uri,
+        handle: m.actor_handle || m.actor_uri,
+        icon: m.actor_icon || null,
+        when,
+      });
+    }
+  }
+  const items = [...byUri.values()]
+    .sort((a, b) => b.when - a.when)
+    .map((c) => ({ ...c, ageMs: Math.max(0, now - c.when) }));
+  res.json({ items });
+});
+
 function pwaAsset(rel, type) {
   return (req, res) => {
@@ -225,4 +262,6 @@
 }
 router.get('/app.js', pwaAsset('js/ward.js', 'application/javascript'));
+// De hemel is een eigen module, zodat model en renderer los staan van de rest.
+router.get('/sky.js', pwaAsset('js/ward-sky.js', 'application/javascript'));
 router.get('/app.css', pwaAsset('css/ward.css', 'text/css'));
 
