# Echo

> **Category:** Landing Page
> **Effect:** A rotating sonar sweep lights phosphor contacts over expanding ping rings on canvas
> **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/echo/index.html`](../public/demos/echo/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. Build one complete valid index.html for a single-viewport landing page for a marine-robotics company named Echo. Document title: `Echo — Sonar Autonomy for the Deep`. Set `<html lang="en">`. ONE file only: plain HTML, inline CSS, one small inline script at the end of body. No build step, no framework, no bundler. Google Fonts is the only permitted network request (system fallback stack required). Every visible pixel comes from CSS gradients, inline SVG, text, and one `<canvas>`. Desktop is one frame with no scroll at `>= 901px`: `html, body { height:100%; overflow:hidden }` and `.page { height:100dvh; overflow:hidden }`. Width `<= 900px` may scroll vertically but must never overflow horizontally. Accessibility is mandatory: real `header`, `nav`, `main`, `footer`; exactly one `h1`; every visible control a named button or anchor; a visible `:focus-visible` ring; readable contrast. The runtime telemetry hook must be exactly `window.__DEMO__ = { name: 'echo', ready: false, frames: 0, effects: [], errors: [] };` and the script must set `ready` true after the first painted sonar frame, increment `frames` once per animation frame inside the requestAnimationFrame loop, push each implemented effect name into `effects`, and attach a window `'error'` listener that pushes into `errors`. Zero console output, no uncaught exceptions, finished index.html under 90 KB. Do NOT build: no video, no WebGL, no Three.js, no Lottie, no GSAP, no jQuery, no Tailwind, no React, no images (remote or base64), no audio, no external scripts or third-party CDN URLs, no forms or inputs, no pricing tables, no testimonials, no world maps, no dashboards, no extra sections beyond header / hero / footer, no footer links beyond the three specified, no cookie banner, no scroll indicator, no hamburger menu above 900px. The single visual idea: a full-bleed canvas sonar scope — concentric ping rings expanding from a fixed origin, a rotating sweep line trailing a fading phosphor wedge, contact blips igniting as the sweep crosses them and decaying over seconds — behind quiet marine-instrument typography.

---

## B. THE BACKDROP — procedural sonar scope (the ONLY visual effect besides grain)

A single `<canvas id="sonar">` fills the viewport: `position:fixed; inset:0; width:100%; height:100%; z-index:0; display:block; pointer-events:none`, repainted every `requestAnimationFrame` tick and NEVER a static image.

**Geometry (canvas 2D, no libraries):**

- Origin `(cx, cy)`, `>= 901px`: `cx = w * 0.70`, `cy = h * 0.52` (scope right of the copy column). `<= 900px`: `cx = w * 0.50`, `cy = h * 0.40`.
- Scope radius `R = min(w*0.30, h*0.50)` at `>= 901px` (432px at 1440×900); `R = min(w*0.46, h*0.34)` at `<= 900px`.
- Paint order per frame, back to front: navy fill, graticule blit, 4 ping rings, 8 trail buckets, leading edge, 30 contact blips, edge-fade gradient, vignette.

**Graticule (drawn ONCE per resize into an offscreen canvas, blitted once per frame):**

- 6 concentric range rings at `R * k / 6` for `k = 1..6`, `lineWidth 1`, stroke `rgba(92,255,157,0.13)`.
- 36 bearing ticks every 10°: 1px segments `R → R + 8px` at alpha 0.16; every third tick (30°) runs `R → R + 14px` at alpha 0.26.
- Four mono bearing labels, drawn once at radius `R + 26px`, 10px, fill `rgba(92,255,157,0.30)`: `000` top, `090` right, `180` bottom, `270` left.
- The offscreen canvas matches the main canvas backing size; per-frame cost is exactly one `drawImage`.

**Ping rings — 4 expanding waves:**

- Wave count `P = 4`, ping period `5.5s`. Wave `k` has phase `pk = ((t / 5.5) + k / 4) % 1` for `k = 0..3` (a new ring every 1.375s).
- Radius `r_k = R * (0.06 + 0.94 * pk^0.82)` — the exponent front-loads expansion so rings decelerate like a real wavefront.
- Stroke: `lineWidth 1` (`2` when `pk < 0.12`, the newborn ring), colour `rgba(92,255,157, alpha)` with `alpha = 0.42 * (1 - pk)^1.5 + 0.02`; one `beginPath` per ring, 4 strokes per frame.

**Sweep and its fading trail:**

- Leading angle `a = (t / 6.4) * TAU - PI/2` (`TAU = Math.PI * 2`; bearing 000 at top, clockwise; one revolution every 6.4s).
- The trail is a wedge spanning `56°` BEHIND the leading edge, split into `8` alpha buckets of `7°` each. Bucket `j = 0..7` covers `[a - (j+1)*7°, a - j*7°]` and fills `rgba(92,255,157, alpha_j)` with `alpha_j = 0.14 * (1 - j/8)^1.7 + 0.012`. One `moveTo; arc; closePath; fill` per bucket — 8 fills per frame, no `filter`/`shadowBlur`.
- Leading edge: one 1px line origin → `R + 10px` at `rgba(92,255,157,0.55)` plus a 2.5px tip dot at alpha 0.9.

**Contact blips — 30 targets (22 at `<= 900px`, 16 at `<= 560px`):**

- Generated ONCE at startup by mulberry32 (seed `20260214`) for identical loads: `ang` in `[0, TAU)`, `rad = 0.10 + rng() * 0.84` of R, `size = 0.8 + rng() * 0.6`, stored `{ang, rad, life, size}`.
- Ignition: track the previous sweep angle; when it crosses a contact's `ang` (wrap-aware), set `life = 1`.
- Decay: `life = max(0, life - dt / 2.6)` — 2.6s fade, so one 6.4s revolution leaves ~12 lit contacts.
- Per contact, TWO primitives: glow — the 64×64 sprite (stops `rgba(92,255,157,0.85)` 0%, `rgba(92,255,157,0.28)` 35%, `rgba(92,255,157,0)` 70%) via `drawImage` in a `(14 + 26 * life) * size` square at `globalAlpha = 0.55 * life^1.8`; then core — filled circle radius `(2.0 + 1.5 * life) * size`, `fillStyle rgba(92,255,157, 0.30 + 0.65 * life^1.4)`. Reset `globalAlpha` after each glow.

**Edge fade and vignette (2 gradients per frame):** radial gradient centred `(cx, cy)`: `rgba(3,12,22,0)` at 0 and at `R * 0.86` → `rgba(3,12,22,0.55)` at `R * 1.06` → `#030c16` at `R * 1.34`; then a full-frame vignette transparent at 45% radius to `rgba(3,12,22,0.52)` at the corners.

