<!-- SOLID CITY · part 2667 -->
PORT', x: 650, z: -1265, y: 34.8, kind: 'natural' },
    { name: 'SHOULDER BENCHES PORT', x: 890, z: -1850, y: 88.14, kind: 'natural' },
    { name: 'WESTERN HEIGHTS PORT', x: 780, z: -1865, y: 84.32, kind: 'natural' },
    { name: 'NORTH BENCHES PORT', x: 665, z: -2060, y: 73.0, kind: 'natural' },
    { name: 'WESTERN BENCHES PORT', x: -365, z: -1075, y: 14.13, kind: 'natural' },
    { name: 'SOUTHERN BENCHES PORT', x: -490, z: -1165, y: 9.16, kind: 'natural' },
    { name: 'BRAID HEAD PORT', x: 890, z: -1345, y: 146.09, kind: 'terrace' },
    { name: 'EASTERN BENCHES PORT', x: 1045, z: -1635, y: 124.58, kind: 'terrace' },
    { name: 'HIGH BENCHES PORT', x: 1205, z: -1560, y: 179.33, kind: 'terrace' },
    { name: 'SUMMIT PORT', x: 1299, z: -1591, y: 198.95, kind: 'terrace' },
    { name: 'FAR NORTH PORT', x: 515, z: -2360, y: 15.29, kind: 'terrace' },
    { name: 'HALL OF BECOMING PORT', x: 1055, z: -295, y: 155.91, kind: 'terrace' },
    { name: 'KAUHALE MAUNA PORT', x: 2035, z: 1995, y: 263.33, kind: 'terrace' },
    { name: 'THE GAP PORT', x: 2059, z: 1870, y: 301.59, kind: 'terrace' }
  ];
  function buildPorts() {
    PORT_SITES.forEach(function (P) {
      var SEG = 24, ctr = [P.x, P.y, P.z], rim = [], k;
      for (k = 0; k < SEG; k++) { var an = k / SEG * Math.PI * 2; rim.push([P.x + Math.cos(an) * 10, P.y, P.z + Math.sin(an) * 10]); }
      var S = { pos: [], nor: [] }, SK = { pos: [], nor: [] }, GL = { pos: [], nor: [] };
      for (k = 0; k < SEG; k++) {
        var r1 = rim[k], r2 = rim[(k + 1) % SEG];
        triPush(S, ctr, r1, r2);
        standTris.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.4, r1[2]], d2 = [r2[0], groundAt(r2[0], r2[2]) - 0.4, r2[2]];
        triPush(SK, r1, r2, d2); triPush(SK, r1, d2, d1);
        var a1 = k / SEG * Math.PI * 2, a2 = (k + 1) / SEG * Math.PI * 2;
        var g1 = [P.x + Math.cos(a1) * 8.55, P.y + 0.02, P.z + Math.sin(a1) * 8.55], g2 = [P.x + Math.cos(a2) * 8.55, P.y + 0.02, P.z + Math.sin(a2) * 8.55];
        var g3 = [P.x + Math.cos(a2) * 9.0, P.y + 0.02, P.z + Math.sin(a2) * 9.0], g4 = [P.x + Math.cos(a1) * 9.0, P.y + 0.02, P.z + Math.sin(a1) * 9.0];
        triPush(GL, g1, g2, g3); triPush(GL, g1, g3, g4);
      }
      pourSurf(S, solid, C.stone);
      pourSurf(SK, solid, C.stoneDark);
      pourSurf(GL, glowB, 0xd8a833);
      // the beacon: one lamp standing on the pad's north rim
      var bx = P.x, bz = P.z - 10.8;
      BASE = M4(bx, P.y - 0.1, bz, 0, 0, 0);
      streetLight();
      BASE = new T.Matrix4();
      foot.push([bx, bz, 0.3, 0.3, P.y - 0.1, P.y + 5.2, 0]);
      foot.push([P.x, P.z, 20, 20, P.y - 6, P.y, 0]);
      ports.push({ name: P.name, x: P.x, z: P.z, y: P.y, kind: P.kind });
    });
  }
  // a junction APRON: a fan whose rim is stitched to the roads
  // themselves — each rim point takes its height from the nearest
  // stations' own planes (grade along, bank across), the highest
  // claim winning, as the world's standing law also takes the
  // highest — so the apron meets every road on that road's own
  // terms, and its skirts fall to the land. A disc planed from one
  // station is a table wherever the host's own surface curves;
  // an apron stitched to all the roads is a junction.
  function apronAt(x, z, host) {
    if (ROADS_REJECTED) return;   // THE REJECTION: no junction discs rise
    var S = { pos: [], nor: [] }, SEG = 20, fr = 7.0, ctr = [x, host.y + 0.03, z];
    var all = [];
    for (var rn in smithRoads) all = all.concat(smithRoads[rn].stations);
    function heightAt(px, pz) {
      var best = null, wsum = 0, ysum = 0;
      for (var q = 0; q < all.length; q++) {
        var s2 = all[q], dx = px - s2.x, dz = pz - s2.z;
        var dd = dx * dx + dz * dz;
        if (dd > 100) continue;
        var y = s2.y + s2.grade * (dx * s2.fx + dz * s2.fz) + Math.tan(s2.bank) * (dx * s2.rx + dz * s2.rz);
        var wgt = 1 / ((dd + 0.5) * (dd + 0.5)); wsum += wgt; ysum += wgt * y;
      }
      best = wsum > 0 ? ysum / wsum : host.y + host.grade * ((px - x) * host.fx + (pz - z) * host.fz) + Math.tan(host.bank) * ((px - x) * host.rx + (pz - z) * host.rz);
      return best + 0.03;
    }
    var rim = [];
    for (var k = 0; k < SEG; k++) { var an = k / SEG * Math.PI * 2, px = x + Math.cos(an) * fr, pz = z + Math.sin(an) * fr; rim.push([px, heightAt(px, pz), pz]); }
    // THE FAIRING OF THE RIM: two passes of a quarter, a half,
    // a quarter around the circle — no single sector is asked
    // to climb the whole braid at once.
    for (var fp = 0; fp < 2; fp++) { var fairs = rim.map(function (r3, ri) { var rp2 = rim[(ri + SEG - 1) % SEG], rn2 = rim[(ri + 1) % SEG]; return [r3[0], rp2[1] * 0.25 + r3[1] * 0.5 + rn2[1] * 0.25, r3[2]]; }); rim = fairs; }
    for (k = 0; k < SEG; k++) {
      var r1 = rim[k], r2 = rim[(k + 1) % SEG];
      triPush(S, ctr, r1, r2);
      standTris.push(ctr[0], ctr[1], ctr[2]