<!-- SOLID CITY · part 3751 -->
application/json', 'X-CSRF-Token': me.csrf || '' }, body: JSON.stringify({ message: msg, history: [] }) })
      .then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { s: r.status, j: j }; }); })
      .then(function (o) {
        var rep = o.j && (o.j.sable || o.j.reply); if (!rep) return fallback('SABLE couldn\u2019t answer (' + o.s + ').');
        var u = rep.toUpperCase(), k = /VOLCANO/.test(u) ? 'volcano' : /ISLAND/.test(u) ? 'island' : /ORBIT/.test(u) ? 'orbit' : /GRAND/.test(u) ? 'grand' : null;
        if (!k) return fallback('SABLE answered, but not with a route.');
        var line = rep.replace(/\s+/g, ' ').trim().slice(0, 160); st.textContent = 'SABLE: \u201c' + line + '\u201d';
        setTimeout(function () { start(k, line); }, 1400);
      });
  }).catch(function () { fallback('Couldn\u2019t reach SABLE.'); });
}

/* ---------------- captions + speech ---------------- */
var capEl = $('w-cap'), capT = 0;
function caption(a, b, say) {
  capEl.querySelector('b').textContent = a; capEl.querySelector('span').textContent = b || ''; capEl.classList.add('on'); capT = 6;
  if (say) speak(say);
}
var speechOk = false;
function unlockSpeech() { try { if (window.speechSynthesis && !speechOk) { var u = new SpeechSynthesisUtterance(' '); u.volume = 0; speechSynthesis.speak(u); speechOk = true; } } catch (e) {} }
function speak(text) {
  if (WS.mute || !window.speechSynthesis) return;
  try {
    speechSynthesis.cancel(); var u = new SpeechSynthesisUtterance(text), vs = speechSynthesis.getVoices();
    var v = vs.filter(function (x) { return /^en/i.test(x.lang) && /samantha|female|zira|aria|jenny|serena|karen|moira|victoria|google uk english female/i.test(x.name); })[0] || vs.filter(function (x) { return /^en/i.test(x.lang); })[0];
    if (v) u.voice = v; u.rate = 1.0; u.pitch = 0.95;
    if (window.CityPlayer) { CityPlayer.duck(true); u.onend = u.onerror = function () { CityPlayer.duck(false); }; }
    speechSynthesis.speak(u);
  } catch (e) {}
}
function setMute(m) { WS.mute = m; localStorage.setItem('sc.watch.mute', m ? '1' : '0'); $('w-mute').textContent = m ? '🔇' : '🔊'; if (m) try { speechSynthesis.cancel(); if (window.CityPlayer) CityPlayer.duck(false); } catch (e) {} }
setMute(WS.mute);

/* ---------------- start / begin / takeover / resume / end ---------------- */
function start(k, why) {
  pick.hidden = true; unlockSpeech();
  if (window.CityHyper && CityHyper.stop) CityHyper.stop();
  WS.routeKey = k; WS.route = ROUTES[k]; WS.remaining = null; resume.hidden = true;
  root.classList.add('watching'); $('w-rname').textContent = WS.route.name; $('w-eta').textContent = '';
  if (S.mode === 'walk') { A.board(); }
  if (S.mode === 'boarded') { WS.state = 'launching'; A.startCountdown(); caption('🎬 ' + WS.route.name, why || 'Launching\u2026 sit back and watch', why ? '' : 'Launching. Sit back and watch.'); return; }
  if (S.mode === 'countdown' || S.mode === 'launch') { WS.state = 'launching'; return; }
  begin(why);
}
function shipF() { return new T.Vector3(0, 0, 1).applyQuaternion(S.ship.q); }
function begin(why, wps) {
  var from = new T.Vector3(S.ship.x, S.ship.y, S.ship.z), list = wps || wpsOf(WS.routeKey);
  WS.path = buildPath(list, from, shipF(), S.ship.speed); WS.path._i = 0;
  WS.s = 0; WS.v = clamp(S.ship.speed, 120, 3000); WS.caps = list.map(function (w, q) { return w.cap ? { s: WS.path.C[WS.path.wi[q]], c: w.cap } : null; }).filter(Boolean); WS.capI = 0;
  WS.state = 'route'; S.mode = 'watch'; S.auto = null; A.setFlightUI(false); root.classList.add('watching'); resume.hidden = true;
  WS.offInit = false; WS.flyby = null; WS.bank.copy(UP);
  if (WS.rigT <= 0 || !wps) { setRig('chase'); WS.rigT = 9; }
  if (!wps) caption('🎬 ' + WS.route.name, why || 'About ' + fmtT(WS.path.dur + 25) + ' · touch the stick to take over', why ? '' : WS.route.name.toLowerCase() + '. Enjoy the flight.');
}
function remainingWps() { var p = WS.path; if (!p) return []; var out = []; for (var q = 0; q < p.wps.length; q++) if (p.C[p.wi[q]] > WS.s + 200) out.push(p.wps[q]); return out; }
function takeover(msg) {
  if (WS.state === 'route') WS.remaining = remainingWps(); else if (WS.state === 'landing') WS.remaining = [];
  WS.state = 'paused';
  if (S.mode === 'watch') { S.mode = 'fly'; S.ship.throttle = clamp(S.ship.speed / A.maxSpeed(S.ship.y), 0.15, 1); }
  A.setFlightUI(true); A.resetShipCam(); root.classList.remove('watching', 'watch-full'); resume.hidden = false;
  caption(msg || 'YOU HAVE CONTROL', 'Tap ▶ RESUME WATCH to continue the route', ''); A.help('You have control. RESUME WATCH continues the route.', 4);
}
function doResume() {
  if (WS.state !== 'paused' || !(S.mode === 'fly' || S.mode === 'auto')) { resume.hidden = true; return; }
  root.classList.add('watching'); resume.hidden = true;
  if (!WS.remaining || !WS.remaining.length) { land(); return; }
  begin('', WS.remaining); caption('▶ ' + WS.route.name, 'Resuming the ro