Skip to content
LandingPagePrompts
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)

  1. the <h1> is split into exactly 2 .headline-line elements, each with overflow:hidden
  2. a #aurora canvas exists with non-zero width and paints non-blank pixels, and __DEMO__.frames advances by >= 20 in 1.2s
  3. the footer contains exactly 3 .stat items and the page contains exactly one <h1>
  4. no element overflows the viewport horizontally at 390, 768, 1440 and 1920 widths
  5. __DEMO__.effects includes 'aurora-canvas' and 'palette-crossfade', and __DEMO__.errors is empty
  6. window.__DEMO__.ready becomes true within 5s
  7. window.__DEMO__.frames advances (>= 20 frames in 1.2s)
  8. window.__DEMO__.errors is empty
  9. zero console errors and zero page errors
  10. zero failed network requests
  11. exactly one <h1> with non-empty text
  12. header, main, footer landmarks present
  13. no horizontal overflow at 390 / 768 / 1440 widths
  14. no horizontal overflow after 2s of animation
  15. prefers-reduced-motion: reduce leaves the page usable and errors empty

More landing page prompts