<!-- SOLUV · part 487 -->
 = wall.wall.lawCheck();
      return { minimum: MINIMUM, windowMINIMUM: window.MINIMUM, smallest: +Math.min(minObject.min, wl.minGlyphEm).toFixed(4), minObject, minGlyphEm: wl.minGlyphEm, violations: violations.concat(wl.violations) };
    },
    thrift() {
      const res = performance.getEntriesByType('resource'); const sum = f => res.filter(f).reduce((a, r) => a + (r.transferSize || r.encodedBodySize || 0), 0);
      return { networkBytes: sum(() => true) + (performance.getEntriesByType('navigation')[0]?.transferSize || 0), threeBytes: sum(r => /three/.test(r.name)), fontBytes: sum(r => /woff2/.test(r.name)), lobbyApiBytes: sum(r => /api\/chat\/lobby/.test(r.name)), wall: wall.wall.thrift(), heapMB: performance.memory ? +(performance.memory.usedJSHeapSize / 1048576).toFixed(1) : null };
    },
    destroy() { alive = false; ro.disconnect(); wall.wall.dispose(); plateMat.forEach(m => { m.map.dispose(); m.dispose(); }); renderer.dispose(); wrap.remove(); },
  };
  /* window.soluvKeys: the same keyboard as code, for AI helpers and scripts. post() is the Send button's own path. */
  const lineText = m => (m.king ? '\u265bEpistolary' : (m.from || 'Guest')) + ': ' + m.text;
  const keys = {
    version: 1, page: 'space',
    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, reading, goalY: +g.toFixed(3), y: +st.y.toFixed(3) }; },
    goto(t) {
      if (typeof t === 'number' || /^\d+$/.test(String(t))) { if (+t !== 1) return { ok: false, error: 'Soluv Space has one tower (wall 1, the lobby): open /soluv/' + t + ' for wall ' + t }; goNewest(); return { ok: true, room: 1 }; }
      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' || t === 'read') { goNewest(); return { ok: true, target: 'newest' }; }
      if (t === 'crown') { setReading(false); flyTo(views.top(), 2600); return { ok: true, target: 'crown' }; }
      if (t === 'rise') { flyTo(views.base(), 900); setTimeout(startRise, 950); return { ok: true, target: 'rise' }; }
      if (views[t] && t !== 'top') { setReading(false); flyTo(views[t](), t === 'far' ? 2600 : 1600); return { ok: true, target: t }; }
      return { ok: false, error: "goto needs 'top', 'newest', {line: i}, 1, or a view (base, mid, crown, far, rise)" };
    },
    post(text) { return sendText(text, false); },
    read(n = 20) { return wall.wall.messages.slice(-Math.max(1, n | 0)).map(lineText).join('\n'); },
    state() {
      const s = wall.wall.status;
      return { page: 'space', room: 1, route: wall.wall.route, status: s.state, statusText: s.text, reading, near: !!api.near,
        cameraY: +camera.position.y.toFixed(3), riding: rideGoal !== null || !!fling || !!rise, wallTopY: +topY.toFixed(3),
        messages: wall.wall.messages.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.space = api; window.soluvSpace = api; window.soluvKeys = keys;
  return api;
}

function injectStyle() {
  if (document.getElementById('sp-style')) return;
  const s = document.createElement('style'); s.id = 'sp-style';
  const F = '"Soluv Condensed","Roboto Condensed","Arial Narrow",sans-serif';
  s.textContent = `
  .sp-root{display:block;overflow:hidden;background:#000;touch-action:none;-webkit-user-select:none;user-select:none}
  .sp-wrap{position:absolute;inset:0;font-family:${F};color:#dff6ff;display:flex;flex-direction:column}
  .sp-canvas{flex:1 1 0;min-height:0;width:100%;display:block;touch-action:none}
  .sp-top{flex:none;padding:max(8px,env(safe-area-inset-top)) 72px 6px 8px;z-index:3;pointer-events:none;display:flex;flex-direction:column;gap:4px;background:#000;border-bottom:1px solid #0e3a48}
  .sp-title{font:300 15px/1 ${F};letter-spacing:.42em;color:#9fe9ff;text-shadow:0 0 8px #00d0ff99}
  .sp-title b{font-weight:700;color:#e8fdff}
  .sp-status{font-size:13px;line-height:1.3;color:#8fd9ec;background:linear-gradient(90deg,#00121bcc,#00121b00);border-left:2px solid #5ff4ff;padding:4px 8p