<!-- SOLID CITY · part 2907 -->
"stat"><strong id="statRescued">0</strong><span>Pods rescued</span></div>
          <div class="stat"><strong id="statLost">0</strong><span>Pods lost</span></div>
          <div class="stat"><strong id="statHuman">0</strong><span>Human extractions</span></div>
          <div class="stat"><strong id="statAI">0</strong><span>AI extractions</span></div>
          <div class="stat"><strong id="statIntercept">0</strong><span>Interceptions</span></div>
          <div class="stat"><strong id="statLatency">—</strong><span>Median handoff ms</span></div>
          <div class="stat"><strong id="statHandoffs">0</strong><span>Handoffs</span></div>
          <div class="stat"><strong id="statRejected">0</strong><span>Stale rejected</span></div>
          <div class="stat"><strong id="statRepairs">0</strong><span>Repair actions</span></div>
          <div class="stat"><strong id="statEvents">0</strong><span>Coordination events</span></div>
        </div>
        <div class="log" id="eventLog" aria-live="polite"></div>
      </details>
      <details>
        <summary>How the rescue works</summary>
        <div class="story">
          <h3>Four moments, every life</h3>
          <ol><li><b>Approach:</b> navigate within the pod's rescue ring.</li><li><b>Stabilize:</b> extraction holds position while the rig calms the pod.</li><li><b>Recover:</b> the tractor brings the pilot aboard.</li><li><b>Protect:</b> defend the recovered pod until it reaches the blue gate.</li></ol>
          <h3>Twin-lightning law</h3>
          <p>Each station has one owner and an ownership epoch. A claim advances the epoch immediately; commands from the previous owner are rejected. AI advisories remain separate from actuator commands, so the crew may warn you but cannot override you.</p>
        </div>
      </details>
    </div>
  </main>
  <div class="toast" id="toast" role="status"></div>

<script>
(function(){
  'use strict';
  const $ = (s,root=document)=>root.querySelector(s);
  const $$ = (s,root=document)=>Array.from(root.querySelectorAll(s));
  const canvas=$('#battle'), ctx=canvas.getContext('2d');
  const W=960,H=600;
  let running=false,last=0,elapsed=0,adviceTimer=0,spawnTimer=0,finishTimer=0;
  let toastTimer=0;

  class ControlCoordination {
    constructor(tasks,onEvent){
      this.tasks={}; this.onEvent=onEvent; this.seen=new Set();
      tasks.forEach(name=>this.tasks[name]={owner:'ai',epoch:1,seq:0,connected:true});
    }
    owner(task){return this.tasks[task].owner}
    claim(task,owner){
      const t=this.tasks[task], started=performance.now();
      if(!t || t.owner===owner) return null;
      const from=t.owner; t.owner=owner; t.epoch++; t.seq=0;
      const latency=Math.max(0,performance.now()-started);
      this.onEvent('handoff',{task,from,to:owner,epoch:t.epoch,latency});
      return {task,owner,epoch:t.epoch,latency};
    }
    release(task){return this.claim(task,'ai')}
    command(task,owner,epoch,action){
      const t=this.tasks[task]; if(!t) return false;
      const key=task+':'+owner+':'+epoch+':'+action;
      if(t.owner!==owner || t.epoch!==epoch){this.onEvent('rejected',{task,owner,epoch,reason:'stale ownership'});return false}
      if(this.seen.has(key)){this.onEvent('rejected',{task,owner,epoch,reason:'duplicate command'});return false}
      this.seen.add(key); if(this.seen.size>160)this.seen.clear();
      return true;
    }
    snapshot(){return JSON.parse(JSON.stringify(this.tasks))}
  }

  let events=[], coordination;
  const state={};
  function resetState(){
    Object.assign(state,{
      ship:{x:118,y:310,vx:0,vy:0,angle:0,hp:100,shield:100,cargo:null,repairCd:0,pulse:0,fireCd:0},
      gate:{x:885,y:300,r:56},
      pods:[
        {id:'Aster',x:365,y:165,hp:100,status:'adrift',stabilize:0,story:'Test pilot Aster · oxygen stable'},
        {id:'Morrow',x:555,y:410,hp:88,status:'adrift',stabilize:0,story:'Survey pilot Morrow · hull breach'},
        {id:'Kite',x:735,y:190,hp:74,status:'adrift',stabilize:0,story:'Courier Kite · power failing'}
      ],
      hazards:[{x:305,y:360,r:58,spin:0},{x:505,y:235,r:47,spin:1.7},{x:700,y:365,r:52,spin:2.4}],
      debris:Array.from({length:20},(_,i)=>({x:230+(i*197)%590,y:75+(i*83)%450,r:4+(i%5)*2,vx:-2+(i%4),vy:-1+(i%3)})),
      enemies:[],shots:[],rescued:0,lost:0,risk:0,missionEnd:false,extractHeld:false,stabilizeHeld:false,keys:{},
      advisory:'Corridor scan complete. Three life signs located.'
    });
    events=[]; elapsed=0; spawnTimer=0; finishTimer=0;
    coordination=new ControlCoordination(['navigation','extraction','protection','support'],recordEvent);
    updateStations(); updateUI(); renderLog();
  }

  function recordEvent(type,data={}){
    const ev={type,data,t:elapsed,real:performance.now()}; events.push(ev);
    if(events.length>250)events.shift();
    if(type==='handoff'){
      const n=data.to==='human'?'You claimed '+pretty(data.task):'AI resumed '+pretty(data.task);
      showToast(n+' · epoch '+d