**Grain (CSS, separate element):** `div.grain`, `position:fixed; inset:-50%; width:200%; height:200%; pointer-events:none; opacity:0.05; z-index:5`, background = inline SVG `feTurbulence` data-URI (`fractalNoise`, `baseFrequency="0.8"`, `numOctaves="2"`, `stitchTiles="stitch"`, then `feColorMatrix saturate 0`), tile 160px, `grain 0.9s steps(4) infinite` jittering `translate` among `(0,0) (-2%,1%) (1%,-2%) (-1%,2%)`.

**Per-frame cost budget:** 4 ring strokes + 8 trail fills + 2 leading-edge primitives + 30 × 2 blip primitives + 1 graticule blit + 2 gradient fills + 1 clear = **≤ 78 draw primitives per frame**, ≤ 3 gradient objects per frame, **zero `ctx.filter` / `ctx.shadowBlur` calls ever** (glow is a pre-rendered sprite). Resolution trick: cap backing-store DPR at `Math.min(devicePixelRatio, 2)` desktop, `1.5` at `<= 900px`; `canvas.width = innerWidth * dpr`, `canvas.height = innerHeight * dpr`, `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)`, graticule rebuilt at that size. Read `innerWidth/innerHeight` only on resize — NEVER `getBoundingClientRect` in the loop. Cap `dt` at 0.05s. While `document.hidden`, increment `frames` and skip the paint.

---

## C. LAYER STACK (back → front)

1. `html, body` — deep navy `#030c16`, forced twice (see Failure Guards).
2. `canvas#sonar` — z-index 0, the scope: graticule, pings, sweep, contacts, edge fade, vignette.
3. `.scrim` — z-index 1, fixed `linear-gradient(180deg, rgba(3,12,22,0.78) 0%, rgba(3,12,22,0.22) 36%, rgba(3,12,22,0.18) 62%, rgba(3,12,22,0.86) 100%)` guaranteeing 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. `.grain` — z-index 5, above all content.

---

## D. TOKEN TABLE (defaults at ~1440px)

