<!-- SOLID CITY · part 3749 -->
,
  island: { name: 'ISLAND HOP', blurb: 'Five outer isles over open ocean', build: function () {
    var r = [W(-200, 280, 420, 210), W(0, 30, 380, 200, CAP.city), W(-1500, 2500, 350, 350, CAP.coast)];
    [1, 2, 3, 4, 5].forEach(function (k) { var I = isle(k); r.push(W(I.x, I.z, top(I) + 500, 470, isleCap(I))); });
    r.push(W(KO.x - 3000, KO.z - 1500, top(KO) + 600, 420, CAP.kohala), W(-700, -2400, 380, 330, CAP.coast), W(PAD.x - 1800, PAD.z + 900, 480, 230, CAP.home));
    return r;
  } },
  orbit: { name: 'ORBIT & BACK', blurb: 'Climb above the atmosphere and come home', build: function () {
    return [W(-200, 260, 420, 220), W(0, 30, 400, 220, CAP.city), W(-3000, -1500, 3000, 700), W(-11000, -4000, 18000, 1800), W(-20000, 2000, 50000, 2600, CAP.orbit),
      W(-14000, 12000, 55000, 2600), W(-6000, 9000, 16000, 1700), W(-2000, 2500, 2500, 560), W(PAD.x - 1800, PAD.z + 900, 480, 230, CAP.home)];
  } }
};
var ORDER = ['grand', 'volcano', 'island', 'orbit'];
function wpsOf(k) { var r = ROUTES[k], f = r.vk || 1; return r.build().map(function (w) { var o = {}; for (var q in w) o[q] = w[q]; o.v = w.v * f; return o; }); }

/* ---------------- path: centripetal Catmull-Rom, clearance envelope, arc-length samples ---------------- */
function buildPath(wps, from, fwd, v0) {
  var f = fwd.clone(); f.y = Math.max(f.y, 0); if (f.lengthSq() < 0.01) f.set(0, 0, -1); f.normalize();
  var pts = [from.clone(), from.clone().addScaledVector(f, Math.max(500, (v0 || 200) * 2.5))];
  pts[1].y = Math.max(pts[1].y, from.y + 60);
  wps.forEach(function (w) { pts.push(new T.Vector3(w.x, w.y, w.z)); });
  var curve = new T.CatmullRomCurve3(pts, false, 'centripetal', 0.5);
  var len = curve.getLength(), N = Math.round(clamp(len / 30, 200, 9000)); curve.arcLengthDivisions = N * 2;
  var sp = curve.getSpacedPoints(N), n = sp.length, X = new Float32Array(n), Yv = new Float32Array(n), Z = new Float32Array(n), F = new Float32Array(n);
  for (var i = 0; i < n; i++) {
    X[i] = sp[i].x; Yv[i] = sp[i].y; Z[i] = sp[i].z;
    var fl = gnd(X[i], Z[i]) + 150;
    if (X[i] * X[i] + Z[i] * Z[i] < 950 * 950) fl = Math.max(fl, A.solidsTop(X[i], Z[i], 60) + 90);
    F[i] = fl;
  }
  var ds = len / (n - 1), g = 0.38 * ds, E = new Float32Array(n);
  E[0] = Math.min(F[0], Yv[0]);
  for (i = 1; i < n; i++) E[i] = Math.max(F[i], E[i - 1] - g);
  for (i = n - 2; i >= 0; i--) E[i] = Math.max(E[i], E[i + 1] - g);
  for (i = 1; i < n; i++) Yv[i] = Math.max(Yv[i], E[i]);
  for (var pass = 0; pass < 3; pass++) { var Y2 = Yv.slice(); for (i = 3; i < n - 3; i++) { var s = 0; for (var k = -3; k <= 3; k++) s += Y2[i + k]; Yv[i] = Math.max(s / 7, F[i]); } }
  Yv[0] = from.y;
  var C = new Float32Array(n); for (i = 1; i < n; i++) C[i] = C[i - 1] + Math.hypot(X[i] - X[i - 1], Yv[i] - Yv[i - 1], Z[i] - Z[i - 1]);
  // waypoint arc positions (monotone nearest-sample search)
  var wi = [], j = 0;
  wps.forEach(function (w) { var best = j, bd = Infinity; for (var q = j; q < n; q++) { var d = Math.hypot(X[q] - w.x, Z[q] - w.z); if (d < bd) { bd = d; best = q; } if (d > bd + 6000) break; } wi.push(best); j = best; });
  // speed profile: smooth blend between waypoint speeds along the arc
  var Vs = new Float32Array(n), prevI = 0, prevV = clamp(v0 || 200, 120, 3000);
  wps.forEach(function (w, q) { var a = prevI, b = Math.max(wi[q], a + 1); for (var t = a; t <= b && t < n; t++) Vs[t] = prevV + (w.v - prevV) * smooth((C[t] - C[a]) / Math.max(1, C[b] - C[a])); prevI = b; prevV = w.v; });
  for (i = prevI; i < n; i++) Vs[i] = prevV;
  var dur = 0; for (i = 1; i < n; i++) dur += (C[i] - C[i - 1]) / Math.max(60, Vs[i]);
  return { X: X, Y: Yv, Z: Z, C: C, V: Vs, n: n, len: C[n - 1], wi: wi, wps: wps, dur: dur };
}
function sampleAt(p, s, out) {
  var i = p._i || 0; while (i < p.n - 2 && p.C[i + 1] < s) i++; while (i > 0 && p.C[i] > s) i--; p._i = i;
  var t = clamp((s - p.C[i]) / Math.max(1e-3, p.C[i + 1] - p.C[i]), 0, 1);
  out.set(p.X[i] + (p.X[i + 1] - p.X[i]) * t, p.Y[i] + (p.Y[i + 1] - p.Y[i]) * t, p.Z[i] + (p.Z[i + 1] - p.Z[i]) * t);
  return i;
}
function idxAt(p, s) { var lo = 0, hi = p.n - 1; while (hi - lo > 1) { var m = (lo + hi) >> 1; if (p.C[m] < s) lo = m; else hi = m; } return lo; }

/* ---------------- state ---------------- */
var WS = { state: 'idle', route: null, routeKey: '', path: null, s: 0, v: 0, rig: 'chase', rigT: 10, manualT: 0, caps: [], capI: 0, bank: new T.Vector3(0, 1, 0), lastUp: new T.Vector3(0, 1, 0), mute: localStorage.getItem('sc.watch.mute') === '1', full: 0, flyby: null, orbA: 0, off: new T.Vector3(), offInit: false, est: {}, remaining: null };
var RIGS = ['chase', 'cockpit', 'side', 'flyby', 'orbit', 'wide'], RIGNAME = { chase: 'CHASE', cockpit: 'COCKPIT', side: 'SIDE', flyby: 'FLYBY', orbit: 'ORBIT CAM', wide: 'WIDE' };

/* ---------------- UI ---------------- */
var root = $('sc'), hud = $('sc-hud');
var lbT = document.createElement('div'); lbT.className = 'w-lb w-lb-top'; var lbB = document.createElem