Skip to content
LandingPagePrompts
Landing Pagestealth/space-bunny-alphaverified 20/20confirmed cross-check

Cascade

A monospace telemetry waterfall sends fourteen streams of compact log tokens through a cool-grey field, with isolated green hot rows and a five-second vertical scan.

The exact prompt

<THE FULL BUILD ORDER, 2200-3200 words>

A. WHAT IS BEING BUILT

Build one self-contained index.html for a fictional log-analytics company named CASCADE. The page is a single-screen cinematic product landing page whose central visual is an instrumented data waterfall: fourteen vertical streams of short operational tokens descend behind a precise, restrained interface. It must feel like observability software under a calibrated microscope, not a science-fiction terminal.

Use document language lang="en". Set the document title to "CASCADE — Log intelligence in motion". Set the viewport meta to width=device-width, initial-scale=1. The application is one semantic document with one real header, one main, one real footer, and one real nav. There must be exactly one h1: “CASCADE”. The h1 text may be visually split with spans, but the accessible name must remain CASCADE.

At viewport widths of 901px and greater, set html, body, and the page shell to height: 100dvh and overflow: hidden. At widths of 900px and below, permit normal vertical document scrolling, keep the header non-sticky, and size the canvas composition to 390x844, 768x1024, 1440x900, and 1920x1080 without horizontal overflow. Never scale the whole page with transform.

Do NOT build a hero carousel, terminal emulator, Matrix rain, animated typing demo, product dashboard mockup, pricing table, blog feed, login form, cookie panel, modal, video, image gallery, 3D scene, particle nebula, katakana, binary glyph field, chart library, or multi-page navigation. Do not use React, Tailwind, GSAP, Three.js, Lottie, a framework, a bundler, a build step, external images, remote fonts other than Google Fonts, mp4, webm, gif, SVG icon assets from a CDN, base64 media, or third-party APIs. Do not put content behind an interaction gate. Do not add more than one h1. Do not use a neon green field, pure black background, glow overload, or persistent scanning overlay that harms legibility.

B. THE BACKDROP

Create one full-bleed canvas element with id="cascade-canvas", CSS width:100% and height:100%, positioned absolute at inset:0. It sits behind the header, hero, and footer. The canvas is decorative with aria-hidden="true". The page background beneath it must be #0b0e12.

Use the canvas resolution trick: set its internal drawing buffer to round(cssWidth * min(devicePixelRatio, 1.5)) by round(cssHeight * min(devicePixelRatio, 1.5)), capped at a maximum width of 2880 and maximum height of 1620. Apply the same cap to the offscreen trail buffer. On resize, preserve normalized column and row state, create new offscreen canvases, clear both buffers to transparent, and restart animation only if motion is permitted. Do not recreate geometry or gradients per frame.

Render fourteen fixed logical columns across the field. Give each column a normalized x center, speed, phase, token interval, width, and opacity. Logical column width is 8.25% of the canvas width and gutters consume the remaining 15.5%. At 1440px, the approximate center coordinates are 67, 186, 305, 424, 543, 662, 781, 900, 1019, 1138, 1257, 1376, 1495, and 1614px after proportional width calculation; the actual coordinates are always derived from column index and canvas width, never hard-coded for one viewport. Map canvas x to the right side of the composition so the 68%-wide hero copy remains on a controlled dark veil.

Each column contains a stack of 9 logical rows. Every row is a short token, not a character field. Alternate among “trace.7f3”, “ingest 142ms”, “GET /v1/logs”, “idx 84.2ms”, “P95 18.7ms”, “batch 064”, “status 204”, “queue 03”, “shard eu-2”, “merge 4.1ms”, “replay 0007”, “evt 91a4”, “edge 11ms”, and “checksum ok”. Use deterministic hash functions for token selection, phase, and hot-row designation; do not call Math.random inside the animation loop.

