<!-- SOLID CITY · part 40 -->
MG_NAMES[before] + ' → ' + DMG_NAMES[SYS[id].dmg]);
  if (SYS[id].dmg === 3) solidSays(SYS[id].label.toLowerCase() + ' is down. We need it back.');
  // flash
  var f = $('flash'); f.style.opacity = '0.55';
  setTimeout(function () { f.style.opacity = '0'; }, 120);
}

/* ---------- Win / lose ---------- */
function failedCount() {
  var n = 0;
  Object.keys(SYS).forEach(function (id) { if (SYS[id].dmg >= 3) n++; });
  return n;
}
function endRun(win, why) {
  if (G.over) return;
  G.over = true; G.result = win ? 'STATION SAVED' : 'STATION LOST';
  G.mode = 'over';
  var rec = {
    t: Date.now(), outcome: G.result, why: why,
    systems: { propulsion: SYS.propulsion.dmg, shielding: SYS.shielding.dmg, comms: SYS.comms.dmg },
    stormLeft: Math.max(0, Math.round(G.stormT))
  };
  var ledger = saveRun(rec);
  showDebrief(rec, ledger);
}
function showDebrief(rec, ledger) {
  $('debrief-title').textContent = rec.outcome;
  $('debrief-title').style.color = rec.outcome === 'STATION SAVED' ? '#51d39b' : '#ff5b66';
  $('debrief-why').textContent = rec.why;
  $('debrief-sys').textContent = 'Propulsion ' + DMG_NAMES[rec.systems.propulsion] +
    ' · Shielding ' + DMG_NAMES[rec.systems.shielding] + ' · Comms ' + DMG_NAMES[rec.systems.comms];
  renderLedger($('debrief-ledger'), ledger);
  $('debrief').style.display = 'flex';
}

/* ---------- Ledger rendering ---------- */
function renderLedger(el, ledger) {
  ledger = ledger || loadLedger();
  if (!ledger.length) { el.innerHTML = '<div class="lempty">No watches yet. The frontier is unwritten.</div>'; return; }
  var html = '';
  var show = ledger.slice(-8).reverse();
  show.forEach(function (r) {
    var d = new Date(r.t);
    var ds = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    var cls = r.outcome === 'STATION SAVED' ? 'win' : 'loss';
    html += '<div class="lrow ' + cls + '"><span>' + ds + '</span><b>' + r.outcome + '</b></div>';
  });
  el.innerHTML = html;
}

/* ---------- Run lifecycle ---------- */
function startRun() {
  // reset systems, applying the frontier's memory: last run's FAILED start DEGRADED
  Object.keys(SYS).forEach(function (id) { SYS[id].dmg = 0; refreshConsole(id); });
  var ledger = loadLedger();
  var scarred = [];
  if (ledger.length) {
    var last = ledger[ledger.length - 1];
    Object.keys(SYS).forEach(function (id) {
      if (last.systems && last.systems[id] >= 3) { SYS[id].dmg = 1; scarred.push(SYS[id].label); refreshConsole(id); }
    });
  }
  G.mode = 'play'; G.stormT = STORM_SECONDS; G.over = false; G.result = null;
  G.holds = null; G.repairProg = 0; G.repairing = false; G.solidRepair = null;
  G.nextStrike = 12; G.elapsed = 0; solidIdleT = 8;
  yaw = 0; pitch = 0;
  camera.position.set(0, 1.7, 18);
  $('brief').style.display = 'none'; $('debrief').style.display = 'none';
  $('feed').innerHTML = '';
  feed('SOLID', 'On station. Storm inbound — two-four-zero seconds.');
  if (scarred.length) feed('SOLID', 'The frontier remembers: ' + scarred.join(', ') + ' bear last watch\'s scars.');
  feed('RIVET', 'Patches ready. Try to keep it in one piece.');
  updateChips(); updateStormHud();
  clock.getDelta();
}
function pauseGame() {
  if (G.mode !== 'play') return;
  G.mode = 'pause'; G.pausedAt = performance.now();
  $('pausemenu').style.display = 'flex';
}
function resumeGame() {
  if (G.mode !== 'pause') return;
  G.mode = 'play'; G.pausedAccum += performance.now() - G.pausedAt;
  $('pausemenu').style.display = 'none';
  clock.getDelta();
}
function quitToBrief() {
  G.mode = 'brief';
  $('pausemenu').style.display = 'none'; $('debrief').style.display = 'none';
  showBrief();
}
function showBrief() {
  var ledger = loadLedger();
  renderLedger($('brief-ledger'), ledger);
  var lastLine = $('brief-last');
  if (ledger.length) {
    var last = ledger[ledger.length - 1];
    lastLine.textContent = 'Last watch: ' + last.outcome + ' — ' + last.why;
    lastLine.style.display = 'block';
  } else lastLine.style.display = 'none';
  $('brief').style.display = 'flex';
}

/* ---------- HUD ---------- */
function updateStormHud() {
  var t = Math.max(0, Math.ceil(G.stormT));
  $('stormtime').textContent = Math.floor(t / 60) + ':' + ('0' + (t % 60)).slice(-2);
  $('stormbar').style.width = (100 * G.stormT / STORM_SECONDS) + '%';
}
function updateRepairHud() {
  if (G.holds && SYS[G.holds].dmg > 0) {
    $('repairprog').style.width = (100 * G.repairProg) + '%';
    $('repairwrap').style.display = 'block';
  } else $('repairwrap').style.display = 'none';
}

/* ---------- Movement & collision ---------- */
function moveTick(dt) {
  var f = 0, s = 0;
  if (keys.KeyW || keys.ArrowUp) f += 1;
  if (keys.KeyS || keys.ArrowDown) f -= 1;
  if (keys.KeyA || keys.ArrowLeft) s -= 1;
  if (keys.KeyD || keys.ArrowRight) s += 1;
  if (joy.active) { f += -joy.dy; s += joy.dx; }
  var len = Math.hypot(f, s);
  if (len > 1) { f /= len; s /= len; }
  var sp = 4.2;
  // yaw=PI fa