Landing Pagestealth/space-bunny-alphaverified 19/19unparsed cross-check
Slipstream
A 200vh sticky canvas spool with five independently drifting rounded-rectangle tape rings and a blended statement crossing its lower third.
The exact prompt
A. WHAT IS BEING BUILT
Build “Slipstream,” a single-page landing experience for Room Tone, a streaming service devoted to recorded music and long, uninterrupted listening. Produce exactly one complete index.html file with semantic HTML, plain CSS, and vanilla JavaScript. Set the document language to en, set the browser tab title to “Slipstream — Long-form listening by Room Tone,” and include one responsive viewport meta tag with width=device-width, initial-scale=1, and viewport-fit=cover.
This is an explicitly scroll-driven concept and is the sole exception to the normal desktop overflow rule. Create one 300dvh scroll stage: a 100dvh opening section followed by a 200dvh sticky section. The opening section scrolls at native speed. The second section uses position: sticky; top: 0; and remains pinned through its parent’s 200dvh range before an ease-out release. On widths from 901px through 1920px, do not disable document scrolling for this concept; allow only vertical scrolling and lock horizontal overflow. At widths of 900px and below, preserve the same 300dvh scroll mechanic while reducing canvas density and typography without changing the sequence.
Use real header, main, section, nav, and footer elements. Include exactly one h1. The canvas is decorative and must use aria-hidden="true"; the ring visual must have a concise text equivalent in visible HTML. Every visible link and button must have a discernible accessible name. Every keyboard-focusable element must receive a visible :focus-visible treatment.
Do NOT build a hero video, audio player waveform, album grid, pricing table, account modal, login form, cursor tracker, magnetic button, 3D engine, horizontal carousel, cookie panel, chat widget, remote image, stock image, icon package, framework, bundler, or secondary page. Do NOT load any media file, base64 asset, third-party script, third-party stylesheet, animation library, or CDN endpoint other than the single permitted Google Fonts stylesheet. Do NOT add more than one h1. Do NOT add horizontal scrolling. Do NOT use SVG filters, canvas filters, canvas shadows, or per-ring shadowBlur.
B. THE BACKDROP
The page field is #0C0B0A. Behind all content, place one fixed full-viewport .field element with a radial-gradient vignette centered at 55% 50%. The radial gradient uses these exact stops: 0% rgba(12,11,10,0); 38% rgba(12,11,10,0.18); 70% rgba(12,11,10,0.62); 100% rgba(12,11,10,0.94). Add a second linear-gradient from rgba(242,239,233,0.025) at 0deg to rgba(242,239,233,0) at 115deg. Do not add stars, grain images, noise textures, or moving particles.
Inside the sticky section, create exactly one canvas element. It must fill the sticky panel at width:100%; height:100%; display:block; transform-origin:50% 50%; transform:scale(1.5); will-change:transform. Apply overflow:hidden to the sticky panel so the 1.5-scaled canvas remains cropped. The 1.5 scale is the focal-anchor treatment: transform-origin is 0% 50%, and the drawing coordinates anchor the spool center near 6% of the logical CSS viewport width. The composition therefore reads as one oversized cropped spool entering from the left, not as a centred icon.
Use a 0.75 backing-resolution trick. On resize, set the canvas backing store to floor(cssWidth × 0.75) by floor(cssHeight × 0.75), capped at 1440 × 960 backing pixels. Set canvas.style.width to the measured CSS pixel width plus "px" and canvas.style.height to the measured CSS pixel height plus "px". Configure the 2D context by calling ctx.setTransform(0.75, 0, 0, 0.75, 0, 0). Cap the effective drawing scale at width 960 and height 640 design units when the viewport exceeds 1280 × 853. This resolution remains at or below 691,200 pixels at 1920 × 1080.
Draw exactly five concentric rounded rectangles. Their logical width multipliers are 1.000, 0.840, 0.680, 0.520, and 0.360. Their logical height multipliers are 1.220, 1.040, 0.860, 0.680, and 0.500. Base dimensions are max(viewportWidth × 1.5, 1300) CSS units wide and max(viewportHeight × 1.5, 1000) CSS units high. Center them at logical x = viewportWidth × 0.06 and y = viewportHeight × 0.50. Corner-radius multipliers are 0.230, 0.205, 0.180, 0.155, and 0.130 of base width. Stroke widths are 2, 4, 7, 10, and 14 CSS units. Ring 0, 1, 3, and 4 use #F2EFE9. Ring 2 alone uses #C8553D. Do not add fill. Line joins and caps are round.
Each ring receives a base rotation of -90 + i × 6 degrees, so the five base angles are -90, -84, -78, -72, and -66 degrees. Rotate the complete group at exactly 360 degrees per 90 seconds. Translate the five rings independently along the horizontal axis at exactly 4, 7, 11, 15, and 18 CSS pixels per second. Use elapsed-seconds delta time and wrap each translation with modulo at the base logical width, preventing unbounded coordinates. The speed differences are intentionally unsynchronised.
Use two drift periods for the backdrop field: a 46-second opacity cycle between 0.78 and 1.00 and a 72-second background-position cycle over 24px and 0px. Apply these through CSS animation only. The rings are canvas-drawn and must not also be duplicated as CSS borders. The canvas must render no more than five path operations, one clearRect, and one fillRect per frame. Keep the per-frame JavaScript budget below 2.2ms at 1440 × 900 in a warm headless run: cache all DOM references before the loop, read scrollY only in the scroll handler, do not call getBoundingClientRect inside rAF, and perform no canvas filter or shadowBlur calls. Target at least 20 delivered animation frames during every 1.2-second observation window at 1440 × 900.
C. THE LAYER STACK
Use this exact front-to-back z-index stack:
1. The document and .field background use z-index:0.
2. The fixed atmosphere layer uses z-index:1 and pointer-events:none.
3. The sticky canvas clipping panel uses z-index:2 and overflow:hidden.
4. The canvas uses z-index:2 within that panel.
5. The release transform layer uses z-index:3 and contains the h1 and supporting copy.
6. A non-interactive lower-third contrast veil uses z-index:4, background:linear-gradient(180deg, rgba(12,11,10,0) 44%, rgba(12,11,10,0.72) 100%), and pointer-events:none.
7. The sticky section’s top edge marker uses z-index:5.
8. The sticky motion control uses z-index:8.
9. The opening section header and statement use z-index:10.
10. The opening section footer uses z-index:10.
11. The skip link uses z-index:100.
The opening section has position:relative; z-index:10; height:100dvh; padding:28px 40px 24px. The sticky wrapper has position:sticky; top:0; height:200dvh; z-index:2. The sticky viewport panel has position:absolute; inset:0; overflow:hidden. The .release-layer has position:absolute; inset:0; z-index:3; display:flex; align-items:flex-end; padding:0 40px 48px; will-change:transform. The h1 is positioned in the lower third, not centred.
D. TOKEN TABLE
Define every used size and colour as a CSS custom property. Use these exact values:
--bg: #0C0B0A;
--surface: #141210;
--surface-raised: #1B1816;
--ink: #F2EFE9;
--dust: #6B6560;
--dust-light: #9A938C;
--oxide: #C8553D;
--oxide-hover: #DF654B;
--hairline: rgba(242,239,233,0.18);
--focus: #F2EFE9;
--font-display: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
--font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
--fs-eyebrow: 11px;
--fs-nav: 12px;
--fs-statement: clamp(19px, 1.55vw, 25px);
--fs-h1: clamp(64px, 8.7vw, 164px);
--fs-body: 15px;
--fs-meta: 10px;
--lh-eyebrow: 1.3;
--lh-nav: 1.2;
--lh-statement: 1.34;
--lh-h1: 0.84;
--lh-body: 1.55;
--tracking-eyebrow: 0.18em;
--tracking-nav: 0.08em;
--tracking-h1: -0.065em;
--space-1: 6px;
--space-2: 10px;
--space-3: 14px;
--space-4: 20px;
--space-5: 28px;
--space-6: 40px;
--space-7: 64px;
--space-8: 96px;
--radius-pill: 999px;
--radius-panel: 18px;
--border-hairline: 1px solid var(--hairline);
--stage-height: 300dvh;
--intro-height: 100dvh;
--pin-height: 200dvh;
--canvas-scale: 1.5;
--canvas-resolution: 0.75;
--release-distance: -10vh;
--ease-release: cubic-bezier(0.16, 1, 0.3, 1);
--ease-ui: cubic-bezier(0.22, 1, 0.36, 1);
--duration-ui: 220ms;
--duration-load: 700ms;
--ring-stagger: 120ms;
--focus-width: 2px;
--focus-offset: 4px;
--content-max: 1440px;
--header-height: 52px;
--footer-height: 36px;
--canvas-stroke-0: 2px;
--canvas-stroke-1: 4px;
--canvas-stroke-2: 7px;
--canvas-stroke-3: 10px;
--canvas-stroke-4: 14px;
E. THE COMPOSITION
Build the opening section as follows. Its content width is min(100%, var(--content-max)). Use a CSS grid with grid-template-columns:minmax(0, 1fr) auto, grid-template-rows:52px 1fr 36px, column-gap:24px, row-gap:20px, and height:100%. The header occupies grid-column:1 / -1 and grid-row:1. The statement occupies grid-column:1, grid-row:2, aligns itself to end, and has a maximum width of 680px. A small index block occupies grid-column:2, grid-row:2, aligns itself to end, and has width:180px. The footer occupies grid-column:1 / -1 and grid-row:3.
The header is a nav with display:grid; grid-template-columns:1fr auto 1fr; align-items:center; height:52px. The left item is the Room Tone wordmark link with exact copy “ROOM TONE”. The centre contains two text links with exact copy “Listen” and “Method”. The right item contains one pill button with exact copy “Pause motion”. The nav’s accessible label is “Primary navigation”. The wordmark uses --font-display, 15px, weight 600, letter-spacing -0.02em. Centre links use --font-mono, --fs-nav, --tracking-nav, uppercase, and --dust-light. The right button uses the pill treatment in section F.
The opening statement has exact copy: “A room for records that refuse to hurry. Start one side. Keep the lights low. Let the next room arrive on its own.” Use --fs-statement, --lh-statement, weight 400, max-width:680px, and colour:var(--ink). Above it, place one eyebrow with exact copy “RECORDED MUSIC / UNBROKEN TIME”. Below it, place one body paragraph with exact copy: “Room Tone streams complete recordings without visible countdowns, editorial interruptions, or algorithmic urgency. One selection stays in motion from its first room tone to its last fade.” The body uses --fs-body, --lh-body, max-width:520px, and colour:var(--dust-light).
The opening index block uses a 1px top border and four stacked metadata rows. The row labels and values must be exact: “FORMAT / LOSSLESS + LOSSLESS”; “SESSION / CONTINUOUS”; “SURFACE / DARK”; “CATALOGUE / 18,420 RECORDS”. Labels use --fs-meta, --tracking-eyebrow, and --dust. Values use --font-mono, 11px, and --ink. The block uses row-gap:12px and padding-top:14px.
The opening footer is a semantic footer with border-top:var(--border-hairline), display:flex, justify-content:space-between, align-items:center, color:var(--dust), font-family:var(--font-mono), font-size:var(--fs-meta), and text-transform:uppercase. Its exact copy is “A LISTENING SERVICE BY ROOM TONE” on the left and “SCROLL INTO THE SPOOL ↓” on the right.
The sticky section begins immediately after the opening section and is 200dvh tall. Its viewport panel contains the canvas, a lower-third veil, the release layer, an edge marker, and the motion control. The edge marker is positioned at top:28px; left:40px and uses exact copy “NOW PLAYING / SIDE A — UNTITLED STUDY IN FOLD”. It has --fs-eyebrow, --tracking-eyebrow, and --dust-light. Place a 6px oxide dot before the text using ::before.
The release layer places the h1 at the left edge. The h1 exact copy is “KEEP THE ROOM TONE.” Use --fs-h1, --lh-h1, --tracking-h1, font-weight:500, max-width:1050px, color:var(--ink), and mix-blend-mode:difference. The text must overlap the lower third of the canvas, producing white-to-black inversions where it crosses #F2EFE9. Beneath the h1, place one paragraph with exact copy: “No timer. No queue shuffle. No seam between records.” It uses --fs-body, --lh-body, color:var(--dust-light), max-width:420px, and margin-top:20px. Keep the h1 and paragraph in a copy column using display:grid; grid-template-columns:minmax(0, 1fr) 360px; column-gap:40px; align-items:end; width:100%; max-width:1440px.
The sticky motion control is positioned at right:40px; bottom:48px. Its exact initial copy is “Pause motion”. It uses the same pill treatment as the header. A second visually hidden status element is unnecessary; update the existing button’s aria-pressed and text copy from JavaScript.
F. THE UI LANGUAGE
Use flat geometry, 1px borders, and one oxide interaction colour. Text links have no permanent background. Their resting colour is var(--dust-light), border-bottom:1px solid transparent, and transition:color 180ms ease, border-color 180ms ease. Hover changes colour to var(--ink) and border-color to var(--oxide). Active colour is var(--oxide-hover).
Pill buttons have height:36px, padding:0 16px, border-radius:var(--radius-pill), border:1px solid rgba(242,239,233,0.28), background:linear-gradient(135deg, #24201E 0%, #151312 100%), color:var(--ink), font-family:var(--font-mono), font-size:11px, line-height:1, letter-spacing:0.06em, text-transform:uppercase, and transition:transform var(--duration-ui) var(--ease-ui), border-color var(--duration-ui) ease, background var(--duration-ui) ease. Hover uses transform:translateY(-2px), border-color:var(--oxide), and background:linear-gradient(135deg, #2B211E 0%, #181311 100%). Active uses transform:translateY(0) scale(0.98). Buttons with aria-pressed="true" use border-color:var(--oxide) and a small inset oxide indicator through box-shadow:inset 0 0 0 1px rgba(200,85,61,0.24); box-shadow is a UI style only and is never used in canvas.
The skip link is position:fixed; top:8px; left:8px; z-index:100; transform:translateY(-160%); padding:10px 14px; background:var(--ink); color:var(--bg); font:12px var(--font-mono). On :focus, transform:translateY(0). Apply outline:2px solid var(--focus); outline-offset:4px to :focus-visible for a, button, and [tabindex]. Do not remove browser outlines.
G. MOTION
Use a single persistent requestAnimationFrame loop for the canvas, group rotation, independent ring translation, and scroll-derived release. Compute scroll state in a passive scroll listener and set a cached numeric value only. Do not query layout inside the loop.
The load sequence uses these exact opacity ramps:
Opening eyebrow: 0 to 1 over 700ms, delay 40ms, using ease-out.
Opening statement: 0 to 1 over 700ms, delay 120ms, using ease-out.
Metadata block: 0 to 1 over 700ms, delay 220ms, using ease-out.
Header: 0 to 1 over 700ms, delay 0ms, using ease-out.
Footer: 0 to 1 over 700ms, delay 300ms, using ease-out.
Canvas ring 0: 0 to 1 over 700ms, delay 140ms, using ease-out.
Canvas ring 1: 0 to 1 over 700ms, delay 260ms, using ease-out.
Canvas ring 2: 0 to 1 over 700ms, delay 380ms, using ease-out.
Canvas ring 3: 0 to 1 over 700ms, delay 500ms, using ease-out.
Canvas ring 4: 0 to 1 over 700ms, delay 620ms, using ease-out.
Sticky h1: 0 to 1 over 700ms, delay 340ms, using ease-out.
Sticky paragraph: 0 to 1 over 700ms, delay 460ms, using ease-out.
Motion control: 0 to 1 over 700ms, delay 540ms, using ease-out.
CSS load animations affect opacity only. Name them reveal-up, reveal-fade, ring-reveal, drift-field, and drift-grain. The ring reveal is represented in canvas by the same delay arithmetic in JavaScript. All resting opacities are 1. The initial class .is-loading sets the animated HTML elements to opacity:0; remove is-loading after the second animation frame so they become visible if CSS animation is disabled.
The ring rotation is defined as groupAngle = elapsedSeconds × 4 degrees per second, which completes 360 degrees in 90 seconds. Independent translations use the exact speeds [4, 7, 11, 15, 18] CSS pixels per second. Clamp deltaSeconds to 0.050 to prevent jumps after a suspended tab. Pause the motion button sets all ring translation and rotation calculations to their current frozen values while preserving the current scroll release position. Its second state is “Play motion”, aria-pressed="true", and its first state is “Pause motion”, aria-pressed="false".
The final 25dvh of the 200dvh pin is the release zone. Convert progress there to 0 through 1 using cubic ease-out: 1 - (1 - progress)^3. Apply translate3d(0, releaseDistance × easedProgress, 0) only to .release-layer. The h1 and paragraph do not receive independent parallax. The sticky panel remains position:sticky until the 300dvh parent ends. No fade-out is applied.
H. THE RESPONSIVE TABLE
At 1440 × 900 and widths from 1201px to 1600px, use --space-6:40px, --fs-h1:clamp(96px, 8.7vw, 144px), stage 300dvh, intro 100dvh, pin 200dvh, opening padding 28px 40px 24px, release padding 0 40px 48px, and copy grid minmax(0, 1fr) 360px.
At 1920 × 1080, override --content-max:1680px, --fs-h1:164px, opening padding 32px 64px 28px, release padding 0 64px 64px, metadata width 220px, and keep the canvas 1.5 scale with the left origin.
At widths from 901px to 1199px, override --space-6:28px, --fs-h1:clamp(76px, 8.7vw, 104px), opening padding 24px 28px 20px, release padding 0 28px 40px, copy grid-template-columns:1fr, copy paragraph max-width:420px, metadata width 160px, and canvas scale 1.35.
At 768 × 1024 and widths from 601px to 900px, use stage 300dvh, intro 100dvh, pin 200dvh, --fs-h1:clamp(68px, 11vw, 92px), --fs-statement:22px, opening padding 22px 24px 18px, release padding 0 24px 38px, header grid-template-columns:1fr auto, centre navigation display:none, statement max-width:560px, metadata width 150px, copy grid-template-columns:1fr, row-gap:16px, and canvas scale 1.25. The document may scroll vertically at this breakpoint; disable horizontal overflow.
At 390 × 844 and widths through 600px, use stage 300dvh, intro 100dvh, pin 200dvh, --fs-h1:clamp(50px, 15.3vw, 68px), --fs-statement:19px, --fs-body:14px, opening padding 18px 18px 16px, release padding 0 18px 32px, header grid-template-columns:1fr auto, centre navigation display:none, opening statement max-width:330px, metadata width 132px, metadata value font-size:9px, copy grid-template-columns:1fr, edge marker left:18px, motion control right:18px, bottom:24px, and canvas scale 1.10. Keep all text inside min-width:0 containers and use overflow-wrap:anywhere where a long catalogue value needs to wrap.
Use max-height:760px at widths from 901px to 1600px. Override opening padding-top:18px, opening padding-bottom:16px, --header-height:44px, --footer-height:30px, --fs-h1:clamp(58px, 8vw, 104px), --fs-statement:18px, release padding-bottom:28px, and motion control bottom:28px. The stage and pin heights remain 300dvh and 200dvh.
Use max-height:620px at all widths. Override opening padding-top:14px, opening padding-bottom:12px, --header-height:40px, --footer-height:26px, --fs-h1:clamp(46px, 7.8vw, 86px), --fs-statement:17px, --fs-body:13px, release padding-bottom:22px, and motion control bottom:18px. Do not remove the footer; reduce its gap and allow its right label to remain on the same line at widths above 600px.
I. FAILURE GUARDS
All visible elements have resting opacity:1. Add .is-loading only before the first paint. Use a two-rAF readiness gate: after DOMContentLoaded and resize measurement, request one rAF, then request a second rAF, and only then remove is-loading, set telemetry ready:true, and start drawing. If requestAnimationFrame is unavailable, use a 120ms timeout that removes is-loading, sets ready:true, starts the fallback interval at 30fps, and increments telemetry frames once per fallback tick.
Respect prefers-reduced-motion: reduce. In that mode, cancel CSS reveal animations, remove is-loading, keep every element at opacity:1, stop ring drift and group rotation at groupAngle:0 and translations:0, disable the background drift animations, and do not apply the release transform. Keep the scroll section readable and fully static. If canvas context acquisition returns null, hide the canvas with display:none and keep the HTML copy visible. If ResizeObserver is unavailable, use window resize. If the canvas backing width is below 2px, use 2px. Never throw from the resize or animation path. Add a window error listener before first paint. Keep a guard around context resetTransform so a lost canvas does not generate repeated console output.
J. THE EXACT JS
Place this JavaScript at the end of body. It must be copied exactly in structure, constants, selectors, hooks, and behaviour:
const NAME = "slipstream";
const state = {
width: 0,
height: 0,
scale: 1,
elapsed: 0,
last: 0,
scrollY: window.scrollY || 0,
scrollDirty: true,
paused: false,
started: false,
ready: false,
fallback: false,
effects: ["ring-drift", "group-rotation", "sticky-release", "load-ramp"]
};
window.__DEMO__ = {
name: NAME,
ready: false,
frames: 0,
effects: [],
errors: []
};
window.addEventListener("error", function(event) {
if (window.__DEMO__.errors.length < 50) {
window.__DEMO__.errors.push(String(event.message || "window error"));
}
});
const root = document.documentElement;
const stage = document.querySelector(".scroll-stage");
const canvas = document.querySelector("#tape");
const ctx = canvas && canvas.getContext("2d", { alpha: true });
const release = document.querySelector(".release-layer");
const motionButtons = Array.from(document.querySelectorAll("[data-motion-toggle]"));
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");
const speeds = [4, 7, 11, 15, 18];
const translations = [0, 0, 0, 0, 0];
const baseRotations = [-90, -84, -78, -72, -66];
const revealDelays = [140, 260, 380, 500, 620];
const strokeWidths = [2, 4, 7, 10, 14];
const revealStart = [0, 0, 0, 0, 0];
const loadedAt = performance.now();
function markEffects() {
window.__DEMO__.effects = state.effects.slice();
}
function setReady() {
if (state.ready) return;
state.ready = true;
root.classList.remove("is-loading");
window.__DEMO__.ready = true;
markEffects();
}
function cubicOut(value) {
const inverse = 1 - value;
return 1 - inverse * inverse * inverse;
}
function measure() {
state.width = window.innerWidth;
state.height = window.innerHeight;
state.scale = state.width <= 600 ? 1.10 : state.width <= 900 ? 1.25 : state.width <= 1199 ? 1.35 : 1.5;
root.style.setProperty("--canvas-scale", String(state.scale));
if (!canvas || !ctx) return;
const width = Math.max(2, Math.min(1440, Math.floor(state.width * 0.75)));
const height = Math.max(2, Math.min(960, Math.floor(state.height * 0.75)));
canvas.width = width;
canvas.height = height;
canvas.style.width = state.width + "px";
canvas.style.height = state.height + "px";
const scaleX = width / state.width;
const scaleY = height / state.height;
const fitScale = Math.min(scaleX, scaleY, state.width > 1280 && state.height > 853 ? 0.6667 : 1);
ctx.setTransform(fitScale, 0, 0, fitScale, 0, 0);
state.scrollDirty = true;
}
function opacityForRing(index, now) {
if (reduced.matches) return 1;
const delay = revealDelays[index];
if (now - loadedAt < delay) return 0;
return Math.min(1, (now - loadedAt - delay) / 700);
}
function updateRelease() {
if (!stage || !release) return;
if (reduced.matches) {
release.style.transform = "translate3d(0, 0, 0)";
return;
}
const top = stage.offsetTop;
const start = top + window.innerHeight;
const end = top + stage.offsetHeight - window.innerHeight;
const releaseStart = end - window.innerHeight * 0.25;
const progress = Math.max(0, Math.min(1, (state.scrollY - releaseStart) / (window.innerHeight * 0.25)));
const eased = cubicOut(progress);
const distance = window.innerHeight * -0.10;
release.style.transform = "translate3d(0, " + distance * eased + "px, 0)";
}
function draw(now, delta) {
if (!ctx || !canvas) return;
if (!state.paused && !reduced.matches) {
state.elapsed += delta;
const groupAngle = state.elapsed * 4;
const baseWidth = Math.max(state.width * 1.5, 1300);
for (let i = 0; i < 5; i += 1) {
translations[i] = (translations[i] + speeds[i] * delta) % baseWidth;
}
const revealOpacity = [1, 1, 1, 1, 1];
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#0C0B0A";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(state.width * 0.06, state.height * 0.50);
ctx.rotate(groupAngle * Math.PI / 180);
const widthMultipliers = [1, 0.84, 0.68, 0.52, 0.36];
const heightMultipliers = [1.22, 1.04, 0.86, 0.68, 0.5];
const radiusMultipliers = [0.23, 0.205, 0.18, 0.155, 0.13];
for (let i = 0; i < 5; i += 1) {
const w = baseWidth * widthMultipliers[i];
const h = Math.max(state.height * 1.5, 1000) * heightMultipliers[i];
const radius = baseWidth * radiusMultipliers[i];
const alpha = Math.min(revealOpacity[i], opacityForRing(i, now));
ctx.save();
ctx.translate(translations[i], 0);
ctx.rotate(baseRotations[i] * Math.PI / 180);
ctx.beginPath();
ctx.roundRect(-w / 2, -h / 2, w, h, radius);
ctx.lineWidth = strokeWidths[i];
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.strokeStyle = i === 2 ? "#C8553D" : "#F2EFE9";
ctx.globalAlpha = alpha;
ctx.stroke();
ctx.restore();
}
ctx.restore();
ctx.globalAlpha = 1;
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#0C0B0A";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(state.width * 0.06, state.height * 0.50);
const baseWidth = Math.max(state.width * 1.5, 1300);
const widthMultipliers = [1, 0.84, 0.68, 0.52, 0.36];
const heightMultipliers = [1.22, 1.04, 0.86, 0.68, 0.5];
const radiusMultipliers = [0.23, 0.205, 0.18, 0.155, 0.13];
for (let i = 0; i < 5; i += 1) {
const w = baseWidth * widthMultipliers[i];
const h = Math.max(state.height * 1.5, 1000) * heightMultipliers[i];
const radius = baseWidth * radiusMultipliers[i];
ctx.save();
ctx.translate(translations[i], 0);
ctx.rotate(baseRotations[i] * Math.PI / 180);
ctx.beginPath();
ctx.roundRect(-w / 2, -h / 2, w, h, radius);
ctx.lineWidth = strokeWidths[i];
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.strokeStyle = i === 2 ? "#C8553D" : "#F2EFE9";
ctx.globalAlpha = opacityForRing(i, now);
ctx.stroke();
ctx.restore();
}
ctx.restore();
ctx.globalAlpha = 1;
}
}
function frame(now) {
if (!state.last) state.last = now;
const delta = Math.min(0.05, Math.max(0, (now - state.last) / 1000));
state.last = now;
draw(now, delta);
if (state.scrollDirty) {
updateRelease();
state.scrollDirty = false;
}
window.__DEMO__.frames += 1;
requestAnimationFrame(frame);
}
function startFallback() {
if (state.fallback) return;
state.fallback = true;
const start = performance.now();
setInterval(function() {
const now = performance.now();
draw(now, Math.min(0.05, (now - state.last) / 1000 || 0.033));
state.last = now;
updateRelease();
window.__DEMO__.frames += 1;
}, 33.333);
setReady();
}
function setPaused(paused) {
state.paused = paused;
for (const button of motionButtons) {
button.textContent = paused ? "Play motion" : "Pause motion";
button.setAttribute("aria-pressed", paused ? "true" : "false");
}
}
for (const button of motionButtons) {
button.addEventListener("click", function() {
setPaused(!state.paused);
});
}
window.addEventListener("scroll", function() {
state.scrollY = window.scrollY || 0;
state.scrollDirty = true;
}, { passive: true });
window.addEventListener("resize", measure, { passive: true });
reduced.addEventListener("change", function() {
state.scrollDirty = true;
});
measure();
markEffects();
if (typeof window.requestAnimationFrame === "function") {
requestAnimationFrame(function() {
requestAnimationFrame(function() {
setReady();
requestAnimationFrame(frame);
});
});
} else {
startFallback();
}
---
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)
- The delivered artifact is one valid index.html using only HTML, CSS, and vanilla JavaScript; loading at 1440x900 creates no horizontal overflow, exactly one h1, and real header, main, nav, section, and footer elements.
- At 1440x900, the scroll stage measures 300dvh, the opening section measures 100dvh, the sticky section measures 200dvh with position:sticky and top:0, and window.__DEMO__ becomes { name: 'slipstream', ready: true, frames: a non-negative integer, effects: ['ring-drift','group-rotation','sticky-release','load-ramp'], errors: [] } after first paint.
- The canvas contains five rounded-rectangle paths per frame with stroke widths [2,4,7,10,14], horizontal speeds [4,7,11,15,18], and exactly one stroke using #C8553D; its backing store uses the 0.75 resolution rule and never exceeds 1440 by 960 pixels.
- At 390x844, 768x1024, 1440x900, and 1920x1080, document.documentElement.scrollWidth does not exceed document.documentElement.clientWidth, all visible controls have accessible names, and every keyboard-focusable control displays the specified focus-visible outline.
- With prefers-reduced-motion: reduce, all visible content reaches opacity 1, canvas translation and group rotation remain fixed, the release transform remains translate3d(0,0,0), and a 1.2-second observation records at least 20 telemetry frames without console output or uncaught exceptions.
- 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