<!-- SOLID CITY · part 3644 -->
UTHERN SLOPE', 8, -36.6, 152, 0);
  plate('THE TARO TERRACES', -10, -14.6, 56, 0);
  plate('THE CRAG LOOKOUT', 78, 18.4, -100, 3.1);
  plate('THE HIDDEN GLOW', -80, -26.6, 52, 1.57);



  // ---------- TRUE-NAME PLATES ----------
  function plate(txt, x, y, z, ry) {
    var c = document.createElement('canvas'); c.width = 256; c.height = 64;
    var g = c.getContext('2d');
    g.fillStyle = '#1c1a20'; g.fillRect(0, 0, 256, 64);
    g.strokeStyle = '#d8aa48'; g.lineWidth = 4; g.strokeRect(3, 3, 250, 58);
    g.fillStyle = '#d8aa48'; g.textAlign = 'center'; g.textBaseline = 'middle';
    g.font = 'bold 25px Georgia, serif'; g.fillText(txt, 128, 34);
    var t = new T.CanvasTexture(c); t.encoding = T.sRGBEncoding;
    var m = new T.Mesh(new T.PlaneGeometry(7, 1.75), new T.MeshBasicMaterial({ map: t, side: T.DoubleSide }));
    m.position.set(x, y, z); m.rotation.y = ry || 0; root.add(m);
    var post = new T.Mesh(new T.BoxGeometry(0.8, 2.2, 0.8), basaltMat);
    post.position.set(x, y - 1.9, z); root.add(post);
    foot.push([x, z, 1, 1, y - 3, y - 0.8, 0]);
  }
  plate('THE FALL', 32, 3.4, -158, -0.5);
  plate('THE SECOND FIRE', -2, -32.6, 130, 0.1);
  plate('THE UPPER HUTS', -59, -14.6, 30, 1.57);
  plate('THE LOWER HUTS', 58, -32.6, 122, -1.57);
  plate("THE ROAD'S END", 24, 12.4, -170, 3.1);

  // ---------- fireflies of warm light: village glow ----------
  var glow = new T.PointLight(0xffb050, 1.4, 220, 1.2);
  glow.position.set(0, 14, 0); root.add(glow);

  root.userData = {
    footprint: foot, center: { x: 0, z: 0 }, radius: 320, height: 60,
    terraceTops: TER.map(function (t) { return { x: t.x, z: t.z, r: t.r, topY: t.y + 1.1 }; }),
    walkRects: (typeof walkRects !== "undefined" ? walkRects : []),
    update: function (t) {
      var hale = haleRef.g;
      if (hale) {
        hale.userData.flame.scale.y = 1 + Math.sin(t * 5.2) * 0.16;
        hale.userData.fireLight.intensity = 2.1 + Math.sin(t * 5.2) * 0.5 + Math.sin(t * 13.7) * 0.2;
      }
      for (var k = 0; k < torches.length; k++) {
        var tc = torches[k];
        tc.fl.scale.y = 1 + Math.sin(t * 6 + tc.seed) * 0.18;
        tc.pl.intensity = 0.95 + Math.sin(t * 6 + tc.seed) * 0.22;
      }
      glow.intensity = 1.35 + Math.sin(t * 1.7) * 0.12;
      for (var fi = 0; fi < fires.length; fi++) { var f = fires[fi]; f.flame.scale.y = 1 + Math.sin(t * 5.6 + f.seed) * 0.16; f.light.intensity = 2.0 + Math.sin(t * 5.6 + f.seed) * 0.5; }
      for (var wi = 0; wi < falls.length; wi++) { falls[wi].material.opacity = 0.68 + Math.sin(t * 7 + wi * 2.1) * 0.14; }
    }
  };
  return root;
}
export default createMaunaVillage;


===== FILE: public/city/voice-speed.js =====
/* SOLID CITY — listener voice speed, V1 (King's decision 2026-10-10)
 * Exactly two options: FAST (default, 1.2x) and SLOW (0.85x).
 * Per-listener; persists in localStorage. Never alters text, speaker
 * identity, voice identity, delivery, or any other listener.
 * V1: no persona, pitch, accent, or other voice controls.
 * Control lives in optional panels (SABLE talk panel), never on the
 * main game screen. Browser playback preserves pitch so the speaker
 * always sounds like themselves.
 */
window.CityVoiceSpeed = (function () {
  var SPEEDS = { fast: 1.2, slow: 0.85 };
  var KEY = 'sc-voice-speed';

  function get() {
    try {
      var v = localStorage.getItem(KEY);
      return v === 'slow' ? 'slow' : 'fast';
    } catch (e) { return 'fast'; }
  }
  function set(name) {
    if (name !== 'fast' && name !== 'slow') return get();
    try { localStorage.setItem(KEY, name); } catch (e) {}
    return name;
  }
  function rate() { return SPEEDS[get()]; }

  // Apply to a SpeechSynthesisUtterance (SABLE's voice today).
  function applyToUtterance(u) { try { u.rate = rate(); } catch (e) {} return u; }
  // Apply to an <audio> element (citizen voices when listener-side
  // rendering lands). Pitch preserved: speed never changes identity.
  function applyToAudio(el) {
    try {
      el.playbackRate = rate();
      if ('preservesPitch' in el) el.preservesPitch = true;
      if ('webkitPreservesPitch' in el) el.webkitPreservesPitch = true;
    } catch (e) {}
    return el;
  }

  function paint() {
    var f = document.getElementById('vs-fast'), s = document.getElementById('vs-slow');
    if (!f || !s) return;
    var cur = get();
    f.classList.toggle('on', cur === 'fast');
    s.classList.toggle('on', cur === 'slow');
    f.setAttribute('aria-pressed', cur === 'fast' ? 'true' : 'false');
    s.setAttribute('aria-pressed', cur === 'slow' ? 'true' : 'false');
  }
  function mountToggle() {
    var f = document.getElementById('vs-fast'), s = document.getElementById('vs-slow');
    if (!f || !s) return;
    f.addEventListener('click', function () { set('fast'); paint(); });
    s.addEventListener('click', function () { set('slow'); paint(); });
    paint();
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded',