<!-- SOLID CITY · part 930 -->
= 'KeyL') toggleLevel(); if (e.code === 'KeyH') returnToCity(); if (e.code === 'KeyE') actPress(); if (window.CityPlayer && e.code === 'KeyM') CityPlayer.toggle(); if (window.CityPlayer && e.code === 'KeyN') CityPlayer.next(); });
addEventListener('keyup', function (e) { input.keys[e.code] = false; });
addEventListener('blur', function () { input.keys = {}; });
// Locked / switched app: the 3D loop already stops (requestAnimationFrame), the music keeps playing
// (CityPlayer, plain <audio>). Stop speech/narration so the music is not left ducked under silence.
document.addEventListener('visibilitychange', function () { if (!document.hidden) return; input.keys = {}; try { if (window.speechSynthesis) speechSynthesis.cancel(); } catch (e) {} if (window.CityPlayer) CityPlayer.duck(false); });
/* throttle slider */
(function () {
  var el = $('sc-thr'), id = null;
  function set(e) { var r = el.getBoundingClientRect(); S.ship.throttle = clamp(1 - (e.clientY - r.top) / r.height, 0, 1); }
  el.addEventListener('pointerdown', function (e) { id = e.pointerId; el.setPointerCapture(id); set(e); e.preventDefault(); });
  el.addEventListener('pointermove', function (e) { if (e.pointerId === id) set(e); });
  el.addEventListener('pointerup', function () { id = null; });
})();
function holdBtn(el, on, off) { el.addEventListener('pointerdown', function (e) { e.preventDefault(); on(); }); ['pointerup', 'pointercancel', 'pointerleave'].forEach(function (ev) { el.addEventListener(ev, off); }); }
holdBtn($('b-boost'), function () { S.ship.boost = true; $('b-boost').classList.add('is-on'); }, function () { S.ship.boost = false; $('b-boost').classList.remove('is-on'); });
function toggleLevel() { S.autoLevel = !S.autoLevel; $('b-level').classList.toggle('is-on', S.autoLevel); }
$('b-level').addEventListener('click', toggleLevel);
$('b-rtc').addEventListener('click', returnToCity);
$('b-land').addEventListener('click', function () { if (S.mode === 'fly' || S.mode === 'auto') startAuto(true); });
$('sc-launch').addEventListener('click', startCountdown);
$('sc-act').addEventListener('click', actPress);
$('p-close').addEventListener('click', function () { $('sc-portal').hidden = true; });
function fitView() { var v = vpSize(); renderer.setSize(v[0], v[1], false); camera.aspect = v[0] / v[1]; camera.updateProjectionMatrix(); }
addEventListener('resize', fitView); addEventListener('orientationchange', function () { setTimeout(fitView, 250); setTimeout(fitView, 800); });
if (window.visualViewport) visualViewport.addEventListener('resize', fitView);
setTimeout(fitView, 0);

/* ---------------- interactions ---------------- */
var actTarget = null;
function actPress() {
  if (!actTarget) return;
  if (actTarget.kind === 'sable') openTalk();
  else if (actTarget.kind === 'soul') openSoul();
  else if (actTarget.kind === 'citizen') { if (window.CityCitizens) CityCitizens.open(actTarget.id); }
  else if (actTarget.kind === 'book') { if (window.CityBook) CityBook.open(0); }
  else if (actTarget.kind === 'podium') { if (window.CityPodium) CityPodium.open('features'); }
  else if (actTarget.kind === 'campus') { if (window.CityCampus) CityCampus.open(actTarget.id); }
  else if (actTarget.kind === 'portal') openPortal(actTarget.d);
  else if (actTarget.kind === 'board') board();
  else if (actTarget.kind === 'exit') disembark();
}
function openPortal(d) {
  $('p-title').textContent = d.title || d.label; $('p-go').href = d.url; $('p-go').textContent = d.go || 'ENTER';
  var np = window.CityPlayer && d === D.studio ? CityPlayer.state() : null;
  $('p-text').textContent = (np && np.title ? 'Now playing on Studio Radio: \u201c' + np.title + '\u201d. ' : '') + d.text;
  $('sc-portal').hidden = false;
}
var ray = new T.Raycaster(), ndc = new T.Vector2();
function onTap(x, y) {
  if (S.mode !== 'walk') return;
  ndc.set(x / innerWidth * 2 - 1, -(y / innerHeight) * 2 + 1); ray.setFromCamera(ndc, camera);
  if (window.CityForge && CityForge.tap(x, y)) return;
  if (ray.intersectObject(sable, true).length) openTalk();
  else if (window.CityCitizens && CityCitizens.tap(ray)) { /* 👥 citizen talk panel opened */ }
  else if (SOUL.group && Math.hypot(S.p.x - D.soul.x, S.p.z - D.soul.z) < 140 && ray.intersectObject(SOUL.group, true).length) openSoul();
}
function board() { S.mode = 'boarded'; avatar.visible = false; S.ship.x = PAD.x; S.ship.y = PAD.y + 2.4; S.ship.z = PAD.z; S.ship.speed = 0; levelShip(Math.PI); help('Ship boarded. Hit the big red LAUNCH button. Warp Speed mini-game: walk through the spaceport gate.'); }
function disembark() { S.mode = 'walk'; avatar.visible = true; S.p.x = PAD.x + 10; S.p.z = PAD.z + 14; S.p.y = groundAt(S.p.x, S.p.z); setFlightUI(false); }
function levelShip(heading) { S.ship.q.setFromAxisAngle(UP, heading); }
var countT = 0, countEnd = 0, countShown = '';
function startCountdown() {
  if (S.mode !== 'boarded') return; S.mode = 'countdown'; countT = 3.0; countEnd = performance.now() + 3000; c