<!-- SOLID CITY · part 122 -->
 'walk' ? ' \uD83D\uDEB6' : '') + '</small>';
    b.addEventListener('click', function () { citizenCard(c); });
    box.appendChild(b);
  });
}
function mountHyper() {
  var bar = $('hc-buttons'); if (!bar || $('hc-citizens')) return;
  var b = document.createElement('button'); b.type = 'button'; b.className = 'hc-btn'; b.id = 'hc-citizens'; b.title = b.ariaLabel = 'CITIZENS: find, go to, watch, talk'; b.setAttribute('aria-label', b.title); b.textContent = '\uD83D\uDC65';
  b.addEventListener('click', listCard);
  var w = $('hc-watch'); if (w && w.nextSibling) bar.insertBefore(b, w.nextSibling); else bar.appendChild(b);
  // typed commands: "find provolta", "watch hury", "talk to boss", "go to grok bot"
  var inp = $('hc-input');
  if (inp) inp.addEventListener('keydown', function (e) {
    if (e.key !== 'Enter') return;
    var m = /^\s*(find|where is|locate|go to|walk to|fly to|take me to|watch|follow|talk to|meet|chat with)\s+(.+?)\s*[.!?]?\s*$/i.exec(inp.value || ''); if (!m) return;
    var nm = m[2].toLowerCase().replace(/^the\s+/, ''), c = ROSTER.filter(function (x) { return x.name.toLowerCase() === nm || x.id === nm.replace(/\s+/g, ''); })[0]; if (!c) return;
    e.preventDefault(); e.stopImmediatePropagation(); inp.value = ''; inp.blur();
    var v = m[1].toLowerCase();
    if (/watch|follow/.test(v)) { if (c.fixed) find(c); else watch(c); } else if (/talk|meet|chat/.test(v)) { var p = myPos(); if (Math.hypot(c.x - p.x, c.z - p.z) < 12) open(c.id); else goTo(c, true); }
    else if (/find|where|locate/.test(v)) find(c); else { find(c); goTo(c, false); }
  }, true);
}

/* ---------------- radar ---------------- */
function radar(g, blip, fl, sc, ax, az) {
  all.forEach(function (c) {
    if (fl && c.st !== 'fly') return;
    blip(c.x, c.z, hex(c.accent), c === follow ? 5 : 3, '');
  });
  var f = window.CityHyper && CityHyper.state.focus; if (f && f.cz) { f.x = f.cz.x; f.z = f.cz.z; }
}

/* ---------------- frame ---------------- */
var ready = false;
function update(dt) {
  if (!ready) return;
  lodT -= dt; if (lodT <= 0) { lodT = 0.5; relod(); }
  for (var i = 0; i < all.length; i++) { var c = all[i]; stepCitizen(c, dt, c.on); }
  // the player can't walk through a citizen either
  if (S.mode === 'walk') for (var j = 0; j < active.length; j++) { var a = active[j]; if (a.st === 'fly') continue; var dx = S.p.x - a.x, dz = S.p.z - a.z, d = Math.hypot(dx, dz); if (d < 0.9 && d > 0.001) { S.p.x = a.x + dx / d * 0.9; S.p.z = a.z + dz / d * 0.9; } }
  for (var n = 0; n < all.length; n++) place(all[n]);
  if (cur && !panel.hidden) { var p = S.p; if (S.mode !== 'walk' || Math.hypot(p.x - cur.x, p.z - cur.z) > 40) { if (cur.st !== 'dwell') closeTalk(); } }
  radioTick(dt);
}
function near(x, z) {
  var best = null, bd = 3.6;
  for (var i = 0; i < active.length; i++) { var c = active[i]; if (c.st === 'fly') continue; var d = Math.hypot(c.x - x, c.z - z); if (d < bd) { bd = d; best = c; } }
  return best;
}
function tap(ray) {
  var hits = [];
  active.forEach(function (c) { if (c.g.visible) hits.push(c.hit); });
  var r = hits.length ? ray.intersectObjects(hits, false) : [];
  if (!r.length) return false;
  var c = r[0].object.userData.cz; if (r[0].distance > 80) return false;
  return open(c.id);
}
function start() { if (ready) return; init(); ready = true; relod(); mountHyper(); }
if (A.isBuilt()) start(); else addEventListener('sc-city-built', start);
setTimeout(mountHyper, 0);

window.CityCitizens = {
  update: update, radar: radar, cam: cam, near: near, tap: tap, open: open, watch: watch, unwatch: unwatch, find: find, list: listCard,
  goTo: goTo, get: function (id) { return BY[id] || null; },
  // 🎮 AI pilots (pilots.js): speech bubble, walk / fly a citizen to a point (stop key 'pilot_<id>' in STOPS)
  say: function (id, text, sec) { var c = BY[id]; if (!c || !c.g) return false; bubble(c, text); if (sec) c.bubT = S.t + sec; return true; },
  walkTo: function (id, x, z, stop) { var c = BY[id]; if (!c) return; c.dest = stop; c.tx = x; c.tz = z; c.path = [[x, z]]; c.pi = 0; c.st = 'walk'; c.stuck = 0; c.stuckT = 0; c.lx = c.x; c.lz = c.z; },
  flyTo: function (id, x, z, stop) { var c = BY[id]; if (!c) return; c.dest = stop; c.tx = x; c.tz = z; if (c.st === 'fly') { c.hdg = Math.atan2(x - c.x, z - c.z); if (c.fph === 'down') c.fph = 'cruise'; } else startFly(c); }, following: function () { return follow; }, myPos: myPos,
  roster: ROSTER, stops: STOPS, active: function () { return active.map(function (c) { return c.id; }); },
  state: function () { return ROSTER.map(function (c) { return { id: c.id, name: c.name, st: c.st, x: Math.round(c.x), y: Math.round(c.y), z: Math.round(c.z), on: !!c.on }; }); }
};
})();


===== FILE: public/city/city.css =====
/* SOLID CITY — red / black / gold, iPhone-first */
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;height:100%;background:#000;overflow:hidden;overscroll-behavior:none;touch-action:none;fon