<!-- SOLID CITY · part 3034 -->
 * abz || 1);
        t = Math.max(0, Math.min(1, t));
        var dx = p[0] - (a[0] + abx * t), dy = p[1] - (a[1] + aby * t), dz = p[2] - (a[2] + abz * t);
        return Math.sqrt(dx * dx + dy * dy + dz * dz);
      }
      var g0 = 0;
      while (g0 < SEG) {
        var ctr2 = [C0.x, C0.y + 0.02, C0.z];
        tri(fans, ctr2, rim[g0], rim[(g0 + 1) % SEG]); stand.push(ctr2[0], ctr2[1], ctr2[2], rim[g0][0], rim[g0][1], rim[g0][2], rim[(g0 + 1) % SEG][0], rim[(g0 + 1) % SEG][1], rim[(g0 + 1) % SEG][2]);
        var g1 = g0;
        while (g1 + 2 < SEG && g1 - g0 < 5) {
          var far = g1 + 2, bad2 = false;
          if (rimProf[g0].length < 3 || rimProf[far].length < 3) break;
          for (var m2 = g0 + 1; m2 < far && !bad2; m2++) {
            if (rimProf[m2].length < 3) { bad2 = true; break; }
            if (fdev(rim[m2], rim[g0], rim[far]) > 0.3) { bad2 = true; break; }
            for (var ln2 = 1; ln2 < 3; ln2++) if (fdev(rimProf[m2][ln2], rimProf[g0][ln2], rimProf[far][ln2]) > 0.3) { bad2 = true; break; }
          }
          if (bad2) break;
          g1++;
          tri(fans, ctr2, rim[g1], rim[(g1 + 1) % SEG]); stand.push(ctr2[0], ctr2[1], ctr2[2], rim[g1][0], rim[g1][1], rim[g1][2], rim[(g1 + 1) % SEG][0], rim[(g1 + 1) % SEG][1], rim[(g1 + 1) % SEG][2]);
        }
        var qA = rimProf[g0], qB = rimProf[(g1 + 1) % SEG];
        var outw = [rim[g0][0] - C0.x, rim[g0][2] - C0.z];
        for (var dk2 = 0; dk2 < Math.min(2, qA.length - 1); dk2++) { if (!qB[dk2 + 1]) continue; quad(fans, qA[dk2], qB[dk2], qB[dk2 + 1], qA[dk2 + 1], outw); quad(fans, qB[dk2], qA[dk2], qA[dk2 + 1], qB[dk2 + 1], outw); }   // the fan's rim: brow painted as the fan
        if (qA.length - 1 > 2 && qB.length - 1 > 2) { quad(bankGrass, qA[2], qB[2], qB[qB.length - 1], qA[qA.length - 1], outw); quad(bankGrass, qB[2], qA[2], qA[qA.length - 1], qB[qB.length - 1], outw); }   // and its deep rim in grass, per the Amendment
        g0 = g1 + 1;
      }
    }
    stats.fairDev = fairDev;
    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, bankGrass: bankGrass,
      shoL: shoL, shoR: shoR, rail: rail, posts: posts, cliffW: cliffW,
      standTris: stand
    };
  }

  return { buildRoad: buildRoad };
}


===== FILE: public/city/roads.js.bak-finish-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 CENTERLINE — a Catmull–Rom spline through surveyed waypoints,
 *     resampled by arc length, so curvature is a property of the road
 *     and not an accident of the drawing;
 *   the PROFILE — station heights faired under a grade cap, 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]