Each row travels downward at a per-column speed from 0.54 to 0.94 canvas heights per second. Rows begin with a phase offset from 0.00 to 0.96 of the cycle. New tokens are placed above the top edge at cycle intervals from 760ms to 1320ms. Each token has a 3px-radius line cap and a tail of 42px. A token is not a full-width line: its width is 56% to 86% of its column width. Draw the tail as a vertical linear gradient from transparent at its top to rgba(148,163,184,0.28) at its lower end, then draw the 2px token head in rgba(203,213,225,0.72). Every 29th token is hot. Hot tokens are only 7px high, width 62% of their column, and use #4ade80 at opacity 0.92. A hot row has a 76x14px background wash using rgba(74,222,128,0.08), with no blur and no shadow. Exactly one hot row may be visible in each column at a time.

Behind the tokens, fill the canvas with a vertical linear gradient: stop 0 is #0b0e12, stop 0.48 is #11161d, stop 1 is #090b0f. Add one static horizontal rule at y=52% using rgba(148,163,184,0.055) and one at y=78% using rgba(148,163,184,0.035). Do not add star points, rain lines, glyph noise, or glowing edges.

The fading trail is produced on an offscreen canvas, not by clearing the visible canvas completely each frame. Every frame, fill the trail buffer with rgba(11,14,18,0.085) using source-over. Draw the newly active token segments onto that buffer, then draw the trail buffer onto the visible canvas with globalAlpha 0.92. This is a fixed-cost two-canvas composite. Avoid ctx.filter and ctx.shadowBlur entirely. Avoid large path counts: use one path per gradient tail and one short line or rounded rectangle per visible token. The per-frame budget is 14 column updates, 126 row visibility checks, 126 maximum token draws, 2 buffer composites, and 1 scan-line draw. It must sustain at least 20 frames per 1.2 seconds on a 1440x900 headless viewport.

The scan line is a 1px vertical line with a 1px adjacent translucent line. Its gradient runs from transparent at y=0, through rgba(203,213,225,0.10) at y=46%, to rgba(74,222,128,0.34) at y=54%, then transparent at y=100%. Sweep from x=0 to canvas width over 5000ms with a 900ms pause after each pass. It is positioned behind all foreground UI and must not cross text at opacity above 0.10 over the hero copy. Cache all canvas dimensions in the loop; do not call getBoundingClientRect, clientWidth, clientHeight, getComputedStyle, or window.innerWidth inside the rAF loop.

C. THE LAYER STACK

Use these layers from back to front. The body background is z-index:0. The full-bleed canvas is z-index:1. A static vignette pseudo-element on the hero is z-index:2, with pointer-events:none. The primary left-side content veil is z-index:3, using a horizontal gradient from rgba(11,14,18,0.96) to rgba(11,14,18,0.60) and width:70%. The header is z-index:10. The main hero content is z-index:11. The footer is z-index:10. The accessibility skip link is z-index:100 only when focused. No layer may create horizontal overflow.

The header has a 1px bottom border in rgba(148,163,184,0.12), height:76px, and padding:0 48px. The logo lockup is on the left. The nav is on the right. The hero uses a max-width:1440px shell centered with 48px side padding and 64px top and bottom space. The footer sits at the bottom edge in the same shell with 24px top padding and 28px bottom padding.

D. TOKEN TABLE

Define every visual token in :root and use only these variables for sizes and colors.

