<!-- SOLID CITY · part 690 -->
P.x, P.z) < -1.5) { P.x -= (P.x - (P.lx || P.x)); P.z -= (P.z - (P.lz || P.z)); gy = groundAt(P.x, P.z); help('The sea is too deep to walk — take a ship from the spaceport.', 3); }
  P.lx = P.x; P.lz = P.z;
  if (RF) {
    if (S.rflyLand) { if (P.y <= gy + 0.02) { P.y = gy; S.rfly = false; S.rflyLand = false; S.rflyV = 0; if (window.CityRoyal && CityRoyal.onLand) CityRoyal.onLand(); } }
    else if (P.y < gy) P.y = gy;
  } else P.y += (gy - P.y) * Math.min(1, dt * 12);
  if (!RF && P.y < Y0 + 0.5 && Math.hypot(P.x, P.z) < 800 && gy < Y0 + 0.05) P.y = Y0 + (Math.hypot(P.x, P.z) < 110 ? 0.12 : 0.02);
  avatar.rotation.y = P.face;
  var sw = Math.sin(P.walk || 0) * (mag > 0.08 ? 0.5 : 0);
  avatar.children.forEach(function (c) { if (c.name === 'leg1' || c.name === 'arm-1') c.rotation.x = sw; if (c.name === 'leg-1' || c.name === 'arm1') c.rotation.x = -sw; });
  // context action
  actTarget = null;
  var dS = Math.hypot(P.x - SABLE_POS.x, P.z - SABLE_POS.z);
  if (dS < 7) actTarget = { kind: 'sable', label: 'TALK TO SABLE' };
  if (!actTarget && Math.hypot(P.x - SOUL.door.x, P.z - SOUL.door.z) < 8) actTarget = { kind: 'soul', label: 'ENTER SOUL PRINT TOWER' };
  if (!actTarget && window.CitySanctuary && window.CityCitizens && CitySanctuary.talkHere(P.x, P.z)) actTarget = { kind: 'citizen', id: 'provolta', label: 'TALK TO PROVOLTA' };   // the sanctuary's talk point
  if (!actTarget && window.CityPodium && CityPodium.actHere(P.x, P.z)) actTarget = { kind: 'podium', label: 'READ' };   // ALL THAT WE CAN DO! podium
  if (!actTarget && window.CityBook && CityBook.actHere(P.x, P.z)) actTarget = { kind: 'book', label: 'READ THE BOOK' };   // the King's Book of Law (Forum Plaza)
  if (!actTarget && window.CityCampus) { var cpt = CityCampus.actHere(P.x, P.z); if (cpt) actTarget = { kind: 'campus', id: cpt, label: 'OPEN COURSE CATALOG' }; }   // a campus hall's forecourt
  if (!actTarget && window.CityCitizens) { var cz = CityCitizens.near(P.x, P.z); if (cz) actTarget = { kind: 'citizen', id: cz.id, label: 'TALK TO ' + cz.name.toUpperCase() }; }
  if (!actTarget) for (var i = 0; i < portals.length; i++) {
    var po = portals[i], dp = Math.hypot(P.x - po.x, P.z - po.z);
    if (dp < 9) actTarget = { kind: 'portal', d: po.d, label: po.d.go ? po.d.go + ' →' : 'ENTER ' + po.d.label.toUpperCase() + ' →' };
    if (dp < 2.2 && S.portalCool <= 0) { openPortal(po.d); S.portalCool = 4; }
  }
  if (!actTarget && Math.hypot(P.x - PAD.x, P.z - PAD.z) < 34) actTarget = { kind: 'board', label: 'BOARD SHIP' };
  if (!actTarget && window.CityHyperTrain) { var trl = CityHyperTrain.actHere(P.x, P.z); if (trl) actTarget = { kind: 'train', label: trl }; }
  if (!actTarget && window.CityFirstParking) { var crt = CityFirstParking.actHere(P.x, P.z); if (crt) actTarget = crt; }
  if (!actTarget && window.CityHyperCity) { var elv = CityHyperCity.actHere(P.x, P.z); if (elv) actTarget = { kind: 'elevator', label: elv }; }
  if (!actTarget && window.CityAnnex) { var aelv = CityAnnex.actHere(P.x, P.z); if (aelv) actTarget = { kind: 'elevator', label: aelv }; }
  if (!actTarget && window.CityQuadrantCrown) { var qelv = CityQuadrantCrown.actHere(P.x, P.z); if (qelv) actTarget = { kind: 'elevator', label: qelv }; }
  S.portalCool -= dt;
}

/* ---------------- flight ---------------- */
function shipVectors() { var f = new V3(0, 0, 1).applyQuaternion(S.ship.q), u = new V3(0, 1, 0).applyQuaternion(S.ship.q), r = new V3(-1, 0, 0).applyQuaternion(S.ship.q); return { f: f, u: u, r: r }; }
function rotLocal(axis, ang) { tq.setFromAxisAngle(axis, ang); S.ship.q.multiply(tq); }
function rotWorld(axis, ang) { tq.setFromAxisAngle(axis, ang); S.ship.q.premultiply(tq); }
function maxSpeed(alt) { return 320 + smooth((alt - 1500) / 22000) * 2900; }
function updateFly(dt) {
  var k = input.keys, sh = S.ship;
  var pitch = -input.sy - input.dragY, roll = input.sx + input.dragX, yaw = 0;
  if (k.KeyW || k.ArrowUp) pitch -= 1; if (k.KeyS || k.ArrowDown) pitch += 1; if (k.KeyA || k.ArrowLeft) roll -= 1; if (k.KeyD || k.ArrowRight) roll += 1;
  if (k.KeyQ) yaw += 1; if (k.KeyE) yaw -= 1;
  if (k.KeyR) sh.throttle = clamp(sh.throttle + dt * 0.5, 0, 1); if (k.KeyF) sh.throttle = clamp(sh.throttle - dt * 0.5, 0, 1);
  var boost = sh.boost || k.ShiftLeft || k.ShiftRight || k.Space;
  pitch = clamp(pitch, -1, 1); roll = clamp(roll, -1, 1);
  var v = shipVectors();
  rotLocal(AX, -pitch * 1.25 * dt);
  rotLocal(AZ, roll * 2.3 * dt);
  rotWorld(UP, (v.r.y * 1.1 + yaw * 0.6) * dt);   // bank-to-turn
  if (S.autoLevel) {
    if (Math.abs(roll) < 0.05) rotLocal(AZ, clamp(v.r.y * 2.2, -1.5, 1.5) * dt);
    if (Math.abs(pitch) < 0.05 && Math.abs(v.f.y) < 0.9) rotLocal(AX, v.f.y * 0.55 * dt);
  }
  S.ship.q.normalize();
  var alt = sh.y, target = sh.throttle * maxSpeed(alt) * (boost ? 2.6 : 1);
  sh.speed += (target - sh.speed) * Math.min(1, dt * (boost ? 1.2 : 0.7));
  moveShip(dt);
}
var warnMsg = '';
function moveShip(dt) {
  var sh = S.ship, f =