```
--bg:            #030c16
--bg-2:          #061524
--text:          #e7f3ef
--text-soft:     #c3d7d4
--muted:         #8ba7ae
--dim:           #55727c
--accent:        #5cff9d
--accent-dim:    rgba(92,255,157,0.55)
--accent-line:   rgba(92,255,157,0.28)
--accent-fill:   rgba(92,255,157,0.06)
--border:        rgba(180,225,214,0.14)
--border-soft:   rgba(180,225,214,0.09)
--logo:          15px
--logo-mark:     20px
--nav:           13.5px
--nav-h:         38px
--btn:           13.5px
--btn-h:         42px
--chip:          11.5px
--chip-h:        28px
--h1:            60px
--lead:          16.5px
--readout:       12.5px
--foot:          12.5px
--header-y:      24px
--header-x:      40px
--hero-x:        40px
--hero-y:        40px
--footer-y:      22px
--copy-max:      560px
--lead-max:      520px
--h1-gap:        20px
--lead-gap:      20px
--action-gap:    28px
--readout-gap:   26px
--sans:          "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif
--mono:          "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
--ease-out:      cubic-bezier(0.16,1,0.3,1)
--enter-dur:     0.95s
```

Fonts (Google Fonts ONLY, one `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>` plus one stylesheet link with `display=swap`): `Space+Grotesk:wght@400;500;700` and `IBM+Plex+Mono:wght@400;500`.

---

## E. THE COMPOSITION — exact build order

Body: `margin:0; min-width:320px; font-family:var(--sans); -webkit-font-smoothing:antialiased` (navy-first rule per I.1).

**HEADER** — `padding: var(--header-y) var(--header-x); display:flex; align-items:center; justify-content:space-between; gap:24px`.

- Left: `a.brand` (href `#`, accessible name "Echo home") — inline SVG mark 20×20: `<circle cx="10" cy="10" r="1.9" fill="#5cff9d"/>` plus two concentric arcs `<path d="M14.4 5.6a6.2 6.2 0 0 1 0 8.8" fill="none" stroke="#5cff9d" stroke-width="1.8" stroke-linecap="round"/>` and `<path d="M17.2 2.8a10 10 0 0 1 0 14.4" fill="none" stroke="#5cff9d" stroke-width="1.4" stroke-linecap="round" opacity="0.55"/>`. Wordmark `echo` in `var(--mono)`, `font-size: var(--logo); font-weight:500; color:var(--text); letter-spacing:0.14em`; trailing `_` cursor in `var(--accent)`, `blink 1.06s steps(1) infinite` (`0%,49% {opacity:1} 50%,100% {opacity:0}`).
- Centre: `nav` with exactly 3 links: `platform`, `fleet`, `science` — all href `#`, mono, `font-size: var(--nav); color: var(--muted)`, `display:flex; align-items:center; gap:30px; height:var(--nav-h)`. Hover: `color: var(--accent)` over 0.18s ease. `platform` (current bearing) is preceded by a 5×5px square `background: var(--accent); box-shadow: 0 0 8px var(--accent-dim); display:inline-block; margin-right:8px`.
- Right: `a.cta` (href `#`, class `btn btn--ghost btn--sm`) text `Request a sea trial`, height `var(--nav-h)`, padding 0 16px.

**MAIN (hero)** — `display:flex; flex-direction:column; justify-content:center; align-items:flex-start; padding: var(--hero-y) var(--hero-x); width:100%; max-width: var(--copy-max); margin-right:auto; min-width:0`. Left-anchored, vertically centred in the 1fr row; the disc owns the right 55%.

1. `.chip` — inline-flex, `height: var(--chip-h); padding: 0 12px; border:1px solid var(--accent-line); border-radius:999px; background: var(--accent-fill)`, mono `font-size: var(--chip); letter-spacing:0.18em; color: var(--accent)`. Exact copy: `DEEP-CLASS SONAR AUTONOMY`. Preceded by a 6px dot (`border-radius:50%; background:var(--accent)`) pulsing per Motion.
2. `h1` — EXACTLY ONE `<h1>`, `var(--sans); font-weight:700; font-size: var(--h1); letter-spacing:-0.045em; line-height:1.02; color:var(--text); margin: var(--h1-gap) 0 0`. Two masked lines (see Motion):
   Line 1: `Sound sees`
   Line 2: `what light cannot.`
   Line 1 wraps `Sound` in `<em>` styled `font-family: var(--mono); font-style:normal; font-weight:400; font-size:0.88em; color: var(--accent); letter-spacing:-0.02em`.
