<!-- SOLID CITY · part 3753 -->
de' ? 120 * hs : 20 * hs);
    camUp.copy(UP).lerp(vU, r === 'chase' ? 0.55 : 0.2).normalize();
    if (r === 'chase') fov = 62 + clamp(sh.speed / 150, 0, 12);
  } else if (r === 'cockpit') {
    camAbs.copy(shipP).addScaledVector(vF, 4.2).addScaledVector(vU, 1.05);
    camLook.copy(camAbs).addScaledVector(vF, 300).addScaledVector(vU, -30);
    camUp.copy(vU); fov = 74;
  } else if (r === 'orbit') {
    WS.orbA += dt * 0.32; var R = 46 * hs;
    camAbs.set(sh.x + Math.cos(WS.orbA) * R, sh.y + 12 * hs, sh.z + Math.sin(WS.orbA) * R);
    camLook.copy(shipP); camUp.copy(UP); fov = 58;
  } else if (r === 'flyby') {
    var v = Math.max(80, sh.speed);
    if (!WS.flyby || tmp.copy(shipP).sub(WS.flyby).dot(vF) > v * 2.2 || WS.flyby.distanceTo(shipP) > v * 7) {
      var p = WS.path, side = Math.random() < 0.5 ? -1 : 1;
      if (p && WS.state === 'route') { sampleAt(p, Math.min(p.len - 1, WS.s + v * 2.6), fpos); p._i = idxAt(p, WS.s); } else fpos.copy(shipP).addScaledVector(vF, v * 2.6);
      fpos.addScaledVector(vR, 45 * side).addScaledVector(UP, 10);
      fpos.y = Math.max(fpos.y, gnd(fpos.x, fpos.z) + 10, A.solidsTop(fpos.x, fpos.z, 3) + 4);
      WS.flyby = fpos.clone();
    }
    camAbs.copy(WS.flyby); camLook.copy(shipP); camUp.copy(UP);
    var half = 34 / Math.min(1, innerWidth / innerHeight) * (innerWidth < innerHeight ? 1.6 : 1); fov = clamp(2 * Math.atan(half / Math.max(1, WS.flyby.distanceTo(shipP))) * 57.3, 5, 70);
  }
  return fov;
}

/* ---------------- per-frame bookkeeping (state machine, HUD) ---------------- */
var hudT = 0;
function update(dt) {
  if (WS.state === 'launching') {
    if (S.mode === 'fly') begin();
    else if (S.mode === 'walk') end();
  } else if (WS.state === 'route') {
    if (S.mode !== 'watch') { if (S.mode === 'fly' || S.mode === 'auto') takeover(); else end(); }
  } else if (WS.state === 'landing') {
    if (S.mode === 'boarded') { end('WELCOME HOME', 'Landed at the Orbital Spaceport · ' + WS.route.name + ' complete'); speak('Welcome home. We have landed at the Orbital Spaceport.'); }
    else if (S.mode === 'fly') takeover();
    else if (S.mode !== 'auto') end();
  } else if (WS.state === 'paused') {
    if (!(S.mode === 'fly' || S.mode === 'auto')) { WS.state = 'idle'; resume.hidden = true; }
  }
  if (WS.state === 'route' || WS.state === 'landing') { WS.rigT -= dt; if (WS.rigT <= 0) nextRig(false); }
  padBtn.hidden = !(S.mode === 'boarded' && WS.state !== 'launching');
  if (capT > 0) { capT -= dt; if (capT <= 0) capEl.classList.remove('on'); }
  if (WS.full > 0) { WS.full -= dt; if (WS.full <= 0) root.classList.remove('watch-full'); }
  hudT -= dt; if (hudT > 0) return; hudT = 0.25;
  if (root.classList.contains('watching')) {
    var sh = S.ship, where = sh.y > 25000 ? 'ABOVE THE ATMOSPHERE' : A.whereName(sh.x, sh.z);
    $('w-loc').innerHTML = '<b>' + esc(where) + '</b><span>' + Math.round(sh.y).toLocaleString() + ' M · ' + Math.round(sh.speed * 3.6).toLocaleString() + ' KM/H</span>';
    var p = WS.path;
    if (p && WS.state === 'route') { var fr = WS.s / p.len; $('w-prog').style.width = (fr * 92).toFixed(1) + '%'; var rem = 0; for (var i = idxAt(p, WS.s); i < p.n - 1; i += 4) rem += (p.C[Math.min(p.n - 1, i + 4)] - p.C[i]) / Math.max(60, p.V[i]); $('w-eta').textContent = fmtT(rem + 25) + ' LEFT'; }
    else if (WS.state === 'landing') { $('w-prog').style.width = '96%'; $('w-eta').textContent = 'LANDING'; }
    else if (WS.state === 'launching') { $('w-prog').style.width = '0%'; $('w-eta').textContent = 'LAUNCH'; }
  }
}
function radar(g, blip, flying, sc, ax, az) {
  var p = WS.path; if (!p || !(WS.state === 'route' || WS.state === 'paused')) return;
  var Wd = g.canvas.width, i0 = WS.state === 'route' ? idxAt(p, WS.s) : 0, step = Math.max(1, Math.round(p.n / 400));
  g.strokeStyle = 'rgba(255,210,122,.75)'; g.lineWidth = 2; g.setLineDash([6, 5]); g.beginPath();
  for (var i = i0; i < p.n; i += step) { var bx = Wd / 2 + (p.X[i] - ax) * sc, by = Wd / 2 + (p.Z[i] - az) * sc; if (i === i0) g.moveTo(bx, by); else g.lineTo(bx, by); }
  g.stroke(); g.setLineDash([]);
  for (var q = 0; q < p.wps.length; q++) { var w = p.wps[q]; if (w.cap && p.C[p.wi[q]] > WS.s) { blip(w.x, w.z, '#ff4d5e', 5, w.cap.cap.split(' ')[0].slice(0, 9)); break; } }
}

/* ---------------- wiring ---------------- */
$('w-cam').addEventListener('click', function () { nextRig(true); });
$('w-mute').addEventListener('click', function () { setMute(!WS.mute); });
$('w-exit').addEventListener('click', exitWatch);
$('w-res').addEventListener('click', doResume);
$('w-res-x').addEventListener('click', function () { resume.hidden = true; WS.state = 'idle'; });
padBtn.addEventListener('click', open);
(function () { // tap the screen while watching = show the full HUD for a few seconds
  var cv = $('sc-gl'), x0 = 0, y0 = 0;
  cv.addEventListener('pointerdown', function (e) { x0 = e.clientX; y0 = e.clientY; });
  cv.addEventListener('pointerup', function (e) { if (!root.cl