<!-- SOLID CITY · part 415 -->
 it.');
  micRecStart();
  micLock.timer = setInterval(function () {
    if (!micLock.on || document.hidden) return;
    if (!window.__sableSpeaking && Date.now() - micLock.lastVoice > 20000) micLockStop();
  }, 1000);
}
function micLockStop(quiet) {
  if (!micLock.on) return;
  micLock.on = false; micLock.aborts = 0;
  try { var CPR2 = window.CityPlayer; if (micLock.radioWas && CPR2 && CPR2.toggle && CPR2.state && !CPR2.state().playing) CPR2.toggle(); } catch (e7) {}
  micLock.radioWas = false;
  try { clearInterval(micLock.timer); } catch (e) {}
  try { if (micLock.rec) { micLock.rec.onend = null; micLock.rec.onerror = null; micLock.rec.stop(); } } catch (e2) {}
  micLock.rec = null;
  $('t-form').style.display = '';
  $('sc-talk').classList.remove('behind');
  if (!quiet) logLine('sys', 'Mic off — quiet 20s.');
}
function micRecStart() {
  var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (!micLock.on || micLock.rec || !SR) return;
  var rec = null;
  try { rec = new SR(); } catch (e) { return; }
  micLock.rec = rec;
  rec.lang = 'en-US'; rec.interimResults = true; rec.continuous = true;
  var finalSent = '';
  rec.onresult = function (ev) {
    micLock.aborts = 0;
    var s = '', fin = false, i;
    for (i = 0; i < ev.results.length; i++) { s += ev.results[i][0].transcript; if (ev.results[i].isFinal) fin = true; }
    if (window.__sableSpeaking) { if (s.trim()) micLock.lastVoice = Date.now(); return; }
    if (s.trim()) micLock.lastVoice = Date.now();
    if (fin && s.trim() && s.trim() !== finalSent) { finalSent = s.trim(); sendTalk(s.trim()); }
  };
  rec.onerror = function (ev) {
    if (ev.error === 'not-allowed' || ev.error === 'service-not-allowed') { logLine('sys', 'Mic blocked.'); micLockStop(true); return; }
    if (ev.error === 'aborted') { micLock.aborts = (micLock.aborts || 0) + 1; if (micLock.aborts >= 3) { logLine('sys', 'Mic keeps aborting — tap the mic button to retry.'); micLockStop(true); } return; }
    logLine('sys', 'Mic: ' + ev.error);
  };
  rec.onend = function () {
    micLock.rec = null;
    if (!micLock.on || (micLock.aborts || 0) >= 3) return;
    setTimeout(micRecStart, 400);
  };
  try { rec.start(); } catch (e) { micLock.rec = null; }
}

/* ---------------- main loop ---------------- */
var workTimes = [], last = performance.now(), fpsAcc = 0, fpsN = 0, fpsVal = 60, running = false, frameTimes = [];
function frame(now) {
  if (!running) return;
  requestAnimationFrame(frame);
  var now0 = performance.now();
  var dt = Math.min(0.1, (now - last) / 1000); last = now; S.t += dt;
  frameTimes.push(dt * 1000); if (frameTimes.length > 600) frameTimes.shift();
  fpsAcc += dt; fpsN++; if (fpsAcc > 0.5) { fpsVal = fpsN / fpsAcc; fpsAcc = 0; fpsN = 0; }
  if (S.mode === 'walk') updateWalk(dt);
  else if (S.mode === 'countdown') { countT = (countEnd - now) / 1000; var ct = countT > 0 ? String(Math.ceil(countT)) : 'GO'; if (ct !== countShown) { countShown = ct; $('sc-count').textContent = ct; } shipFlames.forEach(function (f) { f.scale.setScalar(0.4 + Math.random() * 0.3); }); if (countT < -0.5) { $('sc-count').hidden = true; S.mode = 'launch'; S.launchT = 0; } }
  else if (S.mode === 'launch') updateLaunch(dt);
  else if (S.mode === 'fly') updateFly(dt);
  else if (S.mode === 'auto') updateAuto(dt);
  else if (S.mode === 'watch') { if (window.CityWatch) CityWatch.fly(dt); else S.mode = 'fly'; }
  if (window.CityHyper) CityHyper.update(dt);
  if (window.CityWatch) CityWatch.update(dt);
  if (window.CityForge) CityForge.update(dt);
  if (window.CityCitizens) CityCitizens.update(dt);
  if (window.CityMonuments) CityMonuments.update(dt);
  if (window.CitySanctuary) try { CitySanctuary.update(dt); } catch (e) {}
  if (window.CityCampus) try { CityCampus.update(dt); } catch (e) {}
  if (window.CityBook) try { CityBook.update(dt); } catch (e) {}
  if (window.CityPodium) try { CityPodium.update(dt); } catch (e) {}
  if (window.CityTunnelBore) try { CityTunnelBore.update(dt); } catch (e) {}
  if (window.CityStarwell) try { CityStarwell.update(dt); } catch (e) {}
  if (window.CityHyperTrain) try { CityHyperTrain.update(dt); } catch (e) {}
  if (window.CitySolidSanctuary) try { CitySolidSanctuary.update(dt); } catch (e) {}
  if (window.CityAncestorRoads) try { CityAncestorRoads.update(dt); } catch (e) {}
  if (window.CityColosseum) try { CityColosseum.update(dt); } catch (e) {}
  if (window.CityImperialColosseum) try { CityImperialColosseum.update(dt); } catch (e) {}
  if (window.CityHiddenShip) try { CityHiddenShip.update(dt); } catch (e) {}
  if (window.CityVillageMauna) try { CityVillageMauna.update(dt); } catch (e) {}
  if (window.CityMountainHomes) try { CityMountainHomes.update(dt); } catch (e) {}
  if (window.CityGateAncestors) try { CityGateAncestors.update(dt); } catch (e) {}
  if (window.CityLanternVigil) try { CityLanternVigil.update(dt); } catch (e) {}
  if (window.CityCompassMeridian) try { CityCompassMeridian.update(dt); } c