Landing Pagestealth/space-bunny-alphaverified 20/20partial cross-check
Nebula
Precomputed volumetric nebula drift with 240 depth-sorted stars and restrained observatory copy.
The exact prompt
A. WHAT IS BEING BUILT
Create one file named index.html containing a complete, framework-free landing page for a fictional space-observatory company named “NEBULA”. Set the document title to “NEBULA — Deep Space Observatory”, set the document language to en, and include <meta name="viewport" content="width=device-width, initial-scale=1">. Build with semantic <header>, <nav>, <main>, and <footer> elements. Include exactly one <h1> and no additional h1 elements.
The page is a single-viewport cinematic observatory landing page. It must present a precomputed five-layer canvas nebula, 240 canvas stars, a centred dark readability scrim, compact navigation, one primary call to action, and a small telemetry footer. The visual target is deep black-indigo space with a violet cloud core, cyan lower-left fringe, warm rose right fringe, and crisp white-violet stars.
At widths of 901px and above, set html, body, and the page shell to width:100%; height:100dvh; min-height:100dvh; overflow:hidden. Do not permit document scrolling. At widths of 900px and below, allow vertical document scrolling and prevent horizontal overflow. The page must render without errors at 390x844, 768x1024, 1440x900, and 1920x1080.
Do NOT build a dashboard, article, pricing page, form, modal, carousel, secondary page, login screen, account system, video background, image gallery, loading screen, onboarding sequence, cookie banner, external feed, or multi-section scrolling site. Do NOT use React, Tailwind, GSAP, Three.js, Lottie, SVG filters, CSS animation filters, remote images, external media, base64 image data, third-party scripts, or third-party stylesheets. The only permitted network request is an optional Google Fonts stylesheet; use Georgia as the required system fallback if it is unavailable. Do not generate clouds or stars through per-pixel work in the requestAnimationFrame loop.
B. THE BACKDROP
Use one fixed full-viewport <canvas id="space" aria-hidden="true"> as the visual backdrop. Render the nebula and stars in that single visible canvas. Do not expose any decorative visual as a focus target or duplicate it as DOM markup.
Create five offscreen canvas layers at startup. Use a seeded value-noise generator built from a 256-entry seeded permutation table and deterministic integer hash arithmetic. Each cloud layer must be generated once into its own OffscreenCanvas when supported, otherwise into a detached regular canvas. Each layer uses a logical canvas resolution equal to viewportWidth divided by 2 and viewportHeight divided by 2, clamped to a maximum width of 960 and maximum height of 540. Set each visible canvas backing-store resolution to viewportWidth multiplied by devicePixelRatio and viewportHeight multiplied by devicePixelRatio, capped at a pixel ratio of 2.
Generate the five cloud layers from this fixed layer set:
1. Core violet: 2,048x1,152 offscreen canvas at 0.5 viewport scale, six octaves, base frequency 0.0042, amplitude 0.31, colour stops rgba(55,27,115,0), rgba(124,78,226,0.34), rgba(190,157,255,0.42), rgba(49,18,91,0).
2. Lower cyan veil: 2,048x1,152 offscreen canvas at 0.5 viewport scale, five octaves, base frequency 0.0031, amplitude 0.24, colour stops rgba(17,55,91,0), rgba(30,164,192,0.22), rgba(117,235,230,0.28), rgba(9,39,72,0).
3. Right rose cloud: 2,048x1,152 offscreen canvas at 0.5 viewport scale, five octaves, base frequency 0.0028, amplitude 0.22, colour stops rgba(94,27,70,0), rgba(224,91,138,0.19), rgba(255,182,191,0.22), rgba(75,18,62,0).
4. Upper indigo fog: 2,048x1,152 offscreen canvas at 0.5 viewport scale, four octaves, base frequency 0.0022, amplitude 0.19, colour stops rgba(18,22,68,0), rgba(57,63,154,0.20), rgba(126,122,226,0.18), rgba(9,14,43,0).
5. Dark central dust: 2,048x1,152 offscreen canvas at 0.5 viewport scale, four octaves, base frequency 0.0036, amplitude 0.27, colour stops rgba(4,5,19,0), rgba(5,7,24,0.38), rgba(1,3,13,0.44), rgba(0,0,0,0).
The 2,048x1,152 values describe the maximum precompute texture coordinate system. Actual texture dimensions are viewport-scaled by 0.5 and clamped by the maximum dimensions above. Do not recompute noise when the viewport changes; regenerate all five layers only on a debounced resize of 180ms, and only when the rendered canvas dimensions change.
Draw the backdrop in this fixed order per frame: fill the full visible canvas with #03040c; draw the five cloud textures using source-over with globalAlpha values 0.94, 0.76, 0.68, 0.74, and 0.86; draw the star field; draw the central CSS scrim over the complete canvas; draw a 1px inner border. The cloud drift periods are 74s, 91s, 113s, 137s, and 157s. Each layer receives a fixed sinusoidal translation of no more than 18px horizontally and 12px vertically, plus a scale from 1.00 to 1.025. Translation is derived from elapsed milliseconds and is applied with ctx.setTransform or ctx.translate. Do not alter cloud pixels, filter state, or shadow state during the loop.
Generate exactly 240 star specks. Split them into 3 depth groups: 72 near stars with radius 0.7 to 1.3px and brightness 0.62 to 0.92; 112 middle stars with radius 0.45 to 0.9px and brightness 0.35 to 0.70; 56 far stars with radius 0.25 to 0.55px and brightness 0.20 to 0.48. Seed every position, radius, brightness, and drift phase deterministically. Draw the far group first, then the middle group, then the near group. Apply a near-group horizontal drift of 4px over 62s, a middle-group drift of 2px over 88s, and a far-group drift of 1px over 131s. Draw stars as filled circles with no shadowBlur and no ctx.filter. A star’s twinkle is opacity-only and uses its seeded phase with a 3.8s, 5.1s, or 7.4s period. The total per-frame work must remain below 12ms on a 1440x900 headless viewport. The loop may perform one canvas clear, one background fill, five precomputed drawImage calls, one 240-item star loop, and one scrim draw. Do not allocate arrays, objects, gradients, or strings inside the loop.
C. THE LAYER STACK
Build the stacking context on the page shell with position:relative and isolation:isolate. Use these z-index values from back to front:
- Backdrop canvas: z-index:0.
- Backdrop vignette: z-index:1, pointer-events:none.
- Readability scrim: z-index:2, pointer-events:none.
- Content scrim accent: z-index:3, pointer-events:none.
- Header: z-index:10.
- Main hero: z-index:20.
- Footer: z-index:30.
- Focus ring treatment: z-index:40.
- Reduced-motion notice: z-index:50, display:none by default.
The canvas must use position:fixed; inset:0; width:100%; height:100%; display:block. The page shell must have no transform, filter, or backdrop-filter that creates an unintended containing block for fixed positioning. Do not add a second canvas.
D. TOKEN TABLE
Define these exact custom properties on :root and use them consistently:
--ink: #03040c
--ink-raised: #0a0b1c
--text: #f5f3ff
--text-soft: #c6c1df
--text-muted: #918ba9
--accent: #a78bfa
--accent-bright: #ddd6fe
--accent-deep: #6d4bd1
--cyan: #75ebe6
--rose: #ffb6bf
--line: rgba(245,243,255,0.18)
--line-strong: rgba(221,214,254,0.38)
--scrim: rgba(3,4,12,0.56)
--scrim-soft: rgba(3,4,12,0.28)
--glass: rgba(12,10,31,0.48)
--button-shadow: rgba(0,0,0,0.32)
--font-display: Georgia, 'Times New Roman', serif
--font-ui: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
--page-padding-x: 40px
--page-padding-top: 26px
--page-padding-bottom: 22px
--header-height: 42px
--nav-gap: 26px
--hero-max-width: 920px
--hero-copy-width: 720px
--eyebrow-size: 11px
--eyebrow-tracking: 0.24em
--title-size: clamp(58px, 7.2vw, 122px)
--title-line: 0.88
--title-weight: 400
--title-spacing: -0.065em
--lede-size: clamp(16px, 1.35vw, 21px)
--lede-line: 1.55
--lede-width: 590px
--button-height: 48px
--button-padding-x: 24px
--button-radius: 999px
--pill-height: 30px
--pill-radius: 999px
--footer-gap: 18px
--footer-size: 10px
--footer-tracking: 0.16em
--scrim-center: rgba(3,4,12,0.70)
--scrim-edge: rgba(3,4,12,0)
--ease-ui: cubic-bezier(0.22, 1, 0.36, 1)
--ease-drift: linear
Do not introduce undocumented sizing or colour literals in CSS. The canvas paint may use the fixed colour literals specified in section B.
E. THE COMPOSITION
Header is positioned absolute at top:0, left:0, right:0, height:var(--header-height), padding:var(--page-padding-top) var(--page-padding-x) 0, display:flex, align-items:center, justify-content:space-between. The brand is an anchor with accessible name “NEBULA home” and exact visible text “NEBULA”. Use the display font at 17px, letter-spacing:0.22em, colour:var(--text). Navigation contains exactly three links in this order: “Mission”, “Signals”, and “Contact”. The nav display:flex; gap:var(--nav-gap); align-items:center. Each link is colour:var(--text-soft), font-size:12px, letter-spacing:0.08em, text-decoration:none. Each link has a 4px focus-visible ring.
Main is a full-viewport grid with min-height:100dvh, display:grid; place-items:center; padding:112px var(--page-padding-x) 92px; text-align:center. The hero is width:min(var(--hero-max-width), 100%); margin-inline:auto. The hero contains an eyebrow pill reading “DEEP SPACE OBSERVATORY / 01”, a single h1 with the exact two-line markup “NEBULA<br><span>IN THE DARK</span>”, a lede reading “We map the invisible structures between stars, turning quiet photons into a clearer view of the universe.”, and two controls. The controls are an anchor with visible text “Explore the signal” and a secondary anchor with visible text “Read our mission”. The title span uses colour:var(--accent-bright). The h1 uses the display font, font-size:var(--title-size), line-height:var(--title-line), font-weight:var(--title-weight), letter-spacing:var(--title-spacing), margin:18px 0 24px, text-wrap:balance. The lede uses width:min(var(--lede-width), 100%), margin:0 auto 30px, colour:var(--text-soft), font-size:var(--lede-size), line-height:var(--lede-line), text-wrap:balance. Controls use display:flex; justify-content:center; gap:12px; flex-wrap:wrap.
Footer is absolute at bottom:0, left:0, right:0, padding:0 var(--page-padding-x) var(--page-padding-bottom), display:flex; align-items:center; justify-content:space-between; gap:var(--footer-gap); font-size:var(--footer-size), letter-spacing:var(--footer-tracking), text-transform:uppercase, colour:var(--text-muted). The left text is “NEBULA / OBSERVATORY SYSTEMS”. The right text is “LIVE FIELD WINDOW · 23° 41′ N”. A centred footer rule is not required. Do not place any other visible copy in the viewport.
F. THE UI LANGUAGE
The primary anchor has height:var(--button-height), padding:0 var(--button-padding-x), display:inline-flex, align-items:center, justify-content:center, border-radius:var(--button-radius), background:linear-gradient(135deg, #c4b5fd 0%, var(--accent) 52%, #7c5ce2 100%), color:#100a22, font-size:12px, font-weight:700, letter-spacing:0.06em, text-decoration:none, box-shadow:0 10px 28px var(--button-shadow). Hover state changes transform:translateY(-2px), background:linear-gradient(135deg, #ede9fe 0%, #c4b5fd 52%, #a78bfa 100%), and box-shadow:0 14px 34px rgba(167,139,250,0.28). The secondary anchor has the same height, padding, radius, font-size, letter-spacing, and border treatment, with border:1px solid var(--line-strong), background:rgba(12,10,31,0.36), color:var(--text). Hover state changes background:rgba(167,139,250,0.14), border-color:var(--accent), and transform:translateY(-2px).
The eyebrow pill is display:inline-flex, align-items:center, height:var(--pill-height), padding:0 13px, border:1px solid var(--line), border-radius:var(--pill-radius), background:var(--glass), color:var(--accent-bright), font-size:var(--eyebrow-size), font-weight:700, letter-spacing:var(--eyebrow-tracking), line-height:1. Hover is not required because the pill is not a control. Every anchor must have a visible :focus-visible ring using outline:3px solid var(--accent-bright); outline-offset:4px. Do not remove native focus visibility.
G. MOTION
Use named CSS keyframes exactly as follows:
@keyframes nebulaRise {
from { opacity:0; transform:translate3d(0,18px,0); }
to { opacity:1; transform:translate3d(0,0,0); }
}
@keyframes nebulaFade {
from { opacity:0; }
to { opacity:1; }
}
@keyframes signalPulse {
0%,100% { transform:scale(1); opacity:0.88; }
50% { transform:scale(1.04); opacity:1; }
}
Apply nebulaFade with duration:1200ms, fill-mode:both, to the header, footer, and eyebrow. Apply nebulaRise with duration:1100ms, fill-mode:both, to the h1, lede, primary anchor, and secondary anchor. Use these delays:
header -> nebulaFade -> 80ms
footer -> nebulaFade -> 420ms
eyebrow -> nebulaFade -> 180ms
h1 -> nebulaRise -> 300ms
lede -> nebulaRise -> 460ms
primary anchor -> nebulaRise -> 620ms
secondary anchor -> nebulaRise -> 700ms
Do not animate layout properties. Do not animate opacity below 0.2 after entrance completion. The canvas drift is controlled by JavaScript using elapsed time and transform-equivalent canvas translation, not CSS keyframes. On every frame, increment window.__DEMO__.frames exactly once while the rAF loop is active. Push these effect names into window.__DEMO__.effects in this order: "precomputed-nebula", "depth-stars", "camera-drift". Do not push CSS animation names.
H. RESPONSIVE TABLE
Apply these exact overrides:
Base, 901px and above:
--page-padding-x:40px
--page-padding-top:26px
--header-height:42px
--nav-gap:26px
--title-size:clamp(58px,7.2vw,122px)
--lede-size:clamp(16px,1.35vw,21px)
--button-height:48px
--footer-size:10px
At max-width:900px:
--page-padding-x:24px
--page-padding-top:20px
--header-height:38px
--nav-gap:14px
--title-size:clamp(52px,11vw,88px)
--lede-size:17px
--button-height:46px
--footer-size:9px
Set main padding:104px var(--page-padding-x) 96px. Keep vertical document scrolling. Set header position:relative, footer position:relative, main min-height:auto. The page shell min-height:100dvh.
At max-width:560px:
--page-padding-x:18px
--page-padding-top:16px
--header-height:34px
--nav-gap:9px
--title-size:clamp(46px,14vw,68px)
--lede-size:16px
--lede-line:1.48
--button-height:44px
--button-padding-x:19px
--footer-size:8px
Set header flex-wrap:wrap; row-gap:10px. Set nav gap:var(--nav-gap). Set main padding:94px var(--page-padding-x) 74px. Set footer flex-direction:column; align-items:flex-start; gap:8px.
At max-height:720px and min-width:901px:
--page-padding-top:18px
--page-padding-bottom:16px
--title-size:clamp(50px,6.4vw,92px)
--lede-size:16px
--button-height:42px
Set main padding-top:92px; padding-bottom:74px. Set footer padding-bottom:var(--page-padding-bottom).
At max-height:600px and min-width:901px:
--page-padding-top:14px
--page-padding-bottom:12px
--title-size:clamp(44px,5.6vw,72px)
--lede-size:15px
--lede-line:1.4
--button-height:38px
--button-padding-x:18px
Set main padding-top:72px; padding-bottom:58px. Set h1 margin-top:12px; margin-bottom:16px. Set lede margin-bottom:18px. Set footer-size:8px.
I. FAILURE GUARDS
Set all entrance elements to opacity:1 as their resting state. Apply entrance animation only with animation-delay and animation-fill-mode:both; after animation completion, the computed opacity must remain 1. If CSS animation is unavailable, content must remain visible. Implement a two-rAF fallback: after the first paint, requestAnimationFrame twice; if the telemetry loop has not incremented frames, start the normal loop from that callback and set ready:true only after the first successful paint. Never leave ready:false after two animation frames.
Inside the prefers-reduced-motion: reduce block, set *, *::before, *::after to animation-duration:0.001ms!important; animation-iteration-count:1!important; transition-duration:0.001ms!important; scroll-behavior:auto!important. Set .hero, header, footer, .eyebrow, h1, .lede, .controls to opacity:1!important; transform:none!important. In JavaScript, detect matchMedia('(prefers-reduced-motion: reduce)').matches. If true, render one static frame with elapsed=0, set ready:true after paint, push all three effect names once, and do not schedule another animation frame. Never omit the stars or nebula in reduced-motion mode.
J. THE EXACT JS
Place this script at the end of body. It must use the existing DOM structure, the canvas id “space”, and no external dependency:
(() => {
const telemetry = {
name: 'nebula',
ready: false,
frames: 0,
effects: [],
errors: []
};
window.__DEMO__ = telemetry;
const canvas = document.getElementById('space');
const ctx = canvas.getContext('2d', { alpha: false });
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const TAU = Math.PI * 2;
const MAX_DPR = 2;
const textureScale = 0.5;
const cloudCount = 5;
const starCount = 240;
const clouds = [];
const stars = [];
const permutation = new Uint8Array(256);
let seed = 173;
let width = 0;
let height = 0;
let dpr = 1;
let start = performance.now();
let raf = 0;
let resizeTimer = 0;
function random() {
seed = (seed * 1664525 + 1013904223) >>> 0;
return seed / 4294967296;
}
function hash(x, y) {
return permutation[(x & 255) + permutation[y & 255]] / 255;
}
function noise(x, y) {
const xi = Math.floor(x);
const yi = Math.floor(y);
const xf = x - xi;
const yf = y - yi;
const u = xf * xf * (3 - 2 * xf);
const v = yf * yf * (3 - 2 * yf);
const a = hash(xi, yi);
const b = hash(xi + 1, yi);
const c = hash(xi, yi + 1);
const d = hash(xi + 1, yi + 1);
return (a * (1-u) + b * u) * (1-v) + (c * (1-u) + d * u) * v;
}
for (let i = 0; i < 256; i += 1) permutation[i] = i;
for (let i = 255; i > 0; i -= 1) {
const j = Math.floor(random() * (i + 1));
const temp = permutation[i];
permutation[i] = permutation[j];
permutation[j] = temp;
}
function makeCanvas(w, h) {
const element = typeof OffscreenCanvas !== 'undefined'
? new OffscreenCanvas(w, h)
: document.createElement('canvas');
element.width = w;
element.height = h;
return element;
}
function buildCloud(index) {
const w = Math.max(1, Math.min(960, Math.round(width * textureScale)));
const h = Math.max(1, Math.min(540, Math.round(height * textureScale)));
const layer = makeCanvas(w, h);
const lc = layer.getContext('2d');
const image = lc.createImageData(w, h);
const data = image.data;
const palettes = [
[[0,0,0,0],[55,27,115,0],[124,78,226,87],[190,157,255,107],[49,18,91,0]],
[[0,0,0,0],[17,55,91,0],[30,164,192,56],[117,235,230,71],[9,39,72,0]],
[[0,0,0,0],[94,27,70,0],[224,91,138,48],[255,182,191,56],[75,18,62,0]],
[[0,0,0,0],[18,22,68,0],[57,63,154,51],[126,122,226,46],[9,14,43,0]],
[[0,0,0,0],[4,5,19,0],[5,7,24,97],[1,3,13,112],[0,0,0,0]]
];
const palette = palettes[index];
const octaves = [6,5,5,4,4][index];
const base = [0.0042,0.0031,0.0028,0.0022,0.0036][index];
for (let y = 0; y < h; y += 1) {
for (let x = 0; x < w; x += 1) {
let value = 0;
let amplitude = 1;
let frequency = base;
let total = 0;
for (let octave = 0; octave < octaves; octave += 1) {
value += noise(x * frequency, y * frequency) * amplitude;
total += amplitude;
amplitude *= 0.54;
frequency *= 2.03;
}
value /= total;
const p = Math.max(0, Math.min(0.999, (value - 0.31) / 0.42));
const band = p * 4;
const low = Math.min(3, Math.floor(band));
const mix = band - low;
const a = palette[low];
const b = palette[low + 1];
const i = (y * w + x) * 4;
data[i] = a[0] + (b[0] - a[0]) * mix;
data[i + 1] = a[1] + (b[1] - a[1]) * mix;
data[i + 2] = a[2] + (b[2] - a[2]) * mix;
data[i + 3] = a[3] + (b[3] - a[3]) * mix;
}
}
lc.putImageData(image, 0, 0);
clouds[index] = { layer, period: [74000,91000,113000,137000,157000][index], alpha: [0.94,0.76,0.68,0.74,0.86][index] };
}
function buildStars() {
stars.length = 0;
for (let i = 0; i < starCount; i += 1) {
const group = i < 72 ? 0 : i < 184 ? 1 : 2;
const radius = group === 0 ? 0.7 + random() * 0.6 : group === 1 ? 0.45 + random() * 0.45 : 0.25 + random() * 0.30;
const brightness = group === 0 ? 0.62 + random() * 0.30 : group === 1 ? 0.35 + random() * 0.35 : 0.20 + random() * 0.28;
stars.push({ x: random(), y: random(), radius, brightness, group, phase: random() * TAU, period: group === 0 ? 3800 : group === 1 ? 5100 : 7400 });
}
}
function resize() {
width = Math.max(1, window.innerWidth);
height = Math.max(1, window.innerHeight);
dpr = Math.min(MAX_DPR, window.devicePixelRatio || 1);
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
clouds.length = 0;
for (let i = 0; i < cloudCount; i += 1) buildCloud(i);
buildStars();
}
function render(elapsed) {
const t = reduced ? 0 : elapsed;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.globalAlpha = 1;
ctx.fillStyle = '#03040c';
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < clouds.length; i += 1) {
const cloud = clouds[i];
const phase = t / cloud.period * TAU + i * 0.73;
const x = Math.sin(phase) * (12 + i * 1.5);
const y = Math.cos(phase * 0.91) * (7 + i);
const scale = 1 + (Math.sin(phase * 0.63) * 0.5 + 0.5) * 0.025;
ctx.save();
ctx.globalAlpha = cloud.alpha;
ctx.translate(width / 2 + x, height / 2 + y);
ctx.scale(scale, scale);
ctx.drawImage(cloud.layer, -width / 2, -height / 2, width, height);
ctx.restore();
}
for (let i = 0; i < stars.length; i += 1) {
const star = stars[i];
const drift = (star.group === 0 ? 4 : star.group === 1 ? 2 : 1) * Math.sin(t / (62000 + star.group * 33000) * TAU + star.phase);
const twinkle = 0.72 + 0.28 * Math.sin(t / star.period * TAU + star.phase);
ctx.globalAlpha = star.brightness * twinkle;
ctx.fillStyle = star.group === 0 ? '#f5f3ff' : star.group === 1 ? '#c4b5fd' : '#75ebe6';
ctx.beginPath();
ctx.arc(star.x * width + drift, star.y * height, star.radius, 0, TAU);
ctx.fill();
}
ctx.globalAlpha = 1;
const scrim = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) * 0.72);
scrim.addColorStop(0, 'rgba(3,4,12,0.70)');
scrim.addColorStop(0.48, 'rgba(3,4,12,0.46)');
scrim.addColorStop(1, 'rgba(3,4,12,0.04)');
ctx.fillStyle = scrim;
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = 'rgba(245,243,255,0.10)';
ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
}
function loop(now) {
telemetry.frames += 1;
render(now - start);
if (!reduced) raf = requestAnimationFrame(loop);
}
window.addEventListener('error', event => {
telemetry.errors.push(String(event.message || 'window error'));
});
window.addEventListener('resize', () => {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(resize, 180);
});
resize();
start = performance.now();
render(0);
telemetry.effects.push('precomputed-nebula', 'depth-stars', 'camera-drift');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
telemetry.ready = true;
if (!reduced) loop(start);
});
});
})();
---
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)
- A headless browser at 1440x900 finds document.documentElement.scrollHeight <= 900 and document.documentElement.scrollWidth <= 1440.
- The page contains exactly one h1, semantic header/nav/main/footer elements, and the exact visible strings “NEBULA”, “IN THE DARK”, “Explore the signal”, and “Read our mission”.
- After load, window.__DEMO__.name is “nebula”, window.__DEMO__.ready is true, and window.__DEMO__.effects contains “precomputed-nebula”, “depth-stars”, and “camera-drift”.
- At 390x844, 768x1024, 1440x900, and 1920x1080, document.documentElement.scrollWidth equals or is less than the viewport width and all visible controls have accessible names.
- With prefers-reduced-motion: reduce, the canvas remains painted, the hero text has computed opacity 1, and window.__DEMO__.frames does not increase after the initial static paint.
- 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