<!-- SOLID CITY · part 3223 -->
 too — "they
   * all can contribute to the space traffic"). The placer
   * brings them from the mountain build's published ports;
   * they are landing places for shuttles, ferries, and the
   * grand houses' own craft — not train stations. */
  var EXTRA = (opts.extraStations || []).map(function (p, i) { return { id: p.id || ('pv' + i), name: p.name, x: p.x, z: p.z, gy: p.gy, grand: !!p.grand, ownPad: false }; });
  var LANDINGS = STATIONS.filter(function (s) { return !s.ownPad; }).concat(EXTRA);

  /* ================= ROUTES =================
   * nodes: stations by id, or [x, z] for a flyover bend that is
   * flown but never stopped at (the Coastal bends). */
  var ROUTES = [
    { name: 'THE MOUNTAIN LINE', nodes: ['spaceport', 'southern', 'western', 'gate', 'braid', 'eastern', 'high', 'summit'] },
    { name: 'THE NORTH LINE', nodes: ['eastern', 'shoulder', 'wheights', 'north', 'farnorth'] },
    { name: 'THE GRAND COASTAL', nodes: ['hall', [0, 500], [200, 1500], [1100, 2100], 'gap', 'kauhale'] },
    { name: 'THE WARD LINE', nodes: ['ward7', 'ward5', 'southern'] }
  ];

  var MAT = {
    titanium: new T.MeshLambertMaterial({ color: 0x232c3d }),
    silver: new T.MeshLambertMaterial({ color: 0x9aa4b5 }),
    stone: new T.MeshLambertMaterial({ color: 0x55504c }),
    stoneDark: new T.MeshLambertMaterial({ color: 0x35302c }),
    glass: new T.MeshLambertMaterial({ color: 0x1a2436, transparent: true, opacity: 0.6 }),
    gold: new T.MeshBasicMaterial({ color: 0xd8a833 }),
    goldDim: new T.MeshBasicMaterial({ color: 0x8a6a1f }),
    white: new T.MeshBasicMaterial({ color: 0xf2f6ff }),
    red: new T.MeshBasicMaterial({ color: 0xff4a3d })
  };

  function box(w, h, d, mat, x, y, z) {
    var m = new T.Mesh(new T.BoxGeometry(w, h, d), mat);
    m.position.set(x || 0, y || 0, z || 0);
    return m;
  }

  /* ================= PROFILES =================
   * The track's top surface, sampled every ~8 m along the route
   * polyline: high enough for 25 m of sky under the beam at
   * cruise, pinned at every station to (gy - 0.55) so the train
   * floats its floor flush with the pad, grade-limited to 14%
   * so the climb is a climb and not a wall. */
  function buildProfile(route) {
    var pts = [];
    route.nodes.forEach(function (nd) {
      if (typeof nd === 'string') { var s = ST[nd]; pts.push({ x: s.x, z: s.z, station: s }); }
      else pts.push({ x: nd[0], z: nd[1], station: null });
    });
    var xs = [], zs = [], pin = [], isPin = [];
    for (var i = 0; i < pts.length - 1; i++) {
      var a = pts[i], b = pts[i + 1], L = Math.hypot(b.x - a.x, b.z - a.z), n = Math.max(2, Math.round(L / 8));
      for (var j = (i === 0 ? 0 : 1); j <= n; j++) {
        var t = j / n;
        xs.push(a.x + (b.x - a.x) * t); zs.push(a.z + (b.z - a.z) * t);
        var st = (j === 0 && a.station) ? a.station : (j === n && b.station) ? b.station : null;
        pin.push(st ? st.gy - 0.55 : null); isPin.push(!!st);
      }
    }
    var N = xs.length, y = new Array(N), terr = new Array(N);
    for (var i2 = 0; i2 < N; i2++) { terr[i2] = heightAt(xs[i2], zs[i2]); y[i2] = terr[i2] + 25; }
    for (var i3 = 0; i3 < N; i3++) if (pin[i3] !== null) y[i3] = pin[i3];
    for (var pass = 0; pass < 6; pass++) {
      for (var f = 1; f < N; f++) { var mx = y[f - 1] + 8 * 0.14; if (y[f] > mx) y[f] = mx; if (pin[f] !== null) y[f] = pin[f]; }
      for (var bk = N - 2; bk >= 0; bk--) { var mx2 = y[bk + 1] + 8 * 0.14; if (y[bk] > mx2) y[bk] = mx2; if (pin[bk] !== null) y[bk] = pin[bk]; }
    }
    for (var i4 = 0; i4 < N; i4++) if (pin[i4] === null) { var floor = Math.min(terr[i4] + 25, terr[i4] + 10); if (y[i4] < floor) y[i4] = floor; }
    var cum = [0];
    for (var i5 = 1; i5 < N; i5++) cum.push(cum[i5 - 1] + Math.hypot(xs[i5] - xs[i5 - 1], zs[i5] - zs[i5 - 1], y[i5] - y[i5 - 1]));
    var stationsAt = [];
    for (var i6 = 0; i6 < N; i6++) if (pin[i6] !== null) {
      /* recover which station: nearest pinned node */
    }
    route.pts = { xs: xs, ys: y, zs: zs, cum: cum, len: cum[N - 1], pin: pin };
    route.stops = [];
    var nodeIdx = 0;
    route.nodes.forEach(function (nd) {
      if (typeof nd !== 'string') return;
      var s = ST[nd];
      var best = 0, bd = 1e12;
      for (var q = 0; q < N; q++) { var d = (xs[q] - s.x) * (xs[q] - s.x) + (zs[q] - s.z) * (zs[q] - s.z); if (d < bd) { bd = d; best = q; } }
      route.stops.push({ station: s, s: cum[best], idx: best });
      nodeIdx++;
    });
    route.stops.sort(function (p, q2) { return p.s - q2.s; });
    return route;
  }
  ROUTES.forEach(buildProfile);

  function sample(route, s) {
    var P = route.pts;
    if (s <= 0) s = 0; if (s > P.len) s = P.len;
    var lo = 0, hi = P.cum.length - 1;
    while (lo < hi - 1) { var mid = (lo + hi) >> 1; if (P.cum[mid] <= s) lo = mid; else hi = mid; }
    var span = P.cum[hi] - P.cum[lo] || 1, t = (s - P.cum[lo]) / span;
    return {
      x: P.xs[lo] + (P.xs[hi] - P.xs[lo]) * t,
      y: