<!-- SOLUV · part 499 -->
Id: s.camId, joinAt: s.joinAt, leaveAt: s.leaveAt, recordingId: s.recordingId, finalMsg: s.finalMsg })); },
    setLive(speaker, on = true) { const s = findSession(speaker); if (!s) return false; const m = S.msgs[s.finalMsg]; if (!m) return false; m.live = !!on; m.camId = s.camId; meta.cams = Math.min(meta.maxCams, S.msgs.filter(x => x.live).length); drawHeaderSoon(); relayout(); return true; },
    endTransmission(speaker) { const s = findSession(speaker); if (!s) return null; let hit = null; for (const m of S.msgs) if (m.live && (m.user || 'guest:' + m.from) === s.key) { m.live = false; m.lastFrame = 'frame:' + s.camId + ':' + m.id; m.lastClip = 'clip:' + s.recordingId; hit = m; } meta.cams = S.msgs.filter(x => x.live).length; drawHeaderSoon(); relayout(); return hit; },
    leaveRoom(speaker) { const s = findSession(speaker); if (!s) return null; s.leaveAt = { msg: s.finalMsg, t: Date.now() }; const m = S.msgs[s.finalMsg]; if (m) { m.recLink = s.recordingId; m.leaveAt = s.leaveAt; } relayout(); return { ...s }; },
    setRoom({ mode, access, cams, maxCams } = {}) { if (mode) meta.mode = mode; if (access) meta.access = access; if (maxCams != null) meta.maxCams = maxCams | 0; if (cams != null) meta.cams = Math.min(meta.maxCams, cams | 0); drawHeaderSoon(); root.dispatchEvent({ type: 'change' }); return { ...meta }; },
    append(list) { return addMsgs(Array.isArray(list) ? list : [list]); }, // local/test growth (the server is the real source)
    /* ----- SOLID: the wall and its gold-block footprint are a collider. Nothing passes through, nothing stands inside. -----
       colliderLocal(): Box3 in the wall's own space (x across the face, y up, z out of the face). Use with wall.matrixWorld.
       collider(): world-space Box3 (axis-aligned; loose if the wall is rotated: prefer resolve()/blocks()). */
    colliderLocal() { const W = Wem * EM, D = L.DEPTH * EM; return new T.Box3(new T.Vector3(-W / 2 - 4 * EM, 0, -D / 2 - 5 * EM), new T.Vector3(W / 2 + 4 * EM, (L.PLINTH + S.Hem) * EM + L.EDGE * EM, D / 2 + 5 * EM)); },
    collider() { root.updateMatrixWorld(true); return api.colliderLocal().applyMatrix4(root.matrixWorld); },
    /* resolve(point, radius, prev): returns a corrected copy of a world point (avatar centre, camera) so a sphere of
       `radius` never enters the wall. With `prev` (last good position) it stops on the side it came from, even if one
       step would have jumped clean through the slab. */
    resolve(point, radius = 0.3, prev = null) {
      root.updateMatrixWorld(true);
      const inv = new T.Matrix4().copy(root.matrixWorld).invert(), sc = root.matrixWorld.getMaxScaleOnAxis(), r = radius / sc;
      const b = api.colliderLocal(); b.min.subScalar(r); b.max.addScalar(r);
      const q = point.clone().applyMatrix4(inv), pq = prev ? prev.clone().applyMatrix4(inv) : null;
      const insideXY = q.x > b.min.x && q.x < b.max.x && q.y < b.max.y;
      // tunnelling guard: crossed the slab in z within its face area during one step
      if (pq && insideXY && ((pq.z >= b.max.z && q.z < b.max.z) || (pq.z <= b.min.z && q.z > b.min.z))) { q.z = pq.z >= b.max.z ? b.max.z : b.min.z; return q.applyMatrix4(root.matrixWorld); }
      if (!b.containsPoint(q)) return point.clone();
      const pen = [['x', q.x - b.min.x, b.min.x], ['x', b.max.x - q.x, b.max.x], ['z', q.z - b.min.z, b.min.z], ['z', b.max.z - q.z, b.max.z], ['y', b.max.y - q.y, b.max.y]];
      let pick = null;
      if (pq) for (const [ax, , v] of pen) { if ((v === b.max[ax] && pq[ax] >= v) || (v === b.min[ax] && pq[ax] <= v)) { pick = [ax, 0, v]; break; } }
      if (!pick) pick = pen.sort((a, c) => a[1] - c[1])[0];
      q[pick[0]] = pick[2];
      return q.applyMatrix4(root.matrixWorld);
    },
    /* distanceTo(point): metres from a world point (avatar, camera) to the wall's solid box. 0 = touching.
       Hosts use it for 'near the wall' (show the composer). Nothing here limits how many people stand there. */
    distanceTo(point) { root.updateMatrixWorld(true); const q = point.clone().applyMatrix4(root.matrixWorld.clone().invert()); return api.colliderLocal().distanceToPoint(q) * root.scale.x; },
    blocks(point, radius = 0) { return !api.resolve(point, radius).equals(point); },
    canPlace(box) { // false when a box (world space) would stand inside the wall and block the text
      root.updateMatrixWorld(true); const inv = new T.Matrix4().copy(root.matrixWorld).invert();
      return !box.clone().applyMatrix4(inv).intersectsBox(api.colliderLocal());
    },
    lawCheck() {
      root.updateMatrixWorld(true);
      const out = { minimum: MINIMUM, minObject: null, minGlyphEm: null, lineHeightWorld: null, violations: [] };
      const box = new T.Box3(), size = new T.Vector3();
      root.traverse(o => {
        if (!o.isMesh || o.userData.text) return;
        box.setFromObject(o); box.getSize(size); const m = Math.min(size.x, size.y, size.z);
        const rec = { name: o.name, min: +m.toFixed(4) };