<!-- SOLID CITY · part 574 -->
;
$('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) || '' }, body: JSON.stringify({ message: text, history: [CITY_SEED].concat(talkHist.slice(-8)) }) }); }
  callSable().then(function (r) { return r.status === 502 || r.status === 503 ? new Promise(function (ok) { setTimeout(ok, 1500); }).then(callSable) : r; })
    .then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { s: r.status, j: j }; }); })
    .then(function (o) {
      wait.remove();
      if (o.s === 401) { me = null; logLine('sys', 'Log in to talk to SABLE.'); $('t-login').hidden = false; return; }
      if (o.s === 429) { logLine('sys', 'SABLE: \u201cI\u2019m getting a lot of messages at once. Give me a minute and ask again.\u201d'); return; }
      var reply = o.j && (o.j.sable || o.j.reply);
      if (!reply) { logLine('sys', 'SABLE didn\u2019t answer (' + ((o.j && o.j.error) || o.s) + ').'); return; }
      talkHist.push({ role: 'user', text: text }, { role: 'sable', text: reply });
      if (window.AudioDrive) AudioDrive.receive({ from: 'sable', text: reply, render: function () { logLine('her', reply); } }); else { logLine('her', reply); speak(reply); }
    })
    .catch(function () { wait.remove(); logLine('sys', 'Connection lost.'); })
    .then(function () { talkBusy = false; $('t-send').disabled = false; });
}

/* ---------------- SOUL PRINT talk (Soul Print Tower door; login-gated + rate-limited server-side) ----------------
   Uses /api/city/soulprint (proxies to the existing /api/soulprint/chat relay). Sends the CSRF token,
   renders every reply with textContent (no HTML), keeps only this visit's short history. */
var soulHist = [], soulBusy = false;
function spLine(cls, text) { var d = document.createElement('div'); d.className = cls; d.textContent = text; $('sp-log').appendChild(d); $('sp-log').scrollTop = 1e9; }
function openSoul() {
  $('sc-talk').hidden = true; $('sp-talk').hidden = false;
  getMe().then(function (j) {
    var ok = j && j.loggedIn; $('sp-login').hidden = ok; $('sp-form').style.display = ok ? '' : 'none';
    if (wall.tab === 'wall') spTab('wall');
    if (!$('sp-log').children.length) spLine('sys', ok ? 'The wine-red door glows. Soul Print looks up from the light, ' + (j.name || 'traveler') + '.' : 'The door stays sealed. Soul Print speaks only with citizens of epistles.');
    if (ok && !IS_TOUCH) $('sp-msg').focus();
  });
}
$('sp-close').addEventListener('click', function () { $('sp-talk').hidden = true; try { speechSynthesis.cancel(); } catch (e) {} });
$('sp-form').addEventListener('submit', function (e) { e.preventDefault(); sendSoul($('sp-msg').value); });
function sendSoul(text) {
  text = String(text || '').trim().slice(0, 300); if (!text || soulBusy) return;
  $('sp-msg').value = ''; spLine('me', text); soulBusy = true; $('sp-send').disabled = true;
  var wait = document.createElement('div'); wait.className = 'sys'; wait.textContent = 'Soul Print is listening…'; $('sp-log').appendChild(wait);
  getMe().then(function (m) {
    function call() { return fetch('/api/city/soulprint', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': (m && m.csrf) || '' }, body: JSON.stringify({ message: text, history: soulHist.slice(-6) }) }); }
    // one quiet retry on 502/503 (e.g. the few seconds of a server restart) before showing an error
    return call().then(function (r) { return r.status === 502 || r.status === 503 ? new Promise(function (ok) { setTimeout(ok, 1500); }).then(call) : r; });
  }).then(function (r)