We need output single HTML file self-contained. Use three.js? User didn't explicitly require 3D but general instructions say three.js UMD CDN OK. We need create Remotion-style cinematic motion graphics video ad, auto-playing ~30s loop, zero input. Visible + animating first frame. Controls on plain keydown. Art direction dramatic lighting, atmosphere, gradient sky, particles, cohesive polished HUD, no overlapping UI. We need build HTML/CSS/JS. It can use canvas for particles and typography animations, maybe Three.js for background gradient sky and particles, but simpler canvas 2D is fine. However instruction says three.js UMD CDN OK and dramatic lighting, atmosphere, gradient sky, particles. We can use Three.js for a 3D background: gradient-like shader on a plane, particles floating, dramatic lighting. But UI overlay with CSS animations for kinetic typography. Need progress bar at bottom. No overlapping UI. Auto-play loop ~30s. Controls on plain keydown: maybe space to pause/play, R to restart, arrow keys to skip? We need use keydown on window. Need scenes: 1 Hook: Your AI agents should be making you money. Kinetic typography maybe words fly in. 2 Count-up stats: 3,600+ founders, 38 countries, $59/mo. Animate numbers. 3 What-you-get cards: Agent OS, weekly calls, templates, community. Cards slide/fade in grid. 4 Testimonial quotes: maybe two quotes fade. 5 CTA: Join AIPB — aiprofitboardroom.com. Big button/text. Color palette dark premium, gold + aubergine. Gold #D4AF37, aubergine #3B0F3B maybe. Dark background. We need ensure first frame visible and animating. Use requestAnimationFrame. Need handle resize. Use Three.js scene with orthographic or perspective camera. Could use shader plane that fills screen. Simpler: CSS background gradient with canvas particles. But Three.js can give dramatic lighting/atmosphere. Let's design a Three.js scene: large sphere/plane with custom shader: dark aubergine to deep black with golden light beams. Add floating golden particles. Maybe a subtle rotating wireframe torus or geometry? But no blocking start screens. Alternatively use CSS-based background gradient, and Three.js for particles only. It might be easier and performant. But the prompt says dramatic lighting, atmosphere, gradient sky, particles. We can use a full-screen Three.js shader background. Let's implement a Three.js scene with a PlaneGeometry and ShaderMaterial, using uniforms time, resolution, colors for aubergine/gold/dark. Vertex shader passes uv. Fragment shader creates gradient sky with noise and golden light rays. Then add particles as Points with custom shader to make gold dust. This is self-contained. Need manage z-index: canvas behind HUD. HUD uses CSS absolute layers. No overlapping UI elements; but scenes overlap in time with fades. We can use a single overlay container with scene divs that transition opacity/transform. Progress bar at bottom. Need timeline: total duration 30s. Use performance.now() start time. Loop after 30s. Show scenes based on elapsed: 0-4s: Scene 1 Hook 4-9s: Scene 2 Stats (count up maybe from 4-8) 9-16s: Scene 3 Cards 16-23s: Scene 4 Testimonials 23-30s: Scene 5 CTA Progress bar width = elapsed/30*100. Need kinetic typography: animate each word with CSS keyframes triggered by adding active class. For hook, split words and animate. For stats, count numbers using JS. Cards slide in with stagger. Testimonials fade/slide. CTA pulse. Need ensure no start screen. The video starts immediately. Need keydown controls: Space toggle pause/play, Left/Right skip 5s, R restart, M toggle mute? No audio likely. We'll implement pause by stopping timeline accumulation. When paused, don't update elapsed but still render particles. Skip adjusts startTime offset. Need maybe visual HUD: top-left logo "AIPB", top-right "AI Profit Boardroom". Progress bar bottom. No overlapping UI. Need ensure single HTML file. Inline all CSS and JS. Use three.js UMD CDN. Let's plan code structure: HTML head: meta viewport, title, style. CSS: * box-sizing. body margin 0 overflow hidden background #0d020d color #f3e5ab font-family 'Inter' or system-ui. #canvas-container position fixed inset 0 z-index 0. #stage position fixed inset 0 z-index 1 pointer-events none; display flex flex-direction justify-content center align-items center; overflow hidden. .scene position absolute; inset 0; display flex; flex-direction column; justify-content center; align-items center; opacity 0; transition opacity 0.6s, transform 0.8s; transform translateY(20px); padding 5vw. .scene.active opacity 1 transform translateY(0) pointer-events auto. HUD: .hud-logo position fixed top left z-index 10. .hud-brand top right. .progress-container fixed bottom left right height 4px bg rgba(255,255,255,.1). .progress-fill height 100% bg linear-gradient gold. .controls-hint maybe bottom center small but ensure no overlap; maybe hide? Could place small text bottom center above progress bar. But might overlap; fine. Typography classes: .kinetic-line font-size clamp 2.5rem, font-weight 800 letter-spacing -0.02em text-shadow gold glow. .stat-number font-size clamp 4rem, gradient text. .stat-label uppercase tracking. .card-grid display grid etc. .card background rgba(255,255,255,.05) border 1px solid rgba(212,175,55,.25) border-radius 1rem padding 2rem backdrop-filter blur etc. .card-title gold. .quote text italic. .cta-button gold gradient. Animations: @keyframes fadeInUp, popIn, shimmer. Use .active .word nth-child stagger. For hook, split into spans with inline-block. For stats, count-up. For cards, transition delay based on index. For quotes, slide in. JS: - Import Three.js via global THREE from CDN. Use importmap? Since UMD CDN, can use then THREE global. We'll use importmap maybe better. The instruction says three.js UMD CDN OK (global THREE). We'll use script tag with UMD build. - Create renderer, scene, camera. Use OrthographicCamera for shader plane. Plane geometry 2x2. ShaderMaterial. Uniforms: uTime, uResolution, uColor1 (aubergine), uColor2 (near black), uColor3 (gold). Fragment shader: gradient from top to bottom mix colors, add some sin waves for rays, noise. Particles: BufferGeometry with many points, PointsMaterial? Use shader material for golden sparkle. Points with random positions in a large sphere. Animate rotation. Add maybe additive blending. - Timeline logic: const DURATION = 30000; let startTime = performance.now(); let paused = false; let pauseTime = 0; let skipOffset = 0; let elapsed = 0; In animate loop: const now = performance.now(); if (!paused) elapsed = (now - startTime) % DURATION; else elapsed = pauseTime. But if we skip, adjust startTime. Simpler: maintain currentTime variable and update dt each frame when not paused. When skip, add/subtract. When restart, set currentTime=0 and startTime=now. For looping, if currentTime >= DURATION, currentTime -= DURATION. Use currentTime to update scene classes. Determine active scene index based on segments. We can use a function updateScenes(t). For each scene div, set active if t within segment with optional fade-in before start? Use CSS transitions. But if we simply toggle active at start, opacity transitions from previous. We can also set a small pre-active state? Fine. For scene transitions, we can have overlapping intervals with lower z-index? Since scenes are absolute and opacity transitions, overlapping content may create mixed text. Better to have non-overlapping segments with fade durations less than segment. E.g. scene active from start+0.5 to end-0.5? But if we toggle active exactly, opacity will crossfade 0.6s. That's acceptable. But if both scenes active simultaneously, content overlaps. To avoid, ensure only one active at a time. Use small gaps? But then black gaps. We can allow crossfade but content may be unreadable for a moment. Use z-index: later scene above earlier and fade in; earlier still visible underneath until it fades out. If we make fade duration 0.4s, okay. To avoid overlap, end previous active 0.4s before next starts. But then blank. Maybe crossfade is cinematic. We'll manage with z-index ordering: scene with higher index on top; previous fades out behind. Overlap brief. Segment definitions with active windows: Scene1: 0-4s active; fade out 3.6-4.4? We can set active true 0-4, next active 3.7-... So overlap 0.3s. Acceptable. Scene2: 3.8-8.5 active. Scene3: 8.3-15.5 active. Scene4: 15.3-22.5 active. Scene5: 22.3-30 active. This yields crossfades. Need update z-index or ensure later scenes have higher z-index. Could assign z-index based on index or active order. Since scene divs in DOM order later on top. We'll set z-index in CSS dynamic via data-index. Use .scene { z-index: calc(var(--i) * 10); } so later scenes above. Progress bar update. Count-up stats: For scene 2, we want numbers count up. Need update numbers each frame while active. Have stat elements. When scene active, compute local progress = (t - scene2Start) / scene2Duration. Map to target values. Use easeOutExpo. For founders target 3600, countries 38, price 59 (price maybe just show $59 not count? Could count from 0 to 59). Update text. Cards: scene3. On active, add active class; use CSS transition delays. Since active class toggles, transitions will re-trigger each loop. Good. Testimonials: scene4. Quotes fade in with delay. CTA: scene5. Pulse/glow. Hook: scene1. Words animate with active class and CSS nth-child delays. Potential issue: When looping, scene classes reset. Need remove active and re-add. updateScenes each frame sets active based on t. CSS transitions will re-trigger when class toggles. Fine. Need handle keydown: Space: toggle pause. When pausing, store elapsed; when resuming, adjust startTime = now - elapsed. If using currentTime variable, simpler: paused? paused=false; startTime = now - currentTime; else paused=true. ArrowRight: currentTime = min(DURATION-0.001, currentTime+5000); if not paused startTime = now - currentTime. ArrowLeft: currentTime = max(0, currentTime-5000); startTime = now - currentTime. R: currentTime=0; startTime=now. Maybe F toggles fullscreen. Need maybe no audio. We can include no audio. If we wanted background music, auto-play audio blocked. Skip. Need ensure visible first frame: at load, updateScenes(0) before animation loop and render. Start animation loop. Good. Need implement Three.js shader. Let's write a nice fragment shader: uniform float uTime; uniform vec2 uResolution; uniform vec3 uColorA; // aubergine uniform vec3 uColorB; // deep black uniform vec3 uColorC; // gold void main() { vec2 uv = gl_FragCoord.xy / uResolution.xy; // vertical gradient vec3 col = mix(uColorA, uColorB, smoothstep(0.0, 1.0, uv.y)); // add subtle vignette vec2 center = uv - 0.5; float vignette = 1.0 - dot(center, center) * 1.2; col *= vignette; // golden rays from top float ray = sin(uv.x * 12.0 + uTime * 0.3) * 0.5 + 0.5; ray *= smoothstep(1.0, 0.0, uv.y) * 0.08; col += uColorC * ray; // horizontal shine float shine = sin(uv.y * 20.0 - uTime * 0.5) * 0.5 + 0.5; shine *= 0.03; col += uColorC * shine; gl_FragColor = vec4(col, 1.0); } Colors: aubergine #1a051a maybe rgb(0.102,0.02,0.102). deep black #050105. gold #D4AF37 rgb(0.831,0.686,0.216). Particles: Use Points with BufferGeometry. Attributes position and size? Use ShaderMaterial for round soft particles. Vertex shader: attribute float size; varying float vAlpha; void main() { vec4 mvPosition = modelViewMatrix * vec4(position, 1.0); gl_PointSize = size * (300.0 / -mvPosition.z); vAlpha = 0.6 + 0.4*sin(position.x*10.0 + uTime); gl_Position = projectionMatrix * mvPosition; } Fragment shader: uniform vec3 uColor; varying float vAlpha; void main() { float d = length(gl_PointCoord - 0.5); if (d > 0.5) discard; float glow = 1.0 - d*2.0; glow = pow(glow, 1.8); gl_FragColor = vec4(uColor, vAlpha * glow); } Use additive blending, depthWrite false. Particle positions random in range [-50,50] xyz. Number ~400. Rotate slowly. Camera: PerspectiveCamera fov 60, position z 30. Or use Orthographic for background plane and particles? For particles, perspective gives depth. We'll use PerspectiveCamera for scene, but background plane at z=-10 with large size? Simpler: use two scenes? Or use one PerspectiveCamera. The shader plane can be positioned at z=-20 with width/height based on camera frustum. But easier: use a large Box? Actually we can use a full-screen quad with OrthographicCamera and separate particle scene with PerspectiveCamera using renderer autoClear? Could use a single scene with PerspectiveCamera. Place plane far back: geometry width 200 height 200 at z=-80. With fov 60 and z distance, visible. Particles around z -20 to 20. But plane might clip? Use plane at z=-100, scale accordingly. Could also use scene.fog? Not needed. Alternatively use CSS gradient background and Three.js only for particles. That might be simpler and robust. But instruction says gradient sky. CSS gradient is fine. But let's include Three.js for particles and maybe subtle lighting. We can set CSS background: radial-gradient etc. Then Three.js transparent canvas with particles. However art direction says dramatic lighting, atmosphere, gradient sky, particles. CSS can do gradient sky. But maybe they want Three.js. We can use Three.js with a shader background and transparent overlay. Let's implement shader plane. Need compute plane size to fill camera view. Use function: given fov, distance, height = 2*tan(fov/2)*distance; width = height*aspect. Create PlaneGeometry(width*1.2, height*1.2) at z=-distance. Camera: PerspectiveCamera(60, aspect, 0.1, 1000). Position z = 30. Plane at z=-60 maybe. Distance = 90. height = 2*tan(30deg)*90 ≈ 103.9. width = height*aspect. Good. Particles positions within sphere radius 60 around camera. Use Points. Lighting: maybe add a PointLight gold at z=10, intensity 1. But particles and shader only. Could add a subtle torus? Not needed. Need maybe dramatic spotlight effect on text via CSS text-shadow and backdrop blur. Potential issue: Three.js canvas performance and CSS animations both fine. Need ensure no overlapping UI. The HUD elements (logo, brand, progress bar) are small at edges. Scene content centered. Good. Need maybe CTA button clickable? Overlay pointer-events none except active scene. Add pointer-events auto to .scene.active and maybe CTA button. But video auto-playing; click can open URL? We'll make CTA text not interactive maybe. But could add a link. Since pointer-events none on stage, active scene can have pointer-events auto. We'll set .scene { pointer-events:none } .scene.active { pointer-events:auto }. But if user clicks CTA, open aiprofitboardroom.com in new tab. That could be acceptable. But auto-playing ad might have link. We'll include an styled as button. Need handle reduced motion? Not required but nice. Now let's craft exact scene content and timings. Scene 1: Hook "Your AI agents" large, "should be making you money." maybe second line. Use split words. Kinetic: words fly in from bottom with stagger. Add gold underline. Duration 0-4s. HTML:
Your AI agents
should be making you money.
CSS: .hook-line font-size clamp(3rem, 8vw, 7rem); font-weight 800; line-height 1.1; text-align center; opacity 0 transform translateY(40px) scale(0.95); transition all 0.8s cubic-bezier(.2,.8,.2,1); .scene.active .hook-line { opacity 1 transform none } transition-delay nth-child. Use nth-child(1) delay 0.2s, nth-child(2) delay 0.7s. Add text-shadow gold glow. Scene 2: Stats Title "The numbers behind the boardroom". Stats grid 3 columns. HTML:

The numbers behind the boardroom

0
Founders
0
Countries
0
Membership
CSS .section-title etc. .stats-grid grid. .stat-number gradient text. Count-up in JS. Scene 3: Cards Title "What you get inside". 4 cards. HTML:

What you get inside

Agent OS

The operating system for building money-making AI agents.

Weekly Calls

Live strategy sessions with founders deploying AI at scale.

Templates

Plug-and-play automations, prompts and SOPs.

Community

A network of operators, not lurkers.

CSS card grid 2x2 or 4 columns depending width. Stagger transitions using nth-child. Add glassmorphism. Scene 4: Testimonials Title "What founders say". Two quotes. HTML:

What founders say

"AIPB turned my AI experiments into actual revenue in 30 days."
— Sarah K., SaaS Founder
"The best ROI on any community I've joined. Full stop."
— Marcus T., Agency Owner
CSS quotes stagger. Scene 5: CTA

Join the AI Profit Boardroom

aiprofitboardroom.com

Claim your seat
CSS pulse glow. HUD:
AI Profit Boardroom
Maybe a tiny controls hint bottom center: "Space pause • ← → skip • R restart". Ensure not overlapping progress bar. Place bottom 16px center with z-index above progress. But progress bar at bottom 0 height 4px. Hint at bottom 12px. Fine. Could be small opacity .5. Need ensure no overlapping UI: The hint and progress bar don't overlap content much. Fine. Now JS timeline: const DURATION = 30000; const SEGMENTS = [ {id:'scene1', start:0, end:4000}, {id:'scene2', start:3800, end:8500}, {id:'scene3', start:8300, end:15500}, {id:'scene4', start:15300, end:22500}, {id:'scene5', start:22300, end:DURATION} ]; But for crossfade, start earlier than previous end. Need ensure active windows overlap. For scene1 end 4000, scene2 start 3700 => overlap 300ms. Good. But we need z-index so scene2 above scene1. Since later scene has higher z-index, fine. Overlap might cause text overlap but brief. However count-up for stats should start when scene2 becomes active at 3700. We can compute local progress = (t - segment.start)/(segment.end - segment.start) clamped. Use easing. Need ensure at t=0, scene1 active. updateScenes(t) loops segments, set active if t >= start && t < end. For final scene end = DURATION; at t==DURATION loop resets to 0, so fine. But because of overlap, count-up will start 300ms before scene1 fully fades. Not big. Maybe better to define segments without overlap and use CSS transition to fade. If only one active at a time, but transition duration 0.6s means at switch there is a 0.6s period where previous is still transitioning out and next transitioning in if we remove active before adding next. But if we switch exactly, both have active? No, previous loses active at same time next gains. Since transition out takes 0.6s, previous remains visible. So crossfade unavoidable. We can set transition out faster or use display none after transition. Simpler: allow crossfade. Use z-index ordering and shorter transition 0.4s. Potential issue: Scene content may overlap badly (e.g., stats over hook). Crossfade 0.3s is fine. Need maybe use opacity and visibility to avoid interaction on inactive scenes. Use .scene { opacity:0; visibility:hidden; transition: opacity 0.5s, transform 0.7s, visibility 0.5s; } .scene.active { opacity:1; visibility:visible; }. This ensures inactive not clickable. Good. Now count-up: Use requestAnimationFrame to update. Could store target values and update every frame based on progress. Use easeOutExpo. For price, prefix $ suffix /mo. For founders, suffix +. For countries no suffix. Function updateStats(t): if t < segment.start or t > segment.end return. const p = clamp((t - segment.start)/(segment.end - segment.start),0,1); const eased = 1 - Math.pow(2, -10*p); // easeOutExpo For each .stat-number, value = Math.floor(eased*target). At end ensure target. Update textContent = prefix + value + suffix. Maybe for founders use 3,600 formatting with comma. Use toLocaleString. Cards and quotes rely on CSS active class with transition delays. We need ensure active class triggers transitions. Since class toggled each frame, but if it remains active, no re-trigger. At loop reset, class removed for all then re-added after 0ms. CSS transition will re-trigger. Good. Hook lines rely on active class with delays. Good. Progress bar: width percent. Three.js animate: update uniforms, rotate particle group, render. Need handle window resize: update camera aspect, renderer size, shader resolution uniform, recompute plane size? We can just update renderer size and camera aspect, and plane scale maybe. We created plane geometry based on initial size. On resize, plane may not fill if aspect changes. For shader background, plane should fill view. We can update plane geometry or scale. Simpler: on resize, compute new plane width/height and update geometry or scale. Since plane at z=-distance, scale = (newWidth/oldWidth, newHeight/oldHeight). But easier: dispose geometry and recreate. Or set plane.scale.set(newWidth/oldWidth, newHeight/oldHeight,1). We can store base plane width/height and scale accordingly. Let's implement resize: compute new width/height, set plane.scale.set(newW/baseW, newH/baseH,1). Also update uResolution. Particles: On resize, no change. Camera: updateProjectionMatrix. Now code. Need ensure no external assets except three.js UMD CDN. Use Google Fonts? Could use @import or link. But self-contained can include Google Fonts via link. However if offline, fallback. Use system-ui fonts. For premium look, maybe use 'Inter' from Google Fonts. Could include . That's external CSS, maybe acceptable. But to keep self-contained, use system fonts: 'Inter', system-ui, -apple-system, sans-serif. If Inter not loaded, system-ui. We can use CSS @font-face? Not needed. Better avoid external fonts. Use system-ui. Need maybe include a favicon? Not necessary. Need ensure no blocking start screens. The canvas will render immediately. CSS scene active at t=0. Potential issue: If using Three.js from CDN, loading may take time but first frame visible after script loads. Since script in head, body hidden until loaded. That's okay. But "Visible + animating first frame" means once loaded. We can add a small inline script at end to init. Use DOMContentLoaded. Need maybe use importmap with module script? UMD global is simpler. Use . But r128 is old; use latest r128 or newer. The prompt says UMD CDN OK. We'll use cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js. Need ensure global THREE. Yes. Now let's write the HTML. Ensure single file, no markdown fences. Inline CSS and JS. Let's produce final. But first consider if we need to include controls on plain keydown. We'll add window.addEventListener('keydown', ...). Space default should prevent scroll. Arrow keys maybe default no. We'll e.preventDefault for space. Need maybe display pause indicator briefly? Not necessary. Need maybe ensure CTA link opens. The stage is pointer-events none; active scene pointer-events auto; CTA button is link. Need maybe ensure no text selection. CSS user-select none. Need maybe add a subtle vignette overlay via CSS radial gradient for atmosphere. That can complement Three.js background. Let's design CSS colors: bg #0a020a; text #f4ecd8; gold #d4af37; aubergine #2e0a2e. Gradient text: background: linear-gradient(135deg, #f9e9a8, #d4af37); -webkit-background-clip:text; color transparent. Card glass: background rgba(255,255,255,.04); border 1px solid rgba(212,175,55,.18); box-shadow 0 8px 32px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.06); backdrop-filter blur(12px). CTA button: background linear-gradient gold, hover scale. Hook text-shadow: 0 0 40px rgba(212,175,55,.25), 0 4px 20px rgba(0,0,0,.6). Progress fill gradient: linear-gradient(90deg, #b8860b, #f9e9a8, #d4af37). Now code details. Three.js init: const container = document.getElementById('canvas-container'); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 1000); camera.position.z = 30; const renderer = new THREE.WebGLRenderer({antialias:true, alpha:false}); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio,2)); container.appendChild(renderer.domElement); Background plane: const fov = camera.fov * (Math.PI/180); const distance = 90; const height = 2 * Math.tan(fov/2) * distance; const width = height * camera.aspect; const planeGeo = new THREE.PlaneGeometry(width, height); const planeMat = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader, depthWrite:false }); const plane = new THREE.Mesh(planeGeo, planeMat); plane.position.z = -distance; scene.add(plane); const basePlaneSize = {width, height}; Particles: const count=500; const positions = new Float32Array(count*3); const sizes = new Float32Array(count); for i: positions random in sphere radius 60; sizes random 1-4. geo.setAttribute('position', new THREE.BufferAttribute(positions,3)); geo.setAttribute('size', new THREE.BufferAttribute(sizes,1)); mat = new THREE.ShaderMaterial({ uniforms:{uTime:{value:0}, uColor:{value:new THREE.Color('#d4af37')}}, vertexShader, fragmentShader, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false }); const particles = new THREE.Points(geo, mat); scene.add(particles); In animate: planeMat.uniforms.uTime.value = time*0.001; particlesMat.uniforms.uTime.value = time*0.001; particles.rotation.y = time*0.00005; particles.rotation.x = Math.sin(time*0.0002)*0.1; Add maybe subtle ambient? Not needed. Resize: camera.aspect = w/h; camera.updateProjectionMatrix(); renderer.setSize(w,h); const height2 = 2*Math.tan(fov/2)*distance; const width2 = height2*camera.aspect; plane.scale.set(width2/basePlaneSize.width, height2/basePlaneSize.height,1); planeMat.uniforms.uResolution.value.set(w,h); Now timeline logic. Let's structure update(t): function updateScenes(t) { // Update active classes SEGMENTS.forEach(seg => { const el = document.getElementById(seg.id); const active = t >= seg.start && t < seg.end; if (active) el.classList.add('active'); else el.classList.remove('active'); }); // Count stats const s2 = SEGMENTS[1]; if (t >= s2.start && t < s2.end) { const p = Math.min(1, (t - s2.start)/(s2.end - s2.start)); const eased = 1 - Math.pow(2, -10*p); document.querySelectorAll('.stat-number').forEach(el => { const target = +el.dataset.target; const prefix = el.dataset.prefix || ''; const suffix = el.dataset.suffix || ''; const val = Math.floor(eased * target); el.textContent = prefix + val.toLocaleString() + suffix; }); } else { // keep final values? Not needed. } // progress document.getElementById('progressFill').style.width = (t/DURATION*100)+'%'; } Need maybe reset stat values at beginning? Not necessary; they will update when active. But when scene2 not active, the stat text may remain stale. At t just before scene2, it may show previous loop's final values. To avoid, we can reset to 0 when not active and before start. But not critical. Better: at every frame, if not active, set to 0 maybe. But if scene2 ended, keep target? For loop, at t=0 we want 0. So set to 0 when t < s2.start. When t >= s2.end, set to target. Let's handle: if (t < s2.start) set 0; else if (t >= s2.end) set target; else count. Similarly hook words animate via CSS; no JS. Need ensure scene transitions with z-index. In CSS: .scene { z-index: calc(var(--i) * 10); } Set inline style --i via data-index in HTML or JS. We can set in JS: document.querySelectorAll('.scene').forEach((el,i)=>el.style.setProperty('--i', i));. Now keydown: document.addEventListener('keydown', e => { if (e.code === 'Space') { e.preventDefault(); togglePause(); } else if (e.code === 'ArrowRight') { skip(5000); } else if (e.code === 'ArrowLeft') { skip(-5000); } else if (e.code === 'KeyR') { restart(); } else if (e.code === 'KeyF') { toggleFullscreen(); } }); Implement togglePause: paused = !paused; if (!paused) startTime = performance.now() - currentTime; else currentTime = performance.now() - startTime; But we update currentTime each frame. Let's maintain currentTime and lastTime. let currentTime = 0; let lastTime = performance.now(); let paused = false; In animate(now): const dt = now - lastTime; lastTime = now; if (!paused) { currentTime += dt; if (currentTime >= DURATION) currentTime -= DURATION; } updateScenes(currentTime); renderer.render(...); requestAnimationFrame(animate); Skip(delta): currentTime = Math.max(0, Math.min(DURATION-0.001, currentTime+delta)); updateScenes(currentTime); // immediate Restart: currentTime = 0; Pause: paused = !paused; lastTime = performance.now(); This is clean. Need ensure first frame visible. Call updateScenes(0) and renderer.render before starting loop. Now CSS animations details. Hook: #scene1 .hook-line { opacity:0; transform: translateY(50px) scale(0.96); filter: blur(4px); transition: all 0.9s cubic-bezier(.22,1,.36,1); } #scene1.active .hook-line:nth-child(1) { opacity:1; transform:none; filter:blur(0); transition-delay:0.2s; } #scene1.active .hook-line:nth-child(2) { opacity:1; transform:none; filter:blur(0); transition-delay:0.8s; } Maybe add underline. Stats: .section-title transform etc. .stat transform. Add active delays. Cards: .cards-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(220px,1fr)); gap:1.5rem; width: min(1100px,90vw); } .card { opacity:0; transform: translateY(40px); transition: all 0.7s cubic-bezier(.22,1,.36,1); } #scene3.active .card:nth-child(1) { opacity:1