# Thermal

> **Category:** Landing Page
> **Effect:** A diffusing 48x27 canvas heat map mapped through an inferno palette with a live hottest-cell readout
> **Platform:** cmd-code (`cmdcode`)
> **Model:** `xiaomi/mimo-v2.6-pro` — Xiaomi MiMo v2.6 Pro (xiaomi/mimo-v2.6-pro)
> **License:** Free / MIT — copy, paste, ship.
> **Self-contained:** yes — one `index.html`, no build step, no external media.

---

## 🎬 Live demo

Open [`public/demos/thermal/index.html`](../public/demos/thermal/index.html) — generated by the model
above from the prompt below, with no human edits to the markup.

## ✅ Verification

<!-- VERIFICATION:START -->
_Pending — this block is written by `tools/verify.mjs` after the build run._
<!-- VERIFICATION:END -->

## 📋 Prompt

```text
## 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".
```

---

*LandingPagePrompts Prompt Library — free, verified, model-attributed.*
