<!-- SOLID CITY · part 1241 -->
on (j) {
    var first = !wall.n; (j.msgs || []).forEach(wallAdd);
    if (first && !wall.n && !$('sp-wempty')) { var e = document.createElement('div'); e.className = 'sys'; e.id = 'sp-wempty'; e.textContent = 'No check-ins in the last 2 days yet. The room clears messages after 2 days.'; $('sp-wall').appendChild(e); }
    if (first) $('sp-wall').scrollTop = 1e9;
  }).catch(function () {}).then(function () { wall.inflight = false; }); }
  wall.timer = setTimeout(wallPoll, 4000);
}
function wallPost(text) {
  text = String(text || '').trim().slice(0, 500); if (!text || wall.busy) return;
  wall.busy = true; $('sp-wsend').disabled = true;
  getMe().then(function (m) {
    return fetch('/api/soluv-room', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': (m && m.csrf) || '' }, body: JSON.stringify({ msg: text, localKey: 'city' + Date.now().toString(36) }) });
  }).then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { s: r.status, j: j }; }); })
    .then(function (o) {
      if (o.j && o.j.ok) { $('sp-wmsg').value = ''; if (o.j.msg) wallAdd(o.j.msg); $('sp-wall').scrollTop = 1e9; }
      else { var e = document.createElement('div'); e.className = 'sys'; e.textContent = o.s === 403 ? 'Session check failed. Reload and try again.' : o.s === 429 ? 'Slow down a little.' : 'Post failed (' + ((o.j && o.j.error) || o.s) + ').'; $('sp-wall').appendChild(e); }
    }).catch(function () {}).then(function () { wall.busy = false; $('sp-wsend').disabled = false; });
}
$('sp-tab-talk').addEventListener('click', function () { spTab('talk'); });
$('sp-tab-wall').addEventListener('click', function () { spTab('wall'); });
$('sp-close2').addEventListener('click', function () { $('sp-close').click(); });
$('sp-wform').addEventListener('submit', function (e) { e.preventDefault(); wallPost($('sp-wmsg').value); });
$('sp-close').addEventListener('click', function () { clearTimeout(wall.timer); });
(function () { // push-to-talk via the browser's speech recognition
  var SR = window.SpeechRecognition || window.webkitSpeechRecognition, rec = null, btn = $('t-ptt'), heard = '';
  if (!SR) { btn.addEventListener('click', function () { logLine('sys', 'Voice input isn\u2019t supported in this browser — type instead.'); }); return; }
  function start(e) { e.preventDefault(); if (rec) return; heard = ''; rec = new SR(); rec.lang = 'en-US'; rec.interimResults = true; rec.continuous = true;
    rec.onresult = function (ev) { var s = ''; for (var i = 0; i < ev.results.length; i++) s += ev.results[i][0].transcript; heard = s; $('t-msg').value = s; };
    rec.onerror = function (ev) { logLine('sys', 'Mic: ' + ev.error); };
    rec.onend = function () { btn.classList.remove('is-on'); var h = heard; rec = null; if (h.trim()) sendTalk(h); };
    try { speechSynthesis.cancel(); } catch (x) {} rec.start(); btn.classList.add('is-on'); }
  function stop() { if (rec) try { rec.stop(); } catch (x) {} }
  btn.addEventListener('pointerdown', start); btn.addEventListener('pointerup', stop); btn.addEventListener('pointercancel', stop); btn.addEventListener('pointerleave', stop);
})();

/* ---------------- 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) {}
  // ship + avatar + sable placement (scene space = absolute - origin)
  ship.positio