<!-- SOLUV · part 459 -->
 minGlyphEm: null, minGlyphQuad: null, glyphsChecked: 0, violations: [] };
      const box = new THREE.Box3(), size = new THREE.Vector3();
      world.traverse(o => {
        if (o === world || o.type === 'Group') return;
        if (o.isPoints) { const sz = o.geometry.attributes.size; if (!sz || !o.visible) return; let m = Infinity; for (let i = 0; i < sz.count; i++) m = Math.min(m, sz.getX(i) * WS); if (m < Infinity) { if (out.minSprite === null || m < out.minSprite.size) out.minSprite = { size: +m.toFixed(4), name: o.name || 'points' }; if (m < MINIMUM - 1e-6) out.violations.push({ sprite: o.name, size: m }); } return; }
        if (o.geometry && o.geometry.isInstancedBufferGeometry) return; // text batches: checked per glyph below
        if (!o.geometry || !o.geometry.attributes.position || !o.geometry.attributes.position.count) return;
        box.setFromObject(o); box.getSize(size); const m = Math.min(size.x, size.y, size.z);
        const rec = { type: o.type, name: o.name || o.parent?.name || '', min: +m.toFixed(4), size: [size.x, size.y, size.z].map(v => +v.toFixed(3)) }; out.objects.push(rec);
        if (!out.minObject || m < out.minObject.min) out.minObject = rec; if (m < MINIMUM - 1e-6) out.violations.push(rec);
      });
      for (const B of [faceBatch, capBatch]) for (let i = 0; i < B.n; i++) {
        const o = i * STRIDE, sc = B.data[o + 15], qh = (B.data[o + 6] - B.data[o + 4]) * sc * WS, qw = (B.data[o + 5] - B.data[o + 3]) * sc * WS, em = qh / ATLAS_CELL_EM;
        out.glyphsChecked++; if (out.minGlyphEm === null || em < out.minGlyphEm) out.minGlyphEm = em; const q = Math.min(qh, qw); if (out.minGlyphQuad === null || q < out.minGlyphQuad) out.minGlyphQuad = q;
        if (em < MINIMUM - 1e-6) out.violations.push({ glyph: i, em });
      }
      out.minGlyphEm = out.minGlyphEm && +out.minGlyphEm.toFixed(4); out.minGlyphQuad = out.minGlyphQuad && +out.minGlyphQuad.toFixed(4);
      const ab = new THREE.Box3().setFromObject(avatar), as = new THREE.Vector3(); ab.getSize(as); out.avatarHeight = +as.z.toFixed(4);
      const eye = avatar.children[avatar.children.length - 1]; new THREE.Box3().setFromObject(eye).getSize(as); out.avatarEye = +Math.min(as.x, as.y, as.z).toFixed(4);
      out.smallest = Math.min(out.minObject ? out.minObject.min : Infinity, out.minSprite ? out.minSprite.size : Infinity, out.minGlyphEm ?? Infinity);
      out.objects = out.objects.sort((a, b) => a.min - b.min).slice(0, 8); return out;
    },
    thrift() { return { indexBytes: stats.indexBytes, tileBytesFetched: stats.tileBytes, tileBytesResident: stats.tileBytesResident || 0, tilesLoaded: stats.tilesLoaded, tilesTotal: stats.tilesTotal, tilesEvicted: stats.tilesEvicted || 0, layoutCache: layoutCache.size, heapMB: performance.memory ? +(performance.memory.usedJSHeapSize / 1048576).toFixed(1) : null }; },
    route: roomN || null, room(n) { return sel[n - 1] || null; }, goRoom(n) { const p = sel[n - 1]; if (!p) return null; history.replaceState(null, '', '#/soluv/' + n); return flyTo(capViewFor(p), 1800); },
    zone(n) { return sel[n - 1]?.zone || null; },
    avatar, sel, castle, capViewFor, endTransmission(id, speaker) { const p = pages[id]; const m = p?.msgs && [...p.msgs].reverse().find(q => q.live && (!speaker || q.spk === speaker)); const ok = endTransmission(m); if (ok) { dirty = true; } return ok ? m : null; }, viewFor: (name, id) => viewState(name, pages[id]), setPillars,
    setRoom(id, { mode, access, cams } = {}) { // future: wire to real rooms. Only labels change here; no camera/audio is opened.
      const p = pages[id]; if (!p) return false; if (mode) p.mode = mode; if (access) p.access = access; if (cams != null) p.cams = Math.min(p.maxCams, cams); p.labelV++; dirty = true; return true; },
    setCameraView, cycleView, simulateTick, mostActive, goMostActive, joinPage, tapAt, get cameraView() { return VIEWS[viewIdx] || null; }, stopMotion,
    destroy() { alive = false; ro.disconnect(); clearInterval(sim.timer); renderer.dispose(); wrap.remove(); container.classList.remove('ws-root'); },
  };
  async function setPillars(n) {
    n = clamp(n | 0, 1, pages.length);
    const u = new URL(location.href); u.searchParams.set('pillars', n); history.replaceState(null, '', u);
    api.destroy(); const fresh = await mountWordSky(container, { ...opts, pillars: n }); return fresh;
  }
  wrap.querySelector('.ws-np').textContent = sel.length >= 1000 ? (sel.length / 1000).toFixed(1).replace('.0', '') + 'k' : String(sel.length);
  badge.textContent = `TEST TEXT · ${sel.length.toLocaleString()} pillars · ${stats.wordsTotal.toLocaleString()} words · ${sel.length - stats.archivePillars} chat · ${stats.archivePillars} archive (Project Gutenberg, public domain) · ${stats.lockedPillars} sealed`;
  if (roomN && sel[roomN - 1]) { const p = sel[roomN - 1]; statusEl.textContent = `${p.route} · ${p.hl}`; setTimeout(() => flyTo(sel.length === 1 ? homeState() : capViewFor(p), 1800), 300); }
  contai