Landing Pagestealth/space-bunny-alphapartial cross-check
Solstice
A 60-second solar arc continuously remaps the page from dawn amber through noon white-blue to dusk magenta.
The exact prompt
A. WHAT IS BEING BUILT
Build one complete, valid index.html for a cinematic single-viewport landing page named “SOLSTICE”. The document title must be “SOLSTICE — Solar Energy, From First Light to Full Potential”. Set the root language to lang="en". The visual subject is a solar-energy company whose day begins in amber dawn, reaches white-blue solar noon, and closes in magenta dusk while the sun travels from left to right on a wide 60-second arc. The interface must communicate dependable energy, precision, and continuous operation rather than a consumer electricity shop.
The page structure must use one real header element, one nav element, one main element, one real footer element, and exactly one h1. The h1 contains two spans: the eyebrow line “ENERGY IN MOTION” and the company line “SOLSTICE”; the company line is the page’s principal title. Use semantic section elements for the hero copy, telemetry cluster, primary actions, and status rail. Every visible link, button, and clock remains a real interactive or semantic element with an accessible name. Decorative SVG, gradients, and generated particles must use aria-hidden="true".
Desktop viewport rule: from 901px through 1920px, set html and body to width:100%; height:100%; and the document shell to min-height:100dvh; height:100dvh; overflow:hidden. The only allowed vertical scrolling is at 900px and below, where set height:auto, min-height:100dvh, overflow-x:hidden, and allow normal document scrolling. At every tested width, html, body, and the document shell must have width:100% and max-width:100%; no child may extend beyond the viewport. Prevent horizontal overflow with overflow-x:clip on the page shell, with overflow-x:hidden as the fallback.
Responsive targets are 390x844, 768x1024, 1440x900, and 1920x1080. The implementation must remain legible and free of clipped text at all four sizes. The single rAF visual system must sustain at least 20 frames per 1.2 seconds on a 1440x900 headless viewport. Cache all DOM reads outside the loop. In the loop, animate only transform and opacity. Do not read layout measurements in the loop and do not call getBoundingClientRect, offsetWidth, offsetHeight, getComputedStyle, canvas measureText, or querySelector from the loop.
Do NOT build a navigation drawer, hamburger menu, account area, pricing table, product marketplace, article grid, form, modal, cookie banner, authentication flow, logo carousel, third-party map, image gallery, or secondary page. Do not add a second h1. Do not use React, Tailwind, GSAP, Three.js, Lottie, WebGL, remote media, mp4, webm, animated GIF, remote image, base64 image, icon font, analytics request, or third-party CDN. The only permitted network request is one Google Fonts stylesheet for “DM Sans” at weights 400, 500, 600 and “IBM Plex Mono” at weights 400, 500, loaded with display=swap. Include robust system fallbacks. The resulting file must be under 90 KB and produce no console output.
B. THE BACKDROP
Create a full-viewport .backdrop fixed at inset:0 with z-index:0 and overflow:hidden. It must contain 24 deterministic CSS-drawn star or dust points, 9 large atmospheric cloud ellipses, 1 SVG horizon system, 1 canvas particle field, and 1 canvas sun/reflection compositor. All visible imagery is CSS, SVG, or canvas. Do not use external images.
The page atmosphere has exactly 12 radial-gradient and linear-gradient layers. Its base background must be a linear gradient to bottom. The initial dawn gradient stops are 0% #24152d, 32% #5a2942, 58% #c55245, 78% #f59e54, 92% #fed7a1, and 100% #fff0c7. The noon gradient stops are 0% #123a67, 30% #276b98, 55% #8fcbe3, 78% #dff4fb, 92% #fff9df, and 100% #ffffff. The dusk gradient stops are 0% #18152f, 30% #3d244e, 54% #773356, 76% #bd4768, 91% #e46b6d, and 100% #f7a56f. At runtime, interpolate all six stop colors from dawn to noon during progress 0.000–0.500, then from noon to dusk during progress 0.500–1.000. Convert each #RRGGBB component to a normalized channel, perform linear RGB interpolation, clamp the result to 0–255, and round to the nearest integer. Reassign the complete background string at most 30 times per second, every 33ms, while the sun transform updates every animation frame. This is a scripted compositor update, not layout measurement.
Add a full-width upper glow with a radial-gradient centered at the sun’s x position and 50% y position. Dawn center color is rgba(255,183,91,0.88), noon is rgba(255,255,231,0.98), and dusk is rgba(242,91,122,0.82). At rAF time, set CSS custom properties --sun-x and --glow-x and rewrite the glow background string. The glow uses blur-radius equivalent 70px through one absolutely positioned ellipse with filter:blur(38px); its opacity is 0.72. A second narrow horizon bloom is a 12px-high ellipse centered on the horizon with blur radius 24px and opacity 0.68.
The 24 stars have opacity 0.16–0.54, diameter 1–3px, and individual drift periods between 36s and 72s. Their horizontal travel ranges from 0px to 16px; vertical travel ranges from 0px to 8px. They loop without resizing. The 9 cloud ellipses use widths from 280px to 760px, heights from 32px to 88px, blur from 18px to 44px, horizontal opacity 0.08–0.20, and drift periods from 70s to 118s. Cloud drift is transform-only and stays within x:-220px to x:220px relative to its placement.
The horizon is one SVG with viewBox="0 0 1920 1080" and preserveAspectRatio="none. It contains exactly one hard line from x=0 to x=1920 at y=742. The line uses stroke-width:1, vector-effect:non-scaling-stroke. It appears #fff2ce at dawn, #e8fbff at noon, and #ffc0a8 at dusk; interpolate this color in the same palette update. Beneath it, use one SVG rect of height 338 with a 42px vertical blur filter and a 52px linear gradient whose top color is the interpolated horizon color at 0.36 alpha and whose final color is transparent. Do not blur the hard line itself.
The canvas compositor is 480x270 CSS pixels by default, backing-store resolution 960x540 using devicePixelRatio clamped to 2, so it stays below 1036800 pixels at 1920x1080. At smaller viewports, set backing-store width to floor(cssWidth * 2) and height to floor(cssHeight * 2), with those totals capped at 960x540. Scale with CSS width:100%; height:100%. Redraw it when its backing resolution changes and when the 33ms palette tick changes. Precompute all 72 particles as objects containing normalized x, base y, radius, speed, amplitude, phase, and a precomputed warm/cool tint. The sun uses a 22px-radius core, a 7px amber-white corona ring, and a 64px radial corona at 0.16 alpha in the SVG or DOM layer. Its reflection is a canvas ellipse at the same normalized x, below the horizon, with radiusX 15px, radiusY 4px, vertical scale 0.24, and blur 12px represented by three concentric translucent ellipses rather than ctx.filter. Precompute color strings when the palette changes. Per frame, the rAF handler performs 72 cheap transform-equivalent canvas redraws only when a new palette bucket is active, and the visible sun, reflection, glow, and horizon wrapper are moved using cached normalized coordinates. The per-frame work budget is no more than 1 style write for sun transform, 1 style write for reflection transform, 1 style write for glow x, and 1 canvas redraw only on 33ms boundaries. Target 72 or fewer canvas path fills and 25 or fewer color string updates per 33ms palette bucket.
C. THE LAYER STACK
Build the visual and content stack back to front with these exact z-index values:
z-index 0: .backdrop, containing the base gradient, stars, clouds, upper atmospheric bloom, and solar glow.
z-index 1: .star-field with 24 generated children.
z-index 2: .cloud-field with 9 generated children.
z-index 3: .solar-system, containing the .sun-disc, its two corona rings, and its .sun-reflection.
z-index 4: .horizon-svg with the soft bloom rect and hard 1px line.
z-index 5: .particle-canvas.
z-index 6: .vignette, a pointer-events:none radial overlay that darkens all edges by 18%.
z-index 10: header.site-header.
z-index 20: main.hero.
z-index 30: footer.site-footer and the bottom status rail.
z-index 40: .focus-halo if required, though prefer native outline.
z-index 50: .grain, an inline SVG feTurbulence texture at opacity 0.035, pointer-events:none.
The header, main, and footer must be position:relative with their declared z-index. The canvas and horizon are aria-hidden. Ensure pointer events do not obstruct controls. Decorative layers must not be focusable.
D. TOKEN TABLE
Define every reusable visual value in :root using these exact tokens:
--ink:#fdfaf3;
--ink-muted:rgba(253,250,243,0.68);
--ink-dim:rgba(253,250,243,0.46);
--accent:#fb923c;
--accent-bright:#ffd29a;
--accent-deep:#b7432f;
--line:rgba(255,255,255,0.22);
--glass:rgba(17,23,39,0.24);
--glass-strong:rgba(13,18,32,0.58);
--focus:#fff4c7;
--header-h:72px;
--footer-h:72px;
--page-x:clamp(24px,5.2vw,100px);
--page-y:clamp(28px,4.4vh,44px);
--content-max:1760px;
--hero-columns:minmax(0,1.35fr) minmax(320px,0.65fr);
--hero-gap:clamp(36px,7vw,128px);
--h1-size:clamp(64px,8.4vw,150px);
--h1-line:0.82;
--h1-tracking:-0.075em;
--eyebrow-size:11px;
--eyebrow-tracking:0.28em;
--lede-size:clamp(16px,1.18vw,20px);
--lede-line:1.55;
--lede-width:560px;
--button-h:54px;
--button-pad-x:25px;
--button-radius:999px;
--pill-pad-x:14px;
--pill-h:32px;
--clock-size:12px;
--clock-tracking:0.14em;
--card-w:356px;
--card-pad:24px;
--radius-card:18px;
--radius-control:999px;
--sun-size:44px;
--reflection-w:32px;
--reflection-h:8px;
--horizon-y:68.7%;
--motion-fast:180ms;
--motion-base:520ms;
--motion-slow:900ms;
--ease-out:cubic-bezier(0.16,1,0.3,1);
--ease-arc:cubic-bezier(0.22,0.68,0.18,1);
--safe-bottom:env(safe-area-inset-bottom,0px);
--safe-right:env(safe-area-inset-right,0px);
--safe-left:env(safe-area-inset-left,0px).
Do not introduce off-token typography, spacing, color, or sizing for major components. Sub-components may use 4px multiples or explicit pixel values documented in the composition.
E. THE COMPOSITION
Create a .site-shell with max-width:1920px; margin-inline:auto; position:relative; min-height:100dvh; background:transparent. At desktop, the shell forms a 72px header, flexible hero, and 72px footer using display:grid; grid-template-rows:var(--header-h) minmax(0,1fr) var(--footer-h).
The header is a real header.site-header with height:72px; display:grid; grid-template-columns:1fr auto 1fr; align-items:center; padding:0 var(--page-x). The left anchor is an accessible home link named “SOLSTICE home”; its visible text is “SOLSTICE” in 13px, 600 weight, letter-spacing:0.18em. The middle nav contains exactly two links: “Mission” and “Technology”. Right-align a visible status pill containing a 6px green dot and the exact text “GRID ONLINE”. Give the nav 8px gap. On widths below 701px, hide the center nav with display:none; keep the logo and status pill.
The main.hero occupies the center grid row and uses max-width:var(--content-max); width:100%; margin:0 auto; padding:var(--page-y) var(--page-x); display:grid; grid-template-columns:var(--hero-columns); align-items:center; gap:var(--hero-gap). Left hero copy contains exactly this hierarchy and copy: eyebrow “ENERGY IN MOTION”; the single h1 with “SOLSTICE”; lede “Capture the day’s brightest hours. Solstice turns clean solar power into steady, intelligent energy for every horizon.”; CTA row with a primary link named “Explore our technology” with visible label “EXPLORE TECHNOLOGY” and a secondary link named “See our mission” with visible label “OUR MISSION”. The h1 is 1–150px according to the responsive tokens. Its eyebrow span is 11px, 500 weight, uppercase, 0.28em tracking, and must be positioned as a block above the company line. The lede max-width is 560px. Buttons sit in a 12px gap flex row.
The right hero instrument is a 356px wide .instrument-card with 24px padding, 18px radius, border:1px solid rgba(255,255,255,0.18), background:linear-gradient(145deg,rgba(14,20,38,0.56),rgba(14,20,38,0.22)), backdrop-filter:blur(18px), box-shadow:0 24px 70px rgba(7,10,24,0.24). It contains a top row labelled “LOCAL SOLAR TIME” and a 12px monospace simulated clock with exact initial text “05:42:18”. The clock has aria-label="Simulated local solar time"; do not mark it aria-live. Add a horizontal 1px separator with 18px vertical margins. Add a 184px-wide arc mini-chart rendered as inline SVG: one 0.8px background arc, one 2px accent arc with stroke-dasharray:289 and stroke-dashoffset:289, and a 5px moving sun marker. Beneath it, place three numeric rows: “CAPTURE” with value “24.8 GWh”, “PEAK OUTPUT” with value “1.42 GW”, and “CO₂ AVOIDED” with value “18,604 t”. Each label is 9px uppercase with 0.18em tracking; each value is 15px monospace. Add a footer pill with exact text “98.7% AVAILABILITY”.
The bottom footer is a real footer.site-footer with height:72px; padding:0 var(--page-x); display:grid; grid-template-columns:1fr auto 1fr; align-items:center; border-top:1px solid var(--line); background:linear-gradient(to top,rgba(7,10,24,0.36),transparent). Left text is “POWERED BY THE SUN”. Center is a status rail containing “DAWN”, a 54px line, “NOON”, and “DUSK”; its active label changes by phase. Right text is “40.7128° N / 74.0060° W”. Use 10px monospace uppercase text and 0.18em tracking.
The hard horizon visually crosses at 68.7% viewport height. Keep it behind content but above the lower fields. Place the sun arc from x=8% at y=79% through x=50% at y=17% to x=92% at y=79%. The reflection’s center is x equal to the sun x and y equal to 74.5% plus 7.5% of the sun’s altitude loss. Its opacity is 0.20 at dawn, 0.14 at noon, and 0.25 at dusk.
F. THE UI LANGUAGE
The primary link is 54px high with 25px horizontal padding, 999px radius, 12px monospace font, 600 weight, 0.12em tracking, and no visible border. Its exact resting gradient is linear-gradient(135deg,#fff1ce 0%,#ffd09a 48%,#fb923c 100%); text is #2a1730. On hover, transform:translateY(-2px); background:linear-gradient(135deg,#ffffff 0%,#ffe4bd 48%,#ffad65 100%); box-shadow:0 10px 28px rgba(251,146,60,0.30). On active, transform:translateY(0); box-shadow:none. Its focus-visible ring is 3px solid var(--focus), offset 4px.
The secondary link is 54px high, 25px horizontal padding, 999px radius, 1px solid rgba(255,255,255,0.52), background:rgba(12,18,34,0.18), text #fffaf1. On hover, background:rgba(255,255,255,0.12), border-color:rgba(255,255,255,0.82), transform:translateY(-2px). On active, transform:translateY(0).
The status pill is 32px high with 14px horizontal padding, 999px radius, border:1px solid rgba(255,255,255,0.22), background:rgba(11,17,32,0.26), and 10px monospace uppercase text. The online dot is 6px, #9ff0bb, with box-shadow:0 0 12px rgba(159,240,187,0.74).
All links use color:inherit, text-decoration:none, cursor:pointer. Every interactive target has a minimum 44px hit area, including header links whose visual height is smaller. Header link hover opacity transitions from 1 to 0.72 over 180ms. Global focus-visible is outline:3px solid var(--focus); outline-offset:4px. Do not remove outline without replacing it.
G. MOTION
Use the following initial stagger. Delays start after the first paint. Elements must already be present and readable.
Header logo: opacity 0 to 1 and translateY(-10px) to translateY(0), 520ms, delay 80ms.
Header nav links: opacity 0 to 1 and translateY(-8px) to translateY(0), 520ms, delays 140ms and 190ms.
Grid status pill: opacity 0 to 1 and scale(0.96) to scale(1), 520ms, delay 220ms.
Hero eyebrow: opacity 0 to 1 and translateY(16px) to translateY(0), 720ms, delay 100ms.
H1 company line: opacity 0 to 1 and translateY(28px) to translateY(0), 900ms, delay 160ms.
Hero lede: opacity 0 to 1 and translateY(18px) to translateY(0), 720ms, delay 300ms.
Primary CTA: opacity 0 to 1 and translateX(-16px) to translateX(0), 720ms, delay 410ms.
Secondary CTA: opacity 0 to 1 and translateX(-16px) to translateX(0), 720ms, delay 470ms.
Instrument card: opacity 0 to 1 and translateY(22px) to translateY(0) and scale(0.98) to scale(1), 900ms, delay 280ms.
Footer status rail: opacity 0 to 1 and translateY(10px) to translateY(0), 720ms, delay 520ms.
Footer metadata: opacity 0 to 1, 720ms, delay 600ms.
Named keyframes must be enter-up, enter-right, reveal-card, and drift-cloud. enter-up runs from opacity:0; transform:translateY(22px) to opacity:1; transform:translateY(0). enter-right runs from opacity:0; transform:translateX(-18px) to opacity:1; transform:translateX(0). reveal-card runs from opacity:0; transform:translateY(22px) scale(0.98) to opacity:1; transform:translateY(0) scale(1). drift-cloud alternates 0%, 100%, and 0% translate positions defined per child. Apply animation-fill-mode:both. Do not animate height, width, top, left, filter, background-position, or box-shadow continuously in the rAF loop.
The solar cycle duration is 60000ms. Progress starts at 0 on load and advances from elapsed modulo 60000 divided by 60000. Sun x is lerp from 0.08 to 0.92. Sun y follows a quarter ellipse: y=0.79-0.62*sqrt(max(0,1-pow((x-0.5)/0.42,2))). This places the apex at progress 0.5. The active phase label is DAWN for p<0.25, NOON for p from 0.25 through 0.75, and DUSK for p>0.75. The clock maps p=0 to 05:42:00 and p=0.5 to 12:00:00 and p=1 to 18:18:00, using piecewise linear interpolation in minutes. Format as HH:MM:SS.
H. RESPONSIVE TABLE
At 1440x900 and 1920x1080, use the default tokens. Keep the hero centered, card width 356px, header/footer 72px, and page-x clamp values.
At 768x1024 and all widths from 901px to 1100px, set --page-x:42px; --page-y:30px; --hero-columns:minmax(0,1.2fr) minmax(290px,0.8fr); --hero-gap:42px; --card-w:310px; --card-pad:20px; --h1-size:92px; --lede-size:17px. Footer status rail remains visible. At widths 901–1100, header nav gap is 6px and header status pill hides only its text through display:none inside the pill while preserving the named anchor if that pill is interactive; because the status pill is non-interactive, hide the entire pill and its adjacent gap with display:none.
At max-width:900px, set html, body, and .site-shell to height:auto, min-height:100dvh, overflow:visible; set .site-header height:64px; grid-template-columns:1fr auto; padding-inline:22px. Set .site-footer height:auto, min-height:72px; padding:20px 22px calc(20px + var(--safe-bottom)); grid-template-columns:1fr auto; row-gap:16px. Set main.hero height:auto; grid-template-columns:1fr; gap:32px; padding:54px 22px 46px. Set --h1-size:78px; --page-x:22px; --card-w:min(100%,420px); --instrument-card max-width:100%. Put the instrument after the copy. The horizon remains fixed and follows the viewport while the document scrolls.
At max-width:600px, set --page-x:18px; --page-y:22px; --h1-size:clamp(58px,19vw,82px); --eyebrow-size:10px; --lede-size:16px; --lede-line:1.5. Header padding-inline:18px; footer padding-inline:18px. Hero padding:46px 18px 40px. CTA row is display:grid; grid-template-columns:1fr; width:min(100%,340px); gap:10px; both controls width:100%. Footer metadata spans both columns on its own row; footer grid-template-columns:1fr. Instrument width:100%, max-width:420px, and min-width:0. Status rail display:none. Header center nav remains display:none.
At max-height:760px and min-width:901px, set --page-y:20px; --button-h:48px; --card-pad:18px; --h1-size:104px; --lede-size:16px. Header and footer remain 64px and 64px. Hero grid is align-items:center; instrument arc chart height is 154px; three telemetry rows use 13px vertical padding.
At max-height:680px and min-width:901px, set --page-y:14px; --header-h:60px; --footer-h:60px; --button-h:44px; --card-pad:15px; --card-w:310px; --h1-size:82px; --lede-size:15px; --lede-line:1.38. Reduce eyebrow top/bottom spacing by 6px, CTA top margin to 24px, card row gaps to 9px, and mini-chart to 128px. If content would exceed 600px of hero height, allow only the main hero to scroll internally; keep header and footer fixed to the shell.
I. FAILURE GUARDS
Every entry animation must start with opacity 0 only within a .motion-ready wrapper, and after animation completion its computed opacity must be 1. If a CSS animation is unsupported, failed, or cancelled, JavaScript must clear animation-name and set opacity 1 and transform:none on all [data-enter] elements after 1800ms. Resting opacity for all visible content must be 1.
The 60-second cycle must start only after first paint. Wrap runtime setup in requestAnimationFrame, then requestAnimationFrame, then call startRuntime(). This two-rAF fallback guarantees CSS entry animations and initial paint have begun before the telemetry object is marked ready. A missing canvas 2D context must not throw: hide only the canvas particle layer, keep CSS sun, reflection, gradient, and clock active, push “canvas-unavailable” into effects, and continue the rAF loop. Missing document.fonts must not block readiness.
Inside the prefers-reduced-motion:reduce block, set all *, *::before, and *::after to animation-duration:0.001ms!important; animation-iteration-count:1!important; transition-duration:0.001ms!important; scroll-behavior:auto!important. Set [data-enter], .star, .cloud, .grain to animation:none!important; opacity:1!important; transform:none!important. Freeze the sun at x=0.50, y=0.17, palette at noon, clock at 12:00:00, and the mini-chart at noon. Continue one requestAnimationFrame telemetry loop only if necessary to increment frames, but do not repaint changing visual state. The page must remain complete and readable.
J. THE EXACT JS
Place one script before </body>. Use “use strict”. Create the exact telemetry object first with:
window.__DEMO__ = { name: 'solstice', ready: false, frames: 0, effects: [], errors: [] };
Register window error handling immediately, with preventDefault disabled and no console output. Push String(event.message || 'window-error') into window.__DEMO__.errors. Register unhandledrejection similarly. Register each completed entry animation by listening for animationend on [data-enter], pushing “enter-ui” once. Push “palette-cycle”, “solar-arc”, “canvas-particles”, and “reduced-motion-static” when those systems start. Do not push duplicate effect names.
Cache the following once in startRuntime(): document.documentElement, body, backdrop, sun, corona, reflection, glow, miniMarker, arc, card, clock, phaseLabel, canvas, and a precomputed NodeList of stars, clouds, and [data-enter] elements. Use cached viewport dimensions from innerWidth and innerHeight once per resize event, never inside the loop. The resize handler is passive and only calls resizeCanvas(), which writes canvas dimensions and cached cssWidth/cssHeight values.
Use 72 deterministic particles generated with a seeded integer function:
let seed=73421;
function rand(){ seed=(seed*1664525+1013904223)>>>0; return seed/4294967296; }
Store particle fields as x=rand(), y=0.18+rand()*0.67, r=0.4+rand()*1.8, speed=0.004+rand()*0.012, amp=2+rand()*7, phase=rand()*6.283185307, alpha=0.10+rand()*0.34. Precompute no per-frame object allocations.
Use these exact helpers:
const clamp=(v,a,b)=>Math.min(b,Math.max(a,v));
const lerp=(a,b,t)=>a+(b-a)*t;
const mixColor=(a,b,t)=>'rgb('+Math.round(lerp(a[0],b[0],t))+','+Math.round(lerp(a[1],b[1],t))+','+Math.round(lerp(a[2],b[2],t))+')';
const rgba=(c,a)=>'rgba('+c[0]+','+c[1]+','+c[2]+','+a+')';
const smooth=(t)=>t*t*(3-2*t);
const DAWN=[36,21,45], NOON=[18,58,103], DUSK=[24,21,47];
const DAWN_LOW=[245,158,84], NOON_LOW=[220,244,251], DUSK_LOW=[228,107,109];
const GLOW_DAWN=[255,183,91], GLOW_NOON=[255,255,231], GLOW_DUSK=[242,91,122];
const REF_DAWN=[255,190,103], REF_NOON=[240,253,255], REF_DUSK=[247,112,128];
paletteAt(p) must return p<=0.5 ? {low:mixColor(DAWN_LOW,NOON_LOW,smooth(p*2)),glow:mixColor(GLOW_DAWN,GLOW_NOON,smooth(p*2)),ref:mixColor(REF_DAWN,REF_NOON,smooth(p*2))} : {low:mixColor(NOON_LOW,DUSK_LOW,smooth((p-0.5)*2)),glow:mixColor(GLOW_NOON,GLOW_DUSK,smooth((p-0.5)*2)),ref:mixColor(REF_NOON,REF_DUSK,smooth((p-0.5)*2))}.
Each low and glow color object must have its three channel values available for rgba conversion; therefore use rgb arrays for those entries instead of strings: [36,21,45], [18,58,103], [24,21,47], [245,158,84], [220,244,251], [228,107,109], and glow arrays as listed. The final CSS background is:
'linear-gradient(to bottom,'+rgba(colors[0],1)+' 0%,'+rgba(colors[1],1)+' 32%,'+rgba(colors[2],1)+' 58%,'+rgba(colors[3],1)+' 78%,'+rgba(colors[4],1)+' 92%,'+rgba(colors[5],1)+' 100%)'
Generate the six arrays by mixing each matching DAWN and NOON stop, or each matching NOON and DUSK stop. Assign it to backdrop.style.background only when the 33ms bucket changes. Set glow.style.transform once per rAF to translate3d(calc(var(--sun-x) - 50%),-18%,0), and set root.style.setProperty('--glow-x',(x*100).toFixed(2)+'%').
For normal motion set start=performance.now() and define frame(now):
window.__DEMO__.frames+=1;
const p=((now-start)/60000)%1;
const x=lerp(0.08,0.92,p);
const q=(x-0.5)/0.42;
const altitude=Math.sqrt(Math.max(0,1-q*q));
const y=0.79-0.62*altitude;
sun.style.transform='translate3d('+(x*100).toFixed(3)+'vw,'+(y*100).toFixed(3)+'vh,0)';
corona.style.transform='translate3d('+(x*100).toFixed(3)+'vw,'+(y*100).toFixed(3)+'vh,0) scale(1)';
reflection.style.transform='translate3d('+(x*100).toFixed(3)+'vw,'+(0.745+(1-altitude)*0.075)*100+'.000vh,0)';
miniMarker.setAttribute('transform','translate(92 '+x*184+',92 -46*altitude+')');
if(now-lastPaint>=33){ lastPaint=now; applyPalette(p); updateClock(p); updateTelemetry(p); drawParticles(p); }
requestAnimationFrame(frame).
Do not call setAttribute on miniMarker every frame; update its transform only in the 33ms palette bucket. The initial transform values are written before the loop.
updateClock(p) maps p to total minutes: p<0.5 ? lerp(342,720,p*2) : lerp(720,1098,(p-0.5)*2). Convert floor(totalMinutes*60) into hours, minutes, seconds, and set clock.textContent to padded values only when the second changes. updateTelemetry(p) sets the active phase text, updates mini arc dashoffset from 289 to 0 by p, and updates reflection opacity.
drawParticles(p) clears with clearRect, paints each particle at px=(particle.x*100*cachedWidth+Math.sin(p*6.283+phase)*amp)*scale, py=(particle.y*100*cachedHeight+p*speed*cachedHeight)%cachedHeight. Keep every wrapped y within cachedHeight. Paint only a 1.4px radius soft point and skip particles where opacity is below 0.12. Do not use ctx.filter, ctx.shadowBlur, or gradients inside the particle loop. If ctx exists, fill a precomputed current particle tint string and call ctx.beginPath/arc/fill at most 72 times.
For reduced motion, set p=0.5, call applyPalette(0.5), updateClock(0.5), updateTelemetry(0.5), and the exact noon transform values x=0.50, y=0.17, reflection y=0.7825. Push “reduced-motion-static”. Increment telemetry frames once per rAF with a separate loop that performs no DOM writes. Set window.__DEMO__.ready=true after startRuntime returns.
---
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, document.documentElement.scrollHeight does not exceed innerHeight, overflow is hidden, and exactly one h1 with the visible text SOLSTICE exists inside real header, main, nav, and footer landmarks.
- Over a 1200ms interval at 1440x900, window.__DEMO__.frames increases by at least 20, window.__DEMO__.ready is true, and the object has the exact keys name: solstice, ready, frames, effects, and errors.
- With prefers-reduced-motion set to reduce, the background and visible content are static, the clock reads 12:00:00, the sun remains at x=50% and y=17%, and every visible text element has opacity 1.
- At 390x844, 768x1024, 1440x900, and 1920x1080, scrollWidth is no greater than clientWidth, all four primary labels are present, and no visible control lacks an accessible name or a 3px focus-visible outline.
- The page creates no network requests other than the permitted Google Fonts stylesheet and contains no img, picture, video, base64 media, external script, external stylesheet, or framework dependency.
- 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