Vanilla-JS canvas game, no build step. ES modules under `js/`, served statically.
-`index.html` — DOM shell: all screens (menu/host/join/charselect/game/result) as overlay divs; loads PeerJS from CDN (online play only), then `js/main.js`.
-`style.css` — overlay/menu styling; canvas letterboxed to 16:9 via CSS.
-`js/net.js` — PeerJS wrapper: `hostRoom`/`joinRoom`; room key = host peer id suffix; signaling via PeerJS cloud, game traffic pure P2P. Runs a 2s heartbeat + 8s watchdog (WebRTC close events are unreliable); broker `network` errors are ignored once the data channel is up.
- **Client never simulates.** In netplay the joiner only sends input bits and renders `applySnapshot` state; anything render reads must survive the snapshot round-trip (player `st` is re-shaped there).
- **Sim events are the only side-channel**: fx, audio, *and* client tile mutations (`tile`/`crack` events) ride on them. Host accumulates `pendingEvs` between snapshots.
- All sim/mapgen randomness must use the seeded RNG — never `Math.random()` inside `js/sim.js`/`js/mapgen.js`/`js/characters.js` paths.
- Velocities must stay <TILE(12px/tick)orAABBcollisiontunnels.
- 7 as an arc end-angle in render means "> 2π" (full circle), a deliberate shorthand.