Index: src/assets/js/ward.js
===================================================================
--- src/assets/js/ward.js	(revision a5a62490bec7673afe4e4c5c788f48466eb96b8b)
+++ src/assets/js/ward.js	(revision 084b046d930ee79f779d73d05b08e90d00fd684f)
@@ -7,5 +7,5 @@
  * pagina en dit bestand tekent hem. Alles wat nieuw is komt van /ward/api/*.
  */
-'use strict';
+import { SkyRenderer } from '/ward/sky.js';
 
 const state = JSON.parse(document.getElementById('ward-state').textContent);
@@ -215,5 +215,53 @@
 }
 
-/** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
+/**
+ * De hemel en de kaarten zijn één scherm met twee standen.
+ *
+ * De hemel draait ALTIJD en ligt achter de kaarten; in kaartenstand is hij
+ * decor en vangt hij geen tikken, in berichtenstand vallen de kaarten weg en
+ * wordt hij de interface. Dat is precies de beweging uit Shaer: de kaarten
+ * vallen weg en wat eronder lag was er al.
+ */
+function wireSky() {
+  const canvas = document.getElementById('sky');
+  const btn = document.getElementById('mode');
+  const main = document.getElementById('app');
+  let convs = [];
+
+  const sky = new SkyRenderer(canvas, (i) => {
+    const c = convs[i];
+    if (c) window.location.href = `/ward/thread?with=${encodeURIComponent(c.uri)}`;
+  });
+
+  let messagesMode = false;
+  const setMode = (on) => {
+    messagesMode = on;
+    sky.interactive = on;
+    canvas.setAttribute('aria-hidden', String(!on));
+    // touch-action pas uitzetten als de hemel echt bedienbaar is: anders kun
+    // je in kaartenstand niet meer scrollen.
+    canvas.style.touchAction = on ? 'none' : '';
+    canvas.style.pointerEvents = on ? 'auto' : 'none';
+    document.body.classList.toggle('messages-mode', on);
+    btn.textContent = on ? state.strings.toCards : state.strings.toSky;
+    btn.setAttribute('aria-pressed', String(on));
+    main.querySelectorAll('section').forEach((sec) => { sec.hidden = on; });
+  };
+
+  btn.addEventListener('click', () => setMode(!messagesMode));
+  setMode(false);
+
+  (async () => {
+    try {
+      const r = await fetch('/ward/api/conversations', { headers: { Accept: 'application/json' } });
+      if (!r.ok) return;
+      convs = (await r.json()).items || [];
+      await sky.setFaces(convs.map((c) => ({ name: c.name, icon: c.icon, ageMs: c.ageMs })));
+      sky.start();
+    } catch { /* zonder hemel blijft het scherm gewoon werken */ }
+  })();
+}
+
+/** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. *//** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
 async function loadChecks() {
   if (!state.guardians.length) return;
@@ -240,4 +288,5 @@
 renderGuardians(null);
 wireBuoy();
+wireSky();
 loadFeed();
 renderOffers();
