<!-- SOLUV · part 483 -->
 tp.position.set(0, 12, 0); tp.userData.billboard = true;
    crown.add(c1, c2, c3, beam, tp);
    crown.traverse(o => { if (o.isMesh && !o.material.isShaderMaterial) applyMinimum(o, THREE); });
    rings.traverse(o => { if (o.isMesh) applyMinimum(o, THREE); });
    need = true;
  }
  layoutCrown();
  const beaconG = new THREE.BufferGeometry(); beaconG.setAttribute('position', new THREE.BufferAttribute(new Float32Array([0, 0, 0]), 3)); beaconG.setAttribute('aSize', new THREE.BufferAttribute(new Float32Array([46]), 1)); beaconG.setAttribute('aPhase', new THREE.BufferAttribute(new Float32Array([0.3]), 1)); beaconG.setAttribute('aColor', new THREE.BufferAttribute(new Float32Array([0.7, 1, 1]), 3));
  const beacon = new THREE.Points(beaconG, new THREE.ShaderMaterial({ vertexShader: STAR_VS, fragmentShader: STAR_FS, uniforms: { uT, uPR: { value: PR } }, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
  beacon.name = 'beacon'; beacon.renderOrder = 6; scene.add(beacon);

  /* ---------- camera: an eye that flies (position + yaw + pitch); the tower is solid ---------- */
  const st = { x: -6, y: 1.6, z: 9, yaw: 0, pitch: 1.5 };
  const lastEye = new THREE.Vector3(-6, 1.6, 9), fwd = new THREE.Vector3();
  let anim = null, rise = null, userMoved = false, reading = false, fling = 0;
  const cssH = () => canvas.clientHeight || innerHeight, cssW = () => canvas.clientWidth || innerWidth;
  const tanH = () => Math.tan(camera.fov * Math.PI / 360);
  function aim(eye, tgt) { const d = [tgt[0] - eye[0], tgt[1] - eye[1], tgt[2] - eye[2]]; return { x: eye[0], y: eye[1], z: eye[2], yaw: Math.atan2(d[0], -d[2]), pitch: Math.atan2(d[1], Math.hypot(d[0], d[2])) }; }
  function applyCamera() {
    st.pitch = Math.max(-1.565, Math.min(1.565, st.pitch));
    const r = Math.hypot(st.x, st.z); if (r > 6000) { st.x *= 6000 / r; st.z *= 6000 / r; }
    st.y = Math.max(r <= PLATFORM_R ? 0.4 : -4000, Math.min(9000, st.y));
    camera.position.set(st.x, st.y, st.z);
    for (const w of walls) camera.position.copy(w.wall.resolve(camera.position, 0.35, lastEye));
    st.x = camera.position.x; st.y = camera.position.y; st.z = camera.position.z; lastEye.copy(camera.position);
    const cp = Math.cos(st.pitch); fwd.set(Math.sin(st.yaw) * cp, Math.sin(st.pitch), -Math.cos(st.yaw) * cp);
    camera.lookAt(camera.position.x + fwd.x, camera.position.y + fwd.y, camera.position.z + fwd.z);
    sky.position.copy(camera.position); stars.position.copy(camera.position); auraU.uCam.value.copy(camera.position);
    need = true;
  }
  const distForPpe = ppe => EM * cssH() / (2 * tanH() * ppe);
  const views = {
    base: () => aim([-6, 1.6, 9], [0, 700, 0.9]),                                // the shot the King loved
    mid: () => aim([-34, topY * 0.47, 30], [0, topY * 0.96, 0]),
    top: () => aim([-16, topY + 26, 22], [0, 0, 3]),
    far: () => aim([1250, 30, 1900], [0, topY * 0.5, 0]),
    live: () => { const a = wall.wall.lineAnchor(); const d = distForPpe(READ_PPE), half = d * tanH(), z = L.TXT_Z * EM + d; const y = a ? a.local.y + half * 0.8 : 4; return { x: 0, y, z, yaw: 0, pitch: 0 }; },
  };
  function setReading(on) { reading = !!on; fling = 0; wrap.classList.toggle('sp-reading', reading); }
  function flyTo(v, ms = 1600, auto = false) { if (!auto) userMoved = true; rise = null; rideGoal = null; setOn(); anim = { a: { ...st }, b: v, t0: performance.now(), ms }; need = true; }
  function stopMotion() { anim = null; rise = null; rideGoal = null; setOn(); }
  /* RIDE: wheel / trackpad / keys / soluvKeys.scroll move up and down smoothly (eased to a goal), clamped:
     in Read mode to the text column, otherwise from the platform to just above the crown. */
  let rideGoal = null;
  const pxWorld = () => reading ? EM / READ_PPE : Math.max(2 * Math.max(1, wall.wall.distanceTo(camera.position)) * tanH() / cssH(), 0.15);
  function clampY(y) { if (reading) { const t = wall.wall.textRect(), half = distForPpe(READ_PPE) * tanH(); return Math.max(t.y0 + half * 0.6, Math.min(t.y1 + half * 0.2, y)); } return Math.max(0.4, Math.min(topY + 60, y)); }
  function haltAuto() { anim = null; rise = null; setOn(); fling = 0; userMoved = true; }
  function rideBy(dy) { rideGoal = clampY((rideGoal === null ? st.y : rideGoal) + dy); need = true; return rideGoal; }
  function enterRead() { if (!reading) { setReading(true); Object.assign(st, views.live()); applyCamera(); } }
  function goTop() { haltAuto(); enterRead(); rideGoal = clampY(1e9); need = true; }
  function goNewest() { setReading(true); flyTo(views.live(), 1200); }
  function goLine(i) { const a = wall.wall.lineAnchor(i); if (!a) return false; setReading(true); const v = views.live(); flyTo({ ...v, y: a.local.y }, 1200); return true; }
  /* Rise: the elevator ride from the foot to the crown, looking up the face as it climbs, the stars opening out */
  function startRise() { userMoved = true; anim = null; rideGoal = null; setReading(false); rise = { t0: perf