<!-- SOLID CITY · part 3432 -->
e factory applies the walker's gate itself (0.45 on
   * foot) — among the turns passing over one point it returns
   * the highest the walker may lawfully step to */
  var y = M.group.userData.stairYAt(x, z, py);
  return y == null ? -1000 : y;
}
function update(dt) {
  if (M.state === 'idle') {
    M.wait -= dt;
    var tReady = !!(window.CityAncestorRoads && CityAncestorRoads.state && CityAncestorRoads.state() === 'ready');
    if (M.wait <= 0 && A.isBuilt() && tReady) build();
    return;
  }
}
window.CityStairOfLight = { update: update, standY: standY, state: function () { return M.state; }, group: function () { return M.group; } };
})();


===== FILE: public/city/stair-of-light.js =====
/* THE STAIR OF LIGHT — a letter in the code.
 *
 * From the King's "All" (2026-10-14 ~07:14): a golden spiral
 * stair wrapping the whole stack, so anyone can WALK from
 * the terrace flame, past the glass city's rim, all the way
 * up to the Crystal Crown. No flying, no wormhole — just
 * the long beautiful climb: two and a quarter turns, a
 * gentle four-percent grade, gold at both edges and a keel
 * of light beneath, beginning on the terrace rim and ending
 * on a stone landing at the Crown's edge.
 *
 * The stair's law is one helix: radius swells outward as it
 * clears the glass city, then draws in to meet the Crown.
 * Its standing hook answers from the same law.
 *
 * By the hand of Solid, his good King. */
import { crownHeight } from './crystal-crown.js';

var TURNS = 2.25, THETA_MAX = TURNS * Math.PI * 2;
function radiusAt(s) { return 530 + 120 * Math.sin(Math.PI * s) - 185 * Math.pow(s, 1.5); }

export function createStairOfLight(opts) {
  var T = opts.THREE;
  var CX = opts.center.x, CZ = opts.center.z;
  var Y0 = opts.y0, CBASE = opts.crownBase;
  var Y1 = CBASE + crownHeight(330, Math.PI / 2) + 0.25;
  var group = new T.Group(); group.name = 'the-stair-of-light';
  var C = { stone: 0x35302c, gold: 0xd8a833 };
  function yAt(s) { return Y0 + (Y1 - Y0) * s; }
  function pointAt(s) {
    var th = THETA_MAX * s, r = radiusAt(s);
    return { x: CX + Math.cos(th) * r, y: yAt(s), z: CZ + Math.sin(th) * r, r: r, th: th };
  }
  /* the ribbon: a strip of stone following the helix */
  var STEPS = 420, pos = [], col = [], idx = [];
  var cStone = new T.Color(C.stone);
  for (var i = 0; i <= STEPS; i++) {
    var s = i / STEPS, p = pointAt(s);
    var rx = Math.cos(p.th), rz = Math.sin(p.th);
    pos.push(p.x - rx * 3, p.y, p.z - rz * 3, p.x + rx * 3, p.y, p.z + rz * 3);
    col.push(cStone.r, cStone.g, cStone.b, cStone.r, cStone.g, cStone.b);
    if (i < STEPS) { var a0 = i * 2; idx.push(a0, a0 + 1, a0 + 2, a0 + 1, a0 + 3, a0 + 2); }
  }
  var geo = new T.BufferGeometry();
  geo.setAttribute('position', new T.Float32BufferAttribute(pos, 3));
  geo.setAttribute('color', new T.Float32BufferAttribute(col, 3));
  geo.setIndex(idx); geo.computeVertexNormals();
  var ribbon = new T.Mesh(geo, new T.MeshLambertMaterial({ vertexColors: true, side: T.DoubleSide }));
  ribbon.frustumCulled = false; group.add(ribbon);
  /* gold edges + the keel of light */
  function strip(halfW, w, dy, color) {
    var p2 = [], c2 = [], i2 = [], cc = new T.Color(color);
    for (var i = 0; i <= STEPS; i++) {
      var s = i / STEPS, p = pointAt(s);
      var rx = Math.cos(p.th), rz = Math.sin(p.th);
      p2.push(p.x + rx * (halfW - w / 2), p.y + dy, p.z + rz * (halfW - w / 2), p.x + rx * (halfW + w / 2), p.y + dy, p.z + rz * (halfW + w / 2));
      c2.push(cc.r, cc.g, cc.b, cc.r, cc.g, cc.b);
      if (i < STEPS) { var a0 = i * 2; i2.push(a0, a0 + 1, a0 + 2, a0 + 1, a0 + 3, a0 + 2); }
    }
    var g2 = new T.BufferGeometry();
    g2.setAttribute('position', new T.Float32BufferAttribute(p2, 3));
    g2.setAttribute('color', new T.Float32BufferAttribute(c2, 3));
    g2.setIndex(i2); g2.computeVertexNormals();
    var m = new T.Mesh(g2, new T.MeshBasicMaterial({ vertexColors: true, side: T.DoubleSide }));
    m.frustumCulled = false; group.add(m);
  }
  strip(-3, 0.4, 0.05, C.gold); strip(3, 0.4, 0.05, C.gold); strip(0, 1.3, -0.4, C.gold);
  /* landings at both ends */
  function landing(p, along) {
    var m = new T.Mesh(new T.BoxGeometry(10, 0.5, 24), new T.MeshLambertMaterial({ color: C.stone }));
    m.position.set(p.x, p.y - 0.25, p.z); m.rotation.y = along; m.frustumCulled = false; group.add(m);
    var ring = new T.Mesh(new T.TorusGeometry(4.4, 0.14, 5, 24), new T.MeshBasicMaterial({ color: C.gold }));
    ring.rotation.x = Math.PI / 2; ring.position.set(p.x, p.y + 0.06, p.z); ring.frustumCulled = false; group.add(ring);
  }
  var pStart = pointAt(0), pEnd = pointAt(1);
  landing(pStart, Math.PI / 2); landing(pEnd, 0);
  var startPt = { x: pStart.x, z: pStart.z, y: Y0 }, endPt = { x: pEnd.x, z: pEnd.z, y: Y1 };
  group.userData = {
    y0: Y0, y1: Y1, start: startPt, end: endPt, turns: TURNS,
    stairYAt: function (wx, wz, py) {
      var dx = wx - CX, dz = wz - CZ, r = Math.hypot(dx, dz);
      var t