--page-bg: #0b0e12
--panel-ink: #f1f5f9
--body-ink: #94a3b8
--muted-ink: #64748b
--line: rgba(148,163,184,0.12)
--line-soft: rgba(148,163,184,0.07)
--accent: #4ade80
--accent-soft: rgba(74,222,128,0.12)
--accent-border: rgba(74,222,128,0.42)
--canvas-cap: 2880px
--canvas-height-cap: 1620px
--shell-max: 1440px
--header-height: 76px
--header-pad-x: 48px
--hero-pad-x: 48px
--hero-pad-y: 64px
--footer-pad-x: 48px
--footer-pad-y: 28px
--footer-top: 24px
--nav-gap: 32px
--eyebrow-size: 11px
--eyebrow-track: 0.16em
--h1-size: clamp(48px, 6.5vw, 104px)
--h1-line: 0.92
--h1-track: -0.07em
--lede-size: 17px
--lede-line: 1.55
--lede-measure: 470px
--action-gap: 12px
--button-height: 46px
--button-pad-x: 20px
--button-radius: 999px
--pill-height: 34px
--pill-pad-x: 12px
--pill-gap: 8px
--footer-text-size: 12px
--footer-gap: 18px
--focus-ring: 2px
--focus-offset: 4px
--scan-duration: 5000ms
--scan-pause: 900ms
--trail-opacity: 0.085
--token-font: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace
--display-font: "Space Grotesk", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif

Load Google Fonts only for Space Grotesk weights 400, 500, 600 and IBM Plex Mono weights 400, 500, 600. Include preconnect links to fonts.googleapis.com and fonts.gstatic.com, but all fallback stacks must render correctly if the request is blocked.

E. THE COMPOSITION

Header markup is real header.header > a.brand[href="#top"] and nav aria-label="Primary navigation". The brand has a 10px 10px 10px 4px accent square and the text “CASCADE”. Its accessible name must be “CASCADE home”. Nav contains three links: “Product”, “Signals”, and “Company”, each linking to an in-page section or top target that exists. Add a header action anchor with exact text “Request access”; it links to #request-access and is focusable.

Main id="top" has section.hero aria-labelledby="hero-title". Place a left content column with width:min(100%, 620px) and position relative. The eyebrow exact copy is “LOG INTELLIGENCE / 01”. It uses an 8px green status dot followed by that text. The h1 exact text is “CASCADE”. Under it, a two-line statement uses exact copy: “See the signal.” on the first line and “Keep the context.” on the second. The supporting paragraph exact copy is “CASCADE turns fragmented events into one calm operational surface. Search, compare, and act on the trail before it disappears.” Set its width to 470px and max-width:100%.

Place two controls below the paragraph. The primary anchor exact text is “Start a quiet shift”; secondary anchor exact text is “Read the field notes”. The first links to #request-access, the second to #signals. Add a small proof row below them containing “14 streams” and “99.98% ingest uptime” separated by a 1px vertical rule. The footer exact left copy is “Operational clarity, without the noise.” The footer right copy is “© 2025 CASCADE SYSTEMS”. A tiny footer status group displays a 6px green dot and “ALL SYSTEMS OBSERVABLE”.

Create one non-interactive signal legend inside the hero at right:100px; bottom:24px; with three items: “TRACE”, “BATCH”, and “HOT ROW”. The first two are 5px grey dots, the last is a 5px green dot. The legend must not intercept pointer events.

The composition is a 12-column conceptual grid: left content occupies columns 1 through 6; empty breathing space occupies columns 7 through 8; the canvas is visible through columns 5 through 12. At desktop, set hero grid-template-columns: repeat(12, minmax(0, 1fr)); gap:24px. The left content starts at column 1 and spans 6 columns. The right visual is a transparent overlay with no border, no card, and no label, allowing the canvas to be the product image. Do not place dashboard rectangles, charts, or fake controls over the waterfall.

F. THE UI LANGUAGE

