diff --git a/.dockerignore b/.dockerignore
index 2d03d7c..dca3d75 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -10,3 +10,7 @@ assets/_*
HANDOFF.md
**/__pycache__
**/.DS_Store
+
+# promo footage is a local build product (tools/capture.py), never served
+promo/clips
+tools
diff --git a/.gitignore b/.gitignore
index 4c38cdd..a09e7ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ scores.json.tmp
assets/_gen/
assets/_refcheck/
assets/__pycache__/
+promo/clips/
diff --git a/PROJECT.md b/PROJECT.md
index 2592f50..c848a90 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -3553,8 +3553,34 @@ never an interstitial. An interstitial after death would land exactly in the "on
Clips the game already contains: the transition (the flagship), a boss walking on, the ball boy
calmly mopping blood, a flame ball starting an epidemic across a lined-up wave, parrying a boss's
volley back into him, a horde, the death + run summary.
- A capture helper on `window.T` to jump straight into a given beat (the transition timeline is on
- `G.transT`) would make clean takes cheap — not yet built.
+ **THE CAPTURE RIG IS BUILT (2026-08-18): `python3 tools/capture.py` and the folder fills up.**
+ `tools/capture.py` serves the repo with `tools/clips.js` injected, opens a throwaway-profile
+ Chrome, and the page stages each highlight through `window.T`, records it, POSTs the webm back;
+ ffmpeg writes 1080x1920 h264/aac into `promo/clips/`. Named scenes shoot alone
+ (`python3 tools/capture.py horde boss`). Seven scenes exist and the list in `clips.js` is the
+ place to add one — the gore side is `transition` (the flagship: four seconds of ordinary tennis
+ before the turn), `horde`, `flamer`, `mopboy`, `death`, and the zombie-tennis side is `boss` and
+ `match`. Nothing is faked for the camera: every clip enters through the same doors a player does
+ (`T.start`, `T.intoSplatter`, `startHorde`, `startBossFight`, `startMatch`), and the two pilots
+ are deliberately mediocre — the tennis one IS the reachability soak's from `test.js`.
+ Four things it learned the hard way, none of them obvious:
+ - **The clip is the canvas BACKING STORE, not the window.** Chrome opened maximised and ignored
+ `--window-size`, and `resizeTo` did nothing; the frame is pinned instead by giving the canvas a
+ 9:16 CSS box, which `render.js` measures (`cv.clientWidth`) through its ResizeObserver.
+ - **`TUNE.lowPower` defaults ON and caps `RES` at 3**, so the recording came out 810x1440 and
+ needed a blurry 1.33x upscale. The rig sets it to 0; with `--force-device-scale-factor=3` the
+ backing store is exactly 1080x1920 and no filter ever touches a pixel.
+ - **Audio has no master bus** — every voice in `audio.js` connects straight to `ac.destination` —
+ so the rig makes `destination` itself a gain node fanning to the speakers AND to a
+ `MediaStreamDestination`, patched on `BaseAudioContext.prototype` (not `AudioContext`'s) before
+ the game module boots. No refactor of the game for a tool that ships to nobody.
+ - **One page load per scene.** A fresh boot is the only teardown guaranteed correct, and it costs
+ a second: the page reloads itself into the next scene and POSTs `/done` when the list is empty.
+ `tools/` is in neither the Dockerfile COPY nor the image, and `promo/clips/` is git- and
+ docker-ignored: the footage is a local build product, not a deployed asset.
+ Still hand-work after the rig: the name mark on screen (item 5 wants it in EVERY clip), any
+ cutting, and the caption. Skipped deliberately — a burned-in watermark is a taste decision and
+ ffmpeg `drawtext` needs a font file the repo does not have in a `drawtext`-friendly format.
5. **How a viewer actually reaches the game — the name does the work, not the link.** TikTok and
Instagram have no clickable caption links, so the bio link costs two taps (profile, then link) and
converts in the single-digit percents. YouTube Shorts descriptions and pinned comments DO click,
diff --git a/index.html b/index.html
index a51f555..fc3cbb9 100644
--- a/index.html
+++ b/index.html
@@ -659,6 +659,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one') {
-
+