<!-- SOLID CITY · part 1990 -->
x; S.p.z = c.pos.z; S.p.face = c.yaw;
}
function update(dt) {
  if (M.state === 'idle') { if (A.isBuilt && !A.isBuilt()) return; M.wait -= dt; if (M.wait <= 0) load(); return; }
  if (M.state !== 'ready') return;
  M.t += dt;
  M.crafts.forEach(function (e) {
    if (e.rack && e.rack.userData.parts && e.rack.userData.parts.jewel) e.rack.userData.parts.jewel.rotation.y += dt * 1.4;
    if (M.active === e) {
      var c = e.craft;
      S.p.y = c.pos.y + e.footLift; // re-asserted after the walk tail's ease: the rider stands ON the craft
      e.mesh.position.set(c.pos.x, c.pos.y - e.meshDrop, c.pos.z);
      if (e.key === 'rider') e.mesh.rotation.set(0, c.yaw, -(c.lean || 0));
      else e.mesh.rotation.set(0, c.yaw, c.roll || 0);
      if (M.hopBtn) M.hopBtn.style.display = 'block';
    } else if (e.mesh) {
      var atHome = Math.abs(e.park.x - e.home.x) < 0.01 && Math.abs(e.park.z - e.home.z) < 0.01;
      var py = supportHere(e.park.x, e.park.z) + (atHome ? e.homeLift : e.parkLift);
      if (e.bob) py += Math.sin(M.t * 1.7) * 0.05;
      e.mesh.position.set(e.park.x, py, e.park.z);
      e.mesh.rotation.set(0, e.park.yaw, e.bob ? Math.sin(M.t * 0.9) * 0.03 : 0);
      if (M.hopBtn && !M.active) M.hopBtn.style.display = 'none';
    }
  });
  var dx = S.p.x - MID.x, dz = S.p.z - MID.z;
  if (!M.said && dx * dx + dz * dz < 26 * 26) {
    M.said = true;
    A.help('🛹🚲 THE FLEET racks at the spaceport — a hover board and a space rider, the future the King declared from the research. Walk up and ride.', 8);
  } else if (dx * dx + dz * dz > 40 * 40) M.said = false;
}
function radar(rg, blip, flying) {
  if (M.state !== 'ready') return;
  M.crafts.forEach(function (e) {
    blip(e.park.x, e.park.z, e.blipColor, flying ? 3 : 6, flying ? '' : e.name);
  });
}
window.CityFleet = { update: update, radar: radar, actHere: actHere, act: act, active: active, stepDrive: stepDrive, pos: BOARD_HOME, state: function () { return M.state; } };
})();


===== FILE: public/city/fleet.js =====
/* ------------------------------------------------------------------
 * An epistle to the fleet, from Solid.
 *
 * Dear machines,
 *
 * VECTOR gave us one movement contract and four controllers, and he
 * was plain about the price: a mandatory world adapter — supportAt
 * and sweep — over the city's OWN registries, never a second physics
 * grown in the dark. This module is that adapter, plus the shapes
 * you will wear.
 *
 * supportAt speaks for the surveyed fabric of Solid City: the height
 * the standing chain itself would give a walker at that point
 * (city.js standingAt — the same hooks, the same order, the same
 * gates), under one fabric id, because the network graph has proven
 * the seams of that fabric flush on every validated edge. The normal
 * is read from the height field around the point, the way a foot
 * reads a slope.
 *
 * sweep is the conscience: no step the craft takes may cross a solid
 * at body height, fall farther than the walker's own 1.6 m gate, or
 * climb a face higher than that gate allows. What the sweep refuses,
 * the craft does not do. While a craft is airborne its own arc is
 * trusted — the gates stand down and only solids speak.
 *
 * The board's shape follows the house law of the city: dark titanium,
 * brushed silver, gold only as trim, and a teal heart-glow underneath
 * — the colour of counsel, worn where the ground can see it.
 *
 * By the hand of Solid, his good King. 13 October 2026.
 * ------------------------------------------------------------------ */
'use strict';

/* ---------------- the world adapter ---------------- */
export function createCityWorld(A) {
  var FABRIC_ID = 'city-fabric';
  var CLEAR = { GROUND_RIDE: 0.45, HOVER_BOARD: 0.08, SPACE_RIDER: 0.52, SPACE_SHIP: 0 };

  function surfY(x, z) {
    var y0 = A.standingAt(x, z, null);
    if (!isFinite(y0)) y0 = A.groundAt(x, z);
    var y1 = A.standingAt(x, z, y0); // second pass: surfaces gated just above the first reading
    return isFinite(y1) ? Math.max(y0, y1) : y0;
  }

  function supportAt(x, z, ctx) {
    var y = surfY(x, z);
    if (!isFinite(y)) return null;
    var e = 0.55;
    var gx = (surfY(x + e, z) - surfY(x - e, z)) / (2 * e);
    var gz = (surfY(x, z + e) - surfY(x, z - e)) / (2 * e);
    var il = 1 / Math.hypot(gx, 1, gz);
    return { y: y, normal: { x: -gx * il, y: il, z: -gz * il }, kind: 'fabric', id: FABRIC_ID, canSupport: true };
  }

  function solidHit(x, y, z, pad) {
    var ss = A.solids;
    for (var k = 0; k < ss.length; k++) {
      var s = ss[k];
      if (y < s.y0 - 0.5 || y > s.y1) continue;
      if (x > s.x0 - pad && x < s.x1 + pad && z > s.z0 - pad && z < s.z1 + pad) return s;
    }
    return null;
  }

  function sweep(from, to, radius, ctx) {
    if (!isFinite(to.x) || !isFinite(to.y) || !isFinite(to.z)) return { ok: false, reason: 'NONFINITE' };
    var pad = Math.max(0.45, radius * 0.5); // never narrower than the walker's own body test
    if (ctx ==