<!-- SOLID CITY · part 95 -->
come is waved back; a laugh is contagious. Responses are
 * throttled per citizen and never interrupt a citizen's own moment. */
function broadcastAttention(from, ev) {
  for (var i = 0; i < M.citizens.length; i++) {
    var c = M.citizens[i];
    if (c === from || c.possessed || c.stopped || c.session) continue;
    if (Math.hypot(ev.x - c.pos.x, ev.z - c.pos.z) > 25) continue;
    c.faceTarget = { x: ev.x, z: ev.z };
    socialPlay(c, 'startle');
  }
}
function broadcastGesture(from, name) {
  for (var i = 0; i < M.citizens.length; i++) {
    var c = M.citizens[i];
    if (c === from || c.possessed || c.stopped) continue;
    var d = Math.hypot(from.pos.x - c.pos.x, from.pos.z - c.pos.z);
    if (name === 'rescueReach' && d <= 25 && !c.session) {
      // a hand is offered: go and take it
      c.faceTarget = { x: from.pos.x, z: from.pos.z };
      c.moveTarget = { x: from.pos.x, z: from.pos.z, stop: 1.3 };
      c.socialGoal = { citizen: from, kind: 'handshake' };
    } else if ((name === 'openWelcome' || name === 'welcomeBecoming' || name === 'bindingWord') && d <= 12 && !c.session) {
      c.faceTarget = { x: from.pos.x, z: from.pos.z };
      socialPlay(c, d < 3.5 ? 'fistBump' : 'wave');
    } else if ((name === 'laugh' || name === 'headShake') && d <= 20 && !c.session) {
      c.faceTarget = { x: from.pos.x, z: from.pos.z };
      socialPlay(c, name === 'laugh' ? 'laugh' : 'headShake');
    } else if (name === 'sentinelSignal' && d <= 25) {
      // Pô has seen something: heads turn her way, quietly
      c.faceTarget = { x: from.pos.x, z: from.pos.z };
    }
  }
}
function socialPlay(c, name) {
  var nowMs = performance.now();
  if (nowMs - (c.lastSocialAt || -1e9) < 2500) return;
  c.lastSocialAt = nowMs;
  if (c.motion) c.motion.play(name);
}

/* ---------- events from the living city ---------- */
function driveEvents(dt) {
  var D = S.drive;
  if (!D || !D.mesh) { M.prevDrive = null; M.spinAcc = 0; return; }
  var pos = D.mesh.position, cur = { speed: Math.abs(D.speed || 0), x: pos.x, z: pos.z, yaw: D.yaw || 0 };
  var prev = M.prevDrive;
  if (prev) {
    // a crash: speed torn away in a breath
    if (prev.speed - cur.speed >= 9 && prev.speed >= 11) {
      fireEvent({ type: 'vehicleCrash', x: cur.x, z: cur.z, speed: prev.speed, label: A.whereName ? A.whereName(cur.x, cur.z) : 'the road', at: performance.now(), outcome: null, carPos: cur });
    }
    // a wild spin, upright and laughing
    var dy = Math.abs(cur.yaw - prev.yaw); if (dy > Math.PI) dy = 2 * Math.PI - dy;
    if (cur.speed > 8) { M.spinAcc += dy; M.spinT += dt; if (M.spinAcc > 5.5 && M.spinT < 2.5) { fireEvent({ type: 'funnyMoment', x: cur.x, z: cur.z, label: A.whereName ? A.whereName(cur.x, cur.z) : 'the road' }); M.spinAcc = 0; M.spinT = 0; } }
    if (M.spinT > 2.5) { M.spinAcc = 0; M.spinT = 0; }
    // near misses: citizens feel the wind of a fast pass
    if (cur.speed >= 12) for (var i = 0; i < M.citizens.length; i++) {
      var c = M.citizens[i], d = Math.hypot(cur.x - c.pos.x, cur.z - c.pos.z);
      if (d < 5 && !c._nm) { c._nm = true; startReaction(c, { type: 'nearMiss', x: cur.x, z: cur.z, label: 'their post' }); }
      if (d > 12) c._nm = false;
    }
    // resolutions for open crash sessions
    for (var j = 0; j < M.citizens.length; j++) {
      var cz = M.citizens[j], ss = cz.session;
      if (ss && ss.ev.type === 'vehicleCrash' && !ss.outcome) {
        var age = performance.now() - ss.t0, moved = Math.hypot(cur.x - ss.ev.x, cur.z - ss.ev.z);
        if (moved > 22 || age > 12000) ss.outcome = 'up';
        else if (age > 9000 && moved < 4 && cur.speed < 1) ss.outcome = 'trapped';
      }
    }
  }
  M.prevDrive = cur;
  if (!D) for (var k = 0; k < M.citizens.length; k++) { var s2 = M.citizens[k].session; if (s2 && s2.ev.type === 'vehicleCrash' && !s2.outcome) s2.outcome = 'up'; }
}
function fireEvent(ev) { for (var i = 0; i < M.citizens.length; i++) startReaction(M.citizens[i], ev); }

/* ---------- fall awareness: a tumble is not a tragedy, until it is ---------- */
var FALL = { prevY: null, vy: 0, peakY: 0, air: false, worstVy: 0 };
function fallEvents(dt) {
  var P = S.p; if (!P || S.mode !== 'walk' || S.drive) { FALL.prevY = null; return; }
  var gy = surfY(P.x, P.z);
  if (FALL.prevY == null) { FALL.prevY = P.y; return; }
  var vy = (P.y - FALL.prevY) / Math.max(dt, 0.001); FALL.prevY = P.y;
  FALL.vy = vy;
  if (P.y > gy + 2) { FALL.air = true; FALL.peakY = Math.max(FALL.peakY, P.y); FALL.worstVy = Math.min(FALL.worstVy, vy); }
  else if (FALL.air && P.y <= gy + 0.35) {
    FALL.air = false;
    var impact = Math.abs(FALL.worstVy); FALL.worstVy = 0; FALL.peakY = 0;
    var label = A.whereName ? A.whereName(P.x, P.z) : 'nearby';
    if (impact >= 13) fireEvent({ type: 'fall', x: P.x, z: P.z, speed: impact, label: label });
    else if (impact >= 8) fireEvent({ type: 'nearMiss', x: P.x, z: P.z, label: label });
    else if (impact >= 4) fireEvent({ type: 'funnyMoment', x