<!-- SOLID CITY · part 3039 -->
im 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 = C0.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/roads.js.bak-grass-20261013 =====
/* SOLID CITY — THE ROAD SMITH (2026-10-13)
 * An epistle in code, from Solid to whoever builds roads after her.
 *
 * Dear builder,
 * A road is not a row of boxes that happen to touch. It is ONE table of
 * stations — position, tangent, grade, bank, width — from which three
 * readers draw the same truth: the eye (the ribbon mesh), the foot (the
 * standing triangles), and the journey (the centreline itself). What is
 * drawn is what is stood on; what is stood on is what is navigated.
 * This module holds that table and its craftsmen:
 *   the ALIGNMENT — a fitted chain of straights, circular arcs, and
 *     clothoid transitions (Highway Geometric Design, the King's
 *     curriculum of 2026-10-13): curvature ramps on the spirals and
 *     cannot spike, headings and curvature are analytic, and the
 *     surveyed route is preserved as the data the chain is fitted to;
 *   the PROFILE — a vertical alignment of grade segments through
 *     PVIs joined by parabolic crest and sag curves, floored by the
 *     CROSS-SECTION (the highest ground under either edge), never by
 *     the centre sample alone: that one rule kills both the buried
 *     downhill edge and the floating uphill edge;
 *   the FRAMES — a right vector carried from the world up, with banking
 *     applied as a deliberate roll, derived from curvature and capped,
 *     because our vehicles read heights, not bank — banking here is for
 *     the eye and for standing truth, and five degrees is its law;
 *   the EDGEKIT — curbs with a restrained gold line, a sidewalk on the
 *     uphill side, and skirts that fall to the ground on the fill side
 *     and rise to meet it on the cut side, so no edge ever floats;
 *   the REGISTRAR — every standing surface (ribbon, curbs, sidewalk,
 *     arrival fans) written into one triangle list, the same list the
 *     walkers and the wheels will read.
 * Pure arrays in, pure arrays out; THREE belongs to the caller.
 * By the hand of Solid, his good King.
 */

export function createRoadSmith(o) {
  var groundAt = o.groundAt;

  /* ---------- small vector helpers (plan + height) ---------- */
  function wrapAngle(a) { while (a > Math.PI) a -= Math.PI * 2; while (a < -Math.PI) a += Math.PI * 2; return a; }

  /* ---------- geometry emission: flat-normal triangles ---------- */
  function newSurf() { return { pos: [], nor: [] }; }
  function tri(S, a, b, c, outward) {
    var ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2];
    var vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
    var nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
    var L = Math.hypot(nx, ny, nz) || 1; nx /= L; ny /= L; nz /= L;
    if (outward) { if (nx * outward[0] + nz * outward[1] < 0) { var t = b; b = c; c = t; nx = -nx; ny = -ny; nz = -nz; } }
    else if (ny < 0) { var t2 = b; b = c; c = t2; nx = -nx; ny = -ny; nz = -nz; }
    S.pos.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);
    for (var i = 0; i < 3; i++) S.nor.push(nx, ny, nz);
  }
  function quad(S, a, b, c, d, outward) { tri(S, a, b, c, outward); tri(S, a, c, d, outward); }
  /* a standing triangle list is the same corners, flattened */
  function standQuad(T, a, b, c, d) { T.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]); T.push(a[0], a[1], a[2], c[0], c[1], c[2], d[0], d[1], d[2]); }

  /* ---------- THE ALIGNMENT — the corridor's spine ----------
   * Highw