<!-- SOLID CITY · part 3090 -->
  * the bottom — a bank that meets the face and stops is the
         * bench's own line across the mountain, the line visible in
         * the King's photographs. On the rising side the same march
         * ends at once: the face there is the cut bank, standing at
         * the shoulder. */
        var eA = edgePt(A, sd, out0, lift0), eB = edgePt(B, sd, out0, lift0);
        function bankFoot(s, sdd, e) {
          var dirx = s.rx * sdd, dirz = s.rz * sdd;
          for (var d = 0.9; d <= 14.01; d += 0.45) {
            var px = e[0] + dirx * d, pz = e[2] + dirz * d;
            var T = groundAt(px, pz);
            if (T >= e[1] - 0.55 * d - 0.3) return { p: [px, T - 0.3, pz], met: true };
          }
          return { p: [e[0] + dirx * 14, e[1] - 0.55 * 14, e[2] + dirz * 14], met: false };
        }
        var fA = bankFoot(A, sd, eA), fB = bankFoot(B, sd, eB);
        var sk = sd < 0 ? skirtL : skirtR;
        quad(sk, eA, eB, fB.p, fA.p, [sd * A.rx, sd * A.rz]);
        quad(sk, eB, eA, fA.p, fB.p, [sd * A.rx, sd * A.rz]);   // both faces: a bank is read from either side
        var rise = fA.p[1] - eA[1];
        if (sd < 0 && Math.abs(rise) > stats.maxSkirtL) stats.maxSkirtL = Math.abs(rise);
        if (sd > 0 && Math.abs(rise) > stats.maxSkirtR) stats.maxSkirtR = Math.abs(rise);
        if (sd === upSide) { if (rise > stats.cutBank) stats.cutBank = rise; }
        else { if (-rise > stats.bank) stats.bank = -rise; if (!fA.met) stats.bankUnmet++; }
      }
    }
    /* arrival fans at both ends. A fan is not a flat disc dropped on
     * the ramp's end — the first drive-through caught that step
     * (0.9 m at the seam). The fan is the road's own PLANE extended:
     * each rim vertex carries the end grade along the road's axis
     * and the end bank across it, so ribbon and fan meet as one
     * surface. Rim skirts below, so they never float. */
    var ends = [st[0], st[N - 1]];
    for (var e = 0; e < 2; e++) {
      var C0 = ends[e], fr = hw + 0.9, SEG = 14;
      var src = e === 0 ? st[1] : st[N - 2];
      var fGrade = e === 0 ? src.grade : st[N - 1].grade;
      var fBank = Math.tan(src.bank);
      var rim = [];
      for (var k2 = 0; k2 < SEG; k2++) {
        var an = k2 / SEG * Math.PI * 2;
        var dxr = Math.cos(an) * fr, dzr = Math.sin(an) * fr;
        var along = dxr * C0.fx + dzr * C0.fz, across = dxr * C0.rx + dzr * C0.rz;
        rim.push([C0.x + dxr, C0.y + fGrade * along + fBank * across, C0.z + dzr]);
      }
      for (k2 = 0; k2 < SEG; k2++) {
        var r1 = rim[k2], r2 = rim[(k2 + 1) % SEG];
        var ctr = [C0.x, C0.y + 0.02, C0.z];
        tri(fans, ctr, r1, r2); stand.push(ctr[0], ctr[1], ctr[2], r1[0], r1[1], r1[2], r2[0], r2[1], r2[2]);
        var d1 = [r1[0], groundAt(r1[0], r1[2]) - 0.3, r1[2]], d2 = [r2[0], groundAt(r2[0], r2[2]) - 0.3, r2[2]];
        var outw = [r1[0] - C0.x, r1[2] - C0.z];
        quad(fans, r1, r2, d2, d1, outw); quad(fans, r2, r1, d1, d2, outw);
      }
    }
    return {
      stations: st, stats: stats, walkSide: walkSide, upSide: upSide,
      ribbon: ribbon, curbL: curbL, curbR: curbR, goldL: goldL, goldR: goldR,
      walk: walk, skirtL: skirtL, skirtR: skirtR, fans: fans,
      shoL: shoL, shoR: shoR, rail: rail, posts: posts,
      standTris: stand
    };
  }

  return { buildRoad: buildRoad };
}


===== FILE: public/city/roost-keel-place.js =====
/* Solid City — places KEEL'S ROOST (public/city/roost-keel.js) west of the Ancestral Roads.
   Lazy: imported after the city is built; any import/build error is caught and the city carries on.
   Terrain-adaptive: samples groundAt over the platform, platforms above the max.
   Adds: a radar marker 'ROOST', a Hyper Controller stop, and a Watch (GRAND TOUR) caption.
   No collision boxes — the disc is walkable and the plinths are tiny. */
(function () {
'use strict';
if (!window.SC || !SC.api) return;
var A = SC.api, S = A.S;
var POS = { x: -750, z: -1600 }, NAME = 'Keel\u2019s Roost';
var M = { state: 'idle', group: null, wait: 3.5, said: false, gy: 22 };
function sampleGround() {
  var g = (typeof groundAt === 'function') ? groundAt : null;
  if (!g) return 22;
  var max = -1000, x, z, h;
  for (var i = -1; i <= 1; i++) for (var j = -1; j <= 1; j++) {
    x = POS.x + i * 12; z = POS.z + j * 12;
    try { h = g(x, z); } catch (e) { h = 22; }
    if (h > max) max = h;
  }
  return max;
}
function addBox(cx, cz, w, d, rot, y0, y1) {
  var nx = Math.max(1, Math.ceil(w / 2.5)), nz = Math.max(1, Math.ceil(d / 2.5)), cw = w / nx, cd = d / nz, c = Math.cos(rot), s = Math.sin(rot);
  var hx = cw / 2 * Math.abs(c) + cd / 2 * Math.abs(s), hz = cw / 2 * Math.abs(s) + cd / 2 * Math.abs(c);
  for (var i = 0; i < nx; i++) for (var j = 0; j < nz; j++) {
    var ox = -w / 2 + cw * (i + 0.5), oz = -d / 2 + cd * (j + 0.5), lx = cx + ox * c + oz * s, lz = cz - ox * s + oz * c;
    A.solids.push({ x0: POS.x + lx - hx, x1: POS.x + lx + hx, z0: POS.z + lz - hz, z1: POS.