# scene: the chain scene, once The live devnet animation on the site's home fold and `/live`, and the miner app's "The chain, live" card and its Inspect view, are ONE renderer fed ONE JSON shape. This folder is the source; everything else is a copy or a reader. | File | What it is | Copies | |---|---|---| | `live-dag.js` | EMBER 02 `IgneumDag` 2.0.4: lanes per vote key, parent curves, the selected chain, checkpoint bands, proof glow; the box height follows the lanes (`onSize`, `autoHeight`) | `site/live-dag.js`, `app/igneum-app/ui/live-dag.js` | | `proof-core.js` | `IgneumProof` 2.0.0: the selected block's shards | `site/proof-core.js`, `app/igneum-app/ui/proof-core.js` | | `tokens.css` | The fourteen palette tokens the two scripts read from `:root`, dark and light, the brand package's values | the `scene-tokens` block in `site/site.css` and `app/igneum-app/ui/app.css` | | `feed-contract.md`, `feed-contract.json` | The feed shape, in words and as key lists | read by `tools/scene/feed-contract.mjs` and the app's `src/live.rs` test | | `fixtures/live-2026-10-07.json` | One recorded `/api/live?window=300` reply | the parity, paint and contract tests | `node tools/scene/sync.mjs` writes the copies; `--check` is the gate line (byte-equal scripts, an equal token block, the token names defined nowhere else, a print block excepted); `--self-test` proves the check on known-failed cases first. A side (site or app) takes part once its `live-dag.js` copy exists; a `release-*` branch checks the app's copies only, because the release branches carry the site tree as it was when they were cut and the site deploys from master alone. ## The one rule for the two surfaces Same renderer, same tokens, same feed, same mount options: `window: 60`, `fps: 60`, `poll: false`, the page polls `/api/live?window=300` every 2 s and pushes. What may differ is the box the scene sits in (the home hero is 500 px tall, `/live` 420 px, the app's card 220 px in `compact` mode and its Inspect view the `/live` height), and the app's viewpoint: its `mine` option names this machine's vote keys, so its own blocks glow and its lane reads YOUR KEY. That is an overlay on the same picture, never a second renderer, never a rewritten feed. The phone rule (30 s window, four lanes, bigger nodes) keys on the viewport width under 720 px, not on the canvas width: a 640 px hero on a laptop is not a phone. The box's height follows the lanes (2.0.4): wanted height = top padding + axis padding + lanes shown times `laneHeight` (46 px, 40 on a phone, 26 compact; at most `maxLanes` 7, `narrowLanes` 4 on a phone, never under 2). The renderer calls `onSize(heightPx, {lanes, laneHeight, narrow, compact})` whenever that changes and exposes `getWantedHeight()`; a page that lays out its own box sets its height from the callback. With `autoHeight` (on by default when the host set no CSS height on the canvas, forced either way by the option, never in compact mode) the renderer sets `canvas.style.height` itself and shows every lane up to the cap. Every current host sets a height (/live 420 px, the hero 500 px, the app's card 220 px and its Inspect view 420 px), so until a page opts in the frames are unchanged. ## The tests (all in `tools/ci/pre-push.sh`) | Line | What it proves | |---|---| | `tools/scene/sync.mjs --self-test && --check` | the copies are the source, the tokens live once | | `tools/scene/paint-test.cjs` | a push paints with the document hidden, the observer silent and no animation frame (the blank `/live` of 7 October 2026) | | `node --test tools/scene/feed-contract.test.mjs` | the fixture validates; a rewritten miner, a float `now`, a stray key are refused | | `tools/scene/parity-remote.sh` | on build-2: the fixture through the site's `/live` and the app's UI, three frames each, pixel-equal apart from the app's lane label; a changed token fails first |