<!-- SOLID CITY · part 3752 -->
ute', '');
}
function land() { WS.state = 'landing'; S.mode = 'auto'; S.auto = { phase: 'cruise', land: true }; A.setFlightUI(false); root.classList.add('watching'); }
function end(msg, sub) { WS.state = 'idle'; WS.path = null; resume.hidden = true; root.classList.remove('watching', 'watch-full'); if (msg) caption(msg, sub || '', ''); }
function exitWatch() {
  if (WS.state === 'route' || WS.state === 'landing') { if (S.mode === 'watch') { S.mode = 'fly'; S.ship.throttle = clamp(S.ship.speed / A.maxSpeed(S.ship.y), 0.15, 1); } if (S.mode === 'auto') { S.mode = 'fly'; S.auto = null; } A.setFlightUI(true); A.resetShipCam(); }
  if (WS.state === 'launching') { WS.state = 'idle'; }
  end('YOU HAVE CONTROL', 'Watch ended');
}

/* ---------------- flight along the path ---------------- */
var tmp = new T.Vector3(), tA = new T.Vector3(), tB = new T.Vector3(), mtx = new T.Matrix4(), qq = new T.Quaternion(), Tn = new T.Vector3(), xA = new T.Vector3(), yA = new T.Vector3();
function userInput() {
  var I = A.input, k = I.keys;
  return Math.abs(I.sx) + Math.abs(I.sy) > 0.25 || k.KeyW || k.KeyA || k.KeyS || k.KeyD || k.ArrowUp || k.ArrowDown || k.ArrowLeft || k.ArrowRight || k.KeyQ || k.KeyE || k.KeyR || k.KeyF || k.Space;
}
function tangentAt(p, i, out) { var a = Math.max(0, i - 2), b = Math.min(p.n - 1, i + 3); return out.set(p.X[b] - p.X[a], p.Y[b] - p.Y[a], p.Z[b] - p.Z[a]).normalize(); }
function fly(dt) {
  var p = WS.path; if (!p || WS.state !== 'route') { S.mode = 'fly'; return; }
  if (userInput()) { takeover(); return; }
  var i = idxAt(p, WS.s), want = p.V[i];
  WS.v += (want - WS.v) * Math.min(1, dt * 0.8);
  WS.s += WS.v * dt;
  if (WS.s >= p.len - 1) { S.ship.speed = WS.v; land(); return; }
  i = sampleAt(p, WS.s, tmp);
  S.ship.x = tmp.x; S.ship.y = tmp.y; S.ship.z = tmp.z; S.ship.speed = WS.v;
  // orientation: tangent + coordinated-turn bank (up tilts toward the centripetal acceleration)
  tangentAt(p, i, Tn);
  var ia = Math.max(0, i - 6), ib = Math.min(p.n - 1, i + 6);
  tangentAt(p, ia, tA); tangentAt(p, ib, tB);
  var dS = Math.max(1, p.C[ib] - p.C[ia]);
  var lat = tB.sub(tA).multiplyScalar(WS.v * WS.v / dS);           // centripetal accel (m/s²)
  lat.addScaledVector(Tn, -lat.dot(Tn)); lat.multiplyScalar(0.55);
  var lm = lat.length(), maxL = 9.8 * Math.tan(0.6); if (lm > maxL) lat.multiplyScalar(maxL / lm);
  var want3 = tmp.copy(UP).multiplyScalar(9.8).add(lat).normalize();
  WS.bank.lerp(want3, Math.min(1, dt * 1.6)).normalize();
  yA.copy(WS.bank).addScaledVector(Tn, -WS.bank.dot(Tn));
  if (yA.lengthSq() < 0.01) yA.copy(WS.lastUp).addScaledVector(Tn, -WS.lastUp.dot(Tn));
  yA.normalize(); WS.lastUp.copy(yA);
  xA.crossVectors(yA, Tn).normalize();
  mtx.makeBasis(xA, yA, Tn); qq.setFromRotationMatrix(mtx);
  S.ship.q.slerp(qq, Math.min(1, dt * 5));
  // captions at landmarks (4 s lead)
  while (WS.capI < WS.caps.length && WS.s >= WS.caps[WS.capI].s - WS.v * 4) { var c = WS.caps[WS.capI++].c; caption(c.cap, c.sub, c.say); }
}

/* ---------------- cinematic cameras ---------------- */
function setRig(r) { WS.rig = r; WS.flyby = null; WS.offInit = false; var el = $('w-cam'); if (el) el.querySelector('span').textContent = RIGNAME[r]; }
function nextRig(manual) {
  var high = S.ship.y > 12000, opts = RIGS.filter(function (r) { return r !== WS.rig && !(high && r === 'flyby'); });
  if (manual) { var k = (RIGS.indexOf(WS.rig) + 1) % RIGS.length; if (high && RIGS[k] === 'flyby') k = (k + 1) % RIGS.length; setRig(RIGS[k]); WS.rigT = 15; }
  else { setRig(opts[Math.floor(Math.random() * opts.length)]); WS.rigT = 8 + Math.random() * 7; }
}
var vF = new T.Vector3(), vU = new T.Vector3(), vR = new T.Vector3(), shipP = new T.Vector3(), want = new T.Vector3(), fpos = new T.Vector3();
function cam(dt, camAbs, camLook, camUp) {
  var on = (WS.state === 'route' && S.mode === 'watch') || (WS.state === 'landing' && (S.mode === 'auto' || S.mode === 'boarded'));
  if (!on) return 0;
  var sh = S.ship, q = sh.q; shipP.set(sh.x, sh.y, sh.z);
  vF.set(0, 0, 1).applyQuaternion(q); vU.set(0, 1, 0).applyQuaternion(q); vR.crossVectors(vF, vU).normalize();
  var hs = sh.y > 15000 ? 3 : 1, fov = 62, r = WS.rig, k = Math.min(1, dt * 3);
  if (WS.state === 'landing' && (r === 'flyby' || r === 'cockpit')) r = 'chase';
  if (r === 'chase' || r === 'side' || r === 'wide') {
    if (r === 'chase') want.copy(vF).multiplyScalar(-30 * hs).addScaledVector(vU, 8 * hs);
    else if (r === 'side') want.copy(vR).multiplyScalar(38 * hs).addScaledVector(vU, 5).addScaledVector(vF, -8);
    else { var ff = tmp.set(vF.x, 0, vF.z); if (ff.lengthSq() < 0.01) ff.set(0, 0, 1); ff.normalize(); var rr = tA.set(-ff.z, 0, ff.x); want.copy(ff).multiplyScalar(-650 * hs).addScaledVector(rr, 380 * hs).addScaledVector(UP, 320 * hs); fov = 55; }
    if (!WS.offInit) { WS.off.copy(want); WS.offInit = true; } else WS.off.lerp(want, k);
    camAbs.copy(shipP).add(WS.off);
    camLook.copy(shipP).addScaledVector(vF, r === 'wi