<!-- SOLID CITY · part 93 -->
openWelcome","line":"Space is made. Now — are you hurt, friend?"}],"soft":[{"at":0,"look":true,"line":"A knock at my gate! No crowding a knock. You whole?"}],"resolve":{"up":{"line":"There we go — up! Room and welcome work wonders."},"trapped":{"line":"He is stuck fast — keep the lane CLEAR, rescue comes through it!","action":{"type":"requestAssistance","need":"rescue"}}}},"heardOnly":[{"at":0,"look":true,"line":"Hey — that was a crash! Clear a lane, wherever it is!"}],"funnyMoment":[{"at":0,"look":true,"line":"Oh! That is a story for the gate — you are a guest AND a legend now!"}],"nearMiss":[{"at":0,"look":true,"line":"Careful, friend! The gate wants you arriving whole."}]},"po":{"vehicleCrash":{"hard":[{"at":0,"look":true,"line":"I see it. Nobody move the vehicle."},{"at":600,"action":{"type":"approach","to":"vehicle"}},{"at":2600,"gesture":"sentinelSignal","line":"The flaw is here. I can see exactly where she failed. Driver — your state, report."}],"mid":[{"at":0,"look":true,"line":"A crash. Note everything before it changes. Are you hurt?"},{"at":600,"action":{"type":"approach","to":"vehicle"}},{"at":2400,"gesture":"sentinelSignal","line":"Quietly now. The vehicle has filed its report — I am reading it. Your turn: are you whole?"}],"soft":[{"at":0,"look":true,"line":"A tap. Even taps are data. Are you unhurt?"}],"resolve":{"up":{"line":"The driver walks. The vehicle does not lie — I will file what it told me."},"trapped":{"line":"Driver trapped. The weakness is mapped — rescue, enter from the clear side.","gesture":"sentinelSignal","action":{"type":"requestAssistance","need":"rescue"}}}},"heardOnly":[{"at":0,"look":true,"line":"A crash. Note the time. Where, exactly?"}],"funnyMoment":[{"at":0,"look":true,"line":"Noted. No flaw found in the machine. That was entirely the operator."}],"nearMiss":[{"at":0,"look":true,"line":"Noted, with the hour. A pattern is forming on this road."}]}},"online":false};
if (!ROSTER) return;

var M = { state: 'idle', wait: 5.0, citizens: [], byId: {}, me: null, voice: true, voiceMine: false, interacted: false,
          panelFor: null, greeted: {}, spinAcc: 0, spinT: 0, prevDrive: null };
var $ = function (sel) { return document.querySelector(sel); };

function surfY(x, z) {
  var s = -1000;
  try { s = A.standingAt(x, z, null); } catch (e) {}
  if (s == null || s <= -999) s = A.groundAt(x, z);
  return s;
}
function api(path, opts) {
  return fetch(path, Object.assign({ credentials: 'same-origin', headers: { 'Content-Type': 'application/json' } }, opts || {}))
    .then(function (r) { return r.json().catch(function () { return { ok: false, status: r.status }; }); })
    .catch(function () { return { ok: false, error: 'network' }; });
}

/* ---------- nameplates ---------- */
function plateTexture(c) {
  var cv = document.createElement('canvas'); cv.width = 512; cv.height = 128;
  var g = cv.getContext('2d');
  g.fillStyle = 'rgba(10,14,22,0.72)';
  g.beginPath(); if (g.roundRect) g.roundRect(56, 18, 400, 92, 22); else g.rect(56, 18, 400, 92); g.fill();
  g.strokeStyle = '#d8a833'; g.lineWidth = 3; g.stroke();
  g.fillStyle = '#e8edf5'; g.font = '600 44px system-ui, sans-serif'; g.textAlign = 'center';
  g.fillText(c.name, 256, 66);
  g.fillStyle = '#9aa4b5'; g.font = '28px system-ui, sans-serif';
  var mark = !c.op ? '' : c.op.type === 'human' ? '◆ ' + (c.op.name || 'inhabited') : c.op.type === 'unoccupied' ? '○ resting' : '● AI';
  g.fillText(mark, 256, 100);
  var tex = new T.CanvasTexture(cv); tex.anisotropy = 4; return tex;
}
function drawPlate(c) {
  var old = c.plate.material.map;
  c.plate.material.map = plateTexture(c); c.plate.material.needsUpdate = true;
  if (old) old.dispose();
}

/* ---------- speech: bubbles + voice ----------
 * The voice ladder, in order: the citizen's own pre-rendered
 * Kokoro recording (instant, works with the whole API dark) →
 * live Kokoro synthesis of a generated line → the device voice →
 * the bubble alone. The body never waits for any of them. */
var VOICE_FILES = null, liveAudio = null;
function playWav(url) {
  try {
    if (window.speechSynthesis) speechSynthesis.cancel();
    if (liveAudio) { try { liveAudio.pause(); } catch (e) {} liveAudio = null; }
    liveAudio = new Audio(url);
    var p = liveAudio.play(); if (p && p.catch) p.catch(function () {});
  } catch (e) {}
}
function deviceSpeak(line) {
  if (!window.speechSynthesis) return;
  try {
    speechSynthesis.cancel();
    var u = new SpeechSynthesisUtterance(line.replace(/[—–]/g, ', '));
    u.rate = 1.02; speechSynthesis.speak(u);
  } catch (e) {}
}
function speak(c, line, secs) {
  c.bubbleText = line; c.bubbleUntil = performance.now() + (secs || 4.2) * 1000;
  if (panelCitizen() === c) addLine(c.name, line, 'them');
  if (!M.voice || !M.interacted) return;
  var f = VOICE_FILES && VOICE_FILES[c.id] && VOICE_FILES[c.id][line];
  if (f) { playWav('/city/voices/' + c.id + '/' + f); return; }
  if (REACT.online) {
    fetch('/api/citize