# MASTER PROMPT — KAIRO 回路 (WordPress / UiChemy edition, responsive build)
## ▶ HOW TO USE THIS PROMPT
This is the **WordPress build** of KAIRO, live at
`https://demo.uichemy.com/kairo-回路-a-city-that-never-resolves/` (page id **338**, one
Composer widget **`rnsifsa`** labelled "KAIRO City", published).
It is the companion to `KAIROMASTERPROMPT.md`, which remains the authority for the
**standalone page**. This document does not repeat the 2,437-line scene module; it
carries every WordPress-specific block verbatim, plus the one mechanical edit that turns
the standalone module into the hosted one.
**INSTRUCTION TO CLAUDE:** build through the `uichemy-demo-wordpress-uichemy-mcp` server,
putting each block in the location its heading names — **not** all into the section. The
section's `js` field stays EMPTY on purpose; read RULE 1 before you doubt that.
---
## OVERVIEW
- **Stack:** vanilla HTML + CSS + JS + Three.js 0.165.0 (ES module from a CDN). No build step.
- **Scene:** one fixed WebGL canvas behind the whole page; the camera walks a night street on
waypoints pinned to the six chapters (`#top` `#about` `#gallery` `#story` `#districts` `#contact`).
- **Fonts:** Space Grotesk · Zen Kaku Gothic New · JetBrains Mono.
- **Photographs:** six CC0 plates, now served from the WordPress media library.
## ASSET MANIFEST (WordPress media library)
| Asset | URL |
|---|---|
| `01-wall.jpg` | `https://demo.uichemy.com/wp-content/uploads/2026/09/01-wall.jpg` |
| `02-gate.jpg` | `https://demo.uichemy.com/wp-content/uploads/2026/09/02-gate.jpg` |
| `03-pagoda.jpg` | `https://demo.uichemy.com/wp-content/uploads/2026/09/03-pagoda.jpg` |
| `04-wheel.jpg` | `https://demo.uichemy.com/wp-content/uploads/2026/09/04-wheel.jpg` |
| `05-rain.jpg` | `https://demo.uichemy.com/wp-content/uploads/2026/09/05-rain.jpg` |
| `06-spring.jpg` | `https://demo.uichemy.com/wp-content/uploads/2026/09/06-spring.jpg` |
All six were sideloaded from the jsDelivr copies by the page writer (`upload_images: true`),
which rewrites each `<img src>` for you. Licences and Wikimedia sources are listed in
`KAIROMASTERPROMPT.md`; every one is CC0.
## ⚠ RULE 1 — the UiChemy editor is lossy, so the moving parts live outside the section
Saving the page from the UiChemy/Elementor editor re-serialises the Composer section and
**silently deletes** what it does not model: `<canvas>`, `<svg>`, the section's JS field, and
`<link>` tags in the page's head code. A scene whose canvas has been deleted throws at import
time and the page is simply black.
So the build is split by what survives an editor save:
| Piece | Lives in | Survives |
|---|---|---|
| Six chapters of markup + the whole stylesheet | the Composer section (`html` + `css`) | ✅ |
| Keyer/scene JavaScript | code file `kairo-scene.js` | ✅ |
| Loader + canvas repair | code file `kairo.js`, referenced from **site-wide** body code | ✅ |
| Fonts, importmap, `body.kairo-page` rules | **site-wide** head code | ✅ |
| `<canvas id="gl">`, `<canvas id="ldcity">`, the six `.tile canvas` plates | rebuilt at runtime by `kairo.js` | ✅ (recreated) |
`kairo.js` re-creates every canvas it needs before importing the scene, so an editor save
costs nothing. The two SVG logo marks are NOT rebuilt — they are the one thing an editor save
still loses.
## ⚠ RULE 2 — Cloudflare caches the code files for 30 days
`/wp-content/uichemy-composer/*.js` is served with `cache-control: max-age=2592000` behind
Cloudflare. `code-file` `patch`/`write` updates the origin, but **visitors keep the old file**.
Bump the query string in the site-code `<script src>` (`kairo.js?v=2` → `?v=3`) on every JS
change, and verify with `curl 'https://…/kairo.js?x=1' | grep <your change>` — a query string
is a fresh cache key.
## ⚠ RULE 3 — absolute CDN URLs are not enough: the importmap must still be emitted
`kairo-scene.js` imports three.js by absolute URL, but the `examples/jsm` addons it pulls
(`EffectComposer`, `RenderPass`, `UnrealBloomPass`, `OutputPass`, `BufferGeometryUtils`)
`import 'three'` **internally**, as a bare specifier. Without a document-level importmap the
module dies with *"Failed to resolve module specifier 'three'"*.
The importmap in B1 maps `three` to the **same URL** the module imports, so both resolve to one
module instance. Only one importmap is allowed per document — check no other page adds its own.
## ⚠ RULE 4 — Elementor forces smooth scrolling; the camera is scroll-driven
Elementor's `frontend.min.css` ships
`@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth } }` on every
page. The standalone page scrolls instantly, and the camera walk is a function of scroll
position, so `kairo.js` sets `document.documentElement.style.scrollBehavior = 'auto'` — on
KAIRO pages only.
## ⚠ RULE 5 — nothing in the ancestor chain may create a containing block
Three frosted panels (`.tile`, `.card`, `.foot`) use `backdrop-filter`, and eight elements are
`position: fixed` (`#gl`, `#loader`, `#vignette`, `#grain`, `#dim`, `.nav`, `.hero`, `.rail`).
Any Elementor ancestor carrying `transform`, `filter`, `perspective`, `contain`, `will-change`
or `opacity < 1` becomes a backdrop root and a containing block: the glass flattens to a plain
translucent box and every fixed element becomes absolutely positioned inside the widget.
Measured clean on this build — no ancestor sets any of them. Do not switch on Elementor motion
effects, entrance animations or sticky on the container. And never add `will-change: opacity`
to `.chapter__inner`; the stylesheet carries a comment saying why.
## ⚠ RULE 6 — responsive is four separate problems, and two of them are in the JS
The page is one fixed WebGL canvas plus six full-height chapters, so making it work on a
phone is not a matter of breakpoints alone.
**1 · Chapter height is `100svh`.** The chapters are the scroll track the camera walk is
measured against. On a phone `100vh` is the LARGE viewport — the height with the URL bar
hidden — so it changes as the bar collapses, which moves every camera waypoint under the
reader mid-scroll. `100svh` does not move. The `100vh` line stays first as the fallback.
This applies in B6 (`.chapter`) **and** in B1 (`body.kairo-page.is-loading`), because the
scroll lock is site-wide.
**2 · `align-items: safe center` on `.chapter`.** A chapter whose copy is taller than the
viewport is still centred, and plain `center` overflows BOTH edges — on a 375x667 phone
that put the eyebrow and the first line of the title above the top of the window, where
they cannot be scrolled to. `safe` falls back to start when there is no room.
**3 · The pixel budget is a function of viewport width** (in `kairo-scene.js`):
```js
const pixelCap = () => Math.min(devicePixelRatio || 1, innerWidth < 700 ? 1.6 : 2)
```
A phone reports `devicePixelRatio` 3, so a flat `min(dpr, 2)` asks a phone GPU for 2x a
tall viewport and runs `UnrealBloomPass` over it. One definition, used by the renderer,
the resize handler and the grain uniform. Drawing buffer at 393x852 goes 1.34 MP → 0.86 MP;
desktop is unchanged at 5.18 MP.
**4 · Resize is deferred to the next frame** (also `kairo-scene.js`). Mobile browsers fire
`resize` every time the URL bar slides — on almost every scroll — and `resize()`
reallocates the composer's render targets. `orientationchange` still calls through
immediately.
Plus, in B6: the four nav links take a row of their own below 820px instead of
`display:none` (a phone had no navigation at all), every fixed edge adds
`env(safe-area-inset-*)` because the document is `viewport-fit=cover`, and the type comes
down a step under `(max-height:720px) and (orientation:portrait)` and
`(max-height:560px) and (orientation:landscape)`.
### Verified on the built page
Every chapter walked on each device. "Offenders" counts any chapter whose first line
starts above the viewport or underneath the fixed bar.
| Device | Overflow-x | Offenders | Nav links | h1 | Console |
|---|---|---|---|---|---|
| 320x568 | 0 | 0 | own row | 34px | clean |
| 375x667 (SE) | 0 | 0 | own row | 34px | clean |
| 360x740 | 0 | 0 | own row | 42px | clean |
| 393x852 (14 Pro) | 0 | 0 | own row | 42px | clean |
| 852x393 (landscape) | 0 | 0 | inline | 46px | clean |
| 820x1180 (iPad) | 0 | 0 | own row | 57px | clean |
Desktop is untouched: at 1440x900 document height (4953px), drawing buffer (2880x1800),
nav height (83px), h1 (92px) and rail width all match the pre-responsive build, and the
camera's settled position and rotation are identical at all six chapters. Sample the
camera only after it has stopped moving — it eases toward its target, so a fixed delay
after a jump reads a number in flight and invents a difference that is not there.
## ⚠ REDEPLOY NOTE (this document is ahead of the live page)
The responsive build has been verified on the standalone page. Page 338 still serves the
**previous** `kairo-scene.js` and the previous section CSS. To bring it level:
1. `code-file` `write` the new `kairo-scene.js` (B4 below — regenerate it from the updated
standalone appendix, which now contains `pixelCap` and the deferred resize).
2. **Bump the query string** in B2 — `kairo.js?v=2` → `?v=3` — or Cloudflare keeps serving
the old module for 30 days (RULE 2). `kairo.js` itself is unchanged; the bump is only
there to change the cache key.
3. `page` `update` with the new B6 `css`, and `platform` `update-site-code` with the new B1.
4. Re-verify at 393x852 and 1440x900 before calling it done.
## WHAT DIFFERS FROM THE STANDALONE SOURCE
Verified on the live page against the standalone at 1440x900: document height **4953px** on
both, canvas **1440x900**, **6/6** gallery plates painted, and camera position, camera
rotation, rail width and all six chapter opacities **identical at all six chapters**, with zero
console errors.
1. **One wrapper.** Everything is wrapped in `<div class="uichemy-kairo-1">`; the widget's own
CSS is auto-prefixed with `.elementor-element-<id>`.
2. **`:root` → the wrapper.** UiChemy's prefixing turns `:root {…}` into
`.elementor-element-x :root`, which matches nothing and would kill every custom property.
All six tokens (`--ink --bone --dim --cyan --magenta --pad`) moved onto `.uichemy-kairo-1`.
`--dim` is deliberately left alone: `:root` declares it as a colour while the JS sets a
numeric `--dim` inline on `#dim`; the inline value shadows the inherited one, as before.
3. **`html` / `body` rules split.** Type, colour and font smoothing moved onto the wrapper; the
page-level half (`background`, `overflow-x`, and the `is-loading` scroll lock) became
`body.kairo-page` in site-wide CSS, with the class added by `kairo.js`.
4. **`<main class="hero">` → `<section>`**, because the theme already owns `<main>`.
5. **Keyframes renamed** `ldspin` → `kairo-ldspin`, `hintpulse` → `kairo-hintpulse`; keyframe
names are global and would collide with a theme or plugin.
6. **The module split in two.** `kairo.js` (B3, loader/guard/repair) and `kairo-scene.js` (the
standalone module, verbatim except its imports — see B4).
7. **Section `js` is empty** (Rule 1).
---
# APPENDIX B · THE WORDPRESS BUILD
## Build steps (uichemy-demo-wordpress-uichemy-mcp)
1. `uichemy-composer/read-skill` `{ "name": "code-to-wordpress" }`, then
`uichemy-composer/describe-site` — `platform.checks.elementor_active` must be true.
2. **B4 → code files.** Produce `kairo-scene.js` from the standalone appendix (one edit, below),
then upload both it and `kairo.js` (B3) with `code-file` `request-upload` + a single `curl`
loop — never read 113KB through the conversation. Confirm with `code-file` `list`.
3. **B1 + B2 → site-wide code.** `platform` `update-site-code` with `site_before_head` = B1 and
`site_before_body` = B2 (append, deduped — it keeps whatever is already there).
4. **B5 + B6 → the page.** `page` `create` with `title`, `status: "draft"`, `label: "KAIRO City"`,
`upload_images: true`, `html` = B5, `css` = B6, `js` = `""`. The six jsDelivr photo URLs in
B5 are rewritten to media-library URLs by the sideloader; check `image_uploads` in the reply.
5. **Read back**: `page` `grep` for `<canvas` and `<svg` (both must survive), `platform`
`get-site-code`, then load the published page and confirm `window.__kabuki` exists, the
loader removes itself, and the six `.tile canvas` plates are painted.
Note: the server lower-cases SVG tags on store (`<fegaussianblur>`, `stddeviation`). Harmless —
the browser's HTML parser restores the correct SVG casing inside `<svg>`.
## B1 · site-wide `site_before_head`
```html
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;700&family=Zen+Kaku+Gothic+New:wght@300;400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/
[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/
[email protected]/examples/jsm/"
}
}
</script>
<style>
/* KAIRO: the body-level half of the source's html/body rules. The class is added
by kairo.js only on a page that contains the section, so no other page is
touched. The background matters because the city is a position:fixed canvas. */
body.kairo-page{margin:0;background:#03060f;overflow-x:hidden}
body.kairo-page.is-loading{overflow:hidden;height:100vh;height:100svh}
</style>
```
## B2 · site-wide `site_before_body`
```html
<script src="/wp-content/uichemy-composer/kairo.js?v=2"></script>
```
## B3 · code file `kairo.js` → `/wp-content/uichemy-composer/kairo.js`
```js
/* KAIRO — loader.
*
* Loaded site-wide (before </body>) but does nothing unless the page actually
* contains the KAIRO section, so three.js is never fetched elsewhere.
*
* It also repairs the markup first. The UiChemy/Elementor editor drops <canvas>
* elements whenever the section is saved from the editor, and the scene module
* throws at import time if #ldcity or #gl is missing. Rebuilding them here means
* an editor save costs nothing.
*/
(function () {
'use strict';
var ROOT = '.uichemy-kairo-1';
var SCENE = '/wp-content/uichemy-composer/kairo-scene.js';
var TILES = ['skyline', 'gate', 'pagoda', 'wheel', 'rain', 'petals'];
function canvas(cls, id, w, h) {
var c = document.createElement('canvas');
if (id) c.id = id;
if (cls) c.className = cls;
if (w) { c.width = w; c.height = h; }
return c;
}
function heal(root) {
/* The fixed WebGL canvas: first child, so it sits under every overlay. */
if (!root.querySelector('#gl')) root.insertBefore(canvas('', 'gl'), root.firstChild);
/* The loader's little skyline. It belongs after .ld__jp, before .ld__bar. */
var inner = root.querySelector('.ld__inner');
if (inner && !root.querySelector('#ldcity')) {
var bar = inner.querySelector('.ld__bar');
inner.insertBefore(canvas('', 'ldcity', 840, 300), bar || null);
}
/* One procedurally painted plate per gallery tile, behind its photograph. */
var tiles = root.querySelectorAll('.tile');
for (var i = 0; i < tiles.length; i++) {
if (tiles[i].querySelector('canvas')) continue;
var c = canvas('', '', 640, 368);
c.setAttribute('data-tile', TILES[i] || TILES[TILES.length - 1]);
tiles[i].insertBefore(c, tiles[i].firstChild);
}
/* Plain overlay divs: cheap to re-add, and the scene throws without them. */
[['div', 'vignette'], ['div', 'grain'], ['div', 'dim']].forEach(function (d) {
if (!root.querySelector('#' + d[1])) {
var el = document.createElement(d[0]); el.id = d[1]; root.appendChild(el);
}
});
if (!root.querySelector('.rail')) {
var rail = document.createElement('div'); rail.className = 'rail'; root.appendChild(rail);
}
}
function start() {
var root = document.querySelector(ROOT);
if (!root || root.getAttribute('data-kairo-ready')) return;
root.setAttribute('data-kairo-ready', '1');
/* The body-level half of the source's html/body rules (background behind the
fixed canvas, no horizontal scrollbar, and the loader's scroll lock) is
carried by this class, so it can never touch another page. */
document.body.classList.add('kairo-page');
/* Elementor's frontend.min.css sets html{scroll-behavior:smooth} on every
Elementor page. The source page scrolls instantly, and the camera walk is
driven by scroll position, so restore the source's behaviour here only. */
document.documentElement.style.scrollBehavior = 'auto';
heal(root);
import(SCENE).catch(function (e) {
document.body.classList.remove('is-loading');
if (window.console) console.error('KAIRO scene failed to load:', e);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
else start();
})();
```
## B4 · code file `kairo-scene.js` → `/wp-content/uichemy-composer/kairo-scene.js`
Not reproduced here: it is the **entire `<script type="module">` block from
`KAIROMASTERPROMPT.md` (lines 472–2909), byte for byte**, with one header comment prepended and
exactly six import lines rewritten — an importmap cannot be relied on to be parsed before a
module on a WordPress page, so the specifiers are made absolute:
```
'three' → 'https://cdn.jsdelivr.net/npm/
[email protected]/build/three.module.js'
'three/addons/…' → 'https://cdn.jsdelivr.net/npm/
[email protected]/examples/jsm/…'
```
Mechanically:
```python
js = open('module.js').read()
js = re.sub(r"from 'three/addons/", "from 'https://cdn.jsdelivr.net/npm/
[email protected]/examples/jsm/", js)
js = js.replace("from 'three'", "from 'https://cdn.jsdelivr.net/npm/
[email protected]/build/three.module.js'")
```
The file to deploy is **118,743 bytes**, sha256 `d5d0a8e53eae10828e3808d7e02365321249580f9a9f766f50c6a8dbdff89417` (the live page still serves the previous 115,517-byte build — see the redeploy note) — verify with
`curl 'https://demo.uichemy.com/wp-content/uichemy-composer/kairo-scene.js?x=1' | shasum -a 256`.
Nothing else in the module changes: no DOM query and no scroll maths. The renderer
does change, but in the standalone source, not here — `pixelCap()` and the deferred
resize are part of the appendix now, so this step stays a pure import rewrite.
## B5 · section `html`
```html
<div class="uichemy-kairo-1">
<div id="loader">
<div class="ld__inner">
<svg class="ld__mark" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="1.1">
<circle cx="20" cy="20" r="17" stroke-opacity=".28"/>
<circle cx="20" cy="20" r="9.5" stroke-opacity=".5"/>
<path d="M20 3v34M3 20h34" stroke-opacity=".22"/>
<circle cx="20" cy="20" r="2.6" fill="currentColor" stroke="none"/>
</svg>
<div class="ld__name display">KAIRO</div>
<div class="ld__jp">回 路</div>
<canvas id="ldcity" width="840" height="300"></canvas>
<div class="ld__bar"><i></i></div>
<div class="ld__row mono">
<span class="ld__task">INITIALISING</span><span class="ld__pct">0%</span>
</div>
</div>
</div>
<canvas id="gl"></canvas>
<div id="vignette"></div>
<div id="grain"></div>
<div id="dim"></div>
<div class="rail"></div>
<header class="nav">
<a class="nav__brand" href="#top">
<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true">
<circle cx="16" cy="16" r="13.4" stroke-opacity=".30"/>
<circle cx="16" cy="16" r="7.4" stroke-opacity=".55"/>
<path d="M16 2.6v26.8M2.6 16h26.8" stroke-opacity=".22"/>
<circle cx="16" cy="16" r="2.1" fill="currentColor" stroke="none"/>
</svg>
<span><b>Kairo</b><i>District 01 · 回路</i></span>
</a>
<nav class="nav__links mono">
<a href="#about">About</a>
<a href="#gallery">Gallery</a>
<a href="#districts">Landmarks</a>
<a href="#contact">Contact</a>
</nav>
<div class="nav__meta mono">23:40 · 17°C · after rain</div>
</header>
<div class="hero" aria-hidden="true">
<div class="hero__hint mono"><i></i> Scroll to enter</div>
</div>
<main class="scroll">
<!-- hero ---------------------------------------------------------- -->
<section class="chapter" id="top">
<div class="chapter__inner">
<div class="eyebrow"><i class="dot"></i> 00 — arrival</div>
<h1>A city<br>that never<br><em>resolves</em>.<span class="jp">回 路 · 第 一 区</span></h1>
<p class="lede">Kairo was not planned. It accumulated — one floor at a time, each one
announcing itself louder than the last, until the buildings disappeared behind
what they were advertising.</p>
<dl class="facts">
<div><dt>District</dt><dd>01</dd></div>
<div><dt>Local time</dt><dd>23:40</dd></div>
<div><dt>Weather</dt><dd>After rain</dd></div>
</dl>
</div>
</section>
<!-- about --------------------------------------------------------- -->
<section class="chapter chapter--right" id="about">
<div class="chapter__inner">
<div class="eyebrow"><i class="dot"></i> 01 — about the district</div>
<h2>Built by<br>whoever got<br>there <em>first</em>.<span class="jp">縦 の 天 気</span></h2>
<p class="lede">Fourteen metres of carriageway, forty of frontage, and no two floors
owned by the same person. The signage hangs out until the sky is a slot.</p>
<dl class="facts">
<div><dt>Street width</dt><dd>14.8 m</dd></div>
<div><dt>Tallest</dt><dd>34 m</dd></div>
<div><dt>Signs</dt><dd>1 400+</dd></div>
</dl>
</div>
</section>
<!-- gallery ------------------------------------------------------- -->
<section class="chapter chapter--wide" id="gallery">
<div class="chapter__inner">
<div class="sec-head">
<div>
<div class="eyebrow"><i class="dot"></i> 02 — gallery</div>
<h2>Six plates<br>from the <em>walk</em>.</h2>
</div>
<p class="lede">Six photographs of the city this one was built from — Shinjuku,
Asakusa, and a wet street somewhere off the main run. Every one public domain.</p>
</div>
<div class="grid">
<figure class="tile"><canvas data-tile="skyline" width="640" height="368"></canvas><img src="https://cdn.jsdelivr.net/gh/truptiPosimyth/landing-assets@main/kairo/photos/01-wall.jpg" alt="Neon shopfronts stacked up a Shinjuku facade at night." loading="lazy" decoding="async" onerror="this.remove()">
<i class="tile__bar"></i>
<figcaption><b>The wall</b><span class="mono">01 / 06</span></figcaption></figure>
<figure class="tile"><canvas data-tile="gate" width="640" height="368"></canvas><img src="https://cdn.jsdelivr.net/gh/truptiPosimyth/landing-assets@main/kairo/photos/02-gate.jpg" alt="The lit eaves of the Senso-ji pagoda against a night sky." loading="lazy" decoding="async" onerror="this.remove()">
<i class="tile__bar"></i>
<figcaption><b>The gate</b><span class="mono">02 / 06</span></figcaption></figure>
<figure class="tile"><canvas data-tile="pagoda" width="640" height="368"></canvas><img src="https://cdn.jsdelivr.net/gh/truptiPosimyth/landing-assets@main/kairo/photos/03-pagoda.jpg" alt="The five-storied pagoda and Hozomon gate at Asakusa, lit at night." loading="lazy" decoding="async" onerror="this.remove()">
<i class="tile__bar"></i>
<figcaption><b>Five storeys</b><span class="mono">03 / 06</span></figcaption></figure>
<figure class="tile"><canvas data-tile="wheel" width="640" height="368"></canvas><img src="https://cdn.jsdelivr.net/gh/truptiPosimyth/landing-assets@main/kairo/photos/04-wheel.jpg" alt="The Tokyo Dome City ferris wheel lit blue after dark." loading="lazy" decoding="async" onerror="this.remove()">
<i class="tile__bar"></i>
<figcaption><b>The wheel</b><span class="mono">04 / 06</span></figcaption></figure>
<figure class="tile"><canvas data-tile="rain" width="640" height="368"></canvas><img src="https://cdn.jsdelivr.net/gh/truptiPosimyth/landing-assets@main/kairo/photos/05-rain.jpg" alt="A figure under an umbrella on a wet, lamp-lit Tokyo street." loading="lazy" decoding="async" onerror="this.remove()">
<i class="tile__bar"></i>
<figcaption><b>After rain</b><span class="mono">05 / 06</span></figcaption></figure>
<figure class="tile"><canvas data-tile="petals" width="640" height="368"></canvas><img src="https://cdn.jsdelivr.net/gh/truptiPosimyth/landing-assets@main/kairo/photos/06-spring.jpg" alt="Tokyo Tower lit for the cherry-blossom season above the skyline." loading="lazy" decoding="async" onerror="this.remove()">
<i class="tile__bar"></i>
<figcaption><b>Spring, briefly</b><span class="mono">06 / 06</span></figcaption></figure>
</div>
</div>
</section>
<!-- story --------------------------------------------------------- -->
<section class="chapter chapter--wide" id="story">
<div class="chapter__inner">
<div class="sec-head">
<div>
<div class="eyebrow"><i class="dot"></i> 03 — the projection</div>
<h2>Someone is<br>painting the <em>tower</em>.</h2>
</div>
<p class="lede">Forty storeys of blank concrete, rented by the hour. Tonight it is an
ocean — reef, current, and something large moving through it.</p>
</div>
<div class="about-cols">
<div>
<p>The slab went up as offices and never filled. For a decade it was the one dark
face on the street, which is exactly what made it worth money again: nobody
rents a wall to be read, they rent it to be unavoidable.</p>
<p>The feed changes on an eighteen-minute cycle. Nobody on the ground has ever
been told what it is advertising, and at this point it would be a disappointment
to find out.</p>
</div>
<div>
<p>Below it the stalls run one storey high for half a block — the only place the
wall drops far enough to see the water, and the reason the district has a
sightline at all.</p>
<p>Stand under it long enough and the light on the wet road changes colour four
times. That is the whole show.</p>
</div>
</div>
</div>
</section>
<!-- districts ----------------------------------------------------- -->
<section class="chapter chapter--wide" id="districts">
<div class="chapter__inner">
<div class="sec-head">
<div>
<div class="eyebrow"><i class="dot"></i> 04 — still broadcasting</div>
<h2>Three things<br>still <em>running</em>.</h2>
</div>
<p class="lede">The district never switched anything off. It just stopped explaining
what any of it was for.</p>
</div>
<div class="cards">
<div class="card"><span>放 送 中</span><h3>The sphere</h3>
<p>Two towers laced together with open decks and a sphere slung in the gap. Every
floor is lit and something is still going out from up there.</p></div>
<div class="card"><span>観 覧 車</span><h3>The wheel</h3>
<p>Seventy-six metres, twenty-four cars, lit magenta, turning all night. You cannot
see anyone in them and the queue barrier is still up.</p></div>
<div class="card"><span>回 路 塔</span><h3>Kairo Tower</h3>
<p>A hundred and sixty-eight metres at the far end of the canyon, washed sodium
gold, with an aviation light that has never once been out.</p></div>
</div>
</div>
</section>
<!-- footer -------------------------------------------------------- -->
<footer class="chapter chapter--wide foot" id="contact">
<div class="chapter__inner foot__inner">
<div class="foot__word display">KAIRO</div>
<div class="foot__jp">回 路 · 第 一 区</div>
<nav class="foot__nav mono">
<a href="#about">About</a>
<a href="#gallery">Gallery</a>
<a href="#districts">Landmarks</a>
<a href="#top">Back to top</a>
</nav>
<div class="foot__bar mono">
<span>回 路 · District 01</span>
<span>Photographs · CC0 via Wikimedia Commons</span>
<span>23:40 · 17°C · after rain</span>
</div>
</div>
</footer>
</main>
</div>
```
## B6 · section `css`
```css
.uichemy-kairo-1{
--ink:#03060f;
--bone:#eaf2ff;
--dim:#8fa2c4;
--cyan:#22e0ff;
--magenta:#ff2d95;
--pad:clamp(18px,3.2vw,52px);
/* the source's html/body defaults, carried on the widget root so the theme's
body type cannot leak in, and so #grain's overlay blend has an opaque
backdrop of its own. body-level scroll locking lives in site-wide code. */
box-sizing:border-box;
-webkit-text-size-adjust:100%;
margin:0; background:var(--ink); color:var(--bone);
font-family:'Zen Kaku Gothic New','Space Grotesk',system-ui,-apple-system,
'Hiragino Sans','Hiragino Kaku Gothic ProN','Noto Sans JP','Yu Gothic',sans-serif;
font-weight:300; font-size:16px; line-height:1.65;
-webkit-font-smoothing:antialiased; overflow-x:hidden;
}
.uichemy-kairo-1 *,.uichemy-kairo-1 *::before,.uichemy-kairo-1 *::after{box-sizing:border-box}
.display{ font-family:'Space Grotesk',system-ui,sans-serif; }
a{color:inherit;text-decoration:none}
.mono{font-family:'JetBrains Mono',ui-monospace,monospace}
#gl{position:fixed; inset:0; width:100%; height:100%; display:block; z-index:0; background:#03060f}
/* ============================================================ loader */
#loader{
position:fixed; inset:0; z-index:100; background:#03060f;
display:flex; align-items:center; justify-content:center;
transition:opacity .9s cubic-bezier(.4,0,.2,1), visibility .9s;
}
#loader.is-done{ opacity:0; visibility:hidden; }
.ld__inner{ width:min(880px, calc(100vw - 400px)); text-align:center; }
.ld__mark{ width:44px; height:44px; color:var(--cyan); opacity:.85;
animation:kairo-ldspin 9s linear infinite; }
@keyframes kairo-ldspin{ to{ transform:rotate(360deg) } }
.ld__name{ margin-top:22px; font-size:34px; font-weight:700; letter-spacing:.30em;
text-indent:.30em; color:var(--bone); }
.ld__jp{ margin-top:6px; font-size:11px; letter-spacing:.62em; text-indent:.62em; color:var(--dim);
font-family:'Hiragino Sans','Hiragino Kaku Gothic ProN','Noto Sans JP','Yu Gothic',sans-serif; }
#ldcity{ display:block; width:100%; height:clamp(170px, 22vh, 205px); margin:30px 0 4px; }
.ld__bar{ margin-top:10px; height:1px; background:rgba(232,240,255,.14); overflow:hidden; }
.ld__bar i{ display:block; height:100%; width:0%; background:linear-gradient(90deg,var(--cyan),var(--magenta));
box-shadow:0 0 12px rgba(34,224,255,.8); transition:width .35s ease; }
.ld__row{ margin-top:12px; display:flex; justify-content:space-between;
font-size:9.5px; letter-spacing:.22em; color:var(--dim); }
/* a 200px gutter is most of a small screen, so hand those back to the page pad */
@media (max-width:1000px){ .ld__inner{ width:calc(100vw - 2 * var(--pad)) } }
#vignette{position:fixed; inset:0; z-index:6; pointer-events:none;
background:radial-gradient(125% 92% at 50% 46%, transparent 46%, rgba(1,2,8,.66) 100%)}
#grain{position:fixed; inset:-1px; z-index:7; pointer-events:none;
opacity:.045; mix-blend-mode:overlay; background-repeat:repeat; background-size:160px 160px}
/* The street goes down as a chapter arrives. The per-chapter scrim below
only darkens the box behind the words; this takes the whole frame with
it, so the copy is read against a city that has stepped back rather than
against a patch cut out of one. It leans toward whichever side the
chapter is on, so the far side of the street stays alive. */
#dim{position:fixed; inset:0; z-index:8; pointer-events:none;
opacity:var(--dim, 0); transition:opacity .14s linear;
/* Tight, and pushed right out to the text column. A wide dim centred on
the chapter's side also lands on that chapter's landmark — the pagoda,
the wheel, the sphere all sit on the same side as the copy that
describes them — which dims the one thing the camera just turned to
show. This reaches the words and stops short of the street. */
background:radial-gradient(var(--dimr, 46% 88%) at var(--dimx, 50%) 50%,
rgba(2,5,13,.92) 0%, rgba(2,5,13,.80) 26%, rgba(2,5,13,.50) 55%,
rgba(2,5,13,.18) 80%, rgba(2,5,13,0) 100%)}
@media (prefers-reduced-motion:reduce){ #dim{ transition:none } }
.nav{
position:fixed; top:0; left:0; right:0; z-index:24;
display:flex; align-items:center; justify-content:space-between;
/* The document is viewport-fit=cover, so without the insets the bar sits
under the status bar in portrait and under the notch in landscape. */
flex-wrap:wrap; row-gap:11px;
padding:calc(20px + env(safe-area-inset-top))
calc(var(--pad) + env(safe-area-inset-right)) 20px
calc(var(--pad) + env(safe-area-inset-left));
pointer-events:none;
background:linear-gradient(180deg, rgba(3,6,15,.78) 0%, rgba(3,6,15,.42) 55%, transparent 100%);
}
.nav > *{ pointer-events:auto }
.nav__brand{ display:flex; align-items:center; gap:12px }
.nav__brand svg{ width:27px; height:27px; color:var(--cyan); flex:none }
.nav__brand b{ display:block; font-family:'Space Grotesk',sans-serif; font-weight:500;
font-size:15px; letter-spacing:.20em; text-transform:uppercase }
.nav__brand i{ display:block; font-style:normal; font-size:9px; letter-spacing:.26em;
color:var(--dim); text-transform:uppercase; margin-top:3px }
.nav__links{ display:flex; gap:30px }
.nav__links a{ position:relative; font-size:10px; letter-spacing:.22em;
text-transform:uppercase; color:#b6c4dc; padding-bottom:4px }
.nav__links a::after{ content:''; position:absolute; left:0; right:100%; bottom:0; height:1px;
background:var(--cyan); transition:right .35s cubic-bezier(.4,0,.2,1) }
.nav__links a:hover,.nav__links a:focus-visible{ color:var(--bone) }
.nav__links a:hover::after,.nav__links a:focus-visible::after{ right:0 }
.nav__meta{ font-size:10px; letter-spacing:.22em; text-transform:uppercase; color:var(--dim) }
@media (max-width:1180px){ .nav__meta{ display:none } }
/* Below 820 the four links used to be display:none, which left a phone with no
navigation at all. They do not fit beside the brand at 360px, so they take a
row of their own and spread across it - no hamburger, nothing hidden behind a
tap, and the bar grows by one line rather than covering the street. */
@media (max-width:820px){
.nav__links{ order:3; width:100%; justify-content:space-between; gap:10px }
.nav__links a{ font-size:9px; letter-spacing:.16em }
}
/* A landscape phone has the width for one row but almost no height. */
@media (max-width:820px) and (max-height:520px){
.nav{ flex-wrap:nowrap }
.nav__links{ order:0; width:auto; gap:16px }
.nav__brand i{ display:none }
}
@media (max-width:380px){ .nav__brand i{ display:none } }
/* ── hero furniture: the scroll cue and a still, as on a title card */
.hero{ position:fixed; inset:0; z-index:18; pointer-events:none;
opacity:var(--hero, 1); transition:opacity .25s linear }
.hero__hint{ position:absolute; left:calc(var(--pad) + env(safe-area-inset-left));
bottom:calc(38px + env(safe-area-inset-bottom));
display:flex; align-items:center; gap:14px;
font-size:10px; letter-spacing:.26em; text-transform:uppercase; color:var(--dim) }
.hero__hint i{ display:block; width:66px; height:1px;
background:linear-gradient(90deg,var(--cyan),transparent);
animation:kairo-hintpulse 2.6s ease-in-out infinite }
@keyframes kairo-hintpulse{ 0%,100%{ opacity:.35; transform:scaleX(.7); transform-origin:left }
50%{ opacity:1; transform:scaleX(1); transform-origin:left } }
.rail{position:fixed; top:0; left:0; height:2px; z-index:30; width:0%;
background:linear-gradient(90deg,var(--cyan),var(--magenta)); box-shadow:0 0 14px rgba(34,224,255,.8)}
.scroll{position:relative; z-index:10}
.chapter{min-height:100vh; min-height:100svh; display:flex;
/* `safe` matters on a short phone. A chapter whose copy is taller than the
viewport is still centred, and plain `center` centres it by overflowing
BOTH edges - on a 375x667 screen that put the eyebrow and the first line of
the title above the top of the window, where they cannot be scrolled to. */
align-items:safe center;
padding:0 calc(var(--pad) + env(safe-area-inset-right)) 0 calc(var(--pad) + env(safe-area-inset-left))}
.chapter__inner{max-width:500px}
.chapter--right{justify-content:flex-end; text-align:right}
.chapter--right .chapter__inner{margin-left:auto}
.eyebrow{font-size:10px; letter-spacing:.26em; text-transform:uppercase;
display:flex; align-items:center; gap:9px; margin-bottom:18px; color:var(--dim)}
.chapter--right .eyebrow{justify-content:flex-end}
.dot{width:6px;height:6px;border-radius:50%;background:var(--cyan);box-shadow:0 0 12px var(--cyan);flex:none}
h1,h2{margin:0; font-family:'Space Grotesk',system-ui,sans-serif; font-weight:700;
letter-spacing:-.035em; line-height:.96; text-transform:uppercase}
h1{font-size:clamp(42px,7.0vw,92px)}
h2{font-size:clamp(32px,4.8vw,62px)}
h1 em,h2 em{ font-style:normal; color:var(--cyan); }
/* The panels sit on a lit street now, so every piece of copy carries its
own shadow — two of them: a tight one for the letterform edge and a wide
soft one that separates the block from whatever sign is behind it. */
h1,h2{ text-shadow:0 2px 18px rgba(2,6,16,.92), 0 0 74px rgba(2,6,16,.78) }
.card h3{ text-shadow:0 2px 14px rgba(2,6,16,.9) }
.eyebrow,.sec-num,.facts dt,.facts dd,.tile figcaption{
text-shadow:0 1px 12px rgba(2,6,16,.95) }
.jp{ display:block; margin-top:10px; font-size:13px; letter-spacing:.42em;
color:var(--dim); text-transform:none; font-weight:400;
font-family:'Zen Kaku Gothic New','Hiragino Sans','Noto Sans JP','Yu Gothic',sans-serif; }
.lede{margin:22px 0 0; max-width:38ch; color:#cad7ec; font-size:16.5px; line-height:1.72;
text-shadow:0 2px 16px rgba(2,6,16,.96), 0 0 44px rgba(2,6,16,.85)}
.chapter--right .lede{margin-left:auto}
/* Each chapter carries a small data block, the way a field note would. */
.facts{ display:flex; gap:30px; margin-top:26px; flex-wrap:wrap; }
.chapter--right .facts{ justify-content:flex-end; }
.facts div{ min-width:74px }
.facts dt{ margin:0 0 5px; font-size:8.5px; letter-spacing:.22em; text-transform:uppercase; color:var(--dim) }
.facts dd{ margin:0; font-size:15px; font-weight:500; font-family:'JetBrains Mono',monospace; color:#dbe6f8 }
/* ── landing sections ────────────────────────────────────────────
Everything below sits over the city, which is untouched: the canvas is
still fixed at z-index 0 and the whole page scrolls above it. These
panels only change what the copy is, not what is behind it. */
.chapter--wide{ align-items:center; padding-top:112px; padding-bottom:84px }
.chapter--wide .chapter__inner{ max-width:1180px; width:100%; margin:0 auto; text-align:left }
.sec-head{ display:flex; align-items:flex-end; justify-content:space-between;
gap:34px; flex-wrap:wrap; margin-bottom:36px }
.sec-head .lede{ margin:0; max-width:46ch }
.sec-num{ font-size:9px; letter-spacing:.26em; text-transform:uppercase; color:var(--dim) }
.about-cols{ display:grid; grid-template-columns:1.1fr .9fr; gap:56px; margin-top:8px }
.about-cols p{ margin:0 0 17px; color:#c2d0e6; font-size:16px; line-height:1.76;
text-shadow:0 2px 16px rgba(2,6,16,.96), 0 0 40px rgba(2,6,16,.85) }
.about-cols p:last-child{ margin-bottom:0 }
@media (max-width:900px){ .about-cols{ grid-template-columns:1fr; gap:26px } }
/* The gallery plates are drawn in the same way the loading screen draws
its skyline — a canvas and a few primitives. No files. */
.grid{ display:grid; grid-template-columns:repeat(3,1fr); gap:22px }
@media (max-width:980px){ .grid{ grid-template-columns:repeat(2,1fr) } }
@media (max-width:620px){ .grid{ grid-template-columns:1fr } }
/* Frosted glass. The panel blurs the street behind it instead of hiding it,
so the city still reads as being back there — an opaque fill would just
punch a hole in the scene. saturate() puts back the colour the blur
washes out, and the inset highlight is the lit top edge of the pane. */
.tile{ margin:0; position:relative; overflow:hidden;
border:1px solid rgba(232,240,255,.16); background:rgba(12,20,34,.52);
-webkit-backdrop-filter:blur(20px) saturate(1.35);
backdrop-filter:blur(20px) saturate(1.35);
box-shadow:inset 0 1px 0 rgba(232,240,255,.11), 0 20px 46px rgba(1,3,9,.42) }
.tile canvas{ display:block; width:100%; height:184px }
/* The photograph covers the drawn plate. Same box, so if the image is
missing the drawing underneath is simply what shows. */
.tile img{ position:absolute; left:0; top:0; width:100%; height:184px;
object-fit:cover; display:block }
.tile__bar{ position:absolute; left:0; right:0; top:184px; height:1px;
background:linear-gradient(90deg, var(--cyan), transparent 72%); opacity:.6 }
.tile figcaption{ display:flex; justify-content:space-between; gap:12px; padding:12px 14px;
font-size:9px; letter-spacing:.20em; text-transform:uppercase; color:var(--dim) }
.tile b{ color:#dce7f8; font-weight:500 }
.cards{ display:grid; grid-template-columns:repeat(3,1fr); gap:20px }
@media (max-width:900px){ .cards{ grid-template-columns:1fr } }
.card{ border:1px solid rgba(232,240,255,.15); background:rgba(12,20,34,.44);
-webkit-backdrop-filter:blur(22px) saturate(1.35);
backdrop-filter:blur(22px) saturate(1.35);
box-shadow:inset 0 1px 0 rgba(232,240,255,.10), 0 20px 46px rgba(1,3,9,.40);
padding:26px 24px }
.card span{ display:block; margin-bottom:15px; font-size:9px; letter-spacing:.24em;
text-transform:uppercase; color:var(--cyan) }
.card h3{ margin:0 0 11px; font-family:'Space Grotesk',system-ui,sans-serif; font-size:17px;
font-weight:700; letter-spacing:-.01em; text-transform:uppercase; color:#e7eefb }
.card p{ margin:0; font-size:14.5px; line-height:1.72; color:#b9c8e0 }
.foot{ min-height:auto; display:block;
padding:84px calc(var(--pad) + env(safe-area-inset-right))
calc(40px + env(safe-area-inset-bottom))
calc(var(--pad) + env(safe-area-inset-left));
background:linear-gradient(180deg, rgba(2,5,13,0) 0%, rgba(4,8,18,.58) 14%,
rgba(4,8,18,.74) 34%, rgba(4,8,18,.82) 62%, rgba(4,8,18,.88) 100%);
-webkit-backdrop-filter:blur(26px) saturate(1.2);
backdrop-filter:blur(26px) saturate(1.2);
/* A backdrop-filter starts abruptly at the element's edge, which drew a
hard seam across the street. Masking the footer fades the blur in with
the fill, so the glass arrives instead of snapping on. */
-webkit-mask-image:linear-gradient(180deg, transparent 0, #000 14%);
mask-image:linear-gradient(180deg, transparent 0, #000 14%) }
.chapter--wide .foot__inner{ max-width:1180px; margin:0 auto; width:100%; text-align:center }
/* The footer is a sign-off, not a sitemap: the name at the size the street
would paint it, then the few links anyone actually wants. */
.foot__word{ font-weight:700; line-height:.82; text-transform:uppercase;
font-size:clamp(64px, 14vw, 196px); letter-spacing:.04em; text-indent:.04em;
color:var(--bone); text-shadow:0 4px 40px rgba(2,6,16,.9), 0 0 120px rgba(2,6,16,.75) }
.foot__jp{ margin-top:10px; font-size:12px; letter-spacing:.56em; text-indent:.56em;
color:var(--dim);
font-family:'Zen Kaku Gothic New','Hiragino Sans','Noto Sans JP','Yu Gothic',sans-serif }
.foot__nav{ display:flex; justify-content:center; flex-wrap:wrap; gap:34px;
margin:30px 0 22px; padding-bottom:24px;
border-bottom:1px solid rgba(232,240,255,.10) }
.foot__nav a{ position:relative; font-size:11px; letter-spacing:.22em;
text-transform:uppercase; color:#c3d1e7; padding-bottom:4px }
.foot__nav a::after{ content:''; position:absolute; left:0; right:100%; bottom:0; height:1px;
background:var(--cyan); transition:right .35s cubic-bezier(.4,0,.2,1) }
.foot__nav a:hover,.foot__nav a:focus-visible{ color:var(--bone) }
.foot__nav a:hover::after,.foot__nav a:focus-visible::after{ right:0 }
@media (max-width:620px){ .foot__nav{ gap:20px } }
.foot__bar{ display:flex; justify-content:space-between; gap:20px; flex-wrap:wrap;
padding-top:24px; font-size:9.5px; letter-spacing:.20em; text-transform:uppercase;
color:var(--dim) }
/* A soft scrim keeps the copy readable when a lit façade passes behind it. */
.chapter__inner{
position:relative;
opacity:var(--o, 1);
transition:opacity .18s linear;
/* No will-change:opacity here. It promotes this element to a BACKDROP
ROOT even while opacity is 1, and a backdrop root clips what a
descendant's backdrop-filter is allowed to sample — which silently
turned the frosted panels below into plain translucent boxes. The
fade is two properties on six elements; it does not need the hint. */
}
/* The per-chapter scrim is gone. Its gradient still carried ~30% alpha at
the pseudo-element's own edge, so the box showed as a rectangle — and
#dim above now darkens the whole frame instead, which cannot show an
edge because it is the whole frame. */
@media (prefers-reduced-motion:reduce){ .chapter__inner{ transition:none } }
.hint{margin-top:36px; font-size:10px; letter-spacing:.24em; text-transform:uppercase;
color:var(--dim); display:flex; align-items:center; gap:12px}
.hint i{display:block; width:52px; height:1px; background:linear-gradient(90deg,var(--dim),transparent)}
.outro{justify-content:center; text-align:center}
/* A short phone held upright - an SE is 667 tall and the bar takes 97 of it.
The type comes down one step so a chapter fits the screen it is on. */
@media (max-height:720px) and (orientation:portrait){
.chapter{ padding-top:104px; padding-bottom:48px }
h1{ font-size:clamp(34px,9vw,52px) }
.lede{ margin-top:16px; font-size:15px }
.facts{ margin-top:18px; gap:24px }
}
/* A phone on its side is 390px tall with a fixed bar at each end. */
@media (max-height:560px) and (orientation:landscape){
.chapter{ padding-top:76px; padding-bottom:46px }
h1{ font-size:clamp(30px,5.4vw,46px) }
h2{ font-size:clamp(26px,4.4vw,38px) }
.lede{ margin-top:14px; font-size:14.5px; line-height:1.6 }
.facts{ margin-top:16px; gap:22px }
.jp{ margin-top:6px; font-size:11px }
.chapter--wide{ padding-top:88px; padding-bottom:56px }
}
@media (max-width:760px){
.chapter--right{justify-content:flex-start; text-align:left}
.chapter--right .chapter__inner{margin-left:0}
.chapter--right .eyebrow{justify-content:flex-start}
.chapter--right .lede{margin-left:0}
.chrome__right{display:none}
}
```
## Integrity (sha256 of each block's exact text)
| Block | sha256 |
|---|---|
| B1 `site_before_head` | `7ea573ab9f2e074321b478f4475a78ce9dd7c3975ca7c7f0aea67b5a6d35cd09` |
| B2 `site_before_body` | `a0224b5b641dc98984134e8a8385d5934149d7c4142c4aac3a897e69c7fd2da4` |
| B3 `kairo.js` (matches the served file) | `2bfec81353dc8a6db6932af29cbfe9c7452d8e61011778a8924e820774253efb` |
| B4 `kairo-scene.js` (to deploy) | `d5d0a8e53eae10828e3808d7e02365321249580f9a9f766f50c6a8dbdff89417` |
| B5 section `html` | `276023eebd18ad5f97709632ab69acbb80ebd6d73249cca954eb4d98fac59423` |
| B6 section `css` | `6e13477fc759b62e933572726fd3a3a48c4d00e0be44b4e31359b471f4d057f8` |
---
# APPENDIX C · THE PREVIEW VIDEO
How the 1 MB WebM walkthrough was produced, in case it needs redoing.
**Record** (Playwright, local preview or the live URL). Record at **deviceScaleFactor 1** —
at 2 the scene renders 3840x2160, the recorder drops frames and the result judders. At 1280x720
the scene holds 76fps and the recording is a constant 25fps.
```js
const ctx = await browser.newContext({ viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1, recordVideo: { dir: DIR, size: { width: 1280, height: 720 } } });
// wait for window.__kabuki && !document.getElementById('loader'), then:
// hold 1800ms on each of the six chapter offsetTops, 900ms eased travel between them.
```
Long holds and short travels are deliberate: motion is what costs bits, a near-still frame
costs almost nothing, so the budget lands on the frames you actually read.
**Encode** (two-pass VP9; the grain overlay is the single most expensive thing on screen at
this bitrate, so it is filtered out before scaling):
```
-ss <after the loader> -to <end>
-vf hqdn3d=2:1.5:6:6,scale=960:540:flags=lanczos,unsharp=3:3:0.6:3:3:0.0
-c:v libvpx-vp9 -b:v 455k -minrate 227k -maxrate 910k -bufsize 2275k
-row-mt 1 -tile-columns 1 -auto-alt-ref 1 -lag-in-frames 25 -arnr-maxframes 15
-arnr-strength 5 -aq-mode 0 -deadline good -cpu-used 0 -g 300 -an -pass 1|2
```
Result: 960x540, 17.16s, **954,221 bytes**. Size scales linearly with `-b:v`, so pick the
bitrate from the clip length: `bytes ≈ b:v × seconds ÷ 8`.