<!-- SOLUV · part 477 -->
Soliditronium fuel.
      const why = r.status === 401 ? 'Log in on epistles.xyz to post'
        : r.status === 429 ? (lobby ? 'The lobby allows 10 posts a minute per person: wait a moment, then send again' : 'This room allows 40 posts a minute per person: wait a moment')
        : r.status === 402 ? (j.error || 'Out of Soliditronium fuel') + ' (lobby posts spend fuel)'
        : r.status === 403 ? (j.error && !/csrf/i.test(j.error) ? j.error : 'Session check failed: reload the page and try again')
        : (j.error || 'HTTP ' + r.status);
      return { ok: false, status: r.status, login: r.status === 401, error: why };
    }
    const m = lobby ? j.message : j.msg; if (m) addMsgs([m], true);
    setStatus('live', liveText());
    return { ok: true, id: m && m.id };
  }

  function startSafe() { return start().catch(e => { const msg = 'Wall failed: ' + (e && e.message || e); S.errors.push(msg); console.error('[soluv-wall] ' + msg); setStatus('error', msg); }); }

  /* ----- sessions and camera placeholders (no camera or audio is ever opened) ----- */
  function trackSession(m, i) {
    const key = m.user || ('guest:' + m.from);
    let s = S.sessions.get(key);
    if (!s) { const n = S.sessions.size; s = { speaker: shortName(m, !!m.king || String(m.user || '').toLowerCase() === KING.toLowerCase()), key, camId: 'cam-' + room + '-' + n, joinAt: { msg: i, t: m.t }, leaveAt: m.leaveAt || null, recordingId: 'rec-' + room + '-' + n, finalMsg: i }; S.sessions.set(key, s); }
    s.finalMsg = i;
  }
  const findSession = sp => [...S.sessions.values()].find(s => s.speaker === sp || s.key === sp || s.key === 'guest:' + sp);
  const api = {
    room, route, get status() { return S.status; }, get messages() { return S.msgs; }, cols: 1, lanes: SEGS,
    get archive() { return { messages: S.archivedMsgs || 0, lines: S.archivedLines || 0, url: archiveUrl }; },
    /* hit(worldPoint): 'archive' when a point on the face (e.g. a tap ray hit) lands on the pinned archive band */
    archiveRect() { if (!(S.archivedMsgs > 0) || !S.lines.length || !S.lines[0].more) return null; const r = baseRow(); return { x0: colX() * EM, x1: (colX() + Math.min(80, TEXT_W)) * EM, y0: (rowTop(r) - L.LINE) * EM, y1: rowTop(r) * EM }; },
    _oldArchiveRect() { if (!S.header || !(S.archivedMsgs > 0)) return null; const h = S.header; return { x0: h.x * EM, x1: (h.x + h.w * 0.62) * EM, y0: (h.yTop - h.h) * EM, y1: (h.yTop - h.h + 3.6) * EM }; },
    update, post, whoAmI, source, start: () => S.started || !source ? null : startSafe(),
    info() { return { room, route, cols: 1, lanes: SEGS, capacityRows: Rmax, archivedMessages: S.archivedMsgs || 0, archiveUrl, rows: S.R, liveRows: S.liveRows, lines: S.lines.length, messages: S.msgs.length,
      widthEm: +Wem.toFixed(2), heightEm: +S.Hem.toFixed(2), widthWorld: +(Wem * EM * scale).toFixed(3), heightWorld: +(S.Hem * EM * scale).toFixed(3), plinthWorld: +(L.PLINTH * EM * scale).toFixed(3),
      widthBlocks: +(Wem * EM * scale / BLOCK).toFixed(3), heightBlocks: +(S.Hem * EM * scale / BLOCK).toFixed(3), tiles: S.tiles.size, status: S.status, meta: { ...meta }, scale }; },
    thrift() { const tiers = {}; for (const t of S.tiles.values()) if (t.tier) tiers[t.tier] = (tiers[t.tier] || 0) + 1; return { pending: S.pending, started: S.started, liveJsonBytes: S.bytes, sseBytes: S.sseBytes, pollBytes: S.pollBytes || 0, textureBytes: S.texBytes, tilesTotal: S.tiles.size, tilesInView: S.inView || 0, tilesReadable: S.readable || 0, tiers, renders: S.renders, wallPpe: +(S.wallPpe || 0).toFixed(2) }; },
    tileRects() { return [...S.tiles.values()].filter(t => t.mesh.visible).map(t => ({ key: t.key, x0: t.x, x1: t.x + t.w, y0: t.yTop - t.h, y1: t.yTop, mesh: t.mesh })).concat(S.header && S.header.mesh.visible ? [{ key: 'header', x0: S.header.x, x1: S.header.x + S.header.w, y0: S.header.yTop - S.header.h, y1: S.header.yTop, mesh: S.header.mesh }] : []); },
    maxLineWidthEm() { let m = 0; for (const l of S.lines) for (const r of l.runs) m = Math.max(m, r.x + r.w); return m; },
    lineAnchor(i) { // wall-local position of live line i (default: newest), for cameras
      const off = S.lines.length && S.lines[0].more ? 1 : 0, n = S.lines.length - off; if (!n) return null; i = i === undefined ? n - 1 : Math.max(0, Math.min(n - 1, i));
      const r = baseRow() + off + i; return { col: 0, row: r, local: new T.Vector3((laneX(0) + SEG_W / 2) * EM, (rowTop(r) - L.LINE / 2) * EM, L.TXT_Z * EM) };
    },
    colCenterX: c => (laneX(c || 0) + Math.min(SEG_W, TEXT_W - (c || 0) * SEG_W) / 2) * EM,   // lane centres (for cameras); the text is one column
    
    textRect() { return { x0: colX() * EM, x1: (colX() + TEXT_W) * EM, y0: S.textBottom * EM, y1: rowTop(Math.max(0, baseRow())) * EM }; },
    sessions() { return [...S.sessions.values()].map(s => ({ speaker: s.speaker, camId: s.camId, joinAt: s.joinAt, leaveAt: s.leaveAt, recordingId: s.recordingId, finalMsg: s.finalMsg })); },
    setLive(