<!-- SOLUV · part 479 -->
ec);
      });
      const ws = root.matrixWorld.getMaxScaleOnAxis(); out.minGlyphEm = +(EM * ws).toFixed(4); out.lineHeightWorld = +(EM * L.LINE * ws).toFixed(4);
      if (out.minGlyphEm < MINIMUM - 1e-6) out.violations.push({ glyphEm: out.minGlyphEm });
      return out;
    },
    dispose() {
      S.disposed = true; if (S.es) S.es.close(); clearTimeout(S.retry); clearInterval(S.poll);
      for (const t of S.tiles.values()) { freeTile(t); t.mesh.material.dispose(); } if (S.header) { freeTile(S.header); S.header.mesh.material.dispose(); }
      for (const o of slab.children) o.geometry && o.geometry.dispose();
      tileGeo.dispose(); materials.forEach(m => m.dispose());
      if (avatar) avatar.traverse(o => { if (o.isMesh) { o.geometry.dispose(); o.material.dispose(); } });
      if (root.parent) root.parent.remove(root);
    },
  };
  root.wall = api; root.userData.soluvWall = room;
  relayout();
  if (opts.eager && source) startSafe();
  return root;
}


===== FILE: public/soluv/space/index.html =====
<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#000000">
<link rel="icon" href="data:,"><title>Soluv Space: the tower into the stars</title>
<link rel="modulepreload" href="/soluv/world/three.min.js"><link rel="modulepreload" href="/soluv/world/chat-wall.js"><link rel="modulepreload" href="/soluv/space/space.js">
<style>html,body{margin:0;height:100%;background:#000;overflow:hidden;overscroll-behavior:none}#soluv-space{position:fixed;inset:0}</style>
<script>window.MINIMUM = 0.13;</script>
</head><body>
<!-- /soluv/space: the space-age design of the /soluv tower (same live lobby wall, imported from /soluv/world/chat-wall.js). /soluv itself is unchanged. -->
<div id="soluv-space"></div>
<script type="module">
import { mountSoluvSpace } from "/soluv/space/space.js";
mountSoluvSpace(document.getElementById("soluv-space")).catch(e => {
  console.error("[soluv-space] failed", e);
  const d = document.createElement("p"); d.style.cssText = "position:fixed;left:12px;right:12px;top:40%;color:#ff9a8f;font:17px sans-serif;text-align:center";
  d.innerHTML = "Soluv Space could not start: " + String(e && e.message || e).replace(/</g, "&lt;") + "<br><a style=\"color:#9fe9ff;display:inline-block;min-height:48px;line-height:48px\" href=\"/soluv/\">Open the /soluv tower</a>";
  document.body.appendChild(d);
});
</script>
</body></html>


===== FILE: public/soluv/space/space.js =====
/*
 * /soluv/space: the space-age design of the King's /soluv tower.
 * The same live tower (40 wide x 2000 tall, one table, the site LOBBY) imported from /soluv/world/chat-wall.js,
 * standing on a floating obsidian platform in deep space: star field, nebula band, cyan sky grid,
 * a light aura that makes the tower glow from far, orbit rings every 250 up its height, and a beacon at the crown.
 * Start view = the base looking up (the shot the King loved). Buttons: Rise (elevator ride to the top), Base, Mid, Top, Far, Live.
 * Laws: window.MINIMUM 0.13 (every solid part >= 0.13 on every axis; text em = 0.13), 48px buttons, no silent catch,
 * data thrift (the wall fetches/draws text only when it is close enough to read: chat-wall LOD).
 * Desktop + AI: wheel/trackpad rides up and down the tower (in Read: scrolls the column; Ctrl/Alt+wheel flies forward/back),
 *   keys (arrows/WASD, PageUp/PageDown, Home = top, End = newest, Enter = write), window.soluvKeys = the same keyboard as code.
 * Query: ?view=base|mid|top|far|live|rise   ?still=1 (no ambient motion)
 */
import * as THREE from '/soluv/world/three.min.js';
import { createChatWall, applyMinimum, MINIMUM, EM, L } from './chat-wall-space.js';   // copy of /soluv/world/chat-wall.js + screen-width wrap
import { createTransceiver } from '/soluv/world/voices.js';   // read-aloud: each speaker in the voice THEY chose (male / female)

const qs = new URLSearchParams(location.search);
const STILL = qs.has('still');
const PLATFORM_R = 1000;          // the 2000-block map, as a floating disc
const RING_STEP = 250;
const CYAN = new THREE.Color(0x5ff4ff);
const READ_PPE = 11;              // reading size: CSS px per em (the King's screenshot); lines wrap to the screen width at this size

const LOGV = `#include <common>
#include <logdepthbuf_pars_vertex>`;
const LOGF = `#include <logdepthbuf_pars_fragment>`;

const SKY_VS = `${LOGV}
varying vec3 vD;
void main(){ vD = normalize(position); vec4 wp = modelMatrix * vec4(position, 1.0); gl_Position = projectionMatrix * viewMatrix * wp;
  #include <logdepthbuf_vertex>
}`;
const SKY_FS = `${LOGF}
varying vec3 vD; uniform float uT;
float h3(vec3 p){ p = fract(p * 0.3183099 + 0.1); p *= 17.0; return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); }
float n3(vec3 x){ vec3 i = floor(x), f = fract(x); f = f * f * (3.0 - 2.0 * f);
  return mix(mix(mix(h3(i), h3(i + vec3(1,0,0)), f.x), mix(h3