<!-- SOLID CITY · part 1452 -->
 S.t;
  for (var i = 0; i < animated.length; i++) animated[i](S.t);
  placeCamera(dt);
  updateTerrain({ x: camAbs.x, y: camAbs.y, z: camAbs.z }, MOBILE ? 4 : 6);
  updateHUD(dt);
  /* the render rest (AI BROWSER MODE): while the visitor stands
   * still, the picture is already drawn — drawing it sixty times
   * a second is the idle burn that killed every inspection. In
   * this mode the render fires on movement, and otherwise on a
   * slow heartbeat (every 20th frame) so living things still stir
   * on the still visitor's screen. */
  if (window.CityAILight) {
    aiF++;
    var aiMoved = Math.abs(S.p.x - aiLX) > 0.05 || Math.abs(S.p.z - aiLZ) > 0.05 || Math.abs((S.camYaw || 0) - aiLYaw) > 0.002;
    if (aiMoved || aiF % 20 === 0) { aiLX = S.p.x; aiLZ = S.p.z; aiLYaw = S.camYaw || 0; renderer.render(scene, camera); }
  } else renderer.render(scene, camera);
  if (PERF) { renderer.getContext().finish(); workTimes.push(performance.now() - now0); if (workTimes.length > 600) workTimes.shift(); }
}

/* ---------------- 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(); requestAnimationFrame(frame);
  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) {} 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 = 300; S