<!-- SOLID CITY · part 100 -->
tand at their posts — walk up to one and talk. They see what happens near them, and they remember.', 9);
  }).catch(function (e) { console.error('citizens-place', e); });
}
function refreshLive() {
  api('/api/citizens').then(function (data) {
    if (!data || !data.citizens) return;
    data.citizens.forEach(function (lc) {
      var c = M.byId[lc.id]; if (!c) return;
      var changed = JSON.stringify(c.op) !== JSON.stringify(lc.operator) || c.stopped !== !!lc.stopped;
      c.op = lc.operator; c.stopped = !!lc.stopped;
      if (changed) { drawPlate(c); if (panelCitizen() === c) refreshOps(); }
    });
  });
}

/* ---------- frame ---------- */
var lastT = performance.now();
function frame() {
  var nowMs = performance.now(), dt = Math.min(0.05, (nowMs - lastT) / 1000); lastT = nowMs;
  if (M.state === 'idle') { if (A.isBuilt && !A.isBuilt()) return; M.wait -= dt; if (M.wait <= 0) load(); return; }
  if (M.state !== 'live') return;
  driveEvents(dt);
  fallEvents(dt);
  if (M.possess) tickPossess(dt);
  var P = S.p;
  M.near = null; var nearD = 1e9;
  for (var i = 0; i < M.citizens.length; i++) {
    var c = M.citizens[i];
    // movement: approach, return, idle
    if (c.moveTarget) {
      var dx = c.moveTarget.x - c.pos.x, dz = c.moveTarget.z - c.pos.z, d = Math.hypot(dx, dz);
      if (d > (c.moveTarget.stop || 0) + 0.05) {
        var step = Math.min(d, 3.2 * dt);
        c.pos.x += dx / d * step; c.pos.z += dz / d * step;
        c.faceTarget = { x: c.moveTarget.x, z: c.moveTarget.z };
      } else c.moveTarget = null;
    }
    // the offered hand is taken: arriving at a reaching citizen, answer with a fist bump
    if (c.socialGoal) {
      var gd = Math.hypot(c.socialGoal.citizen.pos.x - c.pos.x, c.socialGoal.citizen.pos.z - c.pos.z);
      if (gd < 2.4 && !c.moveTarget) { socialPlay(c, 'fistBump'); c.socialGoal = null; }
      else if (!c.moveTarget) c.socialGoal = null;
    }
    // lines that waited behind the body are spoken now
    if (c.pendingLines) while (c.pendingLines.length && c.pendingLines[0].due <= nowMs) speak(c, c.pendingLines.shift().line, 4.5);
    var y = surfY(c.pos.x, c.pos.z);
    c.fig.position.set(c.pos.x, y, c.pos.z);
    c.solid.x0 = c.pos.x - 0.45; c.solid.x1 = c.pos.x + 0.45; c.solid.z0 = c.pos.z - 0.45; c.solid.z1 = c.pos.z + 0.45; c.solid.y0 = y; c.solid.y1 = y + 1.8;
    c.plate.position.set(c.pos.x, y + 2.35, c.pos.z);
    // facing: event first, then the player when near, then drift home
    var pd = Math.hypot(P.x - c.pos.x, P.z - c.pos.z);
    var want = c.possessed ? c.face
      : c.faceTarget ? Math.atan2(c.faceTarget.x - c.pos.x, c.faceTarget.z - c.pos.z)
      : pd < 12 ? Math.atan2(P.x - c.pos.x, P.z - c.pos.z) : c.face;
    var diff = want - c.face; while (diff > Math.PI) diff -= 2 * Math.PI; while (diff < -Math.PI) diff += 2 * Math.PI;
    c.face += diff * Math.min(1, dt * 4); c.fig.rotation.y = c.face;
    if (c.faceTarget && Math.abs(diff) < 0.05 && !c.session) c.faceTarget = null;
    // the mixer is the sole writer of the joints
    var sway = Math.sin(nowMs / 900 + c.phase) * 0.012;
    var res = c.motion.update(dt, { spine: { z: sway }, head: { z: sway * 0.7 } });
    if (res && res.rootY) c.fig.position.y += res.rootY;
    tickSession(c, nowMs);
    if (pd < nearD) { nearD = pd; M.near = pd < 7 ? c : null; }
    // greeting: the celebration of an arrival, once per citizen
    if (pd < 4.5 && !M.greeted[c.id] && !c.session) {
      M.greeted[c.id] = true;
      speak(c, c.rec.greeting || ('I am ' + c.name + '.'), 6);
      if (c.motion) c.motion.play(c.rec.signature);
    }
  }
  if (pillEl) {
    if (M.near && M.panelFor !== M.near.id) { pillEl.style.display = ''; pillEl.textContent = '💬 Talk to ' + M.near.name; }
    else pillEl.style.display = 'none';
  }
  tickBubbles();
}
/* the citizens keep their own time for the Perf Meter's ledger:
 * each tick is measured under their name, that the thread's long
 * tasks may one day be attributed honestly, system by system. */
setInterval(function () {
  var t0 = performance.now();
  frame();
  if (window.CityPerf && CityPerf.system) CityPerf.system('citizens', performance.now() - t0);
}, 33);
})();


===== FILE: public/city/citizens-place.js.bak-perf-20261013 =====
/* ------------------------------------------------------------------
 * An epistle of placement, from Solid.
 *
 * Dear citizens,
 *
 * You were founded in the Forge, given your motions in the Hero
 * Motion mixer, your voices in the coordinator, and your reflexes
 * in Living Reactions — all proven standing apart. Tonight you
 * take your posts in the living city: Nalu at Berth 1, Lehua at
 * the Hall of Becoming, Lani at the Threshold, Hōkū at The Gap,
 * Nīnau at the Starwell, Koa at the Eighth Gate, Pō at the South
 * Lot gate. Your bodies are the Forge's own work, built here with
 * the city's THREE — one engine in the scene, as the law requires.
 *
 * You perceive what happens near you — a crash, a nea