3. `p.lead` — `margin: var(--lead-gap) 0 0; max-width: var(--lead-max); font-size: var(--lead); line-height:1.6; letter-spacing:-0.015em; color: var(--text-soft)`. Exact copy: `Echo builds autonomous sonar vehicles that map, classify and track everything that lives and moves beneath 200 metres — where cameras go blind and light gives up.`
4. `.actions` — `display:flex; gap:12px; flex-wrap:wrap; margin-top: var(--action-gap)`. `a.btn.btn--primary` text `Request a sea trial` (href `#`); `a.btn.btn--ghost` text `Read the tech brief` (href `#`).
5. `.readout` — mono `font-size: var(--readout); color: var(--dim); margin-top: var(--readout-gap); white-space:nowrap`. Exact copy: `CONTACTS 07 · SWEEP 042° · PING 5.5 s · DEPTH 6 000 m`. The four values `07`, `042°`, `5.5`, `6 000` are `color: var(--accent)`. The trailing `· DEPTH 6 000 m` sits in `<span class="opt">` (hidden `<=560px`).

**FOOTER** — `padding: var(--footer-y) var(--header-x); display:flex; align-items:center; justify-content:space-between; gap:20px; border-top:1px solid var(--border-soft)`.

- Left: mono `var(--foot)`, `color: var(--dim)`: `© 2026 Echo Marine Systems`.
- Centre: mono `var(--foot)`, `color: var(--dim)`: `beacon: transmitting` with the same 7px status square (`margin-right:8px`) as the nav marker.
- Right: exactly 3 links `research`, `voyages`, `contact` — mono `var(--foot)`, `color: var(--muted)`, `gap:22px`, href `#`, hover `color: var(--text)`.

---

## F. THE UI LANGUAGE

One button family, two weights. `.btn` is `inline-flex; align-items:center; justify-content:center; height:var(--btn-h); padding:0 20px; border-radius:6px; font-family:var(--mono); font-size:var(--btn); letter-spacing:0.02em; text-decoration:none; transition: transform, background, border-color, box-shadow, color .2s var(--ease-out)`. `.btn--sm` is `height:var(--nav-h); padding:0 16px`. `.btn--primary`: `color:#04140c; font-weight:500; border:1px solid rgba(92,255,157,0.6); background:linear-gradient(180deg, #7dffb4 0%, #5cff9d 52%, #33d97f 100%); box-shadow: 0 14px 40px rgba(92,255,157,0.18), inset 0 1px 0 rgba(255,255,255,0.5)`. Hover: `transform: translateY(-1px); background:linear-gradient(180deg, #a4ffcb 0%, #7dffb4 48%, #46e88d 100%); box-shadow: 0 18px 48px rgba(92,255,157,0.28), inset 0 1px 0 rgba(255,255,255,0.5)`. `.btn--ghost`: `color:var(--text); border:1px solid var(--border); background:rgba(180,225,214,0.04)`. Hover: `transform: translateY(-1px); border-color: var(--accent-line); color: var(--accent); background: rgba(92,255,157,0.07)`. Both `:active` → `transform: translateY(0)`. Nav and footer links are plain mono text, `text-decoration:none`, colour-only hover — no pills, backgrounds, or underlines. Every `a` and `button` gets `:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px }`. Selection: `::selection { background: rgba(92,255,157,0.26); color: #fff }`. No default blue focus ring and 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` plus a modifier and an inline `--d`.

Stagger table (element → modifier → --d):
```
brand            appear--fade   0.08s
nav platform     appear--fade   0.16s
nav fleet        appear--fade   0.24s
nav science      appear--fade   0.32s
header cta       appear--chip   0.40s
chip             appear--pop    0.52s
h1 line 1        appear--mask   0.66s
h1 line 2        appear--mask   0.80s
lead             appear--soft   0.98s
primary button   appear--chip   1.14s
ghost button     appear--chip   1.22s
readout line     appear--read   1.34s  (duration 1.1s)
footer left      appear--fade   1.46s
footer centre    appear--fade   1.54s
footer right     appear--fade   1.62s
```

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(104%) }             to { opacity: 1; transform: none }
in-chip   from { opacity: 0; transform: translateY(10px) scale(0.96) } to { opacity: 1; transform: none }
in-pop    0% { opacity:0; transform: scale(0.9) } 70% { opacity:1; transform: scale(1.03) } 100% { opacity:1; transform:none }
in-read   from { opacity: 0; clip-path: inset(0 100% 0 0) }            to { opacity: 1; clip-path: inset(0 -4% 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.1em 0.10em }` and `.line-inner { display:inline-block }` — type surfaces out of a hard edge like a seabed return. `in-read` wipes the readout left-to-right, echoing the sweep.

