<!-- SOLID CITY · part 25 -->
owns the air.
  // Nothing recorded, nothing retained, nothing replayed.
  function speakOnce(text, profile) {
    try {
      if (typeof speechSynthesis === 'undefined' || !text) return;
      speechSynthesis.cancel();
      var clean = String(text).replace(/[*_#`>\[\]]/g, '').slice(0, 600);
      var u = new SpeechSynthesisUtterance(clean);
      var v = pickVoice(profile);
      if (v) u.voice = v;
      u.rate = (root.CityVoiceSpeed ? root.CityVoiceSpeed.rate() : 1.2);
      u.pitch = profile.pitch || 1.0;
      if (root.CityPlayer) { try { root.CityPlayer.duck(true); } catch (e) {} }
      var done = false;
      function release() {
        if (done) return; done = true;
        try { if (root.CityPlayer) root.CityPlayer.duck(false); } catch (e2) {}
        u.onend = u.onerror = null; // discarded after playback. No hooks remain.
      }
      u.onend = release; u.onerror = release;
      speechSynthesis.speak(u);
    } catch (e) {}
  }

  // THE relay entry point. Every AI/citizen message passes through here.
  // msg: { from, text, render } — render() draws the text in the right panel.
  function receive(msg) {
    var id = 'm' + (++seq) + '_' + Date.now();
    var mode = getMode();
    if (msg && typeof msg.render === 'function') {
      try { msg.render(); } catch (e) {}
    } // text record always; mode gates speech only
    if (mode !== 'text' && msg && msg.text && !spoken[id]) {
      spoken[id] = true; // once per recipient. Never persisted: no replay after reconnect.
      speakOnce(msg.text, voiceProfile(msg.from));
    }
    return id;
  }

  // Receiver lifecycle: bound when the player presses ENTER (game start).
  function attach() { attached = true; paint(); return true; }
  function isAttached() { return attached; }

  function paint() {
    if (typeof document === 'undefined') return;
    var cur = getMode();
    [['rc-text', 'text'], ['rc-audio', 'audio'], ['rc-both', 'both']].forEach(function (p) {
      var b = document.getElementById(p[0]);
      if (!b) return;
      b.classList.toggle('on', cur === p[1]);
      b.setAttribute('aria-pressed', cur === p[1] ? 'true' : 'false');
    });
  }
  function mount() {
    if (typeof document === 'undefined') return;
    [['rc-text', 'text'], ['rc-audio', 'audio'], ['rc-both', 'both']].forEach(function (p) {
      var b = document.getElementById(p[0]);
      if (b) b.addEventListener('click', function () { setMode(p[1]); });
    });
    paint();
  }
  if (typeof document !== 'undefined') {
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', mount);
    else mount();
  }

  root.AudioDrive = { receive: receive, getMode: getMode, setMode: setMode, attach: attach, isAttached: isAttached, voiceProfile: voiceProfile };
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));


===== FILE: public/city/audio-drive.js.bak-relay-fix-20261011 =====
/* KINGDOM COMMUNICATION RELAY — Audio Drive (King's order 2026-10-11)
 * One shared relay. One lightweight receiver per player session, bound at ENTER.
 * Every AI citizen and human player communicates through TEXT.
 * The account's receive mode decides: TEXT only, AUDIO only, or BOTH.
 * For audio-enabled players, incoming text is synthesized ONCE, then discarded.
 *
 * THE AUDIO LAW (enforced here):
 *  1. No recording or archiving of message audio — synthesis only, never capture.
 *  2. No synthesized voice files stored in player accounts — nothing is written.
 *  3. Each message spoken only once per recipient — spoken-id set, session memory.
 *  4. No automatic replay after reconnection — the set is never persisted; history renders as text only.
 *  5. Temporary audio discarded after playback — utterance hooks released on end.
 *  6. Text history independent of audio — render() draws text; speech is separate.
 *  7. Citizen identity and voice profile preserved — stable per-citizen voice mapping.
 *  8. Human mic input transient — the existing path sends text; no audio retained.
 */
(function (root) {
  'use strict';
  var MODE_KEY = 'sc-receive-mode';
  var MODES = { text: 1, audio: 1, both: 1 };
  var spoken = {}; // msgId -> true. Session memory ONLY. Never persisted, never cleared for replay.
  var seq = 0;
  var attached = false;

  function getMode() {
    try {
      var store = (typeof localStorage !== 'undefined') ? localStorage : null;
      var m = store && store.getItem(MODE_KEY);
      if (m && MODES[m]) return m;
    } catch (e) {}
    return 'both';
  }
  function setMode(m) {
    if (!MODES[m]) return getMode();
    try { if (typeof localStorage !== 'undefined') localStorage.setItem(MODE_KEY, m); } catch (e) {}
    paint();
    return m;
  }

  // Voice identity per citizen. SABLE keeps her established voice.
  // Deterministic per-citizen assignment for the rest. No persona controls in V1.
  function voiceProfile(id) {
    if (id === 'sable') return { pitch: 0.95, re: /sama