<!-- SOLUV · part 468 -->
 const MINIMUM = (typeof window !== 'undefined' && window.MINIMUM) || 0.13;
export function applyMinimum(obj, T) { // verbatim rule from Solid's saga.js
  if (!obj) return obj;
  const box = new T.Box3().setFromObject(obj), size = new T.Vector3(); box.getSize(size);
  const s = Math.min(size.x, size.y, size.z);
  if (s > 0 && s < MINIMUM) obj.scale.multiplyScalar(MINIMUM / s);
  return obj;
}
export const EM = MINIMUM;                    // world units per text em
export const L = {                            // layout, in em (1 em = MINIMUM = 0.13 world units)
  COL_W: 26,      // one column = one phone room width (390 px / 15 px)
  GUT: 1.5, PADX_MIN: 2, PADB: 2, HEAD: 54, LINE: 1.3, CHUNK: 16,
  LIVE_MIN_ROWS: 4, DEPTH: 4, PLINTH: 3, TXT_Z: 2.4, EDGE: 2,   // text floats 0.4 em proud of the glass face
};
export const BLOCK = 10;                      // Playa 2.0's biggest (Coarse) gold block: GridHelper(400, 40) -> 10-unit squares
const TIERS = [4, 8, 16, 32];                 // texture device px per em
const LOAD_PPE = 0.4;                         // CSS px per em at which a wall starts fetching its live room (tiny JSON)
const READABLE_PPE = 9;                       // CSS px per em we call "readable"
const TEX_BUDGET = 40 * 1048576;              // bytes of resident text textures per wall
const FONT_FAMILY = 'Soluv Condensed';
const FONT_STACK = '"Soluv Condensed","Roboto Condensed","Arial Narrow",sans-serif';
const KING = 'TheEpistolary';
const COL = { arch: '#93aebb', text: '#efe7d6', king: '#fff3cf', gold: '#e7b53a', cyan: '#5ff4ff', rule: 'rgba(110,220,255,0.20)', dim: '#8fb6c9', bad: '#ff8f80', still: '#8a8f9e' };
const MODULE_URL = import.meta.url;

function hash(s) { let x = 2166136261; for (let i = 0; i < s.length; i++) { x ^= s.charCodeAt(i); x = Math.imul(x, 16777619); } return x >>> 0; }
const tagColor = key => 'hsl(' + (hash(key) % 360) + ',75%,70%)';          // same rule as the dense /soluv room
const shortName = (m, king) => king ? '\u265bEpistolary' : (String(m.from || 'Guest').trim().replace(/^(king|queen|the)\s+/i, '').split(/\s+/)[0].slice(0, 12) || 'Guest');

/* ---------- font (the room's own Roboto Condensed file) ---------- */
let fontPromise = null; export let fontProblem = null;
function loadFont(url) {
  if (fontPromise) return fontPromise;
  if (typeof FontFace === 'undefined') { fontProblem = 'this browser has no FontFace: text uses Arial Narrow'; console.error('[soluv-wall] ' + fontProblem); return (fontPromise = Promise.resolve()); }
  const ff = new FontFace(FONT_FAMILY, 'url("' + url + '")', { weight: '100 900' });
  document.fonts.add(ff);
  fontPromise = ff.load().then(() => document.fonts.load('700 20px "' + FONT_FAMILY + '"')).then(() => {}, e => {
    fontProblem = 'Roboto Condensed failed to load (' + (e && e.message || e) + '): text uses Arial Narrow';
    console.error('[soluv-wall] ' + fontProblem);
  });
  return fontPromise;
}
let MC = null; const mcache = new Map();
function mw(s, bold) {                       // width in em
  const k = (bold ? 'b' : 'r') + s; let w = mcache.get(k); if (w !== undefined) return w;
  if (!MC) MC = document.createElement('canvas').getContext('2d');
  MC.font = (bold ? 700 : 400) + ' 100px ' + FONT_STACK; w = MC.measureText(s).width / 100;
  if (mcache.size > 40000) mcache.clear(); mcache.set(k, w); return w;
}

/* ---------- dense wrapping (same face, 1.3 line height, inline tags, tag once per speaker run) ---------- */
function wrapMessage(prefix, text, kind, maxW, tail) {
  const lines = []; let cur = [], x = 0, fresh = true; // fresh: nothing but prefix on this line yet (no leading space)
  const push = () => { lines.push(cur); cur = []; x = 0; fresh = true; };
  for (const p of prefix) { cur.push({ x, s: p.s, k: p.k, w: p.w }); x += p.w + p.gap; }
  const place = (s, w, k, space) => {
    const last = cur[cur.length - 1], sw = space ? mw(space, k === 'tag') : 0;
    if (last && last.k === k && !fresh) { last.s += space + s; x += sw + w; last.w = x - last.x; }
    else { x += fresh ? 0 : sw; cur.push({ x, s, k, w }); x += w; }
    fresh = false;
  };
  const word = (t, k, space) => {
    const bold = k === 'tag'; let w = mw(t, bold);
    if (!fresh && x + mw(space, bold) + w > maxW) push();
    if (x + w > maxW) {                       // longer than a column: break anywhere (overflow-wrap:anywhere)
      let s = '';
      for (const ch of t) { if (s && x + mw(s + ch, bold) > maxW) { place(s, mw(s, bold), k, space); push(); s = ch; } else s += ch; }
      t = s; w = mw(s, bold);
    }
    place(t, w, k, space);
  };
  String(text).split('\n').forEach((para, pi) => {
    if (pi > 0) push();
    let pend = ' ';
    for (const tok of para.split(/(\s+)/)) {
      if (!tok) continue;
      if (/^\s+$/.test(tok)) { pend = tok.replace(/\t/g, '    '); continue; }
      word(tok, kind, pend); pend = ' ';
    }
  });
  if (tail) word(tail, 'rec', ' ');
  push();
  return lines;
}

/* ---------- shaders (GLSL