Continuous keyframes: `blink` (brand cursor, 1.06s steps(1) infinite); `pulse` on the chip dot — `0%,100% { opacity:.35; transform:scale(.8) } 50% { opacity:1; transform:scale(1) }`, `2.4s ease-in-out infinite`; `grain` (0.9s steps(4)). Canvas sweep, pings, contact decay and grain run continuously, independent of the stagger.

---

## H. RESPONSIVE TABLE — token overrides per breakpoint (exact)

```
>= 1600px:
  --h1: 72px  --logo: 16px  --nav: 14.5px  --nav-h: 42px  --btn: 14.5px
  --btn-h: 46px  --lead: 17.5px  --readout: 13px  --foot: 13px  --chip: 12px
  --header-y: 28px  --header-x: 56px  --hero-x: 56px  --copy-max: 620px  --lead-max: 560px

>= 1920px:
  --h1: 84px  --header-x: 72px  --hero-x: 72px  --copy-max: 680px
  --lead: 18.5px  --readout: 13.5px  --lead-max: 600px

1280px – 1599px:
  --h1: 64px  --header-x: 48px  --hero-x: 48px  --copy-max: 540px

901px – 1279px:
  --h1: 48px  --nav-h: 34px  --header-x: 32px  --hero-x: 32px
  --copy-max: 460px  --lead: 15.5px  --readout: 12px

>= 901px AND max-height 850px:
  --h1: 44px  --hero-y: 24px  --header-y: 18px  --chip-h: 26px  --lead: 15px
  --h1-gap: 14px  --lead-gap: 16px  --action-gap: 18px  --readout-gap: 16px

>= 901px AND max-height 720px:
  --h1: 38px  --hero-y: 14px  --header-y: 12px  --lead: 14px  --readout: 11.5px
  --btn-h: 38px  --chip-h: 24px  --footer-y: 14px  --h1-gap: 12px  --lead-gap: 12px
  --action-gap: 14px  --readout-gap: 12px

>= 901px (desktop lock, all heights):
  html, body { height:100%; overflow:hidden }  .page { height:100dvh; overflow:hidden }

<= 900px:
  no vh lock; page may scroll; nav becomes a full-viewport overlay menu:
  - header keeps brand + burger button (<button class="burger" aria-label="Menu" aria-expanded="false">, three 18×1.5px bars in var(--text), gap 5px)
  - .menu { position:fixed; inset:0; z-index:10; background: rgba(3,12,22,0.92); backdrop-filter: blur(14px); display:flex; flex-direction:column; align-items:flex-start; justify-content:center; padding: 0 32px; gap: 26px; transform: translateY(-100%); transition: transform 0.45s var(--ease-out) }
  - .menu.open { transform: none }
  - menu shows the 3 nav links stacked at --nav: 22px plus the header CTA chip; Escape key, link click, and resize >900px all close it
  --h1: 40px  --header-x: 24px  --hero-x: 24px  --copy-max: 100%  --lead-max: 520px
  --hero-y: 28px  --readout: 11.5px
  .scrim mid stops rise to rgba(3,12,22,0.42) at 36% and rgba(3,12,22,0.38) at 62% (copy sits over the centred scope)

<= 560px:
  --h1: 32px  --lead: 14.5px  --readout: 11px  --header-x: 20px  --hero-x: 20px
  footer becomes flex-direction:column; align-items:flex-start; gap:10px
  header CTA is hidden (it lives inside the menu on mobile)
  the readout drops the DEPTH segment via span.opt { display:none }
```

Canvas constants scale too: at `<= 900px` origin `(w*0.50, h*0.40)`, `R = min(w*0.46, h*0.34)`, contact count 22, DPR cap 1.5; at `<= 560px` contact count 16 and graticule labels drop to `000`/`180` only. Sweep 6.4s and ping 5.5s stay constant at every size.

---

## I. FAILURE GUARDS — the page is never blank, never broken