Primary button: display:inline-flex; align-items:center; justify-content:center; min-height:46px; padding:0 20px; border:1px solid var(--accent-border); border-radius:999px; background:linear-gradient(135deg, #4ade80 0%, #22c55e 100%); color:#07110b; font:600 13px/1 var(--display-font); letter-spacing:-0.01em; text-decoration:none. Hover changes transform:translateY(-2px), background:linear-gradient(135deg, #6ee7a0 0%, #4ade80 100%), and border-color:#86efac. Active changes transform:translateY(0). Do not add a glow shadow.

Secondary button: min-height:46px; padding:0 20px; border:1px solid rgba(203,213,225,0.24); border-radius:999px; background:rgba(15,23,42,0.36); color:#e2e8f0; font:500 13px/1 var(--display-font). Hover border-color:rgba(74,222,128,0.55), background:rgba(30,41,59,0.64), and transform:translateY(-2px). Both buttons have a visible 2px accent focus ring with 4px offset. Do not use outline:none without a replacement.

Pills are 34px high with 12px horizontal padding, 1px solid var(--line), background:rgba(15,23,42,0.54), color:#cbd5e1, font:500 10px/1 var(--token-font), letter-spacing:0.08em, and border-radius:999px. Hover border-color:var(--accent-border), color:var(--accent), and background:var(--accent-soft). Every visible link and control receives an accessible name, including the status and brand links.

G. MOTION

Initial page elements enter once. The animation is staggered from the eyebrow to the proof row. Use only opacity and transform.

Stagger table:
- header: fade-down 480ms cubic-bezier(0.22,1,0.36,1), delay 0ms.
- eyebrow: fade-up 500ms cubic-bezier(0.22,1,0.36,1), delay 100ms.
- h1: fade-up 700ms cubic-bezier(0.22,1,0.36,1), delay 170ms.
- statement: fade-up 600ms cubic-bezier(0.22,1,0.36,1), delay 280ms.
- lede: fade-up 600ms cubic-bezier(0.22,1,0.36,1), delay 360ms.
- actions: fade-up 500ms cubic-bezier(0.22,1,0.36,1), delay 440ms.
- proof row: fade-up 500ms cubic-bezier(0.22,1,0.36,1), delay 520ms.
- signal legend: fade-in 700ms ease-out, delay 800ms.
- footer: fade-up 500ms cubic-bezier(0.22,1,0.36,1), delay 600ms.

Define @keyframes fade-up as from opacity:0; transform:translateY(18px); to opacity:1; transform:translateY(0). Define @keyframes fade-down as from opacity:0; transform:translateY(-10px); to opacity:1; transform:translateY(0). Define @keyframes fade-in as from opacity:0 to opacity:1. Set the canvas to opacity:0 and animate it to opacity:1 over 900ms with delay 0ms. Do not animate width, height, top, left, box-shadow, or color.

The canvas loop has effect names “cascade.tokens”, “cascade.trails”, “cascade.scan”. Push each name exactly once into window.__DEMO__.effects. The scan CSS animation is scan-sweep 5900ms linear infinite, with 0% transform:translateX(-100%), 15% transform:translateX(0%), 84% transform:translateX(100%), 100% transform:translateX(100%). The timing includes the 5000ms sweep and 900ms pause. The line is a child pseudo-element or an absolutely positioned div with transform only.

H. RESPONSIVE TABLE

At 1920x1080: --shell-max:1680px; --header-pad-x:72px; --hero-pad-x:72px; --footer-pad-x:72px; --hero-pad-y:92px; --h1-size:clamp(72px, 7.2vw, 112px). Keep the left veil width:68%.

At 1440x900: --shell-max:1440px; --header-pad-x:48px; --hero-pad-x:48px; --footer-pad-x:48px; --hero-pad-y:64px; --h1-size:clamp(58px, 7vw, 96px). Keep one line for each h1 text span and the lede width at 470px.

At 901px and below: hide the primary action label only if necessary by reducing --hero-pad-x to:28px; set --header-pad-x:28px; --footer-pad-x:28px; --nav-gap:18px; --lede-measure:430px. Keep desktop height:100dvh only at 901px and above.

At max-width:900px: body overflow-y:auto; html,body overflow-x:hidden; .page-shell min-height:100dvh; header height:68px; hero min-height:760px; height:auto; padding-top:56px; padding-bottom:44px; grid-template-columns:1fr; gap:40px. Position the canvas across the full hero at top:0; height:100%; opacity:0.88. The content veil becomes vertical:linear-gradient(180deg,rgba(11,14,18,0.98) 0%,rgba(11,14,18,0.90) 48%,rgba(11,14,18,0.62) 100%). Keep the signal legend at right:24px; bottom:20px. At 390x844, set --header-pad-x:20px; --hero-pad-x:20px; --footer-pad-x:20px; --h1-size:52px; --lede-size:15px; --lede-measure:100%; --action-gap:10px; --button-height:44px; --button-pad-x:16px; --nav-gap:12px; hide the word “Company” from the primary nav but retain a visibly available 44px link named “More” only if the header still needs three destinations. Prefer a two-link nav on mobile: “Product” and “Signals”; no hidden focusable links.

At max-height:820px and min-width:901px: --header-height:68px; --hero-pad-y:34px; --footer-pad-y:18px; --footer-top:14px; --h1-size:clamp(48px, 6.2vw, 82px); --lede-size:15px; --lede-line:1.4. At max-height:680px and min-width:901px: --header-height:60px; --hero-pad-y:22px; --footer-pad-y:12px; --footer-top:10px; --h1-size:48px; --lede-size:14px; --statement-size:20px. Never reduce text below 12px. No horizontal overflow at 390, 768, 1440, or 1920 widths.

I. FAILURE GUARDS

Set every animated foreground element to opacity:1 at rest and use animation-fill-mode:both only for the entrance animation. If JavaScript fails, the page must remain readable, the canvas may show its static CSS background, and controls must still work as anchors. Add a two-rAF fallback: after the first rAF request, request a second rAF; if the first frame callback has not updated window.__DEMO__.frames, set canvas opacity:1, remove all entrance classes, set all animated foreground opacity:1, and cancel the animation class rather than leaving hidden content. Set window.__DEMO__.ready=true only after the first paint using requestAnimationFrame(() => requestAnimationFrame(() => { window.__DEMO__.ready = true; })). The exact telemetry object must be initialized before any resize or animation work.

The error listener is window.addEventListener("error", function(event) { window.__DEMO__.errors.push(String(event.message || "window error")); });. Do not call console.log, console.warn, console.error, or debug. Avoid uncaught exceptions by wrapping setup and resize in try/catch and pushing a concise error string if initialization fails.

In @media (prefers-reduced-motion: reduce), remove all CSS animation and transition declarations with animation:none; transition:none; scroll-behavior:auto; set canvas opacity:1; set every animated foreground opacity:1; preserve the static gradient and token positions. The JavaScript must check matchMedia("(prefers-reduced-motion: reduce)").matches. If true, draw one deterministic frame, set ready after two rAFs, and do not start the continuous loop. If the preference changes, cancel any loop and either start it or stop it accordingly. Canvas movement is decorative, so this static frame must still show fourteen columns and at least four visible token heads.

J. THE EXACT JS

Use this exact initialization and animation structure, with the HTML canvas referenced by id "cascade-canvas". The DOM reads happen only in setup or resize, never in frame().

const canvas = document.getElementById("cascade-canvas");
const ctx = canvas.getContext("2d", { alpha: false });
const trail = document.createElement("canvas");
const trailCtx = trail.getContext("2d");
const shell = document.querySelector(".page-shell");
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");
window.__DEMO__ = { name: "cascade", ready: false, frames: 0, effects: [], errors: [] };
window.addEventListener("error", function(event) {
  window.__DEMO__.errors.push(String(event.message || "window error"));
});

const words = [
  "trace.7f3", "ingest 142ms", "GET /v1/logs", "idx 84.2ms",
  "P95 18.7ms", "batch 064", "status 204", "queue 03",
  "shard eu-2", "merge 4.1ms", "replay 0007", "evt 91a4",
  "edge 11ms", "checksum ok"
];
const columns = Array.from({ length: 14 }, (_, index) => ({
  index,
  phase: (index * 0.071 + 0.13) % 1,
  speed: 0.54 + ((index * 0.037) % 0.40),
  interval: 760 + ((index * 83) % 560),
  width: 0.56 + ((index * 0.013) % 0.30)
}));
let width = 0;
let height = 0;
let dpr = 1;
let last = 0;
let raf = 0;
let running = false;
let fallbackPending = true;
let scanStart = 0;

function resizeCanvas() {
  const rect = shell.getBoundingClientRect();
  width = Math.max(1, Math.round(rect.width));
  height = Math.max(1, Math.round(rect.height));
  dpr = Math.min(window.devicePixelRatio || 1, 1.5);
  const targetWidth = Math.min(2880, Math.round(width * dpr));
  const targetHeight = Math.min(1620, Math.round(height * dpr));
  canvas.width = targetWidth;
  canvas.height = targetHeight;
  trail.width = targetWidth;
  trail.height = targetHeight;
  canvas.style.width = "100%";
  canvas.style.height = "100%";
  trailCtx.clearRect(0, 0, targetWidth, targetHeight);
}

function hash(value) {
  const n = Math.sin(value * 12.9898) * 43758.5453;
  return n - Math.floor(n);
}

function drawStatic() {
  if (!ctx) return;
  ctx.clearRect(0, 0, width, height);
  const base = ctx.createLinearGradient(0, 0, 0, height);
  base.addColorStop(0, "#0b0e12");
  base.addColorStop(0.48, "#11161d");
  base.addColorStop(1, "#090b0f");
  ctx.fillStyle = base;
  ctx.fillRect(0, 0, width, height);
  ctx.strokeStyle = "rgba(148,163,184,0.055)";
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(0, height * 0.52);
  ctx.lineTo(width, height * 0.52);
  ctx.moveTo(0, height * 0.78);
  ctx.lineTo(width, height * 0.78);
  ctx.stroke();
  columns.forEach((column, columnIndex) => {
    const x = width * (0.045 + columnIndex * 0.0825);
    for (let row = 0; row < 9; row += 1) {
      const y = ((row / 9 + column.phase) % 1) * height;
      const hot = (row + columnIndex * 5) % 29 === 0;
      ctx.fillStyle = hot ? "rgba(74,222,128,0.92)" : "rgba(203,213,225,0.66)";
      ctx.fillRect(x, y, Math.max(5, width * 0.82 * column.width * 0.055), hot ? 7 : 3);
    }
  });
}

function frame(time) {
  if (!running || !ctx) return;
  if (!last) last = time;
  const delta = Math.min(40, time - last);
  last = time;
  const scaleX = width;
  const scaleY = height;
  trailCtx.globalCompositeOperation = "source-over";
  trailCtx.fillStyle = "rgba(11,14,18,0.085)";
  trailCtx.fillRect(0, 0, scaleX, scaleY);
  trailCtx.globalCompositeOperation = "source-over";
  columns.forEach((column, columnIndex) => {
    const x = scaleX * (0.045 + columnIndex * 0.0825);
    const cycle = (column.interval / 1000) * column.speed;
    const offset = ((time / 1000 + column.phase) % cycle) / cycle;
    for (let row = 0; row < 9; row += 1) {
      const y = (offset + row / 9) % 1 * scaleY;
      if (y < -48 || y > scaleY + 48) continue;
      const hot = (row + columnIndex * 5) % 29 === 0;
      const tokenWidth = Math.max(5, scaleX * 0.82 * column.width * 0.055);
      const tail = trailCtx.createLinearGradient(x, y - 42, x, y);
      tail.addColorStop(0, "rgba(148,163,184,0)");
      tail.addColorStop(1, hot ? "rgba(74,222,128,0.38)" : "rgba(148,163,184,0.28)");
      trailCtx.strokeStyle = tail;
      trailCtx.lineWidth = hot ? 3 : 2;
      trailCtx.beginPath();
      trailCtx.moveTo(x, y - 42);
      trailCtx.lineTo(x, y);
      trailCtx.stroke();
      trailCtx.fillStyle = hot ? "rgba(74,222,128,0.92)" : "rgba(203,213,225,0.72)";
      trailCtx.fillRect(x - tokenWidth * 0.5, y - 2, tokenWidth, hot ? 7 : 3);
    }
  });
  ctx.drawImage(trail, 0, 0, scaleX, scaleY);
  const scanProgress = (time - scanStart) % 5900;
  if (scanProgress <= 5000) {
    const x = scaleX * (scanProgress / 5000);
    const scan = ctx.createLinearGradient(0, 0, 0, scaleY);
    scan.addColorStop(0, "rgba(203,213,225,0)");
    scan.addColorStop(0.46, "rgba(203,213,225,0.10)");
    scan.addColorStop(0.54, "rgba(74,222,128,0.34)");
    scan.addColorStop(1, "rgba(74,222,128,0)");
    ctx.fillStyle = scan;
    ctx.fillRect(x, 0, 1, scaleY);
  }
  window.__DEMO__.frames += 1;
  fallbackPending = false;
  raf = requestAnimationFrame(frame);
}

function start() {
  if (reduced.matches || running || !ctx) return;
  running = true;
  last = 0;
  scanStart = performance.now();
  window.__DEMO__.effects.push("cascade.tokens", "cascade.trails", "cascade.scan");
  raf = requestAnimationFrame(frame);
}

function stop() {
  running = false;
  if (raf) cancelAnimationFrame(raf);
  raf = 0;
}

window.addEventListener("resize", function() {
  resizeCanvas();
  if (reduced.matches) drawStatic();
});

reduced.addEventListener("change", function() {
  if (reduced.matches) {
    stop();
    drawStatic();
  } else {
    start();
  }
});

resizeCanvas();
drawStatic();
requestAnimationFrame(() => requestAnimationFrame(() => {
  window.__DEMO__.ready = true;
  if (reduced.matches) return;
  if (fallbackPending) {
    canvas.style.opacity = "1";
    document.querySelectorAll("[data-animate]").forEach(function(element) {
      element.style.opacity = "1";
      element.style.transform = "none";
    });
  }
  start();
}));

---

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)

  1. At 1440x900, the document has exactly one visible h1 with text CASCADE, a semantic header, nav, main, and footer, and document.documentElement.scrollHeight is no greater than 900px.
  2. The canvas backing buffer is capped at 2880x1620, window.__DEMO__.name equals cascade, ready becomes true within 2 seconds, frames is greater than 20 within 1.2 seconds, and effects includes cascade.tokens, cascade.trails, and cascade.scan.
  3. At 390x844, 768x1024, 1440x900, and 1920x1080, document.documentElement.scrollWidth is no greater than innerWidth, every visible interactive element has an accessible name, and no element has a focus-visible outline thinner than 2px.
  4. The page contains no img, video, audio, iframe, canvas-adjacent external asset, React, Tailwind, GSAP, Three.js, Lottie, or console output; all visible marks are generated by CSS, SVG, or canvas.
  5. With prefers-reduced-motion: reduce, the canvas and foreground content are readable at opacity 1, no CSS entrance or scan animation is running, the JavaScript rAF loop does not continuously increment frames, and the telemetry object remains initialized with the exact required fields.
  6. window.__DEMO__.ready becomes true within 5s
  7. window.__DEMO__.frames advances (>= 20 frames in 1.2s)
  8. window.__DEMO__.errors is empty
  9. zero console errors and zero page errors
  10. zero failed network requests
  11. exactly one <h1> with non-empty text
  12. header, main, footer landmarks present
  13. no horizontal overflow at 390 / 768 / 1440 widths
  14. no horizontal overflow after 2s of animation
  15. prefers-reduced-motion: reduce leaves the page usable and errors empty

More landing page prompts