# Fringe

> **Category:** Landing Page
> **Effect:** A turbulence-cut pigment panel wipes across and occludes a monumental studio headline.
> **Platform:** cmd-code (`openrouter`)
> **Model:** `stealth/space-bunny-alpha` — Space Bunny Alpha (stealth/space-bunny-alpha)
> **License:** Free / MIT — copy, paste, ship.
> **Self-contained:** yes — one `index.html`, no build step, no external media.

---

## 🎬 Live demo

Open [`public/demos/fringe/index.html`](../public/demos/fringe/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 index.html for Sable & Weft, a print-and-textile studio working in pigment on heavy 12 oz cotton. The page is a single-viewport cinematic portfolio landing screen titled “Fringe — Sable & Weft”. Set the document language to lang="en", the character encoding to UTF-8, and the responsive viewport meta tag to width=device-width, initial-scale=1. The page uses a 5/7 asymmetric editorial composition: copy occupies the left five columns and a 720 × 880 pigment panel occupies the right seven columns, extends beyond the right viewport edge by exactly 12% of its own width, and visually covers the final letters of the headline. The artwork must look like a torn printed textile swatch, not like a conventional rounded card.

At widths of 901 px and above, set html, body, and .site-shell to width:100%; height:100dvh; min-height:100dvh; overflow:hidden. At widths through 900 px, allow normal vertical document scrolling; never allow horizontal scrolling. The fixed visual field may become position:relative and preserve its aspect ratio, but all content must fit the listed responsive layouts without clipping. There is no routing, modal, menu drawer, carousel, form submission, video, image, WebGL scene, text input, or secondary page.

Do NOT build a navigation drawer, account controls, cookie notice, testimonial slider, pricing table, CMS preview, contact modal, scroll-jacked sequence, video background, image gallery, loading percentage, cursor follower, WebGL renderer, framework bundle, or decorative element detached from the printed-swatch concept. Do not use remote media, external scripts, third-party CDN resources, embedded fonts other than Google Fonts, base64 raster content, animated GIFs, or stock imagery. Do not simulate paper with bitmap textures. Do not use repeated geometric holes, polygon clipping, CSS border-radius cards, or regular geometry as the panel edge; the visible perimeter must be authored by SVG turbulence displacement. Do not add console output. Do not create more than one h1.

B. THE BACKDROP

The full viewport background is #12100C. Place one decorative .ambient field behind all content with position:absolute; inset:0; z-index:0; pointer-events:none. It uses two radial-gradient pools only: radial-gradient(circle at 13% 18%, rgba(122,139,92,0.10) 0, rgba(122,139,92,0.035) 23%, transparent 48%), and radial-gradient(circle at 82% 76%, rgba(217,201,163,0.075) 0, rgba(217,201,163,0.018) 28%, transparent 52%). The gradients are composited on #12100C and must remain subtle enough that #E8E2D6 type has at least 12:1 contrast against the darkest region.

Add exactly 3 fine registration rules to the ambient field: one horizontal rule from x=0 to x=100% at y=64px, one vertical rule at x=5.5556vw from y=0 to y=100%, and one vertical rule at x=91.1111vw from y=0 to y=100%. Each is 1 px, rgba(232,226,214,0.07), with no blur. These are alignment marks, not panel boundaries. Do not add more ambient elements.

The hero artwork is one 720 px × 880 px .swatch-wrap positioned in the right grid region. Internally it contains one SVG .edge-mask with width=720 height=880 viewBox="0 0 720 880", defs, one mask, one path, one displacement filter, and one turbulence filter. The source path is a closed, hand-authored irregular contour: M 92 34 C 156 8 252 48 334 24 C 424 -2 508 18 584 8 C 650 0 698 38 688 88 C 678 138 718 182 704 232 C 690 284 724 334 708 388 C 694 436 724 492 700 540 C 678 584 714 632 690 676 C 670 716 692 770 654 806 C 622 838 584 822 548 852 C 506 886 462 862 416 874 C 366 888 328 866 284 874 C 228 884 188 850 142 846 C 94 842 58 812 66 762 C 74 714 22 676 34 628 C 44 588 10 544 28 500 C 46 454 4 410 24 362 C 42 316 0 274 20 224 C 38 180 54 144 48 104 C 42 70 58 46 92 34 Z.

The mask path is filled white. Apply filter="url(#fringe-displace)" to that path. Define the displacement filter in user-space coordinates with x="-20%" y="-20%" width="140%" height="140%" and color-interpolation-filters="sRGB". Inside it, define filter id="fringe-noise" with x="-20%" y="-20%" width="140%" height="140%": feTurbulence type="fractalNoise" baseFrequency="0.014 0.055" numOctaves="4" seed="17" result="noise"; then feDisplacementMap in="SourceGraphic" in2="noise" scale="34" xChannelSelector="R" yChannelSelector="G". The mask uses mask-type:luminance. The displaced white shape reveals the pure-CSS panel only inside the ragged contour; everything outside must show the backdrop. The 12% crop comes from placing the swatch’s rightmost 86.4 px outside the viewport, not from additional masking.

Inside .swatch-art, use a conic-gradient(from var(--a), #1B2A2E 0deg, #7A8B5C 108deg, #D9C9A3 216deg, #1B2A2E 360deg). Layer two radial pools over it at 40% opacity: radial-gradient(circle at 28% 24%, rgba(217,201,163,0.95) 0, rgba(217,201,163,0.28) 24%, transparent 52%), and radial-gradient(circle at 72% 74%, rgba(27,42,46,0.95) 0, rgba(27,42,46,0.26) 30%, transparent 57%). The first begins at 40% opacity as a complete layer, and the second also begins at 40% opacity. Add exactly one inline SVG grain overlay above these layers, with opacity:0.09, mix-blend-mode:soft-light, pointer-events:none, and its own feTurbulence type="fractalNoise" baseFrequency="0.82" numOctaves="3" seed="9". The grain overlay is clipped by the same .edge-mask SVG instance and is not animated through filter primitives per frame.

Animate the swatch’s registered custom property --a from 0deg to 360deg over 26s linear infinite. The only live filter mutation is changing the displacement feTurbulence seed every 900 ms among the fixed sequence 17, 23, 31, 41, 53, 67, 79, 89. Restarting the browser’s SVG noise implementation makes the edge crawl like wet paper without reflowing text or recalculating DOM geometry.

No canvas is used. The intrinsic 720 × 880 SVG is the resolution mechanism: the browser rasterizes the mask at device-pixel-aware resolution and CSS scales the 720 × 880 art box. The right-edge crop is performed by the viewport and transform, never by changing SVG coordinates. The live per-frame budget is at most 3 composited properties across the hero: one registered conic-angle animation on the swatch art, one browser-owned opacity/transform reveal on the swatch, and the rAF callback itself. The JS rAF loop must not query layout, read computed styles, resize observers, canvas pixels, or element bounds. The 900 ms seed interval must change one attribute only when its timestamp is reached. The page should sustain at least 20 frames per 1.2 seconds on a 1440 × 900 headless viewport; the only required rAF bookkeeping is an increment and one timestamp comparison.

C. THE LAYER STACK

Use explicit z-index values and no stacking-context ambiguity. The body background layer is z-index:auto. The .ambient field is z-index:0. The global .shell is position:relative; z-index:1; height:100%; pointer-events:none so decorative overlap does not block controls. The header is z-index:10. The main hero is z-index:1. The hero copy column is z-index:1. The large h1 is z-index:1. The swatch-wrap is position:absolute within the hero’s right region and has z-index:2; this must sit above the h1 so the ragged edge occludes its final letters. The swatch shadow is contained inside the masked SVG and has z-index:0 within the swatch. The swatch grain overlay is z-index:3 within the swatch. The hero metadata rail is z-index:5. The footer is z-index:10. No visible element may have a negative z-index. The focus ring must draw above its control at z-index:11.

The h1 is positioned so the last 3 characters “WEFT” extend beneath the leftmost ragged region of the swatch at 1440 × 900. Do not solve overlap with reduced opacity. At the 1440 breakpoint, the text’s visual right edge is x=956 px, the swatch’s viewport position begins near x=744 px, and at least 54 px of “WEFT” is hidden by opaque panel pixels on first paint. The panel always remains fully opaque where its mask is white.

D. TOKEN TABLE

Define every size and color in :root. Use these exact tokens:
--bg: #12100C;
--ink: #E8E2D6;
--ink-muted: rgba(232,226,214,0.62);
--ink-faint: rgba(232,226,214,0.38);
--moss: #7A8B5C;
--linen: #D9C9A3;
--deep: #1B2A2E;
--line: rgba(232,226,214,0.14);
--line-soft: rgba(232,226,214,0.07);
--shell-pad-x: 5.5556vw;
--shell-pad-y: 32px;
--header-h: 64px;
--footer-h: 58px;
--hero-h: calc(100dvh - 122px);
--grid-columns: minmax(0, 5fr) minmax(0, 7fr);
--grid-gap: 0px;
--headline-size: clamp(84px, 8.1vw, 156px);
--headline-leading: 0.78;
--headline-tracking: -0.072em;
--headline-weight: 600;
--eyebrow-size: 11px;
--eyebrow-leading: 1;
--eyebrow-tracking: 0.19em;
--body-size: 15px;
--body-leading: 1.55;
--meta-size: 10px;
--meta-leading: 1.4;
--meta-tracking: 0.14em;
--rule-size: 1px;
--swatch-w: 720px;
--swatch-h: 880px;
--swatch-crop: 86.4px;
--swatch-top: -96px;
--swatch-radius: 0px;
--button-h: 42px;
--button-pad-x: 18px;
--pill-radius: 999px;
--focus-width: 2px;
--focus-offset: 4px;
--ease-expo: cubic-bezier(.76,0,.24,1);
--reveal-duration: 1.1s;
--type-duration: 600ms;
--grain-seed-ms: 900ms;
--grain-rotate: 26s.

Use border-box sizing globally. Use the system stack -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif for controls and the Google Font request for “DM Mono” only, with ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace as fallback. Load the permitted font with display=swap and no other network request. The display face remains the system sans stack so a failed font request cannot alter the composition.

E. THE COMPOSITION

The document contains one header, one main, and one footer in source order. Header .site-header uses position:absolute; top:0; left:0; right:0; height:var(--header-h); padding:0 var(--shell-pad-x); display:grid; grid-template-columns:5fr 7fr; align-items:center; border-bottom:var(--rule-size) solid var(--line-soft). Its first child is a linked wordmark with exact accessible name “Sable & Weft home” and exact visible text “SABLE / WEFT”. Set it to font:600 13px/1 var(--font-mono); letter-spacing:0.08em. Its second child is a nav with aria-label="Primary navigation", display:flex; justify-content:flex-end; gap:28px. It contains exactly 3 links: “Selected work”, “Material practice”, and “Studio notes”. Each has exact visible copy in that capitalization, 11 px monospace text, and a 1 px bottom rule that expands from 0% to 100% on hover.

Main .hero uses position:absolute; inset:var(--header-h) 0 var(--footer-h); height:var(--hero-h); display:grid; grid-template-columns:var(--grid-columns); overflow:visible. The left .copy-column occupies the 5-column track with padding:clamp(96px,15vh,152px) 0 72px var(--shell-pad-x); position:relative; z-index:1. The right .art-column is the 7-column track and has position:relative; z-index:2; pointer-events:none.

Inside .copy-column, place an eyebrow first with exact text “PRINT + TEXTILE STUDIO / EST. 2018”. It uses --eyebrow-size, --eyebrow-leading, --eyebrow-tracking, color:var(--moss), and margin:0 0 28px. The only h1 has exact content “MADE BY HAND. / HELD BY / WEFT.” Use three block-level lines, font-size:var(--headline-size), line-height:var(--headline-leading), letter-spacing:var(--headline-tracking), font-weight:var(--headline-weight), text-transform:uppercase, color:var(--ink), and width:min(100%, 960px). The line containing “WEFT.” must extend 72 px beyond the nominal 5-column boundary into the art track. The h1 margin is 0. Set paint-order:stroke fill; -webkit-text-stroke:0.35px rgba(232,226,214,0.08) to counter subpixel softness without reducing type contrast.

Place .lede below the h1 with max-width:410px; margin:34px 0 0; exact text: “Pigment, pressure and patience. We print limited editions onto heavy cotton for rooms that keep their marks.” Use --body-size and --body-leading, color:var(--ink-muted). Beneath it place .actions with display:flex; gap:10px; margin-top:28px. The primary control is an anchor with exact visible text “VIEW THE EDITIONS” and accessible name “View the editions”. The secondary control is an anchor with exact visible text “MATERIAL NOTES” and accessible name “Read material notes”. Do not add icons to either control.

Add .edition-note in the lower left with margin-top:clamp(48px,10vh,104px); display:grid; grid-template-columns:1fr 1fr; gap:24px; width:410px; border-top:1px solid var(--line); padding-top:14px. The first block has label “LATEST DROP” and value “No. 08 / WET PAPER”. The second has label “SUBSTRATE” and value “12 OZ COTTON”. Use label color:var(--ink-faint), value color:var(--ink), 10 px monospace, and 14 px vertical separation.

The .art-column contains the only .swatch-wrap. At 1440 × 900, place it at left:-126px relative to the art track, top:var(--swatch-top), width:var(--swatch-w), height:var(--swatch-h), and do not scale it. The viewport crop will hide exactly 86.4 px. At 1920 × 1080, left:-112px and top:-118px. The .edge-mask and .swatch-art are both position:absolute; inset:0; width:720px; height:880px. The art starts at clip-path:inset(0 0 0 100%). A masked drop impression is allowed as a second filled path inside the same mask at translate(10px, 14px), fill rgba(0,0,0,0.28), but it must not extend the 720 × 880 coordinate system or add a CSS shadow.

Place one .material-tag inside the art-column, not inside the masked swatch, at left:42px; bottom:70px; z-index:5; transform:translateX(5.5556vw). It is a 2-row caption with border-left:2px solid var(--linen); padding:2px 0 2px 12px; color:var(--ink). Exact rows: “DYE LOT 08 / OLIVE CHALK” and “HAND-PULLED / 18 VARIATIONS”. Use --meta-size, --meta-leading, --meta-tracking, and opacity:0.84. The tag remains above the panel and therefore is not occluded.

Footer .site-footer is position:absolute; left:0; right:0; bottom:0; height:var(--footer-h); padding:0 var(--shell-pad-x); display:grid; grid-template-columns:5fr 7fr; align-items:center; border-top:1px solid var(--line-soft); color:var(--ink-faint); font:var(--meta-size)/var(--meta-leading) var(--font-mono); letter-spacing:var(--meta-tracking). The first cell has exact text “PORTLAND, OREGON / 45.5152° N”. The second cell has text-align:right and exact text “INDEPENDENT PRESS / 2025”. Neither is interactive.

F. THE UI LANGUAGE

Both action anchors use height:var(--button-h), padding:0 var(--button-pad-x), border-radius:var(--pill-radius), display:inline-flex; align-items:center; justify-content:center; font:600 10px/1 var(--font-mono); letter-spacing:0.12em; text-decoration:none; transition:transform 180ms var(--ease-expo), background-color 180ms ease, color 180ms ease, border-color 180ms ease. The primary uses background:linear-gradient(110deg,#D9C9A3 0%,#B8AE87 100%); color:#12100C; border:1px solid #D9C9A3. On hover, transform:translateY(-2px); background:linear-gradient(110deg,#E8E2D6 0%,#D9C9A3 100%); border-color:#E8E2D6. On active, transform:translateY(0). The secondary uses background:transparent; color:var(--ink); border:1px solid rgba(232,226,214,0.28). On hover, transform:translateY(-2px); background:rgba(232,226,214,0.07); border-color:rgba(232,226,214,0.62). On active, transform:translateY(0).

Navigation links have color:var(--ink-muted), text-decoration:none, and a pseudo-element with height:1px, bottom:-6px, left:0, width:0, background:var(--linen), transform-origin:right center, transition:width 220ms var(--ease-expo), transform 220ms var(--ease-expo). Hover and focus-visible use width:100% and transform-origin:left center. Every visible control receives a 2 px #D9C9A3 focus-visible outline with 4 px offset, never removed without replacement. Hover media queries must not hide focus.

G. MOTION

Register the custom property with @property --a { syntax: "<angle>"; inherits: false; initial-value: 0deg; }. Animate .swatch-art with keyframes fringe-rotate { from { --a:0deg; } to { --a:360deg; } }, 26s linear infinite, animation-delay:-6.4s. Animate .swatch-wrap reveal with fringe-reveal { from { clip-path:inset(0 0 0 100%); transform:translate3d(64px,0,0); opacity:0.34; } to { clip-path:inset(0 0 0 0); transform:translate3d(0,0,0); opacity:1; } }, duration 1.1s, timing cubic-bezier(.76,0,.24,1), delay 120ms, fill-mode:both. The clip-path animation is permitted by the assignment as the one named reveal exception; during the ongoing rAF loop animate only transforms and opacity.

The exact initial stagger is:
.brand -> fade-up -> 0ms
nav -> fade-up -> 80ms
eyebrow -> fade-up -> 260ms
h1 -> fade-up -> 700ms
lede -> fade-up -> 860ms
actions -> fade-up -> 980ms
edition-note -> fade-up -> 1100ms
material-tag -> fade-up -> 1240ms
footer -> fade-up -> 460ms
swatch-wrap -> clip wipe -> 120ms
swatch-art -> conic rotation -> starts at 120ms and runs infinitely.

Define @keyframes fringe-up as from { opacity:0; transform:translate3d(0,18px,0); } to { opacity:1; transform:translate3d(0,0,0); }. Apply it for 600ms with cubic-bezier(.22,1,.36,1) and fill-mode:both. Define @keyframes fringe-rotate exactly as stated above. Define @keyframes fringe-reveal exactly as stated above. Do not add parallax, scroll-linked transforms, letter-by-letter effects, or looping movement to text. Do not alter DOM attributes except the one SVG turbulence seed every 900 ms.

H. RESPONSIVE TABLE

At 1440 × 900, use the root values without breakpoint overrides. The expected headline maximum is 116.64 px from 8.1vw. The art remains 720 × 880 with left:-126px and top:-96px. The right crop remains 86.4 px.

At 1920 × 1080, override --shell-pad-y:36px; --headline-size:155.52px; --lede max-width:430px; .art-column .swatch-wrap { left:-112px; top:-118px; }. The footer stays bottom-anchored and the page remains exactly 100dvh.

At 768 × 1024 and all widths from 601 through 900, set html, body, and .site-shell { height:auto; min-height:100dvh; overflow-x:hidden; overflow-y:auto; }. Change .site-header to position:relative; height:72px; padding:0 32px; grid-template-columns:1fr; nav { display:none; }. Set .hero to position:relative; inset:auto; height:auto; min-height:calc(100dvh - 72px); display:block; overflow:visible; padding:68px 32px 48px. Set .copy-column { padding:0; }. Set --headline-size:clamp(72px,13.2vw,101px); --headline-leading:0.82; .copy-column h1 { width:100%; }. Set .swatch-wrap { position:absolute; left:auto; right:-86.4px; top:332px; width:540px; height:660px; }. Because the internal SVG remains 720 × 880 and the wrapper uses a 0.75 scale, preserve the torn contour and keep 64.8 px outside the viewport. Hide .material-tag at this breakpoint. Set .edition-note { margin-top:610px; }. Set .site-footer { position:relative; height:64px; padding:0 32px; grid-template-columns:1fr 1fr; }. This breakpoint scrolls vertically but has no horizontal overflow.

At 390 × 844, set --shell-pad-x:20px; --headline-size:58px; --headline-leading:0.84; --headline-tracking:-0.065em; --body-size:14px. Set .site-header { height:62px; padding:0 20px; }. Set .hero { min-height:1230px; padding:54px 20px 46px; }. Set .eyebrow { margin-bottom:22px; }. Set .lede { max-width:320px; margin-top:26px; }. Set .actions { flex-direction:column; align-items:flex-start; gap:9px; margin-top:24px; }. Set .swatch-wrap { top:548px; right:-57.6px; width:384px; height:469.333px; }. Preserve an 8% crop. Set .edition-note { position:absolute; left:20px; top:1080px; width:320px; margin-top:0; }. Hide .material-tag. Set .site-footer { position:relative; height:70px; padding:0 20px; grid-template-columns:1fr; align-content:center; gap:5px; }. Footer first and second cells both use text-align:left.

At max-height:760px and min-width:901px, set --header-h:54px; --footer-h:48px; --headline-size:min(8.1vw,104px); .copy-column { padding-top:72px; padding-bottom:36px; }; .lede { margin-top:22px; }; .actions { margin-top:18px; }; .edition-note { margin-top:34px; }; .swatch-wrap { top:-132px; }. The page remains overflow:hidden and all footer content stays visible.

At max-height:680px and min-width:901px, set --header-h:50px; --footer-h:44px; --headline-size:86px; --headline-leading:0.76; .copy-column { padding-top:54px; padding-bottom:24px; }; .eyebrow { margin-bottom:18px; }; .lede { max-width:360px; font-size:13px; margin-top:16px; }; .actions { margin-top:14px; }. Set .edition-note { margin-top:22px; padding-top:10px; }. Set .swatch-wrap { top:-170px; }. Do not scale typography below 86 px.

I. FAILURE GUARDS

The final resting state of every text, control, rule, caption and panel is opacity:1; disabled opacity is never used to hide failed animation. All entrance animations use fill-mode:both and a deterministic final state. If SVG mask support or turbulence fails, the mask path still presents a visible hand-authored contour because the unfiltered white path remains the fallback base, and the panel must remain readable. If the custom-property animation is unsupported, --a remains 0deg and the conic gradient remains valid. If IntersectionObserver is unavailable, immediately mark all entrance targets as finished rather than waiting for observation.

Use a two-rAF fallback after DOMContentLoaded: first requestAnimationFrame schedules a second requestAnimationFrame; the second starts the reveal classes and telemetry loop. If requestAnimationFrame is unavailable, apply the final reveal classes immediately, set ready:true after a 0 ms timeout, and do not increment frames. If document.visibilityState is hidden, keep the page static and resume only after visibility returns; telemetry frames continue only while visible. Never leave controls at opacity:0 if scripting is disabled, so use a .js class on html only after the script begins and provide no-js-safe visible base styles.

The prefers-reduced-motion:reduce block must cancel fringe-up, fringe-rotate and fringe-reveal with animation:none !important; force animation-delay:0s; set transform:none; opacity:1; clip-path:inset(0); stop all CSS transitions and button hover translations. JavaScript must not start the continuous loop, must not change the noise seed, and must set ready:true after the second rAF or timeout. The reduced-motion page remains complete, static and fully interactive.

J. THE EXACT JS

Place this script as the final element in body with defer omitted because it is already last. It must use no imports, no fetch, no external data, no storage, and no console methods. The exact source logic is:

const demo = { name: 'fringe', ready: false, frames: 0, effects: [], errors: [] };
window.__DEMO__ = demo;
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
const reveals = Array.from(document.querySelectorAll('[data-reveal]'));
const swatch = document.querySelector('.swatch-wrap');
const noise = document.querySelector('#fringe-noise-source');
const seeds = [17, 23, 31, 41, 53, 67, 79, 89];
let seedIndex = 0;
let nextSeed = 0;
let running = false;
let startedAt = 0;

window.addEventListener('error', function(event) {
  demo.errors.push(String(event.message || 'Unknown error'));
});

function finishReveals() {
  reveals.forEach(function(element) {
    element.classList.add('is-revealed');
  });
  if (swatch) swatch.classList.add('is-revealed');
}

function startFrameLoop(timestamp) {
  if (!running || motionQuery.matches) {
    demo.ready = true;
    return;
  }
  demo.frames += 1;
  if (!startedAt) startedAt = timestamp;
  if (noise && timestamp >= nextSeed) {
    seedIndex = (seedIndex + 1) % seeds.length;
    noise.setAttribute('seed', String(seeds[seedIndex]));
    nextSeed = timestamp + 900;
  }
  window.requestAnimationFrame(startFrameLoop);
}

function begin() {
  document.documentElement.classList.add('js');
  finishReveals();
  demo.effects.push('swatch-wipe', 'conic-rotation', 'grain-jitter', 'type-stagger');
  if (motionQuery.matches) {
    demo.ready = true;
    return;
  }
  running = true;
  nextSeed = 0;
  window.requestAnimationFrame(startFrameLoop);
}

motionQuery.addEventListener('change', function(event) {
  if (event.matches) {
    running = false;
    finishReveals();
  }
});

document.addEventListener('visibilitychange', function() {
  if (document.hidden) {
    running = false;
  } else if (!motionQuery.matches) {
    running = true;
    nextSeed = 0;
    window.requestAnimationFrame(startFrameLoop);
  }
});

if (typeof window.requestAnimationFrame === 'function') {
  window.requestAnimationFrame(function() {
    window.requestAnimationFrame(begin);
  });
} else {
  window.setTimeout(begin, 0);
}

The final HTML must include the three ambient rules as empty decorative elements with aria-hidden="true", a single SVG mask with aria-hidden="true", and no text duplicated in SVG. Navigation anchors may use href="#editions", "#materials", and "#notes"; those targets must resolve to existing noninteractive landmarks or sections within the same document, and clicking them must not create horizontal movement.

---

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.*