1. **Navy-first:** the FIRST CSS rule is `html, body { background:#030c16 !important; color:#e7f3ef }`; the body tag carries `style="background:#030c16;color:#e7f3ef"`; a later rule restates `html, body { background: var(--bg, #030c16) }`.
2. **Resting opacity is 1:** every `.appear` element's un-animated computed style is its final state (opacity 1, no transform/blur/clip); `fill-mode: both` holds the FROM state only during the delay. If CSS animation or JS fails, the page renders complete and readable.
3. **Two-rAF entrance fallback:** after two nested `requestAnimationFrame` callbacks, inspect every `.appear` with `el.getAnimations({ subtree:false })` (or unconditionally if missing); if none reports `playState` "running"/"finished", add `.is-in` to ALL `.appear` — `.is-in { animation:none !important; opacity:1 !important; transform:none !important; filter:none !important; clip-path:none !important }`.
4. **Canvas fallback:** if the canvas or its 2D context is missing, add `.no-canvas` to `<html>`, push a short string into `window.__DEMO__.errors`, and paint a static CSS scope: `.no-canvas body { background-image: repeating-radial-gradient(circle at 70% 52%, rgba(0,0,0,0) 0 58px, rgba(92,255,157,0.13) 58px 59px) }` so the page still reads sonar.
5. **Reduced motion:** inside `@media (prefers-reduced-motion: reduce)`: `*, *::before, *::after { transition:none !important; animation:none !important }`; force `.appear, .line-inner, .readout` to `opacity:1 !important; transform:none !important; filter:none !important; clip-path:none !important`; brand cursor and chip dot freeze at `opacity:1`; `.grain { display:none }`. The canvas paints exactly ONE static frame — sweep parked at bearing `042°` (drawn with its full trail), ping phases frozen at `pk = 0.18, 0.42, 0.66, 0.90`, every contact at `life = 0.55` — sets `ready = true`, sets `frames = 1`, and schedules no further frames.
6. **No 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: 'echo', ready: false, frames: 0, effects: [], errors: [] };`
   Attach `window.addEventListener('error', e => window.__DEMO__.errors.push(String(e.message || e)))` immediately, then push into `effects` once, in order: `'sonar-sweep'`, `'ping-rings'`, `'contact-blips'`, `'range-graticule'`, `'grain-texture'`.
2. **Setup:** cache `document.documentElement`, `canvas#sonar`, its 2D context (guard null → `.no-canvas` path from I.4 and stop), burger, menu panel, menu links, and `matchMedia('(prefers-reduced-motion: reduce)')`. Constants: `TAU`, sweep 6.4s, ping 5.5s, trail 56°, 8 buckets, decay 2.6s, seed 20260214. Implement `mulberry32(seed)` (3 lines, `Math.imul`); build the contacts ONCE with the breakpoint count (30 / 22 / 16) and the 64×64 glow sprite ONCE, both per section B.
3. **resize()** (on load + resize, via boolean `resizeQueued` + `requestAnimationFrame`, never a timer): read `innerWidth`/`innerHeight` (NOT `getBoundingClientRect`), apply the DPR caps and `setTransform` of section B, recompute `cx, cy, R`, rebuild the offscreen graticule (6 rings, 36 ticks, 4 labels).
4. **drawFrame(t, dt):** execute section B in order — navy clear, one graticule `drawImage`, the 4 ping rings (`pk = ((t/5.5) + k/4) % 1`), the 8 trail buckets and leading edge, contact updates (wrap-aware crossing detection vs the previous sweep angle, `life = Math.max(0, life - dt/2.6)`) with glow + core, edge fade, vignette. Set `window.__DEMO__.ready = true` after the first completed paint; increment `window.__DEMO__.frames` by exactly 1 per rAF tick — even while `document.hidden` — skipping only the paint while hidden.
5. **Loop:** one `requestAnimationFrame` loop, `dt = Math.min(0.05, (now - last) / 1000)`, `last = now`. Reduced motion: run `drawFrame(2.4, 0)` once (frozen per I.5), set `frames = 1`, `ready = true`, schedule nothing more. In the loop: no `getBoundingClientRect`, DOM animation via `transform`/`opacity` only, no `ctx.filter`/`ctx.shadowBlur`.
6. **Entrance fallback:** two nested rAFs after DOMContentLoaded, then the `.is-in` sweep of section I.3.
7. **Menu (mobile only):** burger click toggles `.menu.open` and `aria-expanded`; Escape, any menu link click, or resize to `>900px` closes it.

No other JS. No libraries. No timers. 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.*
