Index: src/assets/js/ward-sky.js
===================================================================
--- src/assets/js/ward-sky.js	(revision 52188632909b3b1b9120748844eed2779f7fc030)
+++ src/assets/js/ward-sky.js	(revision 5412cbbcf42cb02fb3fb6d67ef433290c95bb38c)
@@ -152,8 +152,14 @@
    * @param {(index:number)=>void} onPick  een gezicht aangetikt
    */
-  constructor(canvas, onPick) {
+  constructor(canvas, onPick, onNew) {
     this.canvas = canvas;
     this.g = canvas.getContext('2d');
     this.onPick = onPick;
+    // De (+) in het midden: de deur naar iemand NIEUW. Hij zit op de
+    // OORSPRONG van het veld en pant en zoomt met de gezichten mee, zodat hij
+    // altijd het midden van de vliegende avatars is, waar de hemel ook heen
+    // drijft. De plaatsing houdt daar bewust ruimte vrij (elk gezicht hangt
+    // minstens 1.4 cel naar buiten).
+    this.onNew = onNew;
     this.faces = [];
     this.spots = [];
@@ -211,4 +217,18 @@
     const s = Math.min(Math.max(this.scale, 0.5), 1.6);
     const draw = FACE * s;
+
+    // De (+) eerst, dan de gezichten eroverheen als het krap wordt.
+    if (this.interactive && this.onNew) {
+      const px = cx + this.panX;
+      const py = cy + this.panY;
+      const pr = 22 * s;
+      g.beginPath(); g.arc(px, py, pr, 0, Math.PI * 2);
+      g.fillStyle = 'rgba(255,255,255,0.92)'; g.fill();
+      g.strokeStyle = '#11131a'; g.lineWidth = Math.max(2, 3 * s); g.lineCap = 'round';
+      g.beginPath();
+      g.moveTo(px - pr * 0.45, py); g.lineTo(px + pr * 0.45, py);
+      g.moveTo(px, py - pr * 0.45); g.lineTo(px, py + pr * 0.45);
+      g.stroke();
+    }
 
     for (let i = 0; i < this.spots.length; i++) {
@@ -248,9 +268,16 @@
       if (moved < 6) {
         const r = c.getBoundingClientRect();
-        const hit = nearestFace(e.clientX - r.left, e.clientY - r.top, this.spots, {
+        const x = e.clientX - r.left;
+        const y = e.clientY - r.top;
+        const hit = nearestFace(x, y, this.spots, {
           cx: this.w / 2, cy: this.h / 2, cell: CELL, scale: this.scale,
           panX: this.panX, panY: this.panY,
         });
-        if (hit != null) this.onPick?.(hit);
+        if (hit != null) { this.onPick?.(hit); return; }
+        // Geen gezicht? Dan misschien de (+): zelfde middelpunt als getekend,
+        // met een raakzone die meeschaalt zodat mikken bij elke zoom klopt.
+        const s2 = Math.min(Math.max(this.scale, 0.5), 1.6);
+        const d = Math.hypot(x - (this.w / 2 + this.panX), y - (this.h / 2 + this.panY));
+        if (d <= 26 * s2) this.onNew?.();
       }
     };
Index: src/assets/js/ward.js
===================================================================
--- src/assets/js/ward.js	(revision 52188632909b3b1b9120748844eed2779f7fc030)
+++ src/assets/js/ward.js	(revision 5412cbbcf42cb02fb3fb6d67ef433290c95bb38c)
@@ -232,5 +232,41 @@
     const c = convs[i];
     if (c) openThread(c);
-  });
+  }, () => openFriendsPicker());
+
+  /* ── De (+): de deur naar iemand nieuw ─────────────────────────────────
+     De hemel toont alleen met wie je al sprak; de kiezer toont iedereen met
+     een volgrelatie. Kies iemand en het lege gesprek opent gewoon: het
+     eerste bericht maakt het echt. */
+  const friendsDlg = document.getElementById('friends-dialog');
+  async function openFriendsPicker() {
+    const list = document.getElementById('friends-list');
+    list.replaceChildren();
+    friendsDlg.showModal();
+    try {
+      const r = await fetch('/ward/api/friends', { headers: { Accept: 'application/json' } });
+      const items = r.ok ? (await r.json()).items || [] : [];
+      document.getElementById('friends-empty').hidden = items.length > 0;
+      items.forEach((f) => {
+        const li = el('li', 'card friend');
+        const row = el('button', 'row');
+        row.type = 'button';
+        if (f.icon) {
+          const img = el('img', 'avatar');
+          img.src = f.icon; img.alt = ''; img.loading = 'lazy';
+          row.append(img);
+        }
+        const kolom = el('span', 'wie');
+        kolom.append(el('span', 'name', f.name), el('span', 'hint', f.handle));
+        row.append(kolom);
+        row.addEventListener('click', () => {
+          friendsDlg.close();
+          openThread({ uri: f.uri, name: f.name, icon: f.icon });
+        });
+        li.append(row);
+        list.append(li);
+      });
+    } catch { /* de kiezer blijft leeg staan; annuleren kan altijd */ }
+  }
+  document.getElementById('friends-cancel').addEventListener('click', () => friendsDlg.close());
 
   /* ── Het gesprek: een overlay OVER de levende hemel ────────────────────
