<!-- SOLID CITY · part 108 -->
   var c = panelCitizen(); if (!c) return;
    var mine = c.op && c.op.type === 'human' && M.me && c.op.name === M.me;
    var to = mine ? { type: 'ai' } : (c.op && c.op.type === 'human' ? { type: 'ai' } : { type: 'human' });
    api('/api/citizens/' + c.id + '/handoff', { method: 'POST', body: JSON.stringify({ to: to }) }).then(function (r) {
      if (r.ok) { c.op = r.operator; c.epoch = r.epoch; drawPlate(c); refreshOps(); addLine('system', 'Operator now: ' + (r.operator.type === 'human' ? 'you, inhabiting ' + c.name : r.operator.type) + ' (epoch ' + r.epoch + ').', 'sys'); }
      else addLine('system', r.error || 'Handoff refused.', 'sys');
    });
  });
}
function addLine(who, text, kind) {
  if (!logEl) return;
  var d = document.createElement('div');
  d.style.cssText = kind === 'me' ? 'align-self:flex-end;background:#23314a;border-radius:12px;padding:7px 11px;max-width:85%' : kind === 'sys' ? 'color:#9aa4b5;font-size:12px;align-self:center' : 'align-self:flex-start;background:#1a2436;border-radius:12px;padding:7px 11px;max-width:85%';
  d.textContent = (kind === 'sys' ? '' : who + ': ') + text;
  logEl.appendChild(d); while (logEl.children.length > 30) logEl.removeChild(logEl.firstChild);
  logEl.scrollTop = logEl.scrollHeight;
}
function refreshOps() {
  var c = panelCitizen(); if (!c) return;
  titleEl.textContent = c.name + ' — ' + c.rec.craft;
  var opLine = !c.op ? '' : c.op.type === 'human' ? 'Operated by ' + (c.op.name || 'a human') + ' · epoch ' + c.op.epoch : c.op.type === 'unoccupied' ? 'Unoccupied · resting' : 'Operated by AI · epoch ' + (c.op.epoch || 1);
  opsEl.textContent = opLine + (c.stopped ? ' · ⛔ STOPPED' : '');
  panelEl.querySelector('#cz-inhabit').textContent = (c.op && c.op.type === 'human' && M.me && c.op.name === M.me) ? 'Release' : 'Inhabit';
  panelEl.querySelector('#cz-possess').textContent = (M.possess && M.possess.c === c) ? '🚶 Release body' : '👁 Possess';
  panelEl.querySelector('#cz-voicemine').style.display = (c.op && c.op.type === 'human' && M.me && c.op.name === M.me) ? '' : 'none';
  panelEl.querySelector('#cz-stop').textContent = c.stopped ? '▶ Resume' : '⛔ Stop';
  var inboxEl = panelEl.querySelector('#cz-inbox');
  if (c.op && c.op.type === 'human' && M.me && c.op.name === M.me) {
    api('/api/citizens/' + c.id + '/inbox').then(function (r) {
      if (r.ok && r.inbox.length) { inboxEl.style.display = ''; inboxEl.textContent = '📥 Waiting for your answer: ' + r.inbox.map(function (m) { return m.speaker + ' — “' + m.text + '”'; }).join(' · '); }
      else inboxEl.style.display = 'none';
    });
  } else inboxEl.style.display = 'none';
}
function openPanel(c) {
  M.panelFor = c.id; panelEl.style.display = '';
  logEl.innerHTML = '';
  addLine(c.name, c.rec.greeting || ('I am ' + c.name + '.'), 'them');
  refreshOps();
}
function closePanel() { M.panelFor = null; panelEl.style.display = 'none'; }
function sendPanel() {
  var c = panelCitizen(); if (!c) return;
  var text = inputEl.value.trim(); if (!text) return;
  inputEl.value = '';
  var mine = c.op && c.op.type === 'human' && M.me && c.op.name === M.me;
  addLine(mine ? c.name + ' (you)' : 'You', text, 'me');
  if (c.stopped) { addLine('system', c.name + ' is stopped and cannot answer.', 'sys'); return; }
  if (mine) {
    api('/api/citizens/' + c.id + '/reply', { method: 'POST', body: JSON.stringify({ text: text, epoch: c.op.epoch }) }).then(function (r) {
      if (!r.ok) addLine('system', (r.why || r.error) === 'stale-epoch' ? 'Your operator epoch went stale — someone relieved you.' : 'Reply refused: ' + (r.why || r.error || ''), 'sys');
      else { if (M.voiceMine) speak(c, r.text, Math.max(4, r.text.length / 14)); if (r.gesture && c.motion) c.motion.play(r.gesture); refreshOps(); }
    });
    return;
  }
  if (c.op && c.op.type !== 'ai') { addLine('system', c.op.type === 'human' ? c.name + ' is inhabited — your words wait in their inbox.' : c.name + ' is resting — your words wait in their inbox.', 'sys'); }
  api('/api/citizens/' + c.id + '/say', { method: 'POST', body: JSON.stringify({ text: text }) }).then(function (r) {
    if (r.ok && r.pending) { addLine('system', 'Delivered to ' + c.name + '’s inbox.', 'sys'); if (c.op && c.op.type === 'human' && M.me && c.op.name === M.me) refreshOps(); return; }
    if (!r.ok) {
      // THE GRACEFUL FLOOR: the voice rides the engine, and the
      // engine can fail — unpaid, throttled, gone. The BODY is the
      // game's own and answers anyway: they heard you; watch.
      if (c.motion) { c.faceTarget = { x: S.p.x, z: S.p.z }; c.motion.play('wave'); }
      addLine('system', c.name + '’s voice is carried by the engine, and the engine is silent just now — but they heard you. The body still answers.', 'sys');
      return;
    }
    speak(c, r.text, Math.max(4, r.text.length / 14));
    if (r.gesture && c.motion) c.motion.play(r.gesture);
  });
}

/* ---------- POSSESSION: see the city standing where they stand ----------