Landing Pagestealth/space-bunny-alphaverified 20/20partial cross-check
Prism
A white beam enters an SVG triangular prism and fans into seven breathing spectrum wedges.
The exact prompt
A. BUILD TARGET
Deliverable is exactly one file: index.html. It is a complete, valid, standalone HTML5 document. No build step, no bundler, no framework. Document title is literally: PRISM — Precision Dispersion Optics. The html element carries lang="en". The viewport meta is: width=device-width, initial-scale=1. The single-viewport rule: at min-width 901px, html and body both receive height:100dvh; overflow:hidden; margin:0. At max-width 900px, body receives min-height:100dvh; overflow-y:auto; overflow-x:hidden. The page is a single-viewport cinematic landing page for PRISM, an optics company. The scene: a thin white beam enters a triangular glass prism from the left and disperses on the right into seven spectrum wedges. The copy block sits to the left of the prism; the hero is NOT centred. Do NOT build: scroll-snap sections, a second scroll viewport, centred hero copy, any <img>, <video>, <picture>, <iframe>, <object>, base64 photo blobs, remote image URLs, third-party CDN assets, CSS @import, GSAP, Three.js, Lottie, React, Tailwind, horizontal scrollbars at any tested width, or any element that rests at opacity:0 in the base CSS. Every visible control has an accessible name. There is exactly one <h1>. Real <header>, <nav>, <main>, and <footer> elements are present.
B. THE BACKDROP
One <canvas id="grain"> is the only canvas in the document. Element count for the backdrop: 1 canvas, 1 div.bg-drift, 1 body::after vignette. The canvas sits at z-index:0 and is sized by CSS to width:100%; height:100%; display:block. Canvas resolution trick: on init and on resize (both outside the rAF loop), read canvas.offsetWidth and canvas.offsetHeight once. Set canvas.width = Math.round(offsetWidth * 0.5) and canvas.height = Math.round(offsetHeight * 0.5). Do NOT use devicePixelRatio; the half-resolution backing store upscaled to full CSS size is the fill-rate trick. Draw exactly once per size, never per frame. Draw order on the 2D context: first a radial gradient centred at 70% 50% with inner colour rgba(138,180,255,0.10) at stop 0 and rgba(5,7,13,0) at stop 1 with radius Math.max(w,h)*0.8; then an ImageData noise pass where every 4th byte (alpha) is 8 and RGB bytes are a random 0-255 integer, applied via putImageData over the whole half-res surface. ctx.filter and ctx.shadowBlur are never called; there is no per-item canvas loop. Blur radii: div.bg-drift is a static layer with background: linear-gradient(105deg, transparent 0%, rgba(138,180,255,0.06) 45%, transparent 90%) and filter: blur(30px) applied once at parse time. body background is linear-gradient(180deg, #05070d 0%, #080b14 100%). Drift period: div.bg-drift animates transform: translate3d(-2%,0,0) to translate3d(2%,0,0) over 24s linear infinite alternate. Per-frame cost budget: JS execution <= 1.5ms; zero layout reads inside the loop; compositor paint <= 4ms; total rAF frame budget <= 8ms, guaranteeing the >=20 frames per 1.2s headless smoke threshold at 1440x900 with large headroom.
C. THE LAYER STACK (back to front, z-index values)
z-index:0 — #grain canvas (static noise + vignette gradient).
z-index:1 — div.bg-drift (drifting accent wash).
z-index:2 — div.beam (incoming white beam bar) and div.beam-highlight (travelling highlight child).
z-index:3 — div.fan (dispersion container); inside it, div.wedge:nth-child(1) through (7) receive z-index:31 through 37 respectively.
z-index:4 — svg.prism (glass triangle polygon with inner glow gradient).
z-index:5 — polyline.specular (1px non-scaling specular edge over the prism).
z-index:6 — div.content, which contains header, main.hero, and footer.
z-index:7 — a:focus-visible outline ring (outline, not a box, so it paints above content without a new stacking context).
D. TOKEN TABLE (every size and colour)
--bg-0:#05070d; --bg-1:#080b14; --accent:#8ab4ff; --accent-glow:rgba(138,180,255,0.18); --text:#e6edf7; --text-dim:#93a2bd; --white:#ffffff; --spec:rgba(255,255,255,0.70). Wedge hues: --w1:#ff4d4d; --w2:#ff9f43; --w3:#ffe14d; --w4:#4dff88; --w5:#4de1ff; --w6:#4d6bff; --w7:#b44dff. Sizes: --prism-w:220px; --prism-h:190px; --beam-w:46vw; --beam-h:2px; --wedge-w:52vw; --wedge-h:6px; --pad-x:48px; --pad-y:32px; --gap:24px; --h1:clamp(2.5rem,5vw,4rem); --fs-p:1.0625rem; --radius:999px. Blur radii (one per wedge): --blur-1:4px; --blur-2:6px; --blur-3:8px; --blur-4:7px; --blur-5:5px; --blur-6:9px; --blur-7:6px. Opacity tokens: --wedge-op-1:0.85; --wedge-op-2:0.80; --wedge-op-3:0.90; --wedge-op-4:0.82; --wedge-op-5:0.88; --wedge-op-6:0.78; --wedge-op-7:0.84.
E. THE COMPOSITION
Header: <header> is display:grid; grid-template-columns:auto 1fr auto; padding:var(--pad-y) var(--pad-x); gap:var(--gap). Child 1: <a class="brand" href="#top" aria-label="PRISM home">PRISM</a>. Child 2: <nav aria-label="Primary"> containing four links: <a href="#optics">Optics</a>, <a href="#dispersion">Dispersion</a>, <a href="#lab">Lab</a>, <a href="#contact">Contact</a>. Main hero: <main id="top" class="hero"> is display:grid; grid-template-columns:minmax(320px,38vw) 1fr; align-items:center; gap:48px; padding:0 var(--pad-x). Left column div.copy: <h1>Light, divided.</h1>; <p class="lede">PRISM is an optics studio engineering dispersion-grade glass for spectrometry, imaging, and research.</p>; <a class="btn" href="#contact">Request a quote</a>. Right column div.scene holds the beam, fan, prism, and specular. Footer: <footer> is display:flex; justify-content:space-between; align-items:center; padding:var(--pad-y) var(--pad-x). Left text node: "© 2025 PRISM Optics. All rights reserved." Right text node: "Precision · Dispersion · Clarity". SVG prism: inline <svg class="prism" width="var(--prism-w)" height="var(--prism-h)" viewBox="0 0 220 190" aria-hidden="true"> containing <defs><radialGradient id="glass" cx="50%" cy="40%" r="60%"><stop offset="0%" stop-color="rgba(255,255,255,0.14)"/><stop offset="60%" stop-color="rgba(138,180,255,0.08)"/><stop offset="100%" stop-color="rgba(255,255,255,0.02)"/></radialGradient></defs>, then <polygon points="10,180 110,10 210,180" fill="url(#glass)"/>. Specular: <polyline class="specular" points="10,180 110,10 210,180" fill="none" stroke="var(--spec)" stroke-width="1" vector-effect="non-scaling-stroke"/>. Beam: <div class="beam"><div class="beam-highlight"></div></div>. Fan: <div class="fan"> with seven <div class="wedge" data-i="1"></div> ... up to 7.
F. THE UI LANGUAGE
.btn treatment: display:inline-block; background:linear-gradient(180deg,#8ab4ff,#5a8dee); color:#05070d; padding:14px 28px; border-radius:var(--radius); font-weight:600; text-decoration:none; font-size:1rem; box-shadow:0 6px 20px rgba(90,141,238,0.35); transition:transform 160ms ease, filter 160ms ease. .btn:hover { transform:translateY(-1px); filter:brightness(1.08); } .btn:active { transform:translateY(0); } .btn:focus-visible, a:focus-visible { outline:2px solid var(--accent); outline-offset:3px; } .brand { font-weight:700; letter-spacing:0.18em; color:var(--text); text-decoration:none; } nav a { color:var(--text-dim); text-decoration:none; font-size:0.9375rem; margin-left:20px; transition:color 160ms ease, text-shadow 160ms ease; } nav a:hover { color:var(--white); text-shadow:0 0 12px rgba(138,180,255,0.6); } .lede { color:var(--text-dim); font-size:var(--fs-p); line-height:1.6; max-width:34ch; margin:0 0 28px; } h1 { font-size:var(--h1); line-height:1.05; margin:0 0 20px; color:var(--white); font-weight:600; letter-spacing:-0.02em; }
G. MOTION — STAGGER TABLE AND KEYFRAMES
Element -> animation -> delay:
.brand -> floatIn -> 0ms
nav a:nth-child(1) -> floatInRight -> 80ms
nav a:nth-child(2) -> floatInRight -> 160ms
nav a:nth-child(3) -> floatInRight -> 240ms
nav a:nth-child(4) -> floatInRight -> 320ms
.hero h1 -> fadeUp -> 240ms
.hero .lede -> fadeUp -> 320ms
.btn -> fadeUp -> 400ms
footer -> fadeIn -> 560ms
.beam -> drawBeam -> 0ms
.wedge[data-i="1"] -> fanOut -> 200ms
.wedge[data-i="2"] -> fanOut -> 270ms
.wedge[data-i="3"] -> fanOut -> 340ms
.wedge[data-i="4"] -> fanOut -> 410ms
.wedge[data-i="5"] -> fanOut -> 480ms
.wedge[data-i="6"] -> fanOut -> 550ms
.wedge[data-i="7"] -> fanOut -> 620ms
.fan -> fanBreathe -> 0ms (duration 12s, infinite)
.beam-highlight -> beamTravel -> 0ms (duration 2.4s, linear infinite)
.prism -> glowPulse -> 0ms (duration 12s, infinite)
Named keyframes:
@keyframes floatIn { from { opacity:0; transform:translateY(-8px); } to { opacity:1; transform:none; } }
@keyframes floatInRight { from { opacity:0; transform:translateX(-10px); } to { opacity:1; transform:none; } }
@keyframes fadeUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:none; } }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes drawBeam { from { transform:scaleX(0); transform-origin:left center; } to { transform:scaleX(1); transform-origin:left center; } }
@keyframes fanOut { from { opacity:0; transform:scaleX(0.6) skewY(0deg); } to { opacity:0.88; transform:scaleX(1) skewY(-8deg); } }
@keyframes fanBreathe { 0%,100% { transform:skewY(-6deg) scaleX(0.98); } 50% { transform:skewY(6deg) scaleX(1.02); } }
@keyframes beamTravel { 0% { transform:translateX(-60%); } 100% { transform:translateX(160%); } }
@keyframes glowPulse { 0%,100% { opacity:0.9; } 50% { opacity:1; } }
H. RESPONSIVE TABLE (token overrides per breakpoint, including two max-height breakpoints)
| Breakpoint / Condition | Overrides |
| 1920x1080 base | --prism-w:220px; --prism-h:190px; --beam-w:46vw; --wedge-w:52vw; --pad-x:48px; --h1:clamp(2.5rem,5vw,4rem); |
| 1440x900 | --prism-w:200px; --prism-h:172px; --beam-w:44vw; --wedge-w:50vw; --pad-x:40px; --h1:3.5rem; |
| 768x1024 | .hero { grid-template-columns:1fr; } --prism-w:180px; --prism-h:154px; --beam-w:60vw; --wedge-w:66vw; --pad-x:32px; --h1:3rem; --gap:32px; |
| 390x844 | --prism-w:150px; --prism-h:128px; --beam-w:70vw; --wedge-w:76vw; --pad-x:20px; --pad-y:24px; --h1:2.25rem; --gap:16px; .lede { max-width:100%; } |
| max-height:780px | --prism-w:180px; --prism-h:150px; --wedge-h:5px; |
| max-height:640px | --prism-w:150px; --prism-h:124px; --beam-h:1px; --wedge-h:4px; --pad-y:16px; |
At <=900px, body regains vertical scroll; all above overrides still apply and no horizontal overflow is permitted.
I. FAILURE GUARDS
Resting opacity: every element in the base CSS has opacity:1. Animations are attached only under .is-ready (added by JS) and use animation-fill-mode:both, so if JS never runs the page is fully readable and static. Two-rAF fallback: if window.requestAnimationFrame is undefined, polyfill it as window.requestAnimationFrame = function(cb){ return setTimeout(function(){ cb(Date.now()); }, 16); }; and window.cancelAnimationFrame = function(id){ clearTimeout(id); }. Reduced motion: a @media (prefers-reduced-motion: reduce) block sets *, *::before, *::after { animation:none !important; transition:none !important; }. JS reads matchMedia("(prefers-reduced-motion: reduce)").matches into a variable reduce. If reduce is true, JS still sets ready=true and runs the rAF loop only to increment the telemetry frames counter, but applies zero style updates, leaving the composed scene static. No console method is ever called; no uncaught exceptions are thrown.
J. THE EXACT JS
Insert the following script at the end of <body>, with no template literals and no inner backticks:
<script>
(function(){
var D = window.__DEMO__ = { name:'prism', ready:false, frames:0, effects:[], errors:[] };
window.addEventListener('error', function(e){ D.errors.push(String(e.message)); });
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(cb){ return setTimeout(function(){ cb(Date.now()); }, 16); };
window.cancelAnimationFrame = function(id){ clearTimeout(id); };
}
function drawGrain(c){
var w = c.width = Math.round(c.offsetWidth * 0.5);
var h = c.height = Math.round(c.offsetHeight * 0.5);
var x = c.getContext('2d');
x.clearRect(0,0,w,h);
var g = x.createRadialGradient(w*0.70, h*0.50, 0, w*0.70, h*0.50, Math.max(w,h)*0.80);
g.addColorStop(0, 'rgba(138,180,255,0.10)');
g.addColorStop(1, 'rgba(5,7,13,0)');
x.fillStyle = g;
x.fillRect(0,0,w,h);
var id = x.createImageData(w,h);
var d = id.data;
for (var i=0; i<d.length; i+=4) {
var v = (Math.random()*255)|0;
d[i] = v; d[i+1] = v; d[i+2] = v; d[i+3] = 8;
}
x.putImageData(id,0,0);
}
function init(){
var c = document.getElementById('grain');
if (c && c.offsetWidth) { D.effects.push('canvas-grain'); drawGrain(c); }
D.effects.push('beam-travel');
D.effects.push('fan-breathe');
D.effects.push('prism-glow');
document.documentElement.classList.add('is-ready');
requestAnimationFrame(function loop(){
D.frames++;
requestAnimationFrame(loop);
});
requestAnimationFrame(function(){
requestAnimationFrame(function(){ D.ready = true; });
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else { init(); }
window.addEventListener('resize', function(){
var c = document.getElementById('grain');
if (c && c.offsetWidth) { drawGrain(c); }
});
})();
</script>
---
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)
- window.__DEMO__.ready === true after the load event and two animation frames have elapsed.
- window.__DEMO__.effects contains exactly 'canvas-grain', 'beam-travel', 'fan-breathe', and 'prism-glow'.
- The document contains exactly one h1, and header, nav, main, and footer elements all exist.
- At 1440x900 and 390x844 viewport widths, document.documentElement.scrollWidth is <= window.innerWidth (no horizontal overflow).
- After 1.2 seconds of runtime at 1440x900, window.__DEMO__.frames is >= 20 and window.__DEMO__.errors.length === 0.
- 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