Landing Pagexiaomi/mimo-v2.6-proverified 20/20partial cross-check
Thermal
A diffusing 48x27 canvas heat map mapped through an inferno palette with a live hottest-cell readout
The exact prompt
## A. WHAT IS BEING BUILT
Build one complete, self-contained `index.html` for **Thermal** — an industrial-monitoring company turning sensor meshes into live heat maps. Document title: `Thermal — Continuous Heat Intelligence`. `<html lang="en">`. ONE file: plain HTML + CSS + JS in `<style>` and one `<script>` at end of body. No build step, no framework, no bundler. Google Fonts is the ONLY network request (Inter + IBM Plex Mono, system fallback stacks). Every visible pixel comes from CSS gradients, inline SVG, text, and one `<canvas>`.
**Viewport rule:** at width `>= 901px` the page is one locked frame — `html, body { height:100%; overflow:hidden }` and `.page { height:100dvh; overflow:hidden }`. NO SCROLL on desktop. At width `<= 900px` the page may scroll vertically. No horizontal overflow at any width.
The single visual idea: a full-bleed `<canvas>` heat map — a 48×27 cell grid where three drifting emitters inject heat that diffuses between neighbours every frame, each cell mapped through an inferno-structured ramp. Cool blue edges, violet mid-field, hot magenta core, white-pink peak. Over it: a HUD with a legend strip (20°C → 620°C), a live hottest-cell readout, and a crosshair on that cell. Industrial-monitoring, instrument-grade, cinematic.
**Do NOT build:** no video, no WebGL, no Three.js, no Lottie, no GSAP, no jQuery, no Tailwind, no React, no remote or base64 images, no audio, no forms, no pricing tables, no testimonials, no dashboards, no charts beyond the legend strip, no sections beyond header / hero / footer, no footer links beyond the three specified, no cookie banner, no scroll indicator, no desktop hamburger.
---
## B. THE BACKDROP — procedural heat field (the ONLY visual effect besides grain and the marker)
A single `<canvas id="heat">` fills the viewport: `position:fixed; inset:0; width:100%; height:100%; z-index:0; display:block`, repainted every animation frame via `requestAnimationFrame`. NEVER a static image.
**The field (canvas 2D, no libraries):**
- Grid `W = 48`, `H = 27` at `>= 901px` (1296 cells); at `<= 900px` transpose to `W = 27`, `H = 48` (still 1296) so cells stay near-square on portrait screens.
- Two `Float32Array(1296)` buffers `cur` / `nxt`; index `i = y * W + x`.
- **Diffusion step, once per frame**, clamped (zero-flux) neighbours:
`lap = up + down + left + right - 4 * c`
`v = (c + 0.18 * lap) * 0.9962` (`K = 0.18`, `COOL = 0.9962`)
if `x < 2 || x > W-3 || y < 2 || y > H-3` then `v *= 0.982` (`EDGE_DAMP` — this keeps the rim cold blue and the core hot)
clamp `v` to `[0, 1]`, write `nxt[i]`; swap buffer references after the pass. No allocation in the loop.
- **Emitters: exactly 3**, `k = 0..2`, at time `t` seconds:
`ex = 23.5 + 22 * Math.sin(t * 0.19 + k * 2.094)`
`ey = 13.0 + 11.5 * Math.sin(t * 0.27 + k * 4.189)`
(phases are thirds of a turn). Round to the nearest cell; inject the 3×3 footprint with weights `[[0.28,0.55,0.28],[0.55,1.00,0.55],[0.28,0.55,0.28]] * 0.14` (`INJ`), clamped to 1.0.
- **Pointer injection:** `pointermove` only stores the latest client position. Once per frame map it through the CACHED canvas rect to a cell and inject `0.22` (`POINTER_INJ`) at the centre, `0.11` at its 4-neighbours, clamped to 1.0 — at most one injection per frame. `pointerleave` clears it.
- **Ramp LUT:** build `Uint8Array(768)` (256 RGB triplets) ONCE from 8 stops — `0.00 #030614`, `0.14 #0b2a72`, `0.32 #3a1f92`, `0.50 #8b1c8a`, `0.68 #d4217f`, `0.82 #ff3d8f`, `0.93 #ff9ad0`, `1.00 #ffeaf4` — linearly interpolated. Per cell: `idx = Math.min(255, (Math.pow(v, 0.85) * 255) | 0)`; write `lut[3*idx], lut[3*idx+1], lut[3*idx+2], 255` into the ImageData.
- **The resolution trick (the entire cost saving):** the field renders into an offscreen canvas `field` sized EXACTLY `W × H` (48×27): one `putImageData` per frame. Then ONE `ctx.drawImage(field, 0, 0, W, H, 0, 0, cssW, cssH)` with `imageSmoothingEnabled = true`, `imageSmoothingQuality = 'high'` upscales it — the smoothing IS the bloom, free from the compositor. NEVER set `ctx.filter` or `ctx.shadowBlur`. Main canvas backing store `cssW*dpr × cssH*dpr`, `dpr = Math.min(devicePixelRatio, 2)` (1.5 at `<= 900px`), `ctx.setTransform(dpr,0,0,dpr,0,0)`. `field` is never resized.
- **Overlay pass:** a second offscreen canvas `fx` at `cssW*dpr × cssH*dpr`, redrawn ONLY on resize: (1) cell hairlines `rgba(4,7,22,0.50)` 1px at every boundary, (2) heavy lines `rgba(4,7,22,0.75)` 1px every 8 cells, (3) cool edge tint — radial `rgba(11,42,114,0)` at 42% → `rgba(11,42,114,0.24)` at 100%, (4) vignette — radial `rgba(3,6,20,0)` at 52% → `rgba(3,6,20,0.62)` at 100%. Composited each frame with ONE `ctx.drawImage(fx, 0, 0, cssW, cssH)`.
- **Hottest cell:** the colour pass tracks `maxV / maxX / maxY` and the sum. Write ONE `transform: translate3d(maxX*cellW, maxY*cellH, 0)` to `.hotspot` per frame (cached cell size). Update readout text every 6th frame (`frames % 6 === 0`) only when changed. `T = Math.round(20 + v * 600)` → 20–620°C; mean = `Math.round(20 + (sum/1296) * 600)`.
- **Per-frame cost budget:** 1296-cell diffusion (≈ 6.5k array reads), 1296 LUT lookups, 1 `putImageData`, 2 `drawImage`, 1 DOM transform, ≤ 1 text write — 3 canvas primitives total. DOM writes are transform/opacity only. NEVER call `getBoundingClientRect` in the loop; the canvas rect and cell size are cached on resize and reused by the pointer mapper. If `document.hidden`, keep the rAF alive and keep counting frames but skip painting.
- **Grain (CSS):** `.grain`, `position:fixed; inset:-50%; width:200%; height:200%; pointer-events:none; opacity:0.05`, data-URI SVG `feTurbulence` (`type="fractalNoise" baseFrequency="0.9" numOctaves="2" stitchTiles="stitch"` + `feColorMatrix saturate 0`), tile 160px, `animation: grain 0.9s steps(4) infinite` jittering `translate` among `(0,0) (-2%,1%) (1%,-2%) (-1%,2%)`.
---
## C. THE LAYER STACK (back → front)
1. `html, body` — `#04071a`, forced twice (see Failure Guards).
2. `canvas#heat` — z-index 0: upscaled heat field + cached `fx` overlay.
3. `.scrim` — z-index 1, `position:fixed; inset:0; background: linear-gradient(180deg, rgba(4,7,22,0.74) 0%, rgba(4,7,22,0.20) 28%, rgba(4,7,22,0.18) 58%, rgba(4,7,22,0.82) 100%)` for text contrast top and bottom.
4. `.page` — z-index 2, `position:relative; display:grid; grid-template-rows: auto minmax(0,1fr) auto; min-height:100dvh` (`height:100dvh` inside the desktop lock).
5. `.hotspot` — z-index 3, the hottest-cell crosshair, `position:fixed; pointer-events:none`.
6. `.grain` — z-index 4; browser focus outlines stay visible regardless.
---
## D. TOKEN TABLE (defaults at ~1440px)
```
--bg: #04071a
--text: #eef2fa
--text-soft: #c6d0e4
--muted: #8a93ab
--dim: #5a6480
--accent: #ff3d8f
--accent-dim: rgba(255,61,143,0.55)
--accent-line: rgba(255,61,143,0.30)
--accent-fill: rgba(255,61,143,0.06)
--cool: #2b5fd0
--cool-line: rgba(43,95,208,0.45)
--line: rgba(255,255,255,0.14)
--line-soft: rgba(255,255,255,0.08)
--panel: rgba(4,7,22,0.55)
--panel-line: rgba(255,255,255,0.12)
--logo: 15px
--logo-track: 0.22em
--nav: 12px
--nav-h: 34px
--eyebrow: 11.5px
--h1: 64px
--lead: 17px
--btn: 13px
--btn-h: 42px
--hud-label: 10.5px
--hud-value: 30px
--hud-stat: 11.5px
--legend-label: 10.5px
--legend-h: 8px
--legend-w: 252px
--foot: 12.5px
--header-y: 24px
--header-x: 40px
--hero-x: 40px
--hero-y: 32px
--hero-max: 1180px
--hero-gap: 56px
--hud-w: 296px
--hud-pad: 20px
--foot-y: 22px
--radius-chip: 3px
--radius-panel: 6px
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
--sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif
--ease-out: cubic-bezier(0.16,1,0.3,1)
--enter-dur: 0.95s
--grain-dur: 0.9s
--pulse-dur: 2.4s
--blink-dur: 1.06s
```
Canvas JS constants mirroring the CSS: `FIELD_W 48`, `FIELD_H 27`, `FIELD_W_SM 27`, `FIELD_H_SM 48`, `DPR_MAX 2`, `DPR_MAX_SM 1.5`, `TEMP_MIN 20`, `TEMP_SPAN 600`, `LUT_N 256`, `READOUT_EVERY 6` (plus `K`, `COOL`, `EDGE_DAMP`, `INJ`, `POINTER_INJ` as defined in B). Fonts: one `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>` + one stylesheet (`display=swap`) loading `Inter:wght@400;500;600` and `IBM+Plex+Mono:wght@400;500`.
---
## E. THE COMPOSITION — exact build order
**.page** is the grid container with three rows: `<header>`, `<main>`, `<footer>`.
**HEADER** — `padding: var(--header-y) var(--header-x); display:flex; align-items:center; justify-content:space-between; gap:24px`.
- `a.brand` (href `#`, accessible name "Thermal home"): inline SVG 20×20 — four 7×7 cells (`rx="1"`) at (2,2), (11,2), (2,11), (11,11) filled `#0b2a72`, `#8b1c8a`, `#3a1f92`, `#ff3d8f` — then wordmark `THERMAL`, `var(--mono)`, `font-size:var(--logo); font-weight:500; letter-spacing:var(--logo-track); color:var(--text)` — then a 5px live pixel `background:var(--accent); box-shadow:0 0 8px var(--accent-dim); margin-left:10px` with `blink`.
- `nav`, `display:flex; align-items:center; gap:30px; height:var(--nav-h)`: exactly 4 links `System`, `Sensors`, `Firmware`, `Docs` (rendered uppercase, `font-size:var(--nav); letter-spacing:0.14em; color:var(--muted)`), all `href="#"`, each with an aria-hidden index `01`–`04` in `var(--dim)`, `margin-right:8px`. Hover `color:var(--text)` 0.18s; index stays dim.
- Right: `a.btn.btn--ghost` (href `#`) text `Request a pilot`, header variant `height:var(--nav-h); padding:0 16px`.
- At `<= 900px` the nav is replaced by `button.burger` (`aria-label="Open navigation"`, `aria-expanded="false"`, three 18×1.5px `var(--text)` bars, gap 5px).
**MAIN (hero)** — `display:grid; grid-template-columns: minmax(0,1fr) var(--hud-w); gap:var(--hero-gap); align-items:center; padding:var(--hero-y) var(--hero-x); width:100%; max-width:var(--hero-max); margin:0 auto; min-width:0`. Copy left, HUD right, centred in the 1fr row.
Left column:
1. `.eyebrow` — mono `var(--eyebrow)`, `letter-spacing:0.18em; text-transform:uppercase; color:var(--dim)`. Copy: `PLANT 04 — FURNACE ARRAY B — 20 Hz` with `PLANT 04` in `var(--accent)`.
2. `h1` — EXACTLY ONE `<h1>`, `var(--sans); font-weight:600; font-size:var(--h1); letter-spacing:-0.045em; line-height:1.02; color:var(--text); margin:18px 0 0`. Two masked lines (see Motion): line 1 `Heat is data.`, line 2 `Failure is a signal.` with `<em>signal</em>` = `font-family:var(--mono); font-style:normal; font-weight:400; font-size:0.90em; letter-spacing:-0.02em; color:var(--accent)`.
3. `p.lead` — `font-size:var(--lead); line-height:1.6; color:var(--text-soft); max-width:520px; margin:20px 0 0`. Copy: `Thermal fuses four thousand furnace, bearing and transformer sensors into one continuous heat map, so your team watches the failure form — before the first alarm ever fires.`
4. `.actions` — `display:flex; gap:12px; flex-wrap:wrap; margin-top:28px`: `a.btn.btn--hot` `Request a pilot`, `a.btn.btn--ghost` `Read the spec sheet`, both `href="#"`, `height:var(--btn-h); padding:0 20px; font-size:var(--btn)`.
Right column `aside.hud` — `width:100%; max-width:var(--hud-w); padding:var(--hud-pad); border:1px solid var(--panel-line); border-radius:var(--radius-panel); background:var(--panel); backdrop-filter:blur(8px); display:grid; gap:16px`.
- `.hud-head` — `display:flex; align-items:center; justify-content:space-between`: label `HOTTEST CELL` mono `var(--hud-label); letter-spacing:0.2em; color:var(--dim)` + a 5px accent pixel with `blink`.
- `.hud-value` — mono `var(--hud-value); font-weight:500; color:var(--accent); letter-spacing:0.02em; margin-top:-8px`. JS owns it; static fallback `R14 · C27 · 312°C`. JS format: `R` + (row+1) + ` · C` + (col+1) + ` · ` + T + `°C`.
- `.hud-stats` — a `<dl>`, `display:grid; grid-template-columns:1fr 1fr; gap:10px 16px`, mono `var(--hud-stat)`, labels `var(--dim)`, values `var(--text-soft)`: `MEAN` → JS `214°C` (fallback), `GRID` → `48 × 27`, `EMITTERS` → `3`, `K / COOL` → `0.18 / 0.9962`.
- `.legend` — `width:var(--legend-w); max-width:100%; height:var(--legend-h); border-radius:2px; border:1px solid var(--line-soft); background:linear-gradient(90deg, #030614 0%, #0b2a72 14%, #3a1f92 32%, #8b1c8a 50%, #d4217f 68%, #ff3d8f 82%, #ff9ad0 93%, #ffeaf4 100%)` (the LUT stops). Below: `.legend-scale` `display:flex; justify-content:space-between; margin-top:8px`, mono `var(--legend-label)` in `var(--dim)`: `20°C`, `320°C`, `620°C`.
- `.hud-foot` — mono 10px `var(--dim)`: `SAMPLING 20 Hz · DIFFUSION 48×27`.
**FOOTER** — `padding:var(--foot-y) var(--header-x); display:flex; align-items:center; justify-content:space-between; gap:20px; border-top:1px solid var(--line-soft)`. Left mono `var(--foot)` `var(--dim)`: `© 2026 Thermal Industrial Systems`. Centre: a 7px accent square (`margin-right:8px; box-shadow:0 0 8px var(--accent-dim)`) + `sensor mesh nominal · 4,096 nodes`. Right: exactly 3 links `spec sheet`, `status`, `contact`, mono `var(--foot)`, `var(--muted)`, `gap:22px`, hover `color:var(--text)`.
---
## F. THE UI LANGUAGE
Two button treatments only, both square-cut instrument chips with `border-radius: var(--radius-chip)`. `a.btn` is `inline-flex; align-items:center; justify-content:center; text-decoration:none; font-family:var(--mono); font-weight:500; letter-spacing:0.06em; transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease`.
- `.btn--hot` (hero primary): `color:#1a0512; border:1px solid rgba(255,61,143,0.55); background:linear-gradient(180deg, #ff9ad0 0%, #ff3d8f 52%, #d4217f 100%); box-shadow:0 14px 40px rgba(255,61,143,0.22), inset 0 1px 0 rgba(255,255,255,0.5)`. Hover: `background:linear-gradient(180deg, #ffe0f0 0%, #ff5fa5 50%, #e0268a 100%); transform:translateY(-1px); box-shadow:0 18px 48px rgba(255,61,143,0.32), inset 0 1px 0 rgba(255,255,255,0.6)`. Active: `transform:none`.
- `.btn--ghost` (hero secondary + header CTA): `color:var(--accent); border:1px solid var(--accent-line); background:var(--accent-fill)`. Hover: `background:rgba(255,61,143,0.13); border-color:var(--accent-dim); box-shadow:0 0 16px rgba(255,61,143,0.18)`.
- Nav and footer links are plain mono text — no pills, no backgrounds, no underline ever; hover swaps colour only. Every interactive element gets `:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:3px }`. Selection: `::selection { background: rgba(255,61,143,0.30); color:#fff }`. No default blue focus ring, no text underline anywhere.
---
## G. MOTION — entrance stagger + named keyframes
All entrance animations: `animation-duration: var(--enter-dur); animation-fill-mode: both; animation-timing-function: var(--ease-out); animation-delay: var(--d, 0.08s)`. Each animated element carries `.appear`, one modifier, and an inline `--d`.
Stagger table (element → modifier → --d):
```
brand appear--fade 0.06s
nav 01 System appear--fade 0.14s
nav 02 Sensors appear--fade 0.20s
nav 03 Firmware appear--fade 0.26s
nav 04 Docs appear--fade 0.32s
header cta appear--chip 0.40s
eyebrow appear--fade 0.48s
h1 line 1 appear--mask 0.62s
h1 line 2 appear--mask 0.78s
lead appear--soft 0.96s (duration 1.15s)
hot button appear--chip 1.10s
ghost button appear--chip 1.18s
hud panel appear--hud 1.26s (duration 1.25s)
hud value appear--fade 1.40s
legend strip appear--wipe 1.46s (duration 1.10s)
footer left appear--fade 1.58s
footer centre appear--fade 1.64s
footer right appear--fade 1.70s
```
Named keyframes (all end at opacity 1 / identity transform):
```
in-fade from { opacity: 0 } to { opacity: 1 }
in-soft from { opacity: 0; transform: translateY(14px); filter: blur(4px) } to { opacity: 1; transform: none; filter: blur(0) }
in-mask from { opacity: 0; transform: translateY(105%) } to { opacity: 1; transform: none }
in-chip from { opacity: 0; transform: translateY(8px) scale(0.96) } to { opacity: 1; transform: none }
in-hud from { opacity: 0; transform: translateX(18px) } to { opacity: 1; transform: none }
in-wipe from { opacity: 0; clip-path: inset(0 100% 0 0) } to { opacity: 1; clip-path: inset(0 -2% 0 0) }
```
H1 masking: each line is `<span class="line"><span class="line-inner appear appear--mask">…</span></span>` with `.line { display:block; overflow:hidden; padding:0.04em 0.08em 0.10em }`, `.line-inner { display:inline-block }`. `in-wipe` reveals the legend gradient left→right like a scale being drawn. Continuous keyframes, independent of the stagger:
```
blink 0%,49% { opacity: 1 } 50%,100% { opacity: 0 } (1.06s steps(1) infinite)
hot-pulse 0% { transform: scale(0.55); opacity: 0.85 } 70% { opacity: 0 } 100% { transform: scale(1.7); opacity: 0 } (2.4s ease-out infinite)
grain 0% { translate(0,0) } 25% { translate(-2%,1%) } 50% { translate(1%,-2%) } 75% { translate(-1%,2%) } (0.9s steps(4) infinite)
```
`.hotspot` is positioned ONLY by JS via `transform: translate3d()`; its child `.hotspot-ring` (1px `var(--accent)` square, `opacity:0.8`) runs `hot-pulse`. Crosshair: `::before` 13px×1px bar, `::after` 1px×13px bar, both `background:var(--accent-dim)`, centred in the cell. The field animates via rAF, not CSS.
---
## H. RESPONSIVE TABLE — token overrides per breakpoint (exact)
```
>= 1600px:
--h1: 76px --logo: 16px --nav: 12.5px --btn: 13.5px --btn-h: 44px
--lead: 18px --hud-w: 320px --hud-value: 34px --legend-w: 276px
--header-x: 56px --hero-x: 56px --hero-max: 1320px --hero-gap: 64px --foot: 13px
>= 1920px:
--h1: 88px --lead: 19px --hud-w: 340px --hud-value: 38px --legend-w: 296px
--header-x: 72px --hero-x: 72px --hero-max: 1480px --hero-gap: 72px
1280px – 1599px:
--h1: 62px --header-x: 44px --hero-x: 44px --hero-max: 1200px --hud-w: 300px
901px – 1279px:
--h1: 50px --lead: 15.5px --nav: 11.5px --nav-h: 32px --btn: 12.5px --btn-h: 40px
--header-x: 30px --hero-x: 30px --hero-y: 24px --hero-gap: 36px
--hud-w: 260px --hud-pad: 16px --hud-value: 26px --legend-w: 216px --foot: 12px
>= 901px AND max-height 850px:
--h1: 52px --header-y: 16px --hero-y: 20px --lead: 15.5px --hud-value: 26px
(h1 margin-top 12px, lead margin-top 14px, .actions margin-top 20px, .hud gap 12px)
>= 901px AND max-height 720px:
--h1: 44px --header-y: 12px --hero-y: 12px --foot-y: 12px --eyebrow: 11px
--lead: 14px --hud-pad: 16px --hud-value: 22px --legend-h: 6px --nav-h: 30px --btn-h: 36px
>= 901px (desktop lock, all heights):
html, body { height:100%; overflow:hidden } .page { height:100dvh; overflow:hidden }
<= 900px:
no vh lock; page may scroll; canvas field transposes to 27 x 48; dpr cap 1.5
nav becomes a fixed overlay menu:
- header keeps brand + burger (button.burger, aria-label "Open navigation", aria-expanded toggled)
- .menu { position:fixed; inset:0; z-index:10; background: rgba(4,7,22,0.94); backdrop-filter: blur(14px);
display:flex; flex-direction:column; align-items:flex-start; justify-content:center; padding: 0 32px; gap: 24px;
transform: translateY(-100%); transition: transform 0.45s var(--ease-out) } .menu.open { transform: none }
- menu holds the 4 nav links stacked at --nav: 22px plus the ghost CTA chip; Escape, any link click, and resize > 900px all close it
hero becomes one column: grid-template-columns: 1fr; gap: 28px; align-items: start
--h1: 42px --header-x: 22px --hero-x: 22px --hero-max: 100% --hud-w: 100%
<= 560px:
--h1: 34px --lead: 15px --header-x: 18px --hero-x: 18px --hud-value: 24px
--legend-w: 100% --foot: 11.5px
footer becomes flex-direction:column; align-items:flex-start; gap:10px
header CTA is hidden (it lives inside the menu on mobile)
```
---
## I. FAILURE GUARDS — the page is never blank, never broken
1. **Dark-first:** the FIRST CSS rule is `html, body { background:#04071a !important; color:#eef2fa }`; the body tag carries `style="background:#04071a;color:#eef2fa"`; a later rule restates `html, body { background:#04071a; background: var(--bg, #04071a) }`.
2. **Resting opacity is 1:** every `.appear` element's un-animated state is its final state (opacity 1, no transform, no clip-path); `fill-mode: both` only holds the FROM state during the delay. With animations failed or JS absent the page is complete, including the fallback readouts.
3. **Two-rAF fallback:** after two nested `requestAnimationFrame` ticks, inspect every `.appear` with `el.getAnimations({ subtree:false })`; if none reports `running` or `finished` (or `getAnimations` is missing), add `.is-in` to every `.appear`. `.is-in { animation:none !important; opacity:1 !important; transform:none !important; filter:none !important; clip-path:none !important }`.
4. **Canvas fallback:** if canvas or 2D context is missing, add `.no-canvas` to `<html>` and paint a static heat map in CSS: `.no-canvas body { background: radial-gradient(ellipse at 50% 58%, #ff3d8f 0%, #d4217f 16%, #8b1c8a 34%, #3a1f92 56%, #0b2a72 78%, #030614 100%) }`. HUD, legend and readouts stay readable; push one string into `window.__DEMO__.errors`.
5. **Reduced motion:** inside `@media (prefers-reduced-motion: reduce)`: `*, *::before, *::after { transition:none !important; animation:none !important }`; force `.appear, .line-inner` to `opacity:1 !important; transform:none !important; clip-path:none !important`; hide `.grain` and `.hotspot-ring`; the canvas paints ONE static frame at fixed `t = 6.4s` (field already structured), parks the crosshair on that frame's hottest cell, sets `ready = true` and `frames = 1`, then stops scheduling. Static, complete, readable.
6. **Zero console output** in the happy path: no `console.log`, no thrown exceptions; every canvas call null-guarded.
---
## J. THE EXACT JS (only this, vanilla, one IIFE at end of body)
1. **Telemetry first:** `window.__DEMO__ = { name: 'thermal', ready: false, frames: 0, effects: [], errors: [] };` then push exactly these, once, in order: `'heat-diffusion'`, `'inferno-lookup'`, `'hottest-cell-hud'`, `'grain-texture'`. Immediately attach `window.addEventListener('error', e => window.__DEMO__.errors.push(String(e.message || e)))`.
2. **Cache everything up front:** `document.documentElement`, `canvas#heat`, its 2D context (null → `.no-canvas` path + one errors push), the `field` canvas (`W × H`) and its ImageData, the `fx` overlay canvas, `cur`/`nxt` Float32Array(1296), the LUT, `.hotspot`, `.hud-value`, the mean `dd`, burger, menu, menu links, and `matchMedia('(prefers-reduced-motion: reduce)')`.
3. **Palette build:** interpolate the 8 section-B stops into the 256-entry LUT once at startup.
4. **Layout cache:** `resize()`, scheduled by a boolean `resizeQueued` + `requestAnimationFrame` (never a timer): measure the canvas ONCE with `getBoundingClientRect`, store `cssW, cssH, rect, cellW = cssW / W, cellH = cssH / H`, size the backing store with `dpr = Math.min(devicePixelRatio, innerWidth <= 900 ? 1.5 : 2)`, `ctx.setTransform(dpr,0,0,dpr,0,0)`, redraw `fx` (hairlines every cell, heavy lines every 8 cells, cool edge tint, vignette). Switch the field to 27×48 at `innerWidth <= 900`, back to 48×27 above.
5. **The rAF loop `frame(ts)`:** increment `window.__DEMO__.frames` by exactly 1 every tick. If `document.hidden`, skip painting and reschedule. Otherwise with `t = ts / 1000`: (a) diffusion pass verbatim from B, no allocation, (b) advance and inject the 3 emitters, (c) one queued pointer injection, (d) LUT colour-pass tracking `maxV/maxX/maxY` and the sum, (e) `putImageData`, (f) `drawImage(field …)` upscaled with smoothing, (g) `drawImage(fx …)`, (h) `translate3d(maxX*cellW, maxY*cellH, 0)` on `.hotspot`, (i) every 6th frame, if changed, `R{maxY+1} · C{maxX+1} · {Math.round(20 + maxV*600)}°C` into `.hud-value` and `{Math.round(20 + (sum/1296)*600)}°C` into the mean. On the FIRST completed paint set `window.__DEMO__.ready = true`. Always reschedule `requestAnimationFrame(frame)`.
6. **Entrance fallback:** two nested rAFs after DOMContentLoaded, then the `.is-in` sweep (Failure Guards 3).
7. **Menu (mobile only):** burger toggles `.menu.open` and `aria-expanded`; Escape closes; any menu link click closes; resize to `> 900px` closes.
8. **Reduced motion:** read the query once; if it matches, run one `frame(6400)` with no reschedule — one static paint, `ready = true`, `frames = 1`.
No other JS. No timers. No libraries. No console calls.
---
TECHNICAL CONTRACT (non-negotiable):
1. Deliver ONE single file: a complete, valid `index.html`. No build step, no bundler,
no framework, no package install. It must open correctly by double-clicking the file
and also over http://.
2. Vanilla only. No React, no Tailwind CDN, no GSAP, no Three.js, no jQuery, no Lottie.
Plain HTML + CSS + modern JavaScript in <style> and <script> tags.
3. NO external media. No .mp4, no .webm, no .gif, no remote images, no base64 blobs of
photos. Every visual must be generated procedurally with CSS gradients, SVG, or
<canvas>. This keeps the page free to host and impossible to break with a dead link.
4. Google Fonts is the ONLY permitted network request, and it must be declared with a
<link rel="preconnect"> plus a system-font fallback stack so the page still looks
intentional offline. If you prefer, skip it and use a pure system stack.
5. Responsive and correct at 390x844 (phone), 768x1024 (tablet) and 1440x900 (desktop).
No horizontal scrollbar at any of those widths. Nothing may overflow its container.
6. Accessibility: exactly one <h1>; real <header>/<main>/<footer>/<nav> landmarks; every
interactive element is a <button> or <a> with an accessible name; visible :focus-visible
ring; body text contrast ratio >= 4.5:1 against its background.
7. Motion safety: wrap all continuous motion in
`@media (prefers-reduced-motion: reduce)` rules that reduce it to a static or
near-static state. The page must remain fully readable and usable with motion off.
8. Performance: no layout thrash. Cache DOM reads, batch writes inside a single
requestAnimationFrame loop, use transform/opacity for animation, and never allocate
inside the hot loop. Target 60fps on a 2019 laptop. Total file under 90 KB.
9. Runtime telemetry hook (used by the automated verifier — implement it exactly):
at the top of your script define
window.__DEMO__ = { name: '<slug>', ready: false, frames: 0, effects: [], errors: [] };
then set `ready = true` after the first paint of your main effect, increment
`frames` once per animation frame inside your rAF loop, and push the name of each
implemented effect into `effects`. Add a `window.addEventListener('error', ...)`
handler that pushes messages into `errors`. Do not rename these fields.
10. Zero console output in the happy path. No console.log left behind. No uncaught
exceptions, no failed network requests.
RESPONSE FORMAT:
Return ONLY the raw HTML inside a single ```html fenced code block. No explanation, no
preamble, no commentary before or after the fence. The file must be complete and
immediately runnable — never truncate, never write "// rest of code here".Acceptance criteria (15)
- exactly one h1 with the exact text 'Heat is data. Failure is a signal.' plus semantic header, nav, main, and footer landmarks
- canvas#heat paints a non-blank heat map and its pixel hash changes across consecutive frames at 1440x900, with the field sized 48x27 at >=901px and 27x48 at <=900px
- window.__DEMO__ starts as name 'thermal' with ready false, ready becomes true after the first painted frame, frames increments once per rAF tick reaching at least 20 in 1.2s at 1440x900, effects includes heat-diffusion, inferno-lookup, hottest-cell-hud and grain-texture, and errors stays empty
- the .hud-value readout matches /R\d+ · C\d+ · \d+°C/ and changes over time, and .legend carries the 8-stop thermal linear-gradient containing #ff3d8f with 20°C / 320°C / 620°C scale labels
- no horizontal overflow at 390px, 768px, 1440px, or 1920px; at >=901px body has overflow:hidden and .page height is 100dvh; a prefers-reduced-motion: reduce block forces all content visible and static
- window.__DEMO__.ready becomes true within 5s
- window.__DEMO__.frames advances (>= 20 frames in 1.2s)
- window.__DEMO__.errors is empty
- zero console errors and zero page errors
- zero failed network requests
- exactly one <h1> with non-empty text
- header, main, footer landmarks present
- no horizontal overflow at 390 / 768 / 1440 widths
- no horizontal overflow after 2s of animation
- prefers-reduced-motion: reduce leaves the page usable and errors empty