<!-- SOLID CITY · part 1233 -->
 labelTex('SABLE', 'TAP OR WALK UP TO TALK'), depthTest: true })); tag.scale.set(3.6, 0.9, 1); tag.position.y = 3.1; g.add(tag);
  g.userData.halo = halo;
  return g;
}

/* ================= game state ================= */
var Y0 = CITY_Y, PAD = { x: D.port.x, y: CITY_Y + 2, z: D.port.z };
var S = {
  mode: 'walk',   // walk | boarded | countdown | launch | fly | auto
  p: { x: 6, y: Y0, z: 62, face: Math.PI, vy: 0 },
  camYaw: 0, camPitch: 0.28, camDist: 7.5,
  ship: { x: PAD.x, y: PAD.y + 2.4, z: PAD.z, q: new T.Quaternion(), speed: 0, throttle: 0.5, boost: false },
  autoLevel: true, auto: null, t: 0, launchT: 0, warnT: 0, portalCool: 0, lastPortal: null
};
var avatar = makeAvatar(); scene.add(avatar);
var ship = makeShip(); scene.add(ship);
var sable = makeSable(); scene.add(sable);
var SABLE_POS = { x: -16, y: Y0, z: -24 };
var shipFlames = []; ship.traverse(function (o) { if (o.name === 'flame') shipFlames.push(o); });
ship.position.set(0, 0, 0);

var input = { sx: 0, sy: 0, keys: {}, dragX: 0, dragY: 0, wheel: 0 };
var V3 = T.Vector3, tv = new V3(), tv2 = new V3(), tq = new T.Quaternion(), tm = new T.Matrix4(), UP = new V3(0, 1, 0);
var AX = new V3(1, 0, 0), AZ = new V3(0, 0, 1);

/* ---------------- input: joystick ---------------- */
(function () {
  var st = $('sc-stick'), knob = st.querySelector('i'), id = null, cx = 0, cy = 0, R = 50;
  function set(x, y) { var d = Math.hypot(x, y); if (d > R) { x *= R / d; y *= R / d; } knob.style.transform = 'translate(' + x + 'px,' + y + 'px)'; input.sx = x / R; input.sy = y / R; }
  st.addEventListener('pointerdown', function (e) { id = e.pointerId; st.setPointerCapture(id); var r = st.getBoundingClientRect(); cx = r.left + r.width / 2; cy = r.top + r.height / 2; set(e.clientX - cx, e.clientY - cy); e.preventDefault(); });
  st.addEventListener('pointermove', function (e) { if (e.pointerId === id) set(e.clientX - cx, e.clientY - cy); });
  function end(e) { if (e.pointerId === id) { id = null; set(0, 0); } }
  st.addEventListener('pointerup', end); st.addEventListener('pointercancel', end);
})();
/* look / mouse-fly drag on the canvas, tap to select */
(function () {
  var id = null, lx = 0, ly = 0, sx = 0, sy = 0, moved = 0;
  canvas.addEventListener('pointerdown', function (e) { if (id !== null) return; id = e.pointerId; canvas.setPointerCapture(id); lx = sx = e.clientX; ly = sy = e.clientY; moved = 0; });
  canvas.addEventListener('pointermove', function (e) {
    if (e.pointerId !== id) return; var dx = e.clientX - lx, dy = e.clientY - ly; lx = e.clientX; ly = e.clientY; moved += Math.abs(dx) + Math.abs(dy);
    if (S.mode === 'fly') { input.dragX = clamp((e.clientX - sx) / 120, -1, 1); input.dragY = clamp((e.clientY - sy) / 120, -1, 1); }
    else { S.camYaw -= dx * 0.006; S.camPitch = clamp(S.camPitch + dy * 0.004, -0.35, 1.2); }
  });
  function end(e) { if (e.pointerId !== id) return; id = null; input.dragX = input.dragY = 0; if (moved < 8) onTap(e.clientX, e.clientY); }
  canvas.addEventListener('pointerup', end); canvas.addEventListener('pointercancel', end);
  canvas.addEventListener('wheel', function (e) { e.preventDefault(); if (S.mode === 'fly') S.ship.throttle = clamp(S.ship.throttle - e.deltaY * 0.0008, 0, 1); else S.camDist = clamp(S.camDist + e.deltaY * 0.01, 3, 20); }, { passive: false });
})();
addEventListener('keydown', function (e) { if (e.target && e.target.tagName === 'INPUT') return; input.keys[e.code] = true; if (e.code === '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); }); }
holdB