<!-- SOLID CITY · part 1610 -->
 820) return 'SOLID CITY';
  for (var k = 0; k < VOLCANOES.length; k++) { var V = VOLCANOES[k]; if (V.name && Math.hypot(x - V.x, z - V.z) < V.r * 0.45) return V.name; }
  return landMask(x, z) > 0 ? 'THE ISLANDS' : 'OPEN OCEAN';
}
function updateHUD(dt) {
  hudT -= dt; if (hudT > 0) return; hudT = 0.12;
  var flying = S.mode !== 'walk', x = flying ? S.ship.x : S.p.x, z = flying ? S.ship.z : S.p.z, y = flying ? S.ship.y : S.p.y;
  var spd = flying ? S.ship.speed : (Math.abs(input.sx) + Math.abs(input.sy) > 0.08 || input.keys.KeyW || input.keys.KeyS || input.keys.KeyA || input.keys.KeyD ? 8 : 0);
  var alt = flying ? y - Math.max(0, groundAt(x, z)) : y;
  $('h-mode').textContent = { walk: 'ON FOOT', boarded: 'SHIP · ON PAD', countdown: 'COUNTDOWN', launch: 'LAUNCH', fly: y > 25000 ? 'ORBIT' : y > 12000 ? 'STRATOSPHERE' : 'FLIGHT', auto: 'AUTOPILOT', watch: 'WATCH' }[S.mode];
  $('h-where').textContent = y > 25000 ? 'ABOVE THE ATMOSPHERE' : whereName(x, z);
  $('h-spd').textContent = Math.round(spd * 3.6).toLocaleString();
  $('h-alt').textContent = Math.round(flying ? y : y).toLocaleString();
  $('h-dist').textContent = (Math.hypot(x, z) / 1000).toFixed(1);
  $('sc-thr-fill').style.height = Math.round(S.ship.throttle * 100) + '%';
  var w = $('h-warn'); if (warnMsg && (S.mode === 'fly')) { w.hidden = false; w.textContent = warnMsg; } else w.hidden = true;
  var heading = flying ? Math.atan2(-new V3(0, 0, 1).applyQuaternion(S.ship.q).x, -new V3(0, 0, 1).applyQuaternion(S.ship.q).z) : Math.atan2(-Math.sin(S.camYaw), -Math.cos(S.camYaw));
  drawRadar(x, z, -heading + Math.PI);
  // action buttons
  var act = $('sc-act');
  if (S.mode === 'walk' && actTarget) { act.hidden = false; act.textContent = actTarget.label; }
  else if (S.mode === 'boarded') { act.hidden = false; act.textContent = 'EXIT SHIP'; actTarget = { kind: 'exit' }; }
  else act.hidden = true;
  $('sc-launch').hidden = S.mode !== 'boarded';
  $('sc-stick').style.display = (S.mode === 'walk' || S.mode === 'fly' || S.mode === 'auto' || S.mode === 'watch') && (IS_TOUCH || QS.has('touch')) ? '' : 'none';
  $('b-land').hidden = !((S.mode === 'fly') && Math.hypot(x - PAD.x, z - PAD.z) < 2500 && y < 3000);
  $('b-rtc').classList.toggle('is-on', S.mode === 'auto' && !(S.auto && S.auto.tx != null));
  if (window.CityPlayer) {
    CityPlayer.setCockpit(S.mode !== 'walk');
    var bd = 1e9; for (var mi = 0; mi < RADIO_MASTS.length; mi++) bd = Math.min(bd, Math.hypot(x - RADIO_MASTS[mi].x, z - RADIO_MASTS[mi].z, Math.max(0, y - CITY_Y - 90)));
    CityPlayer.setBoost(1 + 0.3 * smooth((260 - bd) / 200));
  }
  if (helpTimer > 0) { helpTimer -= 0.12; if (helpTimer <= 0) $('sc-help').style.opacity = 0; }
  if (PERF) { var ts = terrainStats(); $('sc-perf').hidden = false; $('sc-perf').textContent = fpsVal.toFixed(0) + ' fps  ' + renderer.info.render.calls + ' calls\n' + Math.round(renderer.info.render.triangles / 1000) + 'k tris  ' + ts.chunks + ' chunks  q' + ts.queue; }
}

/* ---------------- SABLE talk (login-gated, rate-limited server-side) ---------------- */
var me = null, talkHist = [], talkBusy = false;
var CITY_SEED = { role: 'sable', text: 'Welcome to Solid City. I stand in the Forum Plaza, the heart of the capital, on the west coast of the main volcanic island. The Music Studio Tower rises to the east, and its gate opens Solid Sound at /ss. Ships launch from the Orbital Spaceport to the south-west: board one, press the big red LAUNCH button, and fly as far as the ocean goes, past the volcanoes, even above the atmosphere. Return to City autopilot brings you home. The Warp Speed arcade run is through the spaceport gate, and the Forum gate here leads to the boards.' };
function getMe() { return me ? Promise.resolve(me) : fetch('/api/city/me', { credentials: 'same-origin', cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (j) { me = j; return j; }).catch(function () { return { loggedIn: false }; }); }
function logLine(cls, text) { var log = $('t-log'); var le = log.lastElementChild; if (cls === 'sys' && le && le.className === 'sys' && le.textContent === text) return; var d = document.createElement('div'); d.className = cls; d.textContent = text; log.appendChild(d); log.scrollTop = 1e9; }
function marqueeLine(text) { var log = $('t-log'); var old = log.querySelector('.sc-marquee:not(.done)'); if (old) old.classList.add('done'); var d = document.createElement('div'); d.className = 'her sc-marquee'; var s = document.createElement('span'); s.textContent = text; d.appendChild(s); d.style.setProperty('--mqd', Math.max(10, Math.min(45, text.length / 4)) + 's'); d.addEventListener('click', function () { d.classList.toggle('paused'); }); log.appendChild(d); log.scrollTop = 1e9; }
function openTalk() {
  $('sp-talk').hidden = true; $('sc-talk').hidden = false;
  micLockStart();
  getMe().then(function (j) {
    var ok = j && j.loggedIn; $('t-login').hidden = ok; if (!micLock.on) $('t-form').style.display = ok