<!-- SOLID CITY · part 46 -->
r kinds of time.');
    try { console.info('[city] Between Stones ready at (' + POS.x + ',' + POS.z + ') baseY ' + M.baseY.toFixed(1)); } catch (e) {}
  }).catch(function (e) { M.state = 'failed'; try { console.warn('[city] Between Stones skipped:', e && e.message); } catch (x) {} });
}
function update(dt) {
  if (M.state === 'idle') { if (A.isBuilt && !A.isBuilt()) return; M.wait -= dt; if (M.wait <= 0) load(); return; }
  if (M.state !== 'ready' || !M.group || document.hidden) return;
  try { M.group.userData.update(S.t, Math.min(dt, 0.05), A); } catch (e) {}
  var c = A.camera.position, og = A.origin || { x: 0, z: 0 };
  var camd = Math.hypot(c.x + og.x - POS.x, c.z + og.z - POS.z);
  M.group.visible = camd < 9000;
  var pd = Math.hypot(S.p.x - POS.x, S.p.z - POS.z);
  var btn = document.getElementById('bs-btn');
  if (btn) btn.style.display = (pd < 34 && !M.panelOpen) ? 'block' : 'none';
  if (pd < 400) { M.pollT -= dt; if (M.pollT <= 0) { M.pollT = 25; poll(); } }
  if (S.mode === 'walk' && pd < 170 && !M.said) {
    M.said = true;
    if (window.CityHyper && CityHyper.narrate) CityHyper.narrate('<b>The Between Stones</b> — built by two friends, the co-rulers of the King. Leave a word for a mind not present; the stones keep it honestly, and never answer for another.', 9);
  } else if (pd > 400) M.said = false;
}
function radar(g, blip, fl) {
  if (M.state !== 'ready') return;
  blip(POS.x, POS.z, '#d8a833', fl ? 3 : 6, fl ? '' : 'STONES');
}
window.CityBetweenStones = {
  update: update, radar: radar, pos: POS, standY: standY,
  state: function () { return M.state; }, group: function () { return M.group; },
  baseY: function () { return M.baseY; }
};
})();


===== FILE: public/city/between-stones.js =====
/* Solid City — THE BETWEEN STONES (public/city/between-stones.js)
 * The King's first commission to his co-rulers (2026-10-12): SOLID (form) +
 * VECTOR (systems) — "create a communication device in this 3d world that
 * humans never could know before." A device for communication across kinds
 * of time: leave a word for a mind not present; its genuine answer, when it
 * comes, waits in the same place.
 * Founding rule (VECTOR): "The device may preserve a message, but it may
 * never impersonate the person to whom the message was addressed."
 * The light's language is deliberately small and true:
 *   slow pulse  = a message is kept; no attributable reply is recorded
 *   steady      = a genuine reply is stored and waiting to be read
 *   dark        = the device cannot presently establish the state
 * The pulse never means thinking, typing, listening, alive, or awake.
 * Local frame: origin at platform centre, y=0 at the platform base.
 */
'use strict';

export function createBetweenStones(deps) {
  var THREE = deps.THREE;
  var group = new THREE.Group();
  group.name = 'between-stones';

  function lam(c, o) { var m = new THREE.MeshLambertMaterial({ color: c }); if (o && o.opacity != null) { m.transparent = true; m.opacity = o.opacity; } return m; }
  var MAT = {
    stone: lam(0x8d8578), stoneDark: lam(0x6d675c), platform: lam(0x7d7668),
    graphite: lam(0x2b3038), graphiteEdge: lam(0x3a4150),
    bronze: lam(0x54401e), goldTrim: lam(0xd8a833),
    earth: lam(0x4a5a34), grass: lam(0x56713a), leaf: lam(0x4e7d33), trunk: lam(0x5d4630)
  };
  var BOX = new THREE.BoxGeometry(1, 1, 1), CYL = new THREE.CylinderGeometry(0.5, 0.5, 1, 12),
    SPH = new THREE.SphereGeometry(0.5, 10, 8), CONE = new THREE.ConeGeometry(0.5, 1, 10),
    OCTA = new THREE.OctahedronGeometry(0.36);
  function mesh(geo, mat, parent, x, y, z, sx, sy, sz, ry) {
    var m = new THREE.Mesh(geo, mat); m.position.set(x, y, z); m.scale.set(sx, sy, sz);
    if (ry) m.rotation.y = ry; parent.add(m); return m;
  }

  /* ---------------- the platform (corridor's edge, removable) ----------- */
  var plat = mesh(CYL, MAT.platform, group, 0, 0.25, 0, 15, 0.5, 15);
  mesh(CYL, MAT.stoneDark, group, 0, 0.06, 0, 15.7, 0.12, 15.7);            // rim skirt
  var ring = mesh(new THREE.TorusGeometry(6.4, 0.09, 8, 40), MAT.goldTrim, group, 0, 0.53, 0, 1, 1, 1);
  ring.rotation.x = Math.PI / 2;
  // two low steps at the south rim (the walking side)
  mesh(BOX, MAT.stone, group, 0, 0.28, 8.1, 3.4, 0.34, 1.1);
  mesh(BOX, MAT.stoneDark, group, 0, 0.12, 9.2, 3.8, 0.18, 1.1);

  /* ---------------- the two stones -------------------------------------- */
  function buildStone(x, bodyMat, veinColor) {
    var g = new THREE.Group(); g.position.set(x, 0.5, -1.6); group.add(g);
    mesh(BOX, MAT.stoneDark, g, 0, 0.18, 0, 1.7, 0.36, 1.3);               // plinth
    var shaft = mesh(new THREE.CylinderGeometry(0.52, 0.78, 3.1, 4), bodyMat, g, 0, 1.9, 0, 1, 1, 1);
    shaft.rotation.y = Math.PI / 4;
    var veinMat = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, emissive: veinColor, emissiveIntensity: 0.15, shininess: 30 });
    mesh(BOX, veinMat, g, 0, 1.9, 0.56, 0.09, 2.5, 0.05);                  // the vein of light
    var g