<!-- SOLUV · part 447 -->
 / B.cols) | 0, x0 = 0.6 + c * (B.tw + 0.4), y0 = -(B.top + (B.rows - r) * B.th + (B.rows - 1 - r) * 0.4); rect(x0, y0, x0 + B.tw, y0 + B.th); }
        // voice ring at the pillar's foot: lit teal for audio/video rooms, a faint trace otherwise
        const rr = p.w * 0.62 + 4, dst3 = p.mode === 'text' ? aring : aringOn, n = 28;
        for (const z of [0.15, 1.15]) for (let i = 0; i < n; i++) { const a0 = i / n * 2 * Math.PI, a1 = (i + 1) / n * 2 * Math.PI; dst3.push(bx + rr * Math.cos(a0), by + rr * Math.sin(a0), z, bx + rr * Math.cos(a1), by + rr * Math.sin(a1), z); }
      }
      const gp = wp(p, p.w / 2, p.hdr * 0.45, 2.5); spos.push(gp.x, gp.y, gp.z); ssz.push(Math.max(18, p.w * 2.6));
    }
    const bg = new THREE.BufferGeometry(); bg.setAttribute('position', new THREE.Float32BufferAttribute(vpos, 3)); bg.setIndex(ipos);
    const boxes = new THREE.Mesh(bg, new THREE.MeshBasicMaterial({ color: 0x06202e, transparent: true, opacity: 0.72 })); // dark translucent glass boxes.renderOrder = 1; world.add(boxes);
    const og = new THREE.BufferGeometry(); og.setAttribute('position', new THREE.Float32BufferAttribute(opos, 3));
    const ol = new THREE.LineSegments(og, outlineMat); ol.renderOrder = 2; world.add(ol);
    const gg = new THREE.BufferGeometry(); gg.setAttribute('position', new THREE.Float32BufferAttribute(gpos, 3));
    const gl = new THREE.LineSegments(gg, new THREE.LineBasicMaterial({ color: 0xe7b53a })); gl.renderOrder = 2; world.add(gl);
    for (const [arr, col] of [[vband, 0x2a3348], [vbandOn, 0x5a7fb0], [aring, 0x1b2630], [aringOn, 0x2fa59c]]) {
      const g2 = new THREE.BufferGeometry(); g2.setAttribute('position', new THREE.Float32BufferAttribute(arr, 3));
      const ls = new THREE.LineSegments(g2, new THREE.LineBasicMaterial({ color: col })); ls.renderOrder = 2; world.add(ls);
    }
    // the map: 2000 x 2000 blocks (1 block = 1 world unit, as in saga.js), a slab 1 layout unit (= MINIMUM) thick
    const ground = new THREE.Mesh(new THREE.BoxGeometry(MAP_BLOCKS / WS, MAP_BLOCKS / WS, 1), new THREE.MeshBasicMaterial({ color: 0x000000 })); ground.name = 'map 2000x2000 blocks';
    ground.position.set(castle.cx, castle.cy, -0.7); // a slab 1 unit (= MINIMUM) thick, top just under z=0 ground.renderOrder = 0; world.add(ground);
    const rp = [];
    { const gp = [], S = SKY_CELL / WS, E = MAP_BLOCKS / 2 / WS; // black sky grid: square outlines every 50 blocks, like saga.js's GridHelper(20000, 400)
      for (const z of [0.02, -0.98]) for (let v = -E; v <= E; v += S) gp.push(castle.cx - E, castle.cy + v, z, castle.cx + E, castle.cy + v, z, castle.cx + v, castle.cy - E, z, castle.cx + v, castle.cy + E, z);
      const gg2 = new THREE.BufferGeometry(); gg2.setAttribute('position', new THREE.Float32BufferAttribute(gp, 3));
      const grid3 = new THREE.LineSegments(gg2, new THREE.LineBasicMaterial({ color: 0x0c3344 })); grid3.name = 'sky grid'; grid3.renderOrder = 0; world.add(grid3); }
    const loop = (cx, cy, r, n = 96) => { for (const z of [0, 1]) for (let i = 0; i < n; i++) { const a0 = i / n * 2 * Math.PI, a1 = (i + 1) / n * 2 * Math.PI; rp.push(cx + r * Math.cos(a0), cy + r * Math.sin(a0), z, cx + r * Math.cos(a1), cy + r * Math.sin(a1), z); } };
    if (!castle.single) for (const ring of castle.rings) loop(ring.cx, ring.cy, ring.r + (ring.inward ? -14 : 14), 72);
    if (!castle.single) { loop(castle.cx, castle.cy, castle.radius + 120, 160); loop(castle.cx, castle.cy, castle.radius + 135, 160); }
    for (const av of castle.avenues) for (const z of [0, 1]) rp.push(av.x0, av.y0, z, av.x0, av.y1, z, av.x1, av.y0, z, av.x1, av.y1, z);
    const rg = new THREE.BufferGeometry(); rg.setAttribute('position', new THREE.Float32BufferAttribute(rp, 3));
    const rl = new THREE.LineSegments(rg, new THREE.LineBasicMaterial({ color: 0x2c2a3e })); rl.renderOrder = 0; world.add(rl);
    const sg = new THREE.BufferGeometry(); sg.setAttribute('position', new THREE.Float32BufferAttribute(spos, 3));
    sg.setAttribute('size', new THREE.Float32BufferAttribute(ssz, 1)); sg.setAttribute('act', new THREE.BufferAttribute(new Float32Array(sel.length), 1));
    glow = new THREE.Points(sg, glowMat); glow.frustumCulled = false; glow.renderOrder = 3; world.add(glow);
  }
  // ---- scale figure: one avatar, exactly 12 minimums tall (12 x 0.13 = 1.56 world), eyes 1 minimum (0.13) across ----
  // Grid: 1 unit = MINIMUM = one eye = the smallest glyph em. Avatar = 12 units. Mid first-words = 3.2 units, headline caps >= 1 unit.
  const avatar = (() => {
    const p0 = sel[0], g = new THREE.Group(); g.name = 'avatar (12 minimums)';
    const mat = c => new THREE.MeshBasicMaterial({ color: c });
    const box = (w, d, h, x, z, c) => { const m = new THREE.Mesh(new THREE.BoxGeometry(w, d, h), mat(c)); m.position.set(x, 0, z + h / 2); g.add(m); return m; };
    box(1.3, 1.3, 5.0, -0.85, 0, 0x1f2a4a); box(1.3, 1.3, 5.0, 0.85, 0, 0x1f2a4a);    // legs 0..5
    box(3.6, 2.0, 3.8, 0, 5.0, 0x2f5f