<!-- SOLUV · part 458 -->
 p.msgs[j]; if (q.spk === speaker && q.live) { endTransmission(q); break; } }
    let sess = null; if (live && camId) { sess = sessionFor(p, speaker, camId, { msg: p.msgs.length, t: Date.now() }); if (sess.leaveAt) { sess.joinAt = { msg: p.msgs.length, t: Date.now() }; sess.leaveAt = null; sess.recordingId += '+'; } }
    const m = { i: p.msgs.length, lx, ly, x: w0.x, y: w0.y, z: w0.z, n: lines.length, live: live && !!camId, camId, sl, g: (speaker === GOLD || /epistolary/i.test(text)) ? 1 : 0,
      text: lines.join('\n'), spk: speaker, id: p.id * 100000 + p.msgs.length };
    if (sess) sess.last = m;
    p.msgs.push(m); p.words += text.split(/\s+/).length; if (simulated) stats.simulatedWords = (stats.simulatedWords || 0) + text.split(/\s+/).length; else stats.wordsLoaded += text.split(/\s+/).length;

    if (opts.headlineProvider) Promise.resolve(opts.headlineProvider(p, p.msgs)).then(r => r && setHeadline(p.id, r.headline, r.subheads));
    if (!simulated) bumpActivity(p); p.labelV++;
    addLiveStar(m, p);
    dirty = true; return m;
  }

  /* ---------------- benchmark (used by the headless measurement script) ---------------- */
  async function benchmark({ target, inMs = 7000, holdMs = 1000, outMs = 5000 } = {}) {
    stopMotion(); const samples = []; continuous = true;
    frameHook = (dt, cpu) => samples.push({ dt, cpu, glyphs: stats.glyphs, labels: stats.labels, minPx: stats.minTextPx, phase });
    let phase = 'settle';
    Object.assign(st, homeState()); applyCamera(); await new Promise(r => setTimeout(r, 600));
    const tp = pages[target?.page ?? 0], tm = tp.msgs?.[target?.msg ?? 0];
    phase = 'fly-in'; await flyTo(tm ? nearViewFor(tm, tp) : capViewFor(tp), inMs);
    phase = 'hold'; await new Promise(r => setTimeout(r, holdMs));
    phase = 'zoom-out'; await flyTo(homeState(), outMs);
    frameHook = null; continuous = false;
    const summarize = (arr) => {
      const dts = arr.map(s => s.dt).filter(d => d > 0).sort((a, b) => a - b); if (!dts.length) return null;
      const sum = dts.reduce((a, b) => a + b, 0);
      return { frames: dts.length, avgFps: +(1000 * dts.length / sum).toFixed(1), medianFrameMs: +dts[dts.length >> 1].toFixed(1), p95FrameMs: +dts[Math.floor(dts.length * 0.95)].toFixed(1),
        maxFrameMs: +dts[dts.length - 1].toFixed(1), avgCpuMs: +(arr.reduce((a, s) => a + s.cpu, 0) / arr.length).toFixed(2), maxGlyphs: Math.max(...arr.map(s => s.glyphs)), avgGlyphs: Math.round(arr.reduce((a, s) => a + s.glyphs, 0) / arr.length), minTextPxShown: Math.min(...arr.map(s => s.minPx ?? Infinity)) };
    };
    return { flyIn: summarize(samples.filter(s => s.phase === 'fly-in')), hold: summarize(samples.filter(s => s.phase === 'hold')), zoomOut: summarize(samples.filter(s => s.phase === 'zoom-out')), all: summarize(samples.filter(s => s.phase !== 'settle')) };
  }

  async function sample(ms) { // record frames for ms while whatever camera motion is running (used for Glide/Orbit FPS)
    const arr = []; continuous = true; frameHook = (dt, cpu) => arr.push({ dt, cpu, glyphs: stats.glyphs, minPx: stats.minTextPx });
    await new Promise(r => setTimeout(r, ms)); frameHook = null; continuous = false;
    const dts = arr.map(s => s.dt).filter(d => d > 0).sort((a, b) => a - b), sum = dts.reduce((a, b) => a + b, 0);
    return { frames: dts.length, avgFps: +(1000 * dts.length / sum).toFixed(1), medianFrameMs: +dts[dts.length >> 1].toFixed(1), p95FrameMs: +dts[Math.floor(dts.length * 0.95)].toFixed(1),
      maxGlyphs: Math.max(...arr.map(s => s.glyphs)), minTextPxShown: Math.min(...arr.map(s => s.minPx ?? Infinity)) };
  }
  const api = { sample,
    stats, pages, books, index, search, flyTo, setHeadline, addMessage, benchmark, loadAllTiles,
    get view() { return { ...st }; }, setView(v) { anim = null; Object.assign(st, v); applyCamera(); },
    home() { return flyTo(homeState(), 1600); }, homeState, nearViewFor, focal: () => focal,
    setGlow(on) { glow.visible = !!on; dirty = true; },
    pageScreen(id) { const p = pages[id]; if (!p.on) return null; vpm.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(SWS); VP = vpm.elements; const g = wp(p, p.w / 2, p.hdr * 0.45, 2.5); return project(g.x, g.y, g.z) ? { x: P.sx, y: P.sy } : null; },
    leaveRoom(id, speaker) { const p = pages[id]; const ss = p?.sessions?.get(speaker); const m = ss && leaveRoom(p, ss, { msg: ss.last.i, t: Date.now() }); if (m) dirty = true; return m ? { ...ss, last: undefined } : null; },
    sessions(id) { const p = pages[id]; return p?.sessions ? [...p.sessions.values()].map(s => ({ speaker: s.speaker, camId: s.camId, joinAt: s.joinAt, leaveAt: s.leaveAt, recordingId: s.recordingId, finalMsg: s.last?.i })) : []; },
    lawCheck() { // walk the scene: smallest bounding-box dimension of every object, sprite sizes, and every glyph drawn this frame
      world.updateMatrixWorld(true); const out = { minimum: MINIMUM, objects: [], minObject: null, minSprite: null,