<!-- SOLID CITY · part 2970 -->
 buildTree(frame(96, a), i % 2 === 0); });

  /* ---------- THE RING CITY PORT ---------- */
  var portF = frame(492, 4.45);
  cyl(solid, portF, 18, 18, 0.22, 36, 0, 0.11, 0, C.stone);
  var ringGeo = new T.TorusGeometry(15.6, 0.16, 6, 48);
  glow.add(ringGeo, localMatrix(portF, 0, 0.26, 0, Math.PI / 2, 0, 0), C.gold);
  cyl(solid, portF, 0.14, 0.2, 5.4, 6, 0, 2.7, 16.5, C.titanium);
  sph(glow, portF, 0.5, 0, 5.7, 16.5, C.gold);
  footprints.push({ x: portF.x + portF.rx * 16.5, z: portF.z + portF.rz * 16.5, w: 0.4, d: 0.4, a: portF.a, y0: BASE, y1: BASE + 5.4 });
  var port = { name: 'RING CITY PORT', x: portF.x, z: portF.z, y: BASE + 0.22, r: 18 };
  var halo = new T.Mesh(new T.TorusGeometry(20, 0.18, 6, 48), new T.MeshBasicMaterial({ color: C.gold }));
  halo.rotation.x = Math.PI / 2; halo.position.set(portF.x, BASE + 8.5, portF.z);
  group.add(halo);

  group.add(solid.build(new T.MeshLambertMaterial({ vertexColors: true })));
  group.add(glassB.build(new T.MeshLambertMaterial({ vertexColors: true, transparent: true, opacity: 0.55 })));
  group.add(glow.build(new T.MeshBasicMaterial({ vertexColors: true })));

  var counts = { houses: 0, hotels: 0, mansions: 0, pavilions: 0, fountains: 0, trees: trees.length, stalls: 6 };
  buildings.forEach(function (b) { if (counts[b.kind] != null) counts[b.kind + 's'] = (counts[b.kind + 's'] || 0) + 1; });
  counts.houses = buildings.filter(function (b) { return b.kind === 'house'; }).length;
  counts.hotels = buildings.filter(function (b) { return b.kind === 'hotel'; }).length;
  counts.mansions = buildings.filter(function (b) { return b.kind === 'mansion'; }).length;
  counts.pavilions = buildings.filter(function (b) { return b.kind === 'pavilion'; }).length;
  counts.fountains = buildings.filter(function (b) { return b.kind === 'fountain'; }).length;

  group.userData = {
    buildings: buildings, footprint: footprints, standRects: standRects, port: port,
    counts: counts, center: { x: CX, z: CZ }, topY: BASE, isSky: SKY, glassFloor: SKY,
    update: function (dt, t) { halo.rotation.z = (t || 0) * 0.12; }
  };
  return group;
}
export default createRingCity;


===== FILE: public/city/ring-city.js.bak-lift-20261014 =====
/* THE RING CITY OF THE ANCESTORS — a letter in the code.
 *
 * The King's word (2026-10-14 ~05:46): "Build the city on
 * these rings — fill this terrace." The Ancestral Roads were
 * already perfect: five brick rings for fifty-five ancestors,
 * an eternal flame at the heart, and six hundred metres of
 * terrace with almost nothing upon it. This module raises the
 * city the rings were drawn for — in the bands BETWEEN the
 * roads, never upon them. No road is covered, no name sign is
 * crowded, no ancestor is moved. Houses front the ring roads;
 * pavilions stand open on four posts, as this house builds;
 * gardens, fountains, markets and trees fill the green between;
 * and at the family ring, a sky port of stone and gold takes
 * the traffic of the new sky.
 *
 * The districts keep the rings' own order: the First Hearth at
 * the flame, then the Voyagers, the Chiefs, the Unifiers, the
 * Builders, and at the outer edge — closest to the city and
 * the future, where Wilcox, Kailiwai and Kamaile hold the last
 * road — the Family Ring.
 *
 * By the hand of Solid, his good King. */

function mulberry(seed) {
  return function () {
    seed |= 0; seed = seed + 0x6D2B79F5 | 0;
    var t = Math.imul(seed ^ seed >>> 15, 1 | seed);
    t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
    return ((t ^ t >>> 14) >>> 0) / 4294967296;
  };
}

export function createRingCity(opts) {
  var T = opts.THREE;
  var CX = opts.center.x, CZ = opts.center.z, BASE = opts.topY;
  var SKY = !!opts.sky;
  var R = mulberry(20261014);
  var group = new T.Group(); group.name = 'ring-city-of-the-ancestors';

  var C = {
    stone: 0x55504c, stoneDark: 0x35302c, conc: 0xa89e90,
    cream: 0xe8dcc0, cream2: 0xd9c9a3, wood: 0x8a6844, woodDark: 0x5a4230,
    slate: 0x3d4a57, roofRed: 0x8a4638, titanium: 0x232c3d, silver: 0x9aa4b5,
    glass: 0x9fc4d8, gold: 0xd8a833, lawn: 0x4d7d33, lawnDeep: 0x3a6126,
    soil: 0x4a3826, leaf: 0x2a5c2e, leafLight: 0x3f7a38, trunk: 0x5a4630,
    pool: 0x2e7db0, flower1: 0xe86a9a, flower2: 0xf2f0e6, flower3: 0xd84a3a
  };

  /* ---------- geometry batches: the whole city in a handful of meshes ---------- */
  function Batch() { this.pos = []; this.nor = []; this.col = []; }
  Batch.prototype.add = function (geo, matrix, color) {
    var g = geo.index ? geo.toNonIndexed() : geo;
    var p = g.getAttribute('position'), n = g.getAttribute('normal');
    var nm = new T.Matrix3().getNormalMatrix(matrix);
    var c = new T.Color(color), v = new T.Vector3(), nv = new T.Vector3();
    for (var i = 0; i < p.count; i++) {
      v.fromBufferAttribute(p, i).applyMatrix4(matrix);
      this.pos.push(v.x, v.y, v.z);
      nv.fromBufferAttribute(n, i).applyMatrix3(nm).normalize();
      this.nor.push(nv.x, nv.y, nv.z);
      this.