<!-- SOLUV · part 660 -->
x * viewMatrix * wp;
  #include <logdepthbuf_vertex>
}`;
const SKY_FS = `#include <logdepthbuf_pars_fragment>
varying vec3 vD;
void main(){
  #include <logdepthbuf_fragment>
  float lon = atan(vD.z, vD.x) / 6.28318 * 72.0; float lat = asin(clamp(vD.y, -1.0, 1.0)) / 1.5708 * 18.0;
  vec2 g = abs(fract(vec2(lon, lat) - 0.5) - 0.5) / max(fwidth(vec2(lon, lat)), vec2(1e-4));
  float l = 1.0 - min(1.0, min(g.x, g.y) / 1.2);
  float up = smoothstep(-0.02, 0.15, vD.y);
  vec3 col = vec3(0.0) + vec3(0.03, 0.16, 0.22) * l * up * (1.0 - vD.y * 0.5) + vec3(0.0, 0.03, 0.05) * (1.0 - smoothstep(0.0, 0.25, abs(vD.y)));
  gl_FragColor = vec4(col, 1.0);
}`;

export async function mountSoluvWorld(container) {
  const t0 = performance.now();
  injectStyle();
  container.classList.add('sw-root');
  const wrap = document.createElement('div'); wrap.className = 'sw-wrap';
  wrap.innerHTML = `<canvas class="sw-canvas" aria-label="The Soluv world: chat walls on a 2000 block map"></canvas>
  <div class="sw-status" role="status"></div>
  <div class="sw-ctrls">
    <button class="sw-btn" data-a="in" aria-label="Closer">+</button>
    <button class="sw-btn" data-a="out" aria-label="Farther">\u2212</button>
    <button class="sw-btn sw-txt sw-live" data-a="live" aria-label="Newest line">Live</button>
    <button class="sw-btn sw-txt" data-a="far" aria-label="Whole wall">Wall</button>
    <button class="sw-btn sw-txt" data-a="map" aria-label="Whole map">Map</button>
    <button class="sw-btn sw-txt" data-a="glide" aria-label="Glide down the columns">Glide</button>
    <button class="sw-btn sw-txt sw-num" data-a="next" aria-label="Next wall">1</button>
    ${qs.has('place') ? '<button class="sw-btn sw-txt sw-place" data-a="place" aria-label="Place a room: tap two gold blocks">Place</button>' : ''}
    <button class="sw-btn sw-txt sw-voice" data-a="voice" aria-label="Read aloud and voices">Voice</button>
    <a class="sw-btn sw-txt sw-room" href="/soluv/room.html" aria-label="Open the flat dense room">Room</a>
  </div>
  <div class="sw-pick" hidden><div class="sw-pick-t">Which room fills this wall?</div>
    <button type="button" data-room="lobby">Lobby</button><button type="button" data-room="soluv">Soluv room</button>
    <button type="button" data-room="new" disabled title="needs a server change: rooms beyond the lobby and /soluv do not exist yet">New room N (needs server)</button>
    <button type="button" data-room="cancel">Cancel</button></div>
  <div class="sw-toast" hidden><span class="sw-toast-t"></span><button type="button" class="sw-undo">Undo</button></div>
  <button class="sw-tear" type="button" hidden>Tear down</button>
  <a class="sw-archive" hidden target="_blank" rel="noopener"></a>
  <div class="sw-badge"></div><div class="sw-labels" aria-hidden="true"></div>
  <form class="sw-compose" autocomplete="off"><input class="sw-input" maxlength="16000" enterkeyhint="send" aria-label="Write to this wall's room" placeholder="Checking who you are…"><button class="sw-send" type="submit">Send</button><a class="sw-login" href="/login">Log in to post</a></form>
  <button class="sw-goto" type="button" aria-label="Walk up to the wall to post">\u270e Walk up to post</button>`;
  container.appendChild(wrap);
  const canvas = wrap.querySelector('canvas'), statusEl = wrap.querySelector('.sw-status'), badge = wrap.querySelector('.sw-badge'), numBtn = wrap.querySelector('.sw-num');

  const gl2 = canvas.getContext('webgl2', { antialias: true }) ;
  if (!gl2) { statusEl.textContent = 'This browser has no WebGL2: open the flat room instead (Room button).'; throw new Error('no WebGL2'); }
  const renderer = new THREE.WebGLRenderer({ canvas, context: gl2, antialias: true, logarithmicDepthBuffer: true });
  renderer.setPixelRatio(Math.min(2, devicePixelRatio || 1));
  const scene = new THREE.Scene(); scene.background = new THREE.Color(0x000000);
  const camera = new THREE.PerspectiveCamera(50, 1, 0.05, 40000);

  // the map: one 2000 x 2000-block slab (2 minimums thick), grid drawn in the shader
  const groundMat = new THREE.ShaderMaterial({ vertexShader: GROUND_VS, fragmentShader: GROUND_FS });
  const ground = new THREE.Mesh(new THREE.BoxGeometry(MAP, 2 * MINIMUM, MAP), groundMat); ground.position.y = -MINIMUM; ground.name = 'map 2000x2000'; scene.add(ground);
  // the sky: a black grid dome that travels with the camera, so there is no ceiling anywhere
  const sky = new THREE.Mesh(new THREE.SphereGeometry(15000, 64, 32), new THREE.ShaderMaterial({ vertexShader: SKY_VS, fragmentShader: SKY_FS, side: THREE.BackSide, depthWrite: false }));
  sky.name = 'sky grid (follows camera, unlimited)'; sky.renderOrder = -10; scene.add(sky);
  applyMinimum(ground, THREE);

  // walls: wall 1 at the centre; more walls ring the plaza, every one facing the centre
  const walls = [];
  for (let n = 1; n <= pillars; n++) {
    let pos = [0, 0, 0], rot = 0;
    if (n > 1) { let k = n - 2, ring = 1; while (k >= ring * 8) { k -= ring * 8; r