<!-- SOLID CITY · part 324 -->
  $('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, the Double Strike hangar stands south-west past the spaceport \u2014 walk through its gate to fly with the AI \u2014 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 ? '' : 'none'; if (!ok) micLockStop(true);
    if (!$('t-log').children.length) logLine('sys', ok ? 'SABLE turns to you. \u201cHi, ' + (j.name || 'there') + ' \u2014 what can I help with?\u201d' : 'SABLE smiles. Log in to epistles and she\u2019ll talk with you.');
    if (ok && !IS_TOUCH && !micLock.on) $('t-msg').focus();
  });
}
$('t-close').addEventListener('click', function () { $('sc-talk').hidden = true; });
$('t-gear').addEventListener('click', function () { var s = $('t-settings'); s.hidden = !s.hidden; });
$('t-form').addEventListener('submit', function (e) { e.preventDefault(); sendTalk($('t-msg').value); });
function speak(text, pitch) {
  try {
    if (!window.speechSynthesis) return; speechSynthesis.cancel();
    var u = new SpeechSynthesisUtterance(text.replace(/[*_#`>\[\]]/g, '').slice(0, 600)), vs = speechSynthesis.getVoices();
    var v = vs.filter(function (v) { return /en/i.test(v.lang) && /samantha|female|zira|aria|jenny|serena|karen|moira|victoria|google uk english female/i.test(v.name); })[0] || vs.filter(function (v) { return /^en/i.test(v.lang); })[0];
    if (v) u.voice = v; u.rate = (window.CityVoiceSpeed ? CityVoiceSpeed.rate() : 1.0); u.pitch = pitch || 0.95;
    if (window.CityPlayer) CityPlayer.duck(true);
    window.__sableSpeaking = true;
    u.onend = u.onerror = function () { window.__sableSpeaking = false; if (window.CityPlayer) CityPlayer.duck(false); };
    speechSynthesis.speak(u);
  } catch (e) {}
}
function sendTalk(text) {
  text = String(text || '').trim(); if (!text || talkBusy) return;
  $('t-msg').value = ''; logLine('me', text); talkBusy = true; $('t-send').disabled = true;
  var wait = document.createElement('div'); wait.className = 'sys'; wait.textContent = 'SABLE is thinking…'; $('t-log').appendChild(wait);
  function callSable() { return fetch('/api/city/sable', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': (me && me.csrf) || '' }, bo