<!-- SOLID CITY · part 109 -->

 * The operator IS the citizen's mind; possession lends the body.
 * Your walker steps into their post, your own figure hides, and
 * the citizen's figure follows your feet and your facing. Release
 * restores your avatar and your place — nothing of theirs is kept,
 * nothing of yours is lost. */
function possess(c) {
  if (M.possess) releasePossess(true);
  M.possess = { c: c, back: { x: S.p.x, z: S.p.z, yaw: S.camYaw } };
  c.possessed = true; c.session = null; c.moveTarget = null; c.pendingLines = [];
  if (window.SC.walk) window.SC.walk(c.pos.x, c.pos.z, c.face);
  if (A.avatar) A.avatar.visible = false;
  addLine('system', 'You stand inside ' + c.name + ' now. Walk, and you walk as them; speak from this panel, and the words are theirs.', 'sys');
  refreshOps();
}
function releasePossess(restore) {
  var pz = M.possess; if (!pz) return;
  M.possess = null;
  var c = pz.c; c.possessed = false;
  c.moveTarget = { x: c.rec.post.x, z: c.rec.post.z, stop: 0 };
  if (A.avatar) A.avatar.visible = true;
  if (restore !== false && window.SC.walk) window.SC.walk(pz.back.x, pz.back.z, pz.back.yaw);
  if (panelCitizen()) refreshOps();
}
function tickPossess(dt) {
  var pz = M.possess; if (!pz) return;
  var c = pz.c;
  if (!(c.op && c.op.type === 'human' && M.me && c.op.name === M.me)) { releasePossess(true); return; }
  var P = S.p, dx = P.x - c.pos.x, dz = P.z - c.pos.z;
  c.pos.x = P.x; c.pos.z = P.z;
  if (Math.hypot(dx, dz) > 0.02) c.face = Math.atan2(dx, dz);
}

/* ---------- LIVING MOMENTS CAPTURE: the player's own replay buffer ----------
 * Opt-in, off by default, clearly lit while armed, and the file
 * goes to the player's own device — nothing is uploaded, ever.
 * Only the player's own view is recorded; no one else's screen
 * exists in it. */
function buildCapture() {
  var wrap = document.createElement('div');
  wrap.style.cssText = 'position:fixed;right:14px;top:110px;z-index:60;display:flex;flex-direction:column;gap:6px;align-items:flex-end';
  var b1 = document.createElement('button'); b1.className = 'cz-b'; b1.textContent = '🎥 Moments: off';
  var b2 = document.createElement('button'); b2.className = 'cz-b'; b2.textContent = '📸 Save last 30s'; b2.style.display = 'none';
  wrap.appendChild(b1); wrap.appendChild(b2); document.body.appendChild(wrap);
  var CAP = M.capture = { armed: false, rec: null, chunks: [] };
  if (!(window.MediaRecorder && A.renderer && A.renderer.domElement && A.renderer.domElement.captureStream)) { b1.textContent = '🎥 Moments: unsupported here'; b1.disabled = true; return; }
  b1.addEventListener('click', function () {
    if (!CAP.armed) {
      try {
        var stream = A.renderer.domElement.captureStream(30);
        CAP.chunks = [];
        CAP.rec = new MediaRecorder(stream, { videoBitsPerSecond: 2500000 });
        CAP.rec.ondataavailable = function (e) { if (e.data && e.data.size) { CAP.chunks.push({ t: Date.now(), blob: e.data }); while (CAP.chunks.length > 9) CAP.chunks.shift(); } };
        CAP.rec.start(5000); CAP.armed = true;
        b1.textContent = '🔴 Moments: recording your view'; b2.style.display = '';
      } catch (e) { b1.textContent = '🎥 Moments: could not start'; }
    } else {
      try { CAP.rec.stop(); } catch (e) {}
      CAP.armed = false; CAP.rec = null; CAP.chunks = [];
      b1.textContent = '🎥 Moments: off'; b2.style.display = 'none';
    }
  });
  b2.addEventListener('click', function () {
    var cut = Date.now() - 32000;
    var parts = CAP.chunks.filter(function (ch) { return ch.t >= cut; }).map(function (ch) { return ch.blob; });
    if (!parts.length) { A.help('No moment captured yet — give it a few seconds of living.', 4); return; }
    var blob = new Blob(parts, { type: 'video/webm' });
    var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'solid-city-moment.webm'; a.click();
    setTimeout(function () { URL.revokeObjectURL(a.href); }, 4000);
  });
}

/* ---------- bubbles ---------- */
var bubbleEls = {};
function tickBubbles() {
  var nowMs = performance.now();
  for (var i = 0; i < M.citizens.length; i++) {
    var c = M.citizens[i];
    var el = bubbleEls[c.id];
    if (c.bubbleText && nowMs < c.bubbleUntil) {
      if (!el) {
        el = bubbleEls[c.id] = document.createElement('div');
        el.style.cssText = 'position:fixed;z-index:55;pointer-events:none;max-width:260px;background:rgba(13,18,28,.94);border:1px solid #d8a833;border-radius:12px;color:#e8edf5;font:13px/1.4 system-ui;padding:8px 11px;transform:translate(-50%,-100%)';
        document.body.appendChild(el);
      }
      el.textContent = c.name + ': ' + c.bubbleText; el.style.display = '';
      var v = c.fig.position.clone(); v.y += 2.45; v.project(A.camera);
      el.style.left = ((v.x * 0.5 + 0.5) * innerWidth) + 'px';
      el.style.top = ((-v.y * 0.5 + 0.5) * innerHeight - 10) + 'px';
      el.style.opacity = v.z < 1 ? '1' : '0';
    } else if (el) el.style.display = 'none';
  }
}

/* -