<!-- SOLID CITY · part 41 -->
ces -z; forward vector:
  var fx = -Math.sin(yaw), fz = -Math.cos(yaw);
  var rx = Math.cos(yaw), rz = -Math.sin(yaw);
  var nx = camera.position.x + (fx * f + rx * s) * sp * dt;
  var nz = camera.position.z + (fz * f + rz * s) * sp * dt;
  // room bounds
  nx = Math.max(-20, Math.min(20, nx)); nz = Math.max(-19, Math.min(20, nz));
  // console colliders (circle push-out)
  for (var i = 0; i < colliders.length; i++) {
    var c = colliders[i];
    var dx = nx - c.x, dz = nz - c.z;
    var px = c.hw + 0.5 - Math.abs(dx), pz = c.hd + 0.5 - Math.abs(dz);
    if (px > 0 && pz > 0) {
      if (px < pz) nx = c.x + (dx > 0 ? c.hw + 0.5 : -(c.hw + 0.5));
      else nz = c.z + (dz > 0 ? c.hd + 0.5 : -(c.hd + 0.5));
    }
  }
  camera.position.x = nx; camera.position.z = nz;
  camera.rotation.set(0, 0, 0);
  camera.rotation.order = 'YXZ';
  camera.rotation.y = yaw; camera.rotation.x = pitch;
  // shake decay
  if (shake > 0) {
    shake = Math.max(0, shake - dt * 1.4);
    camera.position.x += (Math.random() - 0.5) * shake * 0.35;
    camera.position.y = 1.7 + (Math.random() - 0.5) * shake * 0.3;
  } else camera.position.y = 1.7;
  // walking away from a held console releases it
  if (G.holds) {
    var cg = consoles[G.holds].group.position;
    if (Math.hypot(camera.position.x - cg.x, camera.position.z - cg.z) > 6) releaseHold();
  }
}

/* ---------- Main loop ---------- */
var helperT = 20;
function frame() {
  requestAnimationFrame(frame);
  var dt = Math.min(0.05, clock.getDelta());
  if (G.mode === 'play' && !G.over) {
    G.elapsed += dt;
    G.stormT -= dt;
    updateStormHud();
    moveTick(dt);
    meteorTick(dt);
    solidTick(dt);
    // player repair progress
    if (G.repairing && G.holds && taskOwner(G.holds) === 'YOU') {
      G.repairProg += dt / 4;
      if (G.repairProg >= 1) {
        var id = G.holds;
        SYS[id].dmg = Math.max(0, SYS[id].dmg - 1);
        refreshConsole(id);
        feed('YOU', SYS[id].label + ' repaired → ' + DMG_NAMES[SYS[id].dmg]);
        G.repairing = false;
        releaseHold();
      }
      updateRepairHud();
    }
    // scheduled strikes accelerate as the storm peaks
    G.nextStrike -= dt;
    if (G.nextStrike <= 0) {
      impact();
      var frac = 1 - G.stormT / STORM_SECONDS;
      G.nextStrike = 16 - frac * 8 + Math.random() * 5;
    }
    helperT -= dt;
    if (helperT <= 0) { helperT = 30 + Math.random() * 25; helperChatter(); }
    // win / lose
    if (failedCount() >= 2) endRun(false, 'Two systems down. The storm took her.');
    else if (G.stormT <= 0) endRun(true, 'Rode out the storm. ' + (failedCount() ? 'Scarred, but alive.' : 'All systems green.'));
  } else if (G.mode === 'play' || G.mode === 'pause') {
    moveTick(0); // keep camera settled
  }
  // idle star drift
  if (starPts) starPts.rotation.y += dt * 0.004;
  // console ring pulse for critical/failed
  Object.keys(consoles).forEach(function (id) {
    var d = SYS[id].dmg;
    if (d >= 2) {
      var p = 0.75 + 0.25 * Math.sin(performance.now() / (d === 3 ? 160 : 400));
      consoles[id].ring.scale.set(p, p, 1);
    } else consoles[id].ring.scale.set(1, 1, 1);
  });
  renderer.render(scene, camera);
}

/* ---------- Boot ---------- */
window.addEventListener('load', function () {
  initThree();
  setupTouch();
  updateChips(); updateStormHud();
  $('launch').addEventListener('click', startRun);
  $('again').addEventListener('click', startRun);
  $('pausebtn').addEventListener('click', pauseGame);
  $('resume').addEventListener('click', resumeGame);
  $('quit').addEventListener('click', quitToBrief);
  $('resetfrontier').addEventListener('click', function () {
    clearLedger(); showBrief(); feed('SYS', 'Frontier reset. No watches remembered.');
  });
  $('resetfrontier2').addEventListener('click', function () {
    clearLedger(); quitToBrief();
  });
  var rb = $('repairbtn');
  rb.addEventListener('pointerdown', function (e) { e.preventDefault(); startRepair(); });
  rb.addEventListener('pointerup', stopRepair);
  rb.addEventListener('pointerleave', stopRepair);
  rb.addEventListener('pointercancel', stopRepair);
  showBrief();
  frame();
});

</script>
</body>
</html>


===== FILE: public/city/baseline-meter.js =====
/* ================================================================
   AN EPISTLE OF THE GAUGE — THE CIVIC BASELINE METER

   King, before the nation multiplies further, it must know what
   its own weight costs. This instrument is the first rung of the
   ladder the giants gave us (the three.js craft, by way of
   VECTOR's research): a baseline meter that measures the world
   the way your phone experiences it — frame times, not averages;
   percentiles, not promises.

   It is yours alone: it wakes only for a royal session, and for
   every other visitor it does not exist. When its panel is shut
   it costs the world exactly nothing — no loop runs. Open it,
   walk to one of the five anchors, and press RUN: for sixty
   seconds it records ev