Landing Pagehy4-preview-fverified 20/20confirmed cross-check
Aurora Freight
A procedural aurora light-field behind a bottom-anchored hero and liquid-metal nav
The exact prompt
DESIGN BRIEF — "Aurora Freight"
A. WHAT IS BEING BUILT
Build ONE complete, valid, self-contained `index.html`: a single-viewport dark landing page for
a fictional logistics-AI company called **Aurora Freight**. Document title (exact):
`Aurora Freight — Freight Intelligence, Orchestrated`. `<html lang="en">`.
The page is: pure black (`#000000`) ground; a full-bleed PROCEDURAL aurora rendered on a
<canvas> as the cinematic backdrop; a fixed header with a liquid-metal pill nav; a
bottom-anchored (NOT vertically centred) hero with a two-line masked <h1>; and a three-item
stats footer. That is the whole page. One frame. No scroll on desktop.
DO NOT BUILD — every one of these is a failure:
- No video, no WebGL, no Three.js, no Lottie, no GSAP, no jQuery, no React, no Tailwind CDN.
- No remote images, no .mp4/.webm/.gif, no base64 photo blobs, no third-party CDN of any kind.
Google Fonts (preconnect + stylesheet) is the ONLY network request.
- No extra sections, no feature cards, no logo strip, no pricing table, no testimonial, no FAQ,
no form, no newsletter input, no cookie banner, no footer beyond the three stats.
- No scroll indicator, no mouse-follow parallax, no cursor blob, no custom scrollbar.
- No vertical scrolling on desktop: at >=901px `html, body { height:100%; overflow:hidden }`
and `.page { height:100dvh; overflow:hidden }`. Scroll is permitted ONLY at <=900px.
- No text-shadow glows, no neon outlines on type, no drop shadows under the headline.
- No animated counters, no typing effect, no marquee.
B. THE BACKDROP — PROCEDURAL AURORA LIGHT FIELD
B.1 The canvas element
<canvas id="aurora" class="aurora-canvas" aria-hidden="true"></canvas>
CSS: position:fixed; inset:0; width:100%; height:100%; display:block; z-index:0;
pointer-events:none; background:#000;
It is fixed, not absolute, so it never moves during layout.
B.2 The resolution trick (implement exactly)
const DPR = Math.min(window.devicePixelRatio || 1, 2); // cap DPR at 2
const SCALE = 0.5; // render at half resolution
canvas.width = Math.max(1, Math.round(innerWidth * DPR * SCALE));
canvas.height = Math.max(1, Math.round(innerHeight * DPR * SCALE));
CSS keeps it at 100% x 100%, so the browser upscales the soft field with bilinear smoothing.
Because the content is pure blur, 0.5x is visually identical to 1x and costs 4x less fill.
Also set once after sizing: ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'low'; (cheap, and smoothing is what we want).
Recompute on resize with a debounce of 150ms, and on orientationchange. Clamp: never let
canvas.width * canvas.height exceed 2,600,000 device pixels; if it does, lower SCALE to fit
(SCALE = Math.min(SCALE, Math.sqrt(2600000 / (w*h*DPR*DPR)))).
B.3 The sprite approach — ZERO allocation in the hot loop
Do NOT call createRadialGradient inside the rAF loop. Instead pre-render sprites once:
For each of the 3 palettes (18 sprites total, 6 bands x 3 palettes), create an offscreen
canvas SPRITE = 256 x 256 (document.createElement('canvas')) and paint into it ONE radial
gradient centred at (128,128) with radius 128, stops:
0.00 -> rgba(r,g,b,1.00)
0.35 -> rgba(r,g,b,0.42)
0.62 -> rgba(r,g,b,0.14)
0.82 -> rgba(r,g,b,0.04)
1.00 -> rgba(r,g,b,0.00)
Store in a flat array `sprites[paletteIndex * 6 + bandIndex]`. 18 x 256 x 256 x 4 bytes =
4.7 MB of offscreen memory — acceptable, allocated once at boot.
Per frame the only draw call is ctx.drawImage(sprite, dx, dy, dw, dh) — no gradient object,
no array, no string, no closure is created per frame.
B.4 The six bands — exact parameters
Geometry helpers (w,h = canvas backing-store size in device px):
maxDim = Math.max(w, h)
cx_i = 0.50 * w + ampX_i * w * Math.sin(t * fx_i + phaseX_i)
cy_i = 0.46 * h + ampY_i * h * Math.sin(t * fy_i + phaseY_i)
r_i = radius_i * maxDim // sprite half-width
dw = 2 * r_i
dh = 2 * r_i * aspect_i // squashed vertically -> a band, not a ball
These are independent Lissajous paths (different x and y frequencies per band => the paths
never repeat inside a session).
i | role | radius_i | aspect_i | ampX_i | ampY_i | fx_i (rad/s) | fy_i (rad/s) | phX | phY | alpha
0 | cyan core | 0.62 | 0.58 | 0.30 | 0.20 | 0.0532 | 0.0411 | 0.0 | 1.7 | 0.62
1 | teal | 0.54 | 0.64 | 0.26 | 0.24 | 0.0648 | 0.0480 | 2.1 | 0.4 | 0.55
2 | violet | 0.70 | 0.50 | 0.34 | 0.18 | 0.0439 | 0.0861 | 4.0 | 3.2 | 0.40
3 | blue | 0.48 | 0.70 | 0.22 | 0.28 | 0.0757 | 0.0576 | 5.6 | 2.6 | 0.34
4 | magenta | 0.66 | 0.54 | 0.36 | 0.16 | 0.0376 | 0.1030 | 1.2 | 5.0 | 0.28
(fx 0.0532 rad/s = a 118 s period; fy 0.1030 rad/s = a 61 s period. Slow and cinematic.)
5 | AMBER SWEEP — a warm band that crosses the frame every ~30 s. It is NOT a Lissajous drift;
it is a scripted traversal, so it reads as an event:
cycle = 30 s
p = (t % cycle) / cycle // 0..1
window = p in [0.10, 0.62] // visible for 15.6 s of the 30 s
u = clamp((p - 0.10) / 0.52, 0, 1)
ease = 0.5 - 0.5 * Math.cos(Math.PI * u) // easeInOutSine: no velocity jump
ax = (-0.35 + 1.70 * ease) * w // enters left, exits right
ay = (0.62 - 0.10 * Math.sin(Math.PI * u)) * h
aEnv = Math.pow(Math.sin(Math.PI * u), 0.8) // fade in/out at the edges
alpha = 0.42 * aEnv
radius_5 = 0.40, aspect_5 = 0.46
B.5 Compositing order inside one frame (all in canvas, no CSS filters)
1. ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1;
ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, w, h); // hard black ground each frame
2. ctx.globalCompositeOperation = 'screen'; // light adds, never subtracts
3. for each band i = 0..5:
draw palette A sprite with globalAlpha = alpha_i * aEnv_i * (1 - mix)
draw palette B sprite with globalAlpha = alpha_i * aEnv_i * mix
(aEnv_i is 1 for bands 0-4, and the envelope from B.4 for band 5)
Order 0,1,2,3,4,5 — largest, softest first is not required because 'screen' is commutative.
4. ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1;
Note: 'screen' on black means the field can never darken the page below #000000, and no band
can ever clip to white — the sum stays under ~0.78 luminance because the peak alphas are low.
B.6 Palette cross-fade every 20 s (rAF, cancellable, never snapping)
Three palettes, index cycled 0 -> 1 -> 2 -> 0:
P0 Nordic : #22d3ee, #12a89a, #7c5cff, #2a6bd8, #a855f7, #f0a24a
P1 Ion : #35e0d0, #0f8fbf, #5b6cff, #1b4fd8, #c678f7, #ffb066
P2 Ember drift : #4de3c1, #17997f, #8b5cf6, #2563eb, #d946ef, #f59e0b
State: { fromA, fromB, mix, fading:false, fadeT0:0, fadeDur:2600 }
Every 20.0 s of accumulated time (timer measured in the same rAF, using the clamped dt), start
a fade: set toB = next palette, fadeT0 = now, fading = true.
Each frame while fading: mix goes from the value it had when the fade started (mixStart) to 1
using easeInOutCubic over fadeDur; when done: fromA = fromB; fromB = next(fromB); mix = 0;
fading = false.
CANCELLATION RULE (the important part): if a new fade is requested while `fading === true`,
do NOT restart from 0. Snapshot the CURRENT blended colour as the new starting point:
fromA = currentBlendRgb (computed by lerping P[fromA] and P[fromB] by the live mix,
per channel, rounded to integers, then used as a palette slot of its own),
mixStart = 0, fadeT0 = now, fromB = requested target.
This makes interruptions continuous — the field never snaps.
B.7 Frame budget and dt clamp
- dt = Math.min((now - last) / 1000, 0.05); // clamp so a tab switch cannot teleport bands
- Draw at most ~60 fps: if (now - lastDraw < 15.5) { rAF again; return; }
- Target: the whole frame must cost under 4 ms on a 2019 laptop. With 12 drawImage calls on a
0.5x backing store this is easily met. If `document.hidden` is true, cancel the rAF and
restart it on visibilitychange; never draw while hidden.
- Every rAF tick increments window.__DEMO__.frames (see J) — including when a draw is skipped.
B.8 Grain: SVG feTurbulence at 4%, static (zero per-frame cost)
Inline (NOT a data URI, NOT remote):
<svg class="grain-svg" aria-hidden="true" focusable="false">
<filter id="auroraGrain" x="0" y="0" width="100%" height="100%">
<feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="3"
stitchTiles="stitch" seed="7" result="n"/>
<feColorMatrix type="saturate" values="0"/>
</filter>
<rect width="100%" height="100%" filter="url(#auroraGrain)"/>
</svg>
CSS: .grain-svg { position:fixed; inset:-4%; width:108%; height:108%; z-index:100;
opacity:0.04; mix-blend-mode:soft-light; pointer-events:none; }
A wrapper drift so it does not look like a static plate — compositor-only, 6 discrete steps:
.grain-svg { animation: grain-drift 6s steps(6, end) infinite; will-change: transform; }
@keyframes grain-drift {
0% { transform: translate3d(0,0,0) } 16.6% { transform: translate3d(-1.2%, 0.8%, 0) }
33.3%{ transform: translate3d(0.9%, -1.1%, 0) } 50% { transform: translate3d(-0.7%, -0.6%, 0) }
66.6%{ transform: translate3d(1.1%, 0.5%, 0) } 83.3%{ transform: translate3d(-0.5%, 1.2%, 0) }
100% { transform: translate3d(0,0,0) } }
B.9 Vignette + scrim (CSS, above canvas, below content)
.aurora-scrim is a fixed div at z-index:1 with pointer-events:none and:
background:
radial-gradient(120% 90% at 50% 62%, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 46%,
rgba(0,0,0,0.78) 78%, rgba(0,0,0,0.94) 100%),
linear-gradient(to top, rgba(0,0,0,0.92) 0%, rgba(0,0,0,0.62) 22%,
rgba(0,0,0,0.18) 46%, rgba(0,0,0,0.32) 72%, rgba(0,0,0,0.72) 100%);
The bottom-to-top ramp is the load-bearing part: the bottom 22% of the viewport (where the
hero copy sits) is knocked down to ~92% black, so the headline and lede ALWAYS win, no matter
how bright the aurora is. The top ramp protects the header. Values are not adjustable upward.
C. THE LAYER STACK (back -> front), with z-index
z 0 html, body background:#000000 (see failure guards)
z 0 #aurora fixed canvas, the light field
z 1 .aurora-scrim fixed vignette/scrim gradient
z 2 .page position:relative; display:grid; grid-template-rows:auto 1fr auto;
min-height:100dvh; (desktop lock: height:100dvh; overflow:hidden)
auto header / main / footer children of .page, no z-index of their own
z 100 .grain-svg feTurbulence grain, 4%, soft-light
Nothing else may create a stacking context above 2. The mobile menu overlay, when open, sits at
z-index:90 (below grain, above .page).
D. TOKEN TABLE (declare all of these on :root; default values are the ~1440px design)
Colour
--bg: #000000
--text: #ffffff
--muted: #9a9a9a (7.5:1 on #000 — safe for the lede)
--stat: #d8d8d8
--accent: #22d3ee
--border: rgba(255,255,255,0.16)
--border-soft: rgba(255,255,255,0.12)
--pill-line: rgba(198,198,198,0.55)
--pill-line-hover: rgba(235,235,235,0.9)
--grain-opacity: 0.04
--focus: rgba(120,231,255,0.9)
Type
--logo: 15.5px
--logo-mark: 22px
--nav: 14px
--nav-h: 40px
--btn: 13.5px
--btn-h: 40px
--h1: 48px
--lede: 15.5px
--badge: 12.5px
--stat-size: 13.5px
Space / geometry
--header-y: 22px
--header-x: 40px
--stats-x: 72px
--stats-y: 36px
--hero-gap: 85px
--copy-max: 860px
--lede-max: 470px
--pill-radius: 7px
--pill-pad: 18px
--nav-gap: 10px
--cta-gap: 14px
--hero-btn-gap: 12px
--stat-gap: 36px
Fonts
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif
--font-serif: "Instrument Serif", "Iowan Old Style", "Palatino Linotype", Palatino,
Georgia, serif
Load with <link rel="preconnect" href="https://fonts.googleapis.com">,
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>, and one
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Instrument+Serif:ital@1&display=swap">.
If the request fails the system fallbacks above must still look intentional.
E. THE COMPOSITION — build order
E.1 <body style="background:#000;color:#fff"> (inline attribute, belt and braces).
E.2 <header class="site-header">
padding: var(--header-y) var(--header-x); display:flex; align-items:center; gap:16px;
.brand { display:inline-flex; align-items:center; gap:10px; color:#fff; text-decoration:none }
.brand-mark { width:var(--logo-mark); height:var(--logo-mark); flex:0 0 auto }
An INLINE SVG: a 22x22 viewBox="0 0 22 22" aurora mark — three stacked arcs
(path d="M2 15 C6 7, 10 19, 14 8 S18 12, 20 6" style curves) stroked with
stroke="url(#markGrad)" stroke-width="1.6" fill="none" stroke-linecap="round", where
#markGrad is a linearGradient from #22d3ee to #a855f7, plus a 2.2r circle at (20,6)
filled #f0a24a. aria-hidden="true" (the wordmark carries the name).
.brand-word { font-size:var(--logo); letter-spacing:0.14em; text-transform:uppercase;
font-weight:500; white-space:nowrap } -> text "Aurora Freight"
nav.nav-desktop { margin-left:auto; display:flex; align-items:center; gap:var(--nav-gap) }
ul { display:flex; align-items:center; gap:var(--nav-gap); list-style:none; margin:0; padding:0 }
Four <a class="pill appear"> links, in this order and with these EXACT strings:
"Platform" "Network" "Pricing" "Docs"
Then <a class="btn btn-ghost btn-sm appear"> with the string "Request access", separated
from the last pill by var(--cta-gap).
button.burger (hidden at >=901px): 40x40, aria-label="Open menu", aria-expanded="false",
aria-controls="mobile-menu", border:1px solid var(--border), border-radius:var(--pill-radius),
background:transparent; three 1px x 16px white bars (spans) at 6px vertical spacing.
E.3 <main class="hero">
CSS: .hero { display:flex; align-items:flex-end; justify-content:center;
padding:8px 24px var(--hero-gap); min-height:0 }
.hero-inner { width:100%; max-width:var(--copy-max); margin:0 auto; text-align:center }
Children, in order:
1. p.badge.appear
inline-flex; align-items:center; gap:8px; margin:0 0 26px;
font-size:var(--badge); letter-spacing:0.08em; text-transform:uppercase;
color:var(--muted);
.badge-dot { width:5px; height:5px; border-radius:50%; background:var(--accent);
box-shadow:0 0 10px rgba(34,211,238,0.75) }
Copy (exact): "New — the Aurora Route Graph"
2. h1.hero-title (the ONLY h1 on the page)
font-size:var(--h1); font-weight:500; letter-spacing:-0.045em; line-height:1.12;
margin:0 0 22px; color:var(--text);
Exactly TWO <span class="headline-line"> children, each display:block;
overflow:hidden; padding:0.06em 0.15em 0.14em; (the padding stops the italic serif
and the descenders from being clipped by the mask):
Line 1: Move <em>freight</em> with an AI that
Line 2: already knows the road.
em { font-family:var(--font-serif); font-style:italic; font-weight:400;
font-size:1.08em; letter-spacing:-0.03em; color:var(--muted) } <-- NOT white
3. p.lede.appear
max-width:var(--lede-max); margin:0 auto 30px; font-size:var(--lede);
line-height:1.62; color:var(--muted);
Copy (exact): "Aurora plans, books and tracks every load across 340+ carriers — learning your lanes and your rates in weeks, not quarters."
4. div.hero-actions { display:flex; gap:var(--hero-btn-gap); justify-content:center;
align-items:center; flex-wrap:wrap }
a.btn.btn-solid.appear -> "Request access"
a.btn.btn-line.appear -> "Watch the 2-min tour"
E.4 <footer class="stats">
padding:0 var(--stats-x) var(--stats-y);
ul { display:flex; align-items:center; justify-content:center; gap:var(--stat-gap);
flex-wrap:wrap; list-style:none; margin:0; padding:0 }
Exactly THREE <li class="stat appear"> items:
<b>340+</b> carriers connected
<b>1.2M</b> loads routed monthly
<b>18%</b> lower freight spend
.stat { font-size:var(--stat-size); color:var(--stat); letter-spacing:0.01em;
display:inline-flex; gap:7px; align-items:baseline; white-space:nowrap }
.stat b { color:#ffffff; font-weight:500; font-variant-numeric:tabular-nums }
E.5 Mobile menu (<=900px only)
div#mobile-menu.mobile-menu[hidden] — position:fixed; inset:0; z-index:90;
background:rgba(0,0,0,0.72); backdrop-filter:blur(18px) saturate(1.2);
-webkit-backdrop-filter:blur(18px) saturate(1.2);
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:18px;
Contains the same four links at font-size:22px plus "Request access" as a solid button.
Toggled by .burger; closed by: clicking any link, pressing Escape, or a resize to >=901px.
When open: body gets .menu-open { overflow:hidden } and aria-expanded flips to "true".
F. THE UI LANGUAGE
F.1 Liquid-metal pill (.pill) — the nav links
display:inline-flex; align-items:center; justify-content:center;
height:var(--nav-h); padding:0 var(--pill-pad); border-radius:var(--pill-radius);
overflow:hidden; position:relative; isolation:isolate;
border:1px solid var(--pill-line);
background:linear-gradient(105deg, #050505 0%, #2a2a2a 48%, #4a4a4a 100%);
color:#e9e9e9; font-size:var(--nav); font-weight:500; letter-spacing:0.005em;
text-decoration:none; transition:border-color .35s ease, background .35s ease,
box-shadow .35s ease, color .35s ease;
.pill::before { content:""; position:absolute; inset:-1px; z-index:-1;
background:linear-gradient(115deg, transparent 30%, rgba(255,255,255,0.16) 50%,
transparent 70%);
transform:translateX(-120%); transition:transform .6s ease }
.pill:hover::before { transform:translateX(120%) }
.pill:hover { border-color:var(--pill-line-hover); color:#ffffff;
background:linear-gradient(105deg, #111111 0%, #3a3a3a 45%, #6a6a6a 100%);
box-shadow:0 0 18px rgba(200,210,230,0.18) }
.pill:focus-visible { outline:2px solid var(--focus); outline-offset:3px }
F.2 Buttons (.btn)
display:inline-flex; align-items:center; justify-content:center;
height:var(--btn-h); padding:0 20px; border-radius:var(--pill-radius);
font-size:var(--btn); font-weight:500; letter-spacing:0.01em;
text-decoration:none; cursor:pointer; border:1px solid transparent;
transition:transform .3s ease, background .3s ease, border-color .3s ease,
box-shadow .3s ease, color .3s ease;
.btn-solid { background:linear-gradient(105deg, #e8ecf2 0%, #ffffff 52%, #d3d9e2 100%);
color:#06070a; border-color:rgba(255,255,255,0.5) }
.btn-solid:hover { transform:translateY(-1px);
box-shadow:0 8px 26px rgba(160,190,220,0.20) }
.btn-line { background:rgba(255,255,255,0.02); color:#ffffff;
border-color:var(--border) }
.btn-line:hover { border-color:rgba(255,255,255,0.42);
background:rgba(255,255,255,0.06) }
.btn-ghost { background:linear-gradient(105deg, rgba(255,255,255,0.06) 0%,
rgba(255,255,255,0.02) 55%, rgba(255,255,255,0.10) 100%);
color:#ffffff; border-color:var(--pill-line) } // header CTA, matches the pills
.btn-sm { height:var(--btn-h); padding:0 16px; font-size:var(--btn) }
Hover lift is translateY(-1px) ONLY — nothing bounces, nothing scales on hover.
F.3 Focus ring, global
:focus-visible { outline:2px solid var(--focus); outline-offset:3px; border-radius:inherit }
Never remove outlines; never set outline:0 without a replacement.
F.4 Anti-flash rule (first three rules of the stylesheet, in this order)
1) html, body { background:#000000 !important; color:#ffffff; }
2) (the inline body attribute from E.1)
3) html, body { background:#000000; background:var(--bg, #000000); margin:0; padding:0;
font-family:var(--font-sans); -webkit-font-smoothing:antialiased;
text-rendering:optimizeLegibility; }
G. MOTION
G.1 Entrance system
.appear { opacity:1 } // RESTING STATE IS VISIBLE — see I
.appear { animation-duration:1.05s; animation-fill-mode:both;
animation-timing-function:cubic-bezier(0.16,1,0.3,1);
animation-delay:var(--d, 0.08s); }
.is-in { animation:none !important; opacity:1 !important; transform:none !important;
clip-path:none !important; filter:none !important; }
G.2 Stagger table (element -> animation -> --d)
brand in-scale 0.08s
nav pill 1 in-scale 0.16s
nav pill 2 in-soft 0.28s
nav pill 3 in-scale 0.40s
nav pill 4 in-soft 0.52s
header CTA in-scale 0.34s
badge in-pop 0.22s
h1 line 1 in-mask 0.42s
h1 line 2 in-mask 0.62s
lede in-soft 0.82s (animation-duration:1.25s)
solid CTA in-btn 0.96s
line CTA in-side 1.10s
stat 1 in-stat 1.12s
stat 2 in-stat 1.28s
stat 3 in-stat 1.44s
(the canvas has no entrance animation; it fades in once via .aurora-canvas
{ animation:in-fade 2.2s ease both } with --d 0s)
G.3 Keyframes — every one ends at opacity 1 and the identity transform
@keyframes in-fade { from { opacity:0 } to { opacity:1 } }
@keyframes in-scale{ from { opacity:0; transform:scale(0.84) }
to { opacity:1; transform:scale(1) } }
@keyframes in-soft { from { opacity:0; transform:translateY(14px) }
to { opacity:1; transform:translateY(0) } }
@keyframes in-mask { from { opacity:0; transform:translateY(40%) }
to { opacity:1; transform:translateY(0) } } // clipped by .headline-line
@keyframes in-pop { 0% { opacity:0; transform:scale(0.90) }
70% { opacity:1; transform:scale(1.03) }
100% { opacity:1; transform:scale(1) } }
@keyframes in-btn { from { opacity:0; transform:translateY(18px) scale(0.94) }
to { opacity:1; transform:translateY(0) scale(1) } }
@keyframes in-side { from { opacity:0; transform:translateX(22px) }
to { opacity:1; transform:translateX(0) } }
@keyframes in-stat { from { opacity:0; transform:translateY(20px) }
to { opacity:1; transform:translateY(0) } }
H. RESPONSIVE TABLE — every breakpoint is a token override table
>=1600px
--logo:17px; --logo-mark:24px; --nav:15px; --nav-h:44px; --btn:15px; --btn-h:44px;
--hero-btn-h:48px; --h1:64px; --lede:18px; --badge:13.5px; --stat-size:15px;
--header-y:28px; --header-x:64px; --stats-x:96px; --stats-y:44px;
--copy-max:980px; --lede-max:540px
>=1920px
--h1:76px; --lede:19px; --badge:14px; --stat-size:15.5px; --header-y:30px; --header-x:72px;
--stats-x:120px; --stats-y:52px; --copy-max:1060px; --lede-max:580px; --hero-gap:96px
1280px – 1599px
--h1:54px; --header-x:48px; --copy-max:900px
901px – 1279px
--h1:42px; --nav-h:36px; --hero-gap:64px; --copy-max:760px; --lede-max:440px;
--stats-x:48px; --stat-gap:28px
>=901px AND (max-height:850px)
--h1:40px; --hero-gap:48px; --header-y:18px; --stats-y:28px
>=901px AND (max-height:720px)
--h1:34px; --hero-gap:32px; --lede:15px; --header-y:14px; --stats-y:22px; --stat-gap:24px
>=901px DESKTOP LOCK (apply in the same block)
html, body { height:100%; overflow:hidden }
.page { height:100dvh; overflow:hidden }
ONE FRAME, NO SCROLL.
<=900px (phone + small tablet — scrolling IS allowed here)
Remove the vh lock: html, body { height:auto; overflow:auto } .page { height:auto; min-height:100dvh }
.nav-desktop { display:none } .burger { display:inline-flex }
--h1:40px; --header-y:18px; --header-x:24px; --hero-gap:56px; --stats-x:24px;
--stats-y:32px; --stat-gap:18px; --copy-max:640px; --lede-max:420px
.hero { padding:16px 20px var(--hero-gap) }
.stats ul { justify-content:center }
Menu overlay per E.5.
<=560px
--h1:34px; --lede:15px; --badge:12px; --stat-size:12.5px; --hero-gap:44px;
--header-x:18px; --stats-x:18px
.hero-actions { flex-direction:column; align-items:stretch; width:100% }
.hero-actions .btn { width:100% }
.headline-line { padding:0.06em 0.10em 0.14em }
.stats ul { flex-direction:column; align-items:center; gap:10px } // three stacked lines
No horizontal overflow at 390px, 768px, 1440px or 1920px: never set a width wider than
100vw, keep --copy-max inside a 20px gutter, and let .headline-line shrink (it is a block,
so it wraps rather than overflows).
I. FAILURE GUARDS
I.1 Resting opacity 1. `.appear { opacity:1 }` is the base rule; the animation only ever
animates FROM 0. If CSS animations are unsupported, disabled, or interrupted, every
element is already in its final state. The page is NEVER blank.
I.2 animationend settlement. On each element's own `animationend`, add `.is-in` so the
element drops the animation and keeps its end state (prevents any post-animation
repaint from re-running the keyframe, and frees compositor layers).
I.3 Two-rAF fallback. After two nested requestAnimationFrames from DOMContentLoaded, for
every `.appear`: if `el.getAnimations().some(a => a.playState === 'running' ||
a.playState === 'finished')` is false, add `.is-in`. Guard with
`typeof el.getAnimations === 'function'` — if absent, add `.is-in` to all.
I.4 Canvas fallback. If `canvas.getContext('2d')` returns null, or sprite creation throws,
hide #aurora, keep the black ground + scrim, set `__DEMO__.ready = true`, push
'aurora-canvas-fallback' into effects, and push nothing into errors. The page must still
look finished (it is a black page with a gradient scrim — acceptable).
I.5 prefers-reduced-motion: reduce
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition:none !important; animation:none !important; }
.appear, .headline-line, .badge-dot, h1 em { opacity:1 !important; transform:none !important;
clip-path:none !important; filter:none !important; }
.grain-svg { animation:none !important }
}
Canvas under reduced motion: build the sprites, draw ONE static frame at t = 0 with the
amber band parked off-frame, then keep the rAF loop running ONLY to increment
__DEMO__.frames (no dt advance, no draw). The visual result is a completely static,
fully readable page; telemetry still advances.
J. THE JAVASCRIPT — exactly this much, nothing more
(function () {
'use strict';
window.__DEMO__ = { name: 'aurora-freight', ready: false, frames: 0, effects: [], errors: [] };
var D = window.__DEMO__;
window.addEventListener('error', function (e) {
D.errors.push(String((e && e.message) || 'error'));
});
// --- 1. entrance settlement + fallback (I.2, I.3) ---
var appear = Array.prototype.slice.call(document.querySelectorAll('.appear'));
appear.forEach(function (el) {
el.addEventListener('animationend', function () { el.classList.add('is-in'); });
});
requestAnimationFrame(function () {
requestAnimationFrame(function () {
appear.forEach(function (el) {
var running = false;
if (typeof el.getAnimations === 'function') {
running = el.getAnimations().some(function (a) {
return a.playState === 'running' || a.playState === 'finished';
});
}
if (!running) { el.classList.add('is-in'); }
});
});
});
// --- 2. mobile menu (E.5) ---
var burger = document.querySelector('.burger');
var menu = document.getElementById('mobile-menu');
function setMenu(open) {
if (!burger || !menu) { return; }
menu.hidden = !open;
burger.setAttribute('aria-expanded', open ? 'true' : 'false');
document.body.classList.toggle('menu-open', open);
}
if (burger && menu) {
burger.addEventListener('click', function () {
setMenu(menu.hidden === true);
});
menu.addEventListener('click', function (e) {
if (e.target.closest('a')) { setMenu(false); }
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { setMenu(false); }
});
window.addEventListener('resize', function () {
if (window.innerWidth >= 901) { setMenu(false); }
});
}
// --- 3. aurora canvas (B) ---
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var canvas = document.getElementById('aurora');
var ctx = canvas && canvas.getContext ? canvas.getContext('2d') : null;
if (!ctx) { // I.4
if (canvas) { canvas.style.display = 'none'; }
D.ready = true; D.effects.push('aurora-canvas-fallback');
return;
}
var PALETTES = [
[[34,211,238],[18,168,154],[124,92,255],[42,107,216],[168,85,247],[240,162,74]],
[[53,224,208],[15,143,191],[91,108,255],[27,79,216],[198,120,247],[255,176,102]],
[[77,227,193],[23,153,127],[139,92,246],[37,99,235],[217,70,239],[245,158,11]]
];
var BANDS = [
{ r:0.62, a:0.58, ax:0.30, ay:0.20, fx:0.0532, fy:0.0411, px:0.0, py:1.7, al:0.62 },
{ r:0.54, a:0.64, ax:0.26, ay:0.24, fx:0.0648, fy:0.0480, px:2.1, py:0.4, al:0.55 },
{ r:0.70, a:0.50, ax:0.34, ay:0.18, fx:0.0439, fy:0.0861, px:4.0, py:3.2, al:0.40 },
{ r:0.48, a:0.70, ax:0.22, ay:0.28, fx:0.0757, fy:0.0576, px:5.6, py:2.6, al:0.34 },
{ r:0.66, a:0.54, ax:0.36, ay:0.16, fx:0.0376, fy:0.1030, px:1.2, py:5.0, al:0.28 }
];
var SPRITE = 256, sprites = [];
function buildSprites() {
for (var p = 0; p < PALETTES.length; p++) {
for (var i = 0; i < 6; i++) {
var c = PALETTES[p][i];
var cv = document.createElement('canvas');
cv.width = SPRITE; cv.height = SPRITE;
var g = cv.getContext('2d');
var grd = g.createRadialGradient(128, 128, 0, 128, 128, 128);
var rgb = c[0] + ',' + c[1] + ',' + c[2];
grd.addColorStop(0.00, 'rgba(' + rgb + ',1)');
grd.addColorStop(0.35, 'rgba(' + rgb + ',0.42)');
grd.addColorStop(0.62, 'rgba(' + rgb + ',0.14)');
grd.addColorStop(0.82, 'rgba(' + rgb + ',0.04)');
grd.addColorStop(1.00, 'rgba(' + rgb + ',0)');
g.fillStyle = grd; g.fillRect(0, 0, SPRITE, SPRITE);
sprites.push(cv);
}
}
}
buildSprites();
var DPR = Math.min(window.devicePixelRatio || 1, 2), SCALE = 0.5;
var W = 0, H = 0, maxDim = 0;
function resize() {
W = Math.max(1, Math.round(window.innerWidth * DPR * SCALE));
H = Math.max(1, Math.round(window.innerHeight * DPR * SCALE));
var cap = Math.sqrt(2600000 / (W * H));
if (cap < 1) { W = Math.round(W * cap); H = Math.round(H * cap); }
canvas.width = W; canvas.height = H;
maxDim = Math.max(W, H);
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'low';
draw(0); // repaint immediately after a resize
}
var pa = 0, pb = 1, mix = 0, fading = false, fadeT0 = 0, mixStart = 0;
var FADE_EVERY = 20000, FADE_DUR = 2600, sinceFade = 0;
var CYCLE = 30;
function startFade() {
mixStart = mix; fading = true; fadeT0 = now; // B.6: never restart from 0
}
function tickFade(now, dt) {
sinceFade += dt * 1000;
if (!fading && sinceFade >= FADE_EVERY) { sinceFade = 0; pb = (pb + 1) % 3; startFade(); }
if (fading) {
var k = Math.min(1, (now - fadeT0) / FADE_DUR);
var e = k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2; // easeInOutCubic
mix = mixStart + (1 - mixStart) * e;
if (k >= 1) { pa = pb; pb = (pb + 1) % 3; mix = 0; fading = false; }
}
}
function draw(t) {
ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1;
ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, W, H);
ctx.globalCompositeOperation = 'screen';
for (var i = 0; i < 5; i++) {
var b = BANDS[i];
var cx = 0.50 * W + b.ax * W * Math.sin(t * b.fx + b.px);
var cy = 0.46 * H + b.ay * H * Math.sin(t * b.fy + b.py);
var rr = b.r * maxDim, dw = 2 * rr, dh = 2 * rr * b.a;
var dx = cx - rr, dy = cy - dh / 2;
var al = b.al;
ctx.globalAlpha = al * (1 - mix);
ctx.drawImage(sprites[pa * 6 + i], dx, dy, dw, dh);
ctx.globalAlpha = al * mix;
ctx.drawImage(sprites[pb * 6 + i], dx, dy, dw, dh);
}
// amber sweep, band 5 (B.4)
var p = (t % CYCLE) / CYCLE;
if (p >= 0.10 && p <= 0.62) {
var u = (p - 0.10) / 0.52;
var ease = 0.5 - 0.5 * Math.cos(Math.PI * u);
var ax = (-0.35 + 1.70 * ease) * W;
var ay = (0.62 - 0.10 * Math.sin(Math.PI * u)) * H;
var env = Math.pow(Math.sin(Math.PI * u), 0.8);
var rr5 = 0.40 * maxDim, dw5 = 2 * rr5, dh5 = 2 * rr5 * 0.46;
var al5 = 0.42 * env;
ctx.globalAlpha = al5 * (1 - mix);
ctx.drawImage(sprites[pa * 6 + 5], ax - rr5, ay - dh5 / 2, dw5, dh5);
ctx.globalAlpha = al5 * mix;
ctx.drawImage(sprites[pb * 6 + 5], ax - rr5, ay - dh5 / 2, dw5, dh5);
}
ctx.globalCompositeOperation = 'source-over'; ctx.globalAlpha = 1;
}
var now = 0, last = 0, lastDraw = 0, t = 0, raf = 0;
function frame(ts) {
raf = requestAnimationFrame(frame);
D.frames++;
if (!last) { last = ts; lastDraw = ts; now = ts; return; }
var dt = Math.min((ts - last) / 1000, 0.05);
last = ts; now = ts;
if (reduce) { return; } // I.5: static image, frames still count
t += dt;
tickFade(ts, dt);
if (ts - lastDraw < 15.5) { return; }
lastDraw = ts;
draw(t);
if (!D.ready) { D.ready = true; }
}
function boot() { resize(); draw(0); D.ready = true; raf = requestAnimationFrame(frame); }
var rt = 0;
window.addEventListener('resize', function () {
clearTimeout(rt); rt = setTimeout(resize, 150);
});
window.addEventListener('orientationchange', function () { resize(); });
document.addEventListener('visibilitychange', function () {
if (document.hidden) { cancelAnimationFrame(raf); raf = 0; }
else if (!raf) { last = 0; raf = requestAnimationFrame(frame); }
});
boot();
D.effects.push('aurora-canvas', 'palette-crossfade', 'amber-sweep', 'grain', 'entrance-stagger', 'pill-shine');
})();
No console.log anywhere. No unhandled promise. No network request other than Google Fonts.
File size target: under 90 KB (this page should land around 22-30 KB).
---
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 <h1> is split into exactly 2 .headline-line elements, each with overflow:hidden
- a #aurora canvas exists with non-zero width and paints non-blank pixels, and __DEMO__.frames advances by >= 20 in 1.2s
- the footer contains exactly 3 .stat items and the page contains exactly one <h1>
- no element overflows the viewport horizontally at 390, 768, 1440 and 1920 widths
- __DEMO__.effects includes 'aurora-canvas' and 'palette-crossfade', and __DEMO__.errors is empty
- 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