<!-- SOLUV · part 659 -->
toggle, panel,
    state() { return { readAloud: T.on, myVoice: T.mine, overrides: { ...T.overrides }, deviceVoices: { male: T.picked.male && T.picked.male.name, female: T.picked.female && T.picked.female.name }, spoken: T.spoken, problem: T.problem, canSpeak: !!synth, canDictate: !!SR }; },
  };
}

function injectStyle() {
  if (document.getElementById('sv-style')) return;
  const s = document.createElement('style'); s.id = 'sv-style';
  const F = '"Soluv Condensed","Roboto Condensed","Arial Narrow",sans-serif';
  s.textContent = `
  .sv-panel{position:absolute;left:8px;right:8px;top:calc(max(8px,env(safe-area-inset-top)) + 64px);max-height:min(60vh,520px);overflow:auto;z-index:7;box-sizing:border-box;padding:10px;border-radius:14px;background:#02090ff2;border:1px solid #2bb8d8aa;color:#dff6ff;font:15px/1.35 ${F};box-shadow:0 0 18px #00d0ff44;max-width:560px;margin:0 auto}
  .sv-panel[hidden]{display:none}
  .sv-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}.sv-head b{font-size:17px;letter-spacing:.08em;color:#aef8ff}
  .sv-line{display:flex;gap:8px;flex-wrap:wrap;margin:4px 0}
  .sv-b,.sv-mic{box-sizing:border-box;min-width:48px;min-height:48px;padding:0 14px;border-radius:12px;border:1px solid #2bb8d8aa;background:#00141d;color:#cff7ff;font:600 15px/1 ${F};cursor:pointer;touch-action:manipulation}
  .sv-b.sv-on,.sv-mic.sv-on{background:#0b4a5e;color:#fff}
  .sv-mic{height:48px;min-width:56px;flex:none;border-color:#c9a227aa;color:#ffd98a;background:#2a2008}
  .sv-small{font-size:13px;color:#8fb6c9;margin:4px 0}
  .sv-bad{font-size:14px;color:#ff9a8f;margin:4px 0}
  .sv-row{display:flex;align-items:center;gap:8px;padding:2px 0;border-top:1px solid #0e3a48}
  .sv-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:700}
  .sv-why{font-size:12px;color:#7fb6c9;white-space:nowrap}
  `;
  document.head.appendChild(s);
}


===== FILE: public/soluv/world/world.js =====
/*
 * /soluv world: the King's shared map. 2000 x 2000 blocks floored with Playa 2.0's gold 10-unit blocks, black sky grid, unlimited sky.
 * Wall 1 = the site LOBBY itself (read + post through the lobby's own endpoints). Wall 2 = Playa's /soluv room.
 * /soluv = the whole map (all walls, starting with wall 1). /soluv/N = deep link into wall N.
 * Every wall is a portable createChatWall() object (see chat-wall.js); the world just places them.
 * Desktop + AI: wheel/trackpad rides the tower up and down (Ctrl/Alt+wheel or pinch zooms), arrows/WASD, PageUp/PageDown,
 *        Home = top, End = newest, Enter = write; window.soluvKeys = the same keyboard as code (see the Codex guide #95).
 * Query: ?pillars=N (walls on the map, default 1)  ?test=1 (wall 1 shows TEST TEXT instead of the live room)
 *        ?view=map|wide|far|mid|near|live (start view)
 */
import * as THREE from './three.min.js';
import { createChatWall, applyMinimum, MINIMUM, EM, L, BLOCK } from './chat-wall.js';
import { createTransceiver } from './voices.js';   // read-aloud: each speaker in the voice THEY chose (male / female)

const MAP = 2000, CELL = 50;
const qs = new URLSearchParams(location.search);
const routeMatch = location.pathname.match(/\/soluv\/(\d+)\/?$/) || location.hash.match(/#\/soluv\/(\d+)/);
const deep = routeMatch ? Math.max(1, parseInt(routeMatch[1], 10)) : null;
const pillars = Math.max(1, Math.min(500, parseInt(qs.get('pillars') || '1', 10) || 1, 500), deep || 1);
const test = qs.get('test');

const GROUND_FS = `#include <logdepthbuf_pars_fragment>
varying vec3 vW;
void main(){
  #include <logdepthbuf_fragment>
  vec2 p = vW.xz; vec2 fw = fwidth(p);
  vec2 g = abs(fract(p / ${BLOCK}.0 - 0.5) - 0.5) * ${BLOCK}.0;          // Playa 2.0 gold blocks: 10-unit squares
  float l = 1.0 - min(1.0, min(g.x / (fw.x * 1.2 + 0.03), g.y / (fw.y * 1.2 + 0.03)));
  l *= 1.0 - smoothstep(1.5, 6.0, max(fw.x, fw.y));                          // fade when a block is under a few pixels
  vec2 g2 = abs(fract(p / 100.0 - 0.5) - 0.5) * 100.0;                       // every 10th line a little brighter so the map reads from far
  float l2 = 1.0 - min(1.0, min(g2.x / (fw.x * 1.2 + 0.06), g2.y / (fw.y * 1.2 + 0.06)));
  float edge = smoothstep(${MAP / 2 - 8}.0, ${MAP / 2}.0, max(abs(p.x), abs(p.y)));
  vec3 gold = vec3(0.541, 0.416, 0.110);                                     // 0x8a6a1c
  vec3 col = vec3(0.006, 0.005, 0.003) + gold * 0.45 * l + gold * 0.35 * l2 + vec3(0.83, 0.63, 0.09) * edge * 0.8;
  gl_FragColor = vec4(col, 1.0);
}`;
const GROUND_VS = `#include <common>
#include <logdepthbuf_pars_vertex>
varying vec3 vW;
void main(){ vec4 wp = modelMatrix * vec4(position, 1.0); vW = wp.xyz; gl_Position = projectionMatrix * viewMatrix * wp;
  #include <logdepthbuf_vertex>
}`;
const SKY_VS = `#include <common>
#include <logdepthbuf_pars_vertex>
varying vec3 vD;
void main(){ vD = normalize(position); vec4 wp = modelMatrix * vec4(position, 1.0); gl_Position = projectionMatri