<!-- SOLID CITY · part 3430 -->
ids[i]; if (blocking(s) && distToAABB(p.x, p.z, s) < p.clear) { bad = s; break; } }
      results.push({ name: 'point:' + p.name, ok: !bad, detail: bad ? 'solid within ' + p.clear + 'm at [' + bad.x0.toFixed(0) + ',' + bad.z0.toFixed(0) + ']' : 'clear ' + p.clear + 'm' });
    });
    (spec.corridors || []).forEach(function (cd) {
      var L = Math.hypot(cd.x1 - cd.x0, cd.z1 - cd.z0), steps = Math.max(2, Math.ceil(L / 1.5)), bad = null, badAt = '';
      for (var st = 0; st <= steps && !bad; st++) {
        var px = cd.x0 + (cd.x1 - cd.x0) * st / steps, pz = cd.z0 + (cd.z1 - cd.z0) * st / steps;
        for (var i = 0; i < solids.length; i++) { var s = solids[i]; if (blocking(s) && distToAABB(px, pz, s) < cd.halfW) { bad = s; badAt = px.toFixed(0) + ',' + pz.toFixed(0); break; } }
      }
      results.push({ name: 'corridor:' + cd.name, ok: !bad, detail: bad ? 'intruded at ' + badAt : 'clear, half-width ' + cd.halfW + 'm' });
    });
    (spec.areas || []).forEach(function (a) {
      var bad = null;
      for (var i = 0; i < solids.length; i++) { var s = solids[i]; if (blocking(s) && distToAABB(a.x, a.z, s) < a.r) { bad = s; break; } }
      results.push({ name: 'area:' + a.name, ok: !bad, detail: bad ? 'solid inside r=' + a.r : 'clear r=' + a.r });
    });
    if (spec.ownFootprints) {
      var boxes = spec.ownFootprints.map(function (b) { return { x0: b[0] - b[2] / 2, x1: b[0] + b[2] / 2, z0: b[1] - b[3] / 2, z1: b[1] + b[3] / 2 }; });
      var overlap = null;
      for (var i = 0; i < boxes.length && !overlap; i++) for (var j = i + 1; j < boxes.length; j++) {
        var A = boxes[i], B = boxes[j];
        if (A.x0 < B.x1 && A.x1 > B.x0 && A.z0 < B.z1 && A.z1 > B.z0) overlap = i + 'x' + j;
      }
      results.push({ name: 'own-footprints-overlap', ok: !overlap, detail: overlap ? 'footprints ' + overlap + ' overlap' : boxes.length + ' footprints, no overlaps' });
    }
    return { pass: results.every(function (r) { return r.ok; }), results: results };
  }

  return { palette: palette, marble: marble, bolt: bolt, cloneMarble: cloneMarble, corridor: corridor, pylon: pylon, validateLayout: validateLayout, distToAABB: distToAABB };
}

/* ---------------- THE SPACEPORT (local coords: origin = pad centre) ----------------
   Landmarks in local coords (pad centre = world (-380,330)):
     disembark point (0,-52) · city-road junction east (+64,-52) · workshop gate (-48,-106)
     open hangar centre (+73,+35), its back edge z=+50 · gantry (+30,-6) [existing, untouched]
   standFloors use the same local frame; the placer adds the district offset. */
function createSpaceport(opts) {
  var T = opts.THREE;
  var kit = createSpatialKit(T, { curve: opts.curve });
  var P = kit.palette;
  var g = new T.Group();
  var foot = [];
  var pulse = [];   // materials/meshes animated in update()

  /* Exterior additions are plain and functional only (the King's interiors-only
     clarification): dark-metal bollards flank the pad's north exit — BESIDE the
     path (16m clear gap), never on it — with the city's red glow tips. */
  [-8, 8].forEach(function (bx) {
    var bol = new T.Mesh(new T.CylinderGeometry(0.15, 0.18, 1.1, 10), P.titanium);
    bol.position.set(bx, 0.55, -42); g.add(bol);
    var tip = new T.Mesh(new T.SphereGeometry(0.24, 10, 8), P.redGlow.clone());
    tip.position.set(bx, 1.22, -42); g.add(tip); pulse.push(tip);
    foot.push([bx, -42, 0.6, 0.6, 0, 1.1, 0]);
  });

  /* C3 — the arrivals junction: a plain reserved-zone marker under the disembark
     point (dark disc, the city's red ring) — this ground is the reserved entry/
     spawn zone (section 2) and stays clear of every solid. */
  var med = new T.Mesh(new T.CircleGeometry(5, 40), new T.MeshLambertMaterial({ color: 0x14161c }));
  med.rotation.x = -Math.PI / 2; med.position.set(0, 0.075, -52); g.add(med);
  var medRing = new T.Mesh(new T.RingGeometry(4.85, 5.15, 48), P.redGlow);
  medRing.rotation.x = -Math.PI / 2; medRing.position.set(0, 0.08, -52); g.add(medRing);

  /* C3 — Corridor EAST: disembark -> the city road (main circulation, 6m wide) */
  g.add(kit.corridor(-6, -52, 64, -52, 6));
  /* C3 — Corridor NORTH-WEST: disembark -> the hangar hub's workshop gate (5m wide).
     It ends 7m short of the gate, square to it: validation proved the last metres
     beside the gate post broke the 2m clearance reserve. */
  g.add(kit.corridor(0, -52, -44, -102, 5));

  /* C2 — the arrivals pylon: front-right of a player who has just disembarked and
     faces the city — beside BOTH corridors, inside neither. */
  var pyl = kit.pylon({ x: 6, z: -58, labelTex: opts.labelTex, title: 'ORBITAL SPACEPORT', sub: 'ARRIVALS · BOARD · LAUNCH · FLY' });
  g.add(pyl); foot.push.apply(foot, pyl.userData.footprint);

  /* C5 — the open hangar's INTERIOR, finished (the aesthetic lives indoors — the
     King's clarification): black-red marble floor, and a back wall whose INNER
     (-z) face is black-red marble carrying the twin