<!-- SOLID CITY · part 256 -->
{ clearInterval(wdFb); wdFb = null; } return; }
  var stale = performance.now() - lastFrameAt;
  if (stale < 3000) { if (wdFb) { clearInterval(wdFb); wdFb = null; wdLog('animation frames resumed; limp-home drive off'); } wdCtxAsked = 0; return; }
  wdStalls++; wdLog('frame stale ' + Math.round(stale / 1000) + 's (stall #' + wdStalls + ', pulse ' + frameN + ') — re-kicking');
  try { armFrame(); } catch (e) {}
  try { var gl = renderer.getContext(); if (gl && gl.isContextLost && gl.isContextLost() && wdCtxAsked < 3) { wdCtxAsked++; if (renderer.forceContextRestore) renderer.forceContextRestore(); } } catch (e) {}
  if (!wdFb) {
    wdFb = setInterval(function () {
      if (!running || document.hidden) return;
      if (performance.now() - lastFrameAt > 40) { try { frame(performance.now()); } catch (e) {} }
    }, 33);
  }
}, 1000);

/* ---------------- boot ---------------- */
var cityBuilt = false;
function boot() {
  $('sc-load').textContent = 'Raising the capital…';
  setTimeout(function () {
    buildCity(); cityBuilt = true; try { dispatchEvent(new Event('sc-city-built')); } catch (e) {}
    $('sc-load').textContent = 'Generating the island chain…';
    setTimeout(function () {
      camAbs.set(0, 30, 60);
      for (var n = 0; n < 6; n++) updateTerrain({ x: 0, y: 30, z: 60 }, 120);
      buildMap();
      if (window.CityPlayer) { CityPlayer.mount($('sc-hud')); CityPlayer.onChange(function (s) { NP.playing = s.playing; if (NP.draw && (s.title !== NP.t || s.playing !== NP.p)) { NP.t = s.title; NP.p = s.playing; NP.draw(s.title, s.kind, s.playing); } }); setTimeout(function () { CityPlayer.preload(); }, 200); }
      $('sc-load').textContent = 'Ready.'; $('sc-enter').disabled = false; var lb0 = $('sc-enter-lite'); if (lb0) lb0.disabled = false;
      if (QS.get('back') === 'workshop') { S.p.x = D.workshop.x; S.p.z = D.workshop.z + 62; S.p.face = Math.atan2(-D.workshop.x, -D.workshop.z); } else if (QS.get('back') === 'strike') { S.p.x = D.strike.x; S.p.z = D.strike.z + 58; S.p.face = Math.atan2(-D.strike.x, -D.strike.z); }
      if (QS.has('auto')) enter();
    }, 30);
  }, 30);
}
function enter() {
  if (window.CityPlayer) CityPlayer.gesture();   // the ENTER tap unlocks audio on iOS
  if (QS.get('fs') === '1') { var fe = document.documentElement, rf = fe.requestFullscreen || fe.webkitRequestFullscreen; if (rf) try { var fp = rf.call(fe); if (fp && fp.catch) fp.catch(function () {}); } catch (e) {} }   // from /press-start
  $('sc-boot').hidden = true; $('sc-hud').hidden = false; if (window.AudioDrive) AudioDrive.attach(); running = true; last = performance.now(); armFrame();
  help(IS_TOUCH ? 'Left stick to walk, drag to look. Tap SABLE to talk. The spaceport is to the south-west.' : 'WASD to walk, drag to look, E to interact. Tap SABLE to talk. The spaceport is south-west — board a ship and fly.', 8);
}
$('sc-enter').disabled = true; $('sc-enter').addEventListener('click', function () { try { localStorage.removeItem('sc-lite'); } catch (e) {} window.CityAILight = false; try { DPR = Math.min(window.devicePixelRatio || 1, MOBILE ? 1.35 : 1.75); if (QS.get('dpr')) DPR = +QS.get('dpr'); renderer.setPixelRatio(DPR); fitView(); } catch (e) {} enter(); });
/* THE LITE ENTRANCE (the King's word, 2026-10-13 ~23:04): the
 * lower style of entering, offered on the start screen to phones
 * with less capacity. Same city, lighter load — the AI mode's
 * fittings (half pixels, a resting render), chosen by the player
 * and remembered on their device, so next time the city boots
 * light from the first frame. The full entrance remains, and
 * choosing it clears the memory. */
var liteBtn = $('sc-enter-lite');
if (liteBtn) { liteBtn.disabled = true; liteBtn.addEventListener('click', function () {
  try { localStorage.setItem('sc-lite', '1'); } catch (e) {}
  window.CityAILight = true;
  renderer.setPixelRatio(0.5); renderer.setSize(innerWidth, innerHeight, false);
  enter();
}); }
S.camYaw = 0;
boot();

/* test / screenshot hooks (read-only debugging; no server effect) */
window.SC = {
  state: S, origin: origin,
  stats: function () { var ft = frameTimes.slice().sort(function (a, b) { return a - b; }); var avg = ft.reduce(function (a, b) { return a + b; }, 0) / (ft.length || 1); var wt = workTimes.slice().sort(function (a, b) { return a - b; }); return { workAvgMs: wt.reduce(function (a, b) { return a + b; }, 0) / (wt.length || 1), workP95Ms: wt[Math.floor(wt.length * 0.95)] || 0, fps: fpsVal, avgMs: avg, p95Ms: ft[Math.floor(ft.length * 0.95)] || 0, calls: renderer.info.render.calls, tris: renderer.info.render.triangles, terrain: terrainStats(), solids: solids.length, mem: renderer.info.memory }; },
  resetFrames: function () { frameTimes.length = 0; workTimes.length = 0; },
  fly: function (x, y, z, heading, pitch) { S.mode = 'fly'; avatar.visible = false; S.ship.x = x; S.ship.y = y; S.ship.z = z; S.ship.q.setFromAxisAngle(UP, heading || 0); rotLocal(AX, -(pitch || 0)); S.ship.speed