Landing Pagestealth/space-bunny-alphapartial cross-check
Camber
Two colliding typographic planes sweep with a moving gilt specular while a restrained paper field rotates behind them.
The exact prompt
A. WHAT IS BEING BUILT
Build one self-contained, single-file landing page for Ledger & Vane, a private bank serving families with inherited holdings. The page is titled “Ledger & Vane — Private Banking for Inherited Holdings”. Set the HTML language to lang="en", include a UTF-8 charset, and include the responsive viewport declaration width=device-width, initial-scale=1. The document must use semantic header, nav, main, section, and footer elements.
At viewport widths of 901px and wider, html, body, and the .page shell must be exactly 100dvh high, body must use overflow:hidden, and the layout must not scroll. At widths of 900px and narrower, use the normal document flow, retain overflow-x:hidden, and allow vertical scrolling. The desktop composition must remain visible inside 100dvh at 1440x900 and 1920x1080. Do not create a second page, route, modal, card carousel, dashboard, account login, onboarding sequence, or multi-step application.
Use exactly one h1. Its accessible text must read “Holdings, held in balance.” The h1 must contain two span.line elements: the first reads “Holdings,” and the second reads “held in balance.” Do not create additional h1 elements in the header, footer, hidden templates, or fallback markup.
Do NOT build:
- A centred hero, centred navigation, centred balance column, or centred footer.
- A conventional card grid, floating glass panels, gradient blobs, neon glow, glassmorphism, or a generic fintech dashboard.
- A video, image, GIF, icon font, third-party icon package, Lottie animation, WebGL scene, framework, bundler, build tool, or external media.
- More than the specified two typographic headline planes.
- A second CTA, “Book a demo” control, hamburger menu, cookie panel, chat widget, newsletter form, account portal, or decorative control.
- A horizontal page scrollbar, viewport-scaled body text, simulated cursor movement, scroll hijacking, pointer-following elements, or layout measurement inside the animation frame.
- Any visible text not explicitly specified in section E.
- Any animation that changes layout dimensions, top, left, right, bottom, width, height, filter, box-shadow, or colour during the rAF loop.
The only permitted network request is an optional Google Fonts stylesheet for Inter and Manrope. Use font-display:swap through the Google Fonts request. The CSS fallback stack must be Inter, “Helvetica Neue”, Helvetica, Arial, sans-serif for all text. If the request fails or is offline, the system fallback must render the complete page without reflow that causes horizontal overflow.
B. THE BACKDROP
Create three backdrop layers inside a fixed .backdrop element with position:fixed, inset:0, z-index:0, pointer-events:none, and overflow:hidden.
Layer one is a solid #EFEDE8 field. Layer two is one .orbit element, centred at 50% 50%, with width:82vmax, height:82vmax, border-radius:50%, opacity:0.08, mix-blend-mode:multiply, and transform-origin:50% 50%. Its background must be conic-gradient(from 22deg at 50% 50%, #0B0C0E 0deg, rgba(11,12,14,0) 46deg, #8A8F98 92deg, rgba(11,12,14,0) 164deg, #0B0C0E 224deg, rgba(11,12,14,0) 304deg, #0B0C0E 360deg). Animate orbit from rotate(0deg) to rotate(360deg) over 36000ms linear infinite. This is exactly one degree per second. Do not scale this layer.
Layer three is one canvas#grain with CSS width:100%, height:100%, opacity:0.17, and mix-blend-mode:multiply. Generate a 96x96 procedural paper tile on an OffscreenCanvas when available; otherwise use a detached HTMLCanvasElement. Draw 1,152 fixed marks from a seeded PRNG: 72% use rgba(11,12,14,0.035), 20% use rgba(138,143,152,0.030), and 8% use rgba(11,12,14,0.018). Each mark is a 1px horizontal or vertical line with length from 1px to 3px. Do not use ctx.filter, ctx.shadowBlur, globalCompositeOperation changes, gradients, or image smoothing. The canvas export is 96x96. The displayed CSS layer stretches that 96x96 export across the viewport, which is the canvas resolution trick: one low-resolution source avoids a full-screen backing store while preserving a paper-grain texture.
For a second, wider grain response, create a second 96x96 tile with the same 1,152-mark recipe and a different deterministic seed. Maintain exactly two tile objects in JavaScript. The rAF loop alternates tiles every 1400ms, applies integer translation offsets from the fixed sequence 0,0; 7,5; 13,2; 19,8; 25,4; 31,10; 37,6; 43,11, then repeats. Reset the index to zero after 37,11. Draw the active tile twice per axis at offsets -96, 0 and 0, 96 so translation never reveals an uncovered edge. Clear with setTransform(1,0,0,1,0,0), fill the 192x192 working canvas with rgba(239,237,232,0), and draw four tile copies. Do not scale the context.
The canvas backing store is exactly 192x192, independent of viewport size. Do not multiply by devicePixelRatio. Set the canvas element’s intrinsic width and height attributes to 192. The paper background and all typography remain fully readable if canvas rendering is blocked.
The CSS .grain-sheen element is optional only if it uses opacity and transform. If present, make it position:absolute, inset:-12%, background:radial-gradient(ellipse at 30% 20%, rgba(255,255,255,0.34), rgba(255,255,255,0) 42%), and animate translate3d(-1.5%, 0.8%, 0) to translate3d(1.5%, -0.8%, 0) over 18000ms ease-in-out infinite alternate. It must not use blur, blend mode, or a filter.
The combined per-frame cost budget is: zero DOM reads, zero getBoundingClientRect calls, zero computed-style reads, zero canvas allocation, zero gradient creation, zero text measurement, one 192x192 clearRect, and at most four 96x96 drawImage calls only when the 1400ms grain interval expires. On other rAF ticks, perform only the telemetry frame increment. CSS animations run on the compositor. At 1440x900, this design must sustain at least 20 rAF frames per 1.2 seconds and target 55 or more frames per 1.2 seconds on the reference headless browser.
C. THE LAYER STACK
Build the visual stack back to front with these exact z-index values:
- body background: the default stacking context, colour #EFEDE8.
- .backdrop: position:fixed, z-index:0.
- .orbit: z-index:1 within .backdrop.
- canvas#grain: z-index:2 within .backdrop.
- .grain-sheen: z-index:3 within .backdrop, if used.
- .page: position:relative, z-index:10.
- header.site-header: z-index:30.
- nav.site-nav: position:relative, z-index:31.
- .nav-rule: z-index:32.
- main#main: z-index:20.
- section.hero: z-index:20.
- .eyebrow: z-index:21.
- h1#camber-title: z-index:22.
- .balance-rail: z-index:23.
- .hero-actions: z-index:24.
- .proof-line: z-index:25.
- footer.site-footer: z-index:40.
The headline must physically sit above the rotating conic field. Do not apply mix-blend-mode to either headline line. Only .orbit may use mix-blend-mode:multiply. The canvas may also use mix-blend-mode:multiply, but it must remain behind .page.
D. TOKEN TABLE
Define every recurring size, colour, spacing, font, line-height, duration, and easing value in :root. Use these exact tokens:
--paper: #EFEDE8;
--ink: #0B0C0E;
--graphite: #8A8F98;
--gilt: #C9A227;
--white: #FFFFFF;
--line: rgba(11,12,14,0.18);
--muted-line: rgba(11,12,14,0.10);
--focus: #8A6D0B;
--font-sans: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif;
--gutter: 64px;
--header-top: 28px;
--header-bottom: 24px;
--nav-height: 44px;
--hero-top: 24px;
--hero-bottom: 30px;
--footer-top: 22px;
--footer-bottom: 24px;
--nav-rule: 1px;
--type-display: clamp(72px, 11vw, 168px);
--display-leading: 0.82;
--display-overlap: -0.14em;
--display-weight: 700;
--display-tracking: -0.065em;
--body-size: 15px;
--body-leading: 1.5;
--label-size: 11px;
--label-leading: 1.2;
--label-tracking: 0.16em;
--balance-width: 240px;
--headline-max: 1180px;
--button-height: 46px;
--button-padding-x: 22px;
--button-radius: 999px;
--header-rule-duration: 900ms;
--header-rule-delay: 500ms;
--line-one-duration: 500ms;
--line-one-delay: 0ms;
--line-two-duration: 700ms;
--line-two-delay: 150ms;
--sweep-duration: 9000ms;
--ease-rise: cubic-bezier(.25,.1,.25,1);
--ease-ui: cubic-bezier(.2,.8,.2,1);
--orbit-duration: 36000ms;
--grain-interval: 1400ms;
--motion-distance: 24px;
--mobile-gutter: 20px;
--tablet-gutter: 36px;
--narrow-desktop-gutter: 48px;
--short-heading-size: 86px;
--short-balance-width: 190px;
--short-footer-top: 14px;
--short-footer-bottom: 14px;
Use box-sizing:border-box on html, body, and every descendant. Set body margin:0, min-width:320px, overflow-x:hidden, background:var(--paper), color:var(--ink), font-family:var(--font-sans), font-size:var(--body-size), line-height:var(--body-leading), text-rendering:optimizeLegibility, and -webkit-font-smoothing:antialiased. Set button and a font:inherit.
E. THE COMPOSITION
The .page element is a CSS grid with grid-template-rows:96px minmax(0,1fr) 72px, width:100%, height:100dvh, padding:0 var(--gutter), and overflow:hidden at widths above 900px.
Build the header first. header.site-header is display:grid, grid-template-columns:minmax(220px,1fr) auto, align-items:center, height:96px, padding:var(--header-top) 0 var(--header-bottom), border-bottom:var(--nav-rule) solid transparent, and position:relative. Place the wordmark link at grid-column:1 and the navigation at grid-column:2. The wordmark exact copy is “LEDGER & VANE”. It must be an anchor with href="#main", aria-label="Ledger & Vane home", color:var(--ink), font-size:13px, font-weight:800, letter-spacing:0.18em, text-decoration:none, and white-space:nowrap.
nav.site-nav must use display:flex, align-items:center, gap:28px, height:var(--nav-height), margin:0, padding:0. It contains exactly three visible links in this order: “Our approach” with href="#approach”, “Stewardship” with href="#stewardship”, and “Contact” with href="#contact”. Each link uses color:var(--ink), font-size:12px, font-weight:600, letter-spacing:0.02em, text-decoration:none. The links must not be buttons styled as pills. Add a 1px vertical divider using border-left:1px solid var(--muted-line) to the “Contact” link and padding-left:28px. The nav-rule is a separate .nav-rule element positioned at the bottom of the header: left:0, right:0, bottom:0, height:var(--nav-rule), background:var(--ink), transform:scaleX(0), transform-origin:left center. It must animate to scaleX(1) over 900ms with a 500ms delay.
Build main#main as the second grid row, min-height:0, position:relative, display:grid, place-items:stretch. section.hero is the only child, position:relative, display:grid, grid-template-columns:minmax(0,1fr) var(--balance-width), grid-template-rows:1fr auto auto, column-gap:44px, row-gap:18px, padding:var(--hero-top) 0 var(--hero-bottom), min-height:0, overflow:hidden.
Place .eyebrow in row 1, column 1, align-self:start, margin:0. Its exact copy is “PRIVATE BANKING / FAMILY CAPITAL / EST. 1998”. Use color:var(--graphite), font-size:var(--label-size), line-height:var(--label-leading), letter-spacing:var(--label-tracking), font-weight:700, text-transform:uppercase.
Place h1#camber-title in row 2, column 1, align-self:end, width:min(100%,var(--headline-max)), margin:0 0 0 calc(var(--gutter) * -0.10), font-size:var(--type-display), line-height:var(--display-leading), font-weight:var(--display-weight), letter-spacing:var(--display-tracking), text-transform:none, white-space:nowrap. The h1 is position:relative. The first .line display:block, color:var(--graphite), transform:translateY(0), opacity:1 at rest. The first line exact copy is “Holdings,”. The second .line display:block, color:var(--ink), margin-top:var(--display-overlap), transform:translateY(0), opacity:1 at rest. The second line exact copy is “held in balance.”. Apply the second-line gradient with background-image:linear-gradient(100deg, #0B0C0E 42%, #C9A227 50%, #0B0C0E 58%), background-size:300% 100%, background-position:0% 0, background-repeat:no-repeat, -webkit-background-clip:text, background-clip:text, -webkit-text-fill-color:transparent, and color:transparent only to .line.two. The gilt must appear only in this moving sweep and in no other visible element. Animate background-position from 0% 0 to 300% 0 over 9s linear infinite. Do not animate the gradient angle.
Place .balance-rail in row 2, column 2, align-self:end, justify-self:end, width:var(--balance-width), padding:0 0 2px, border-top:1px solid var(--ink), color:var(--ink), text-align:right. It must contain a definition-style list with three rows. The exact visible copy is:
“Assets under stewardship”
“$4.8B”
“Across 126 family offices”
“Relationship-led since 1998”
“London · Geneva · Singapore”
Use dl, dt, and dd elements. The dt copy is 10px, uppercase, line-height:1.25, letter-spacing:0.12em, and color:var(--graphite). The dd copy is 12px, line-height:1.4, font-weight:650, and color:var(--ink). The first dd uses font-size:24px, line-height:1.05, letter-spacing:-0.03em, and margin-top:7px. Set list rows to margin:0 0 17px. The last row has margin-bottom:0.
Place .hero-actions in row 3, column 1, display:flex, align-items:center, gap:16px, margin:0, align-self:end. The primary visible control is an anchor with href="#contact", exact copy “Arrange a private conversation”, class .button-primary, and accessible name supplied by its visible text. Place a non-interactive .proof-line after it with exact copy “Independent advice. One accountable relationship.” Use color:var(--graphite), font-size:12px, line-height:1.4, letter-spacing:0.01em.
Do not place any control in .balance-rail. The balance column is informational, right-aligned, and aligned to the same bottom edge as the h1.
Build footer.site-footer as the third grid row, display:grid, grid-template-columns:1fr auto 1fr, align-items:center, height:72px, padding:var(--footer-top) 0 var(--footer-bottom), border-top:1px solid var(--muted-line), color:var(--graphite), font-size:10px, line-height:1.3, letter-spacing:0.10em, text-transform:uppercase. The left footer item is “Ledger & Vane / Private bank”. The centre item is “Stewardship, measured in generations.” and must be text-align:center. The right item is an anchor with href="#legal", exact visible copy “Legal & privacy”, and aria-label “Ledger & Vane legal and privacy information”. The right item must be text-align:right and color:var(--graphite). No footer element may be centred vertically as a page-wide CTA; only the centre footer copy has text-align:center.
F. THE UI LANGUAGE
The primary control is a pill with height:var(--button-height), padding:0 var(--button-padding-x), border:1px solid var(--ink), border-radius:var(--button-radius), display:inline-flex, align-items:center, justify-content:center, color:var(--paper), background:linear-gradient(100deg, #0B0C0E 0%, #0B0C0E 68%, #2A2515 100%), font-size:12px, font-weight:700, letter-spacing:0.02em, text-decoration:none, white-space:nowrap, transition:transform 180ms var(--ease-ui), background 180ms var(--ease-ui), color 180ms var(--ease-ui), box-shadow 180ms var(--ease-ui). Its resting box-shadow is 0 0 0 rgba(201,162,39,0). On hover, transform:translateY(-2px), background:linear-gradient(100deg, #1B1C1F 0%, #0B0C0E 72%, #4A3B0E 100%), color:var(--white), box-shadow:0 6px 0 rgba(11,12,14,0.10). On active, transform:translateY(0) and box-shadow:0 2px 0 rgba(11,12,14,0.10). Do not use a gilt fill behind the button.
Navigation links have no pill and no background. On hover, color:var(--graphite). The wordmark changes from var(--ink) to var(--graphite) on hover without changing position. Footer links use a 1px underline with text-underline-offset:3px on hover.
Every visible link, button, and anchor receives a visible :focus-visible ring using outline:2px solid var(--focus), outline-offset:4px, border-radius:inherit. The ring must remain visible over both paper and ink. Do not remove browser focus outlines.
G. MOTION
All motion begins after the two-rAF startup guard. The initial paint must be readable, with line one opacity:1, line two opacity:1, line two transform:translateY(0), and the nav rule at scaleX(1) as the resting state. The entrance animations are one-shot CSS animations that use opacity and transform only.
The exact stagger is:
- header wordmark: opacity 0 to 1 and translateY(8px) to translateY(0), 500ms, ease-out, delay 80ms.
- nav links: opacity 0 to 1 and translateY(8px) to translateY(0), 500ms, ease-out, delays 120ms, 160ms, 200ms respectively.
- .nav-rule: scaleX(0) to scaleX(1), 900ms, cubic-bezier(.2,.8,.2,1), delay 500ms.
- .eyebrow: opacity 0 to 1 and translateY(12px) to translateY(0), 500ms, ease-out, delay 220ms.
- h1 line one: opacity 0 to 1, 500ms, ease-out, delay 0ms.
- h1 line two: opacity 0 to 1 and translateY(24px) to translateY(0), 700ms, cubic-bezier(.25,.1,.25,1), delay 150ms.
- .balance-rail: opacity 0 to 1 and translateY(16px) to translateY(0), 650ms, cubic-bezier(.25,.1,.25,1), delay 300ms.
- .button-primary: opacity 0 to 1 and translateY(12px) to translateY(0), 500ms, ease-out, delay 420ms.
- .proof-line: opacity 0 to 1, 500ms, ease-out, delay 520ms.
- footer items: opacity 0 to 1, 450ms, ease-out, delays 620ms, 680ms, 740ms respectively.
Use these named keyframes exactly or implement equivalent named animations with identical properties:
@keyframes camber-fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes camber-rise-line {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes camber-rise-small {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes camber-rail {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes camber-rule {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
@keyframes camber-sweep {
from { background-position: 0% 0; }
to { background-position: 300% 0; }
}
@keyframes camber-orbit {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
The h1 line two sweep is the only continuously moving text effect. It must run for 9 seconds, linear, infinite, and must not pause on hover. The conic orbit runs for 36 seconds, linear, infinite. The grain translation changes every 1400ms. No infinite entrance animation may repeat.
H. THE RESPONSIVE TABLE
At the base desktop layout, use --gutter:64px, --type-display:clamp(72px, 11vw, 168px), --balance-width:240px, .page grid rows 96px minmax(0,1fr) 72px, and .hero column-gap:44px.
At 1920x1080 or wider, set --gutter:80px, --headline-max:1320px, --balance-width:280px, --hero-top:30px, --hero-bottom:34px, and .hero column-gap:56px. Keep the headline left anchored and the balance rail right aligned. Do not increase the display type above 168px.
At 1440x900, retain --gutter:64px, --type-display:clamp(72px, 11vw, 168px), --balance-width:240px, and --hero column-gap:44px. The h1 width must not exceed 1180px.
At 1200px and below, set --gutter:var(--narrow-desktop-gutter), --balance-width:210px, and .hero column-gap:28px. Set --type-display:clamp(64px, 10.2vw, 124px). Keep one horizontal hero row until 901px.
At 900px and below, change .page to min-height:100dvh, height:auto, overflow:visible; set body overflow-y:auto; set .page grid-template-rows:auto auto auto; set --gutter:var(--tablet-gutter); set --header-top:24px; set --header-bottom:20px; set header.site-header height:auto; set nav.site-nav gap:18px; set .hero grid-template-columns:1fr; set .hero grid-template-rows:auto auto auto auto; set .hero padding-top:36px; set .hero padding-bottom:36px; set .balance-rail justify-self:start; width:100%; max-width:360px; text-align:left; set h1#camber-title white-space:normal; set --type-display:clamp(58px, 10vw, 96px); set --display-overlap:-0.10em; set h1#camber-title margin-left:0; set .hero-actions flex-wrap:wrap; set footer.site-footer grid-template-columns:1fr 1fr; row-gap:12px; height:auto; padding-top:22px; padding-bottom:22px; set footer centre item text-align:right; set footer right item grid-column:2; set footer centre item grid-column:1; set footer right item grid-row:1; set footer centre item grid-row:2. Do not use horizontal scrolling to preserve the original balance-rail position.
At 600px and below, set --gutter:var(--mobile-gutter); set --header-top:20px; set --header-bottom:18px; set .site-header grid-template-columns:1fr; row-gap:16px; set nav.site-nav justify-content:space-between; width:100%; gap:12px; set nav.site-nav a font-size:11px; set nav.site-nav a:last-child padding-left:12px; set .hero padding-top:30px; set .hero padding-bottom:30px; set .eyebrow max-width:280px; line-height:1.45; set --type-display:clamp(54px, 15.3vw, 86px); set --display-leading:0.84; set --display-overlap:-0.08em; set --display-tracking:-0.06em; set .hero-actions align-items:flex-start; flex-direction:column; gap:12px; set .button-primary width:100%; set .proof-line max-width:260px; set .balance-rail max-width:none; set footer.site-footer font-size:9px; letter-spacing:0.08em; set .grain-sheen inset:-20%.
At 480px and below, set --type-display:clamp(50px, 14.5vw, 70px), set .eyebrow font-size:10px, set .balance-rail dd:first-of-type font-size:22px, and set .button-primary font-size:11px, padding-left:16px, padding-right:16px.
Two short-height desktop guards are mandatory. At min-width:901px and max-height:780px, set --hero-top:12px; set --hero-bottom:14px; set --footer-top:10px; set --footer-bottom:10px; set --display-size:var(--short-heading-size); set --type-display:86px; set --balance-width:var(--short-balance-width); set .hero row-gap:10px; set .hero padding-top:12px; set .hero padding-bottom:14px; set .page grid-template-rows:82px minmax(0,1fr) 52px; set header.site-header height:82px; set footer.site-footer height:52px. At min-width:901px and max-height:620px, set --gutter:48px; set --header-top:14px; set --header-bottom:12px; set --hero-top:8px; set --hero-bottom:8px; set --footer-top:8px; set --footer-bottom:8px; set --type-display:72px; set --balance-width:170px; set .balance-rail dt font-size:9px; set .balance-rail dd font-size:10px; set .balance-rail dd:first-of-type font-size:20px; set .hero row-gap:6px; set .page grid-template-rows:68px minmax(0,1fr) 42px; set header.site-header height:68px; set footer.site-footer height:42px. Short-height rules never apply below 901px.
I. FAILURE GUARDS
Every animated element must declare its resting state first: opacity:1, transform:none for normal rise elements, transform:scaleX(1) for .nav-rule, and transform:translateY(0) for the two headline lines. Animation classes are applied by a .motion-ready class on document.documentElement only after two nested requestAnimationFrame callbacks. If JavaScript is disabled, the page must remain fully readable with all content visible, the rule visible, and the second headline in its final position. The CSS animation fallback is allowed to run from the initial paint only when the JavaScript startup guard is unavailable.
If OffscreenCanvas is unavailable, create a normal canvas with width=192 and height=192. If either 2D context creation fails, remove the canvas from the DOM and keep the paper background, orbit, typography, and all content. If canvas drawing throws, remove the canvas, push the error text into window.__DEMO__.errors, disable the rAF loop after recording the error, and set ready=true. Never show a visible error message.
Implement the two-rAF fallback exactly: call requestAnimationFrame(function firstFrame(){ requestAnimationFrame(function secondFrame(){ document.documentElement.classList.add('motion-ready'); startLoop(); window.__DEMO__.ready = true; }); });. If the first rAF is unavailable, use setTimeout(startAndReady, 32). If the second rAF is unavailable, use setTimeout(startAndReady, 32) from inside the first callback. A delayed start must not prevent the page from being readable.
Create window.__DEMO__ before registering the window error listener and before the startup callbacks. The object must use the exact name, properties, initial types, and spelling specified by the contract. Set ready=true only after the first paint and after the two-rAF guard. The error listener must push String(event.message || 'window error') into errors, prevent duplicate insertion of the same message within 1000ms, and never call console methods.
In @media (prefers-reduced-motion: reduce), set *, *::before, and *::after to animation-duration:0.001ms, animation-iteration-count:1, transition-duration:0.001ms, scroll-behavior:auto, and transform:none where a transform is not required for layout. Set .orbit animation:none; set .line.two background-position:50% 0; set canvas#grain opacity:0.11; set the rAF loop to increment frames but do not redraw grain. Keep the rAF loop active for telemetry, with no work other than frames increment. All content must be visible and static.
J. THE EXACT JS
Place this script at the end of body as plain JavaScript. It must not use imports, modules, external libraries, console output, async functions, storage, cookies, service workers, or network requests.
const demo = {
name: 'camber',
ready: false,
frames: 0,
effects: [],
errors: []
};
window.__DEMO__ = demo;
demo.effects.push('line-one-fade', 'line-two-rise', 'specular-sweep', 'nav-rule-scale', 'conic-orbit', 'paper-grain-drift');
const errorTimes = new Map();
window.addEventListener('error', function (event) {
const message = String(event && event.message ? event.message : 'window error');
const now = Date.now();
const previous = errorTimes.get(message);
if (typeof previous !== 'number' || now - previous > 1000) {
demo.errors.push(message);
errorTimes.set(message, now);
}
}, { passive: true });
const canvas = document.getElementById('grain');
let context = null;
let tileA = null;
let tileB = null;
let workingSize = 192;
let grainIndex = 0;
let lastGrainSwap = 0;
let reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let stopped = false;
function makeTile(seed) {
const tile = typeof OffscreenCanvas === 'function' ? new OffscreenCanvas(96, 96) : document.createElement('canvas');
tile.width = 96;
tile.height = 96;
const tileContext = tile.getContext('2d');
if (!tileContext) return null;
let state = seed >>> 0;
const random = function () {
state = (1664525 * state + 1013904223) >>> 0;
return state / 4294967296;
};
for (let i = 0; i < 1152; i += 1) {
const x = Math.floor(random() * 96);
const y = Math.floor(random() * 96);
const length = 1 + Math.floor(random() * 3);
const vertical = random() > 0.5;
tileContext.fillStyle = i % 10 < 7 ? 'rgba(11,12,14,0.035)' : (i % 10 < 9 ? 'rgba(138,143,152,0.030)' : 'rgba(11,12,14,0.018)');
if (vertical) tileContext.fillRect(x, y, 1, length);
else tileContext.fillRect(x, y, length, 1);
}
return tile;
}
function removeCanvas() {
stopped = true;
if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas);
}
function drawGrain() {
if (!context || reducedMotion || !tileA || !tileB) return;
const offsets = [[0,0],[7,5],[13,2],[19,8],[25,4],[31,10],[37,6],[43,11]];
const offset = offsets[grainIndex % offsets.length];
const tile = grainIndex % 2 === 0 ? tileA : tileB;
context.clearRect(0, 0, workingSize, workingSize);
context.drawImage(tile, -96 + offset[0], -96 + offset[1]);
context.drawImage(tile, offset[0], -96 + offset[1]);
context.drawImage(tile, -96 + offset[0], offset[1]);
context.drawImage(tile, offset[0], offset[1]);
}
function frame(timestamp) {
if (stopped) return;
demo.frames += 1;
if (!reducedMotion && timestamp - lastGrainSwap >= 1400) {
lastGrainSwap = timestamp;
grainIndex = (grainIndex + 1) % 8;
drawGrain();
}
window.requestAnimationFrame(frame);
}
function startAndReady() {
if (canvas) {
try {
context = canvas.getContext('2d');
tileA = makeTile(1978);
tileB = makeTile(2025);
if (!context || !tileA || !tileB) removeCanvas();
} catch (error) {
demo.errors.push(String(error && error.message ? error.message : 'canvas setup failed'));
removeCanvas();
}
}
document.documentElement.classList.add('motion-ready');
if (!stopped) window.requestAnimationFrame(frame);
demo.ready = true;
}
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(function firstFrame() {
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(function secondFrame() {
startAndReady();
});
} else {
window.setTimeout(startAndReady, 32);
}
});
} else {
window.setTimeout(startAndReady, 32);
}
---
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)
- At 1440x900, the document has one h1, one header, one main, one footer, and one nav; document.documentElement.scrollHeight is no greater than document.documentElement.clientHeight, and document.documentElement.scrollWidth is no greater than document.documentElement.clientWidth.
- The h1 contains the exact visible strings “Holdings,” and “held in balance.”, the second line uses a 100deg linear-gradient with the gilt stop at 50%, and the rendered second line has a negative top overlap relative to the first line.
- window.__DEMO__ exists with name “camber”, ready is true after 1200ms, frames increases by at least 20 over a 1200ms measurement window, effects contains “specular-sweep”, and errors has length 0 after load.
- At 390x844, 768x1024, 1440x900, and 1920x1080, document.documentElement.scrollWidth is no greater than document.documentElement.clientWidth; the primary control and all three navigation links have non-empty accessible names.
- With prefers-reduced-motion set to reduce, the orbit animation is absent, the second headline line is visible, the navigation rule is visible at full opacity, and the page remains readable without horizontal overflow.
- 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