<!-- SOLUV · part 668 -->
y0 < b.y1 - 1e-6 && b.y0 < a.y1 - 1e-6) out.overlaps.push([w.wall.room, a.key, b.key]); }
        const zs = new Set(rects.map(r => r.mesh.position.z.toFixed(5))); out.textPlanes = zs.size;
      }
      out.maxLineWidthEm = Math.max(...walls.map(w => w.wall.maxLineWidthEm())); out.columnWidthEm = L.COL_W;
      return out;
    },
    destroy() { alive = false; ro.disconnect(); walls.forEach(w => w.wall.dispose()); renderer.dispose(); wrap.remove(); },
  };
  /* window.soluvKeys: the same keyboard as code, for AI helpers and scripts. Every call runs the handler a person's key or
     button runs; post() is the Send button's own path (your login, your CSRF token, the room's normal limits). */
  const lineText = m => (m.king ? '\u265bEpistolary' : (m.from || 'Guest')) + ': ' + m.text;
  const keys = {
    version: 1, page: 'soluv',
    press(key) { key = String(key || ''); if (key === 'Enter' && input.value.trim()) return sendText(input.value, true); const ok = keyAction(key); return { ok, key, error: ok ? null : 'Unknown key: ' + key }; },
    type(text) { input.value += String(text == null ? '' : text); return { ok: true, text: input.value }; },
    clear() { input.value = ''; return { ok: true }; },
    scroll(dy) { haltAuto(); const g = rideBy(-(+dy || 0) * pxWorld()); return { ok: true, goalY: +g.toFixed(3), y: +st.ty.toFixed(3) }; },
    goto(t) {
      if (typeof t === 'number' || /^\d+$/.test(String(t))) {
        const n = +t, base = walls.filter(w => !w.userData.placed), w = base[n - 1];
        if (!w) return { ok: false, error: 'No wall ' + n + ' on this page: open /soluv/' + n };
        current = w; history.replaceState(null, '', '/soluv/' + n + '/' + location.search); refreshStatus(); refreshMe(); flyTo(views.far(), 1800); return { ok: true, room: n };
      }
      const line = t && typeof t === 'object' ? t.line : (/^line:(\d+)$/.exec(String(t)) || [])[1];
      if (line !== undefined) return goLine(+line) ? { ok: true, line: +line } : { ok: false, error: 'No line ' + line + ' on this wall yet' };
      if (t === 'top' || t === 'home') { goTop(); return { ok: true, target: 'top' }; }
      if (t === 'newest' || t === 'live' || t === 'end') { flyTo(views.live()); return { ok: true, target: 'newest' }; }
      if (views[t]) { flyTo(views[t](), t === 'map' ? 1800 : 1400); return { ok: true, target: t }; }
      return { ok: false, error: "goto needs a room number, 'top', 'newest', {line: i} or a view (map, far, wide, mid, near)" };
    },
    post(text) { return sendText(text, false); },
    read(n = 20) { return current.wall.messages.slice(-Math.max(1, n | 0)).map(lineText).join('\n'); },
    state() {
      const s = current.wall.status;
      return { page: 'soluv', room: current.wall.room, route: current.wall.route, status: s.state, statusText: s.text, near: !!api.near,
        cameraY: +camera.position.y.toFixed(3), targetY: +st.ty.toFixed(3), riding: tyGoal !== null || !!flingTy, wallTopY: +wallTop().toFixed(3),
        messages: current.wall.messages.length, walls: walls.length, composer: input.value, guest: !(me && me.username),
        me: me ? { name: me.name, username: me.username, king: !!me.king } : null, lastPost: api.lastPost || null, voice: voice.state() };
    },
    voiceFor: (name, kind) => voice.voiceFor(name, kind),
    readAloud: on => voice.setOn(on),
    myVoice: kind => voice.setMine(kind),
  };
  api.keys = keys; api.voice = voice;
  container.world = api; window.soluvWorld = api; window.soluvKeys = keys;
  container.dispatchEvent(new CustomEvent('soluv-world-ready', { detail: api }));
  return api;
}

function injectStyle() {
  if (document.getElementById('sw-style')) return;
  const s = document.createElement('style'); s.id = 'sw-style';
  s.textContent = `
  .sw-root{display:block;overflow:hidden;background:#000;touch-action:none;-webkit-user-select:none;user-select:none}
  .sw-wrap{position:absolute;inset:0;font-family:"Soluv Condensed","Roboto Condensed","Arial Narrow",sans-serif;color:#dff6ff}
  .sw-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none}
  .sw-status{position:absolute;top:max(8px,env(safe-area-inset-top));left:8px;right:8px;min-height:24px;font-size:15px;line-height:1.3;color:#9fe9ff;z-index:3;pointer-events:none;text-shadow:0 1px 2px #000;background:#00080fcc;border:1px solid #0e3a48;border-radius:10px;padding:6px 10px}
  .sw-status[data-state=error]{color:#ff9a8f;border-color:#6b2420}
  .sw-status[data-state=live]{color:#9ff5c8}
  .sw-ctrls{position:absolute;right:8px;bottom:max(8px,env(safe-area-inset-bottom));display:flex;flex-direction:column;gap:8px;z-index:4}
  .sw-btn{box-sizing:border-box;width:56px;min-width:48px;height:48px;min-height:48px;border-radius:12px;border:1px solid #1d6f86;background:#031822e6;color:#cff7ff;font:600 22px/1 "Soluv Condensed","Roboto Condensed",sans-serif;padding:0;cursor:pointer;touch-action:manipulation;display:flex;align-items:center;justify-content