``` Now let me design the CSS in detail. I want: 1. **Background**: Deep aubergine gradient with subtle radial highlights 2. **Particles**: Gold specks floating 3. **Grain**: Subtle noise overlay 4. **Vignette**: Dark edges 5. **Light beams**: Diagonal gold light rays 6. **Letterbox**: Top/bottom black bars For typography: - "Your AI agents" - massive, white/cream - "should be making you money" - gold accent on key words - Stats - huge numbers with smaller labels - Cards - bold headers with descriptions - Quotes - elegant italic - CTA - bold and impactful Let me think about kinetic typography effects: - Letter-by-letter reveal - Word slides with slight rotation - Scale and fade - Stagger delays For the cards, I'll use 3D transforms with perspective. Let me write the actual code now. I'll be thorough. ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; background: #000; overflow: hidden; font-family: 'Inter', sans-serif; } .stage { position: relative; width: 100vw; height: 100vh; background: radial-gradient(ellipse at center, #2a0e3a 0%, #14081e 50%, #0a0612 100%); overflow: hidden; } /* Letterbox */ .letterbox { position: absolute; left: 0; right: 0; background: #000; z-index: 100; height: 8vh; } .letterbox.top { top: 0; } .letterbox.bottom { bottom: 0; } /* Particles */ .particle { position: absolute; width: 4px; height: 4px; background: radial-gradient(circle, #f4d03f 0%, transparent 70%); border-radius: 50%; pointer-events: none; will-change: transform, opacity; } /* Vignette */ .vignette { position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 30%, rgba(0,0,0,0.7) 100%); pointer-events: none; z-index: 50; } /* Grain */ .grain { position: absolute; inset: 0; opacity: 0.08; mix-blend-mode: overlay; pointer-events: none; z-index: 51; background-image: url("data:image/svg+xml;utf8,"); } /* Light beam */ .beam { position: absolute; width: 200%; height: 200px; top: 50%; left: -50%; background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.08), transparent); transform: translateY(-50%) rotate(-15deg); filter: blur(40px); pointer-events: none; } /* Scenes */ .scene { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; z-index: 10; } .scene.active { pointer-events: auto; } /* Scene 1 - Hook */ .hook { text-align: center; max-width: 90%; } .hook .line { font-family: 'Anton', sans-serif; font-size: clamp(40px, 8vw, 120px); color: #f5e6d3; line-height: 1; letter-spacing: -0.02em; text-transform: uppercase; opacity: 0; transform: translateY(40px); transition: all 1.2s cubic-bezier(0.16, 1, 0.3, 1); } .hook .line.show { opacity: 1; transform: translateY(0); } .hook .line .accent { color: #d4af37; background: linear-gradient(180deg, #f4d03f 0%, #d4af37 50%, #b8860b 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } .hook .line .word { display: inline-block; opacity: 0; transform: translateY(30px); transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); } .hook .line.show .word { opacity: 1; transform: translateY(0); } /* etc */ ``` OK this is getting long. Let me just write the full thing. I'll make it really polished. Let me think about the timeline more carefully and what the master loop does: ```js const TIMELINE = [ { time: 0, action: 'scene1-in' }, { time: 4, action: 'scene1-out' }, { time: 4.5, action: 'scene2-in' }, { time: 9.5, action: 'scene2-out' }, { time: 10, action: 'scene3-in' }, { time: 16.5, action: 'scene3-out' }, { time: 17, action: 'scene4-in' }, { time: 25, action: 'scene4-out' }, { time: 25.5, action: 'scene5-in' }, { time: 30, action: 'reset' } ]; ``` Actually, the simpler approach: use CSS keyframe animations with `animation-delay` based on the master timeline. Or, use a single JS loop that updates classes/styles based on elapsed time. I'll go with the JS loop approach as it gives more control and smoother coordination. Let me think about the count-up for stats. I need to: - 3,600+ founders - count from 0 to 3600 over 1.5s - 38 countries - count from 0 to 38 - $59/mo - count from $0 to $59 For the testimonials, I'll need: - Avatar images (use placeholder gradient circles with initials) - Quote text - Author name + role Let me write the testimonials: 1. "AIPB gave me the playbook to ship my first AI agent in a week. Now it's a $4k/mo business." โ€” Sarah Chen, Founder at ReplicaLabs 2. "The weekly calls alone are worth 10x the price. I've made more here than in any mastermind." โ€” Marcus Webb, Solo Founder 3. "Real operators, no fluff. I cloned a community member's agent and it's already profitable." โ€” Diana Voss, CEO at Helix Now let me think about the design language. I want this to feel like a high-end tech brand ad. Think: - Apple keynote transitions - Stripe's gradient aesthetics - Linear's clean typography - Premium dark with gold accents Let me code it all up now. I'll create the file in one go. For the particles, I'll spawn them in JS with random positions and have them drift up with fading. For the count-up, I'll use requestAnimationFrame with easing. For the progress bar, it's a thin line at the bottom that fills from left to right. For the card slide-in, I'll use 3D transforms with perspective. Let me also add a subtle "film noise" effect using SVG filter or canvas. Alright, let me write the full code. I'll aim for a very polished, premium feel. Actually, let me also add: - A subtle camera "drift" effect (parallax on mouse-less, just slow movement) - Glow/blur effects on text - Subtle grid pattern overlay for tech feel - Brand mark/logo For the brand mark, I'll create a simple geometric "AIPB" monogram or just text. Let me start writing. I'll keep it organized. ```html AI Profit Boardroom
``` OK let me just write the whole thing now. For the camera, I want a slow pan/drift. I can apply a transform: translate() with very slow movement using CSS animation, or update it in JS based on time. Let me think about scene transitions: - Use opacity fade + slight scale/translate - 0.5-0.8s transitions - Easing: cubic-bezier(0.65, 0, 0.35, 1) for smooth For Scene 1 (Hook), I want: - Black at t=0 - Subtle logo/mark fade in - "Your AI agents" appears with letter reveal - "should be making you money" appears word by word - Hold - Fade out to Scene 2 For Scene 2 (Stats), I want: - 3 large numbers appearing sequentially - Counter animation - Each stat has icon/decoration - Subtitle: "The community rewriting the rules" For Scene 3 (Cards), I want: - Title "What you get" - 4 cards slide in from different directions - Each card has icon, title, description - Cards have glassmorphism or solid dark with gold border - Maybe 3D rotation For Scene 4 (Testimonials): - Title: "Founders are shipping" - Quote cards with avatar circles - Smooth crossfade between 3 quotes For Scene 5 (CTA): - Massive "Join AIPB" - URL pulses - Final hold Let me think about the iconography. I can use simple SVG icons or unicode symbols: - Agent OS: ๐Ÿค– or a chip icon - Weekly Calls: ๐Ÿ“ž or video icon - Templates: ๐Ÿ“‹ or grid icon - Community: ๐Ÿ‘ฅ or network icon Actually, let me create simple SVG icons inline. OK I'm going to just write the code now. Let me make it really good. One more thought: I should add some 3D depth. I can use a 3D transform on the whole stage with slight rotation, or have layered elements with different z-depths. For the "cinematic" feel: - Anamorphic lens flare (subtle) - Letterbox bars - Color grading (warm gold highlights, cool aubergine shadows) - Slow camera movements - Smooth focus pulls (blur transitions) Let me write the file. I'll make it comprehensive. Let me start with the CSS: ```css :root { --gold: #d4af37; --gold-light: #f4d03f; --gold-dark: #b8860b; --aubergine: #2d0a2d; --aubergine-mid: #4a1a4a; --aubergine-light: #6a2a5a; --bg: #0a0612; --bg-mid: #14081e; --bg-light: #1f0d2e; --cream: #f5e6d3; --white: #ffffff; } * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; background: #000; overflow: hidden; font-family: 'Inter', -apple-system, sans-serif; color: var(--cream); -webkit-font-smoothing: antialiased; } .stage { position: relative; width: 100vw; height: 100vh; background: radial-gradient(ellipse 80% 50% at 50% 50%, rgba(74, 26, 74, 0.4) 0%, transparent 70%), radial-gradient(ellipse 100% 100% at 50% 50%, #1a0a2a 0%, #0a0612 100%); overflow: hidden; perspective: 1500px; } /* Cinematic letterbox */ .letterbox { position: absolute; left: 0; right: 0; background: #000; height: 7vh; z-index: 100; pointer-events: none; } .letterbox.top { top: 0; box-shadow: 0 4px 30px rgba(212, 175, 55, 0.1); } .letterbox.bottom { bottom: 0; box-shadow: 0 -4px 30px rgba(212, 175, 55, 0.1); } /* Light beams */ .beam-container { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 5; } .beam { position: absolute; width: 200%; height: 100px; background: linear-gradient(90deg, transparent 0%, rgba(244, 208, 63, 0.1) 50%, transparent 100%); filter: blur(50px); transform-origin: 0 0; animation: beamFloat 12s ease-in-out infinite; } .beam-1 { top: 20%; left: -50%; transform: rotate(-12deg); animation-delay: 0s; } .beam-2 { top: 60%; left: -50%; transform: rotate(8deg); animation-delay: -4s; opacity: 0.6; } .beam-3 { top: 40%; left: -50%; transform: rotate(-5deg); animation-delay: -8s; opacity: 0.4; } @keyframes beamFloat { 0%, 100% { transform: translateX(0) rotate(-12deg); } 50% { transform: translateX(20%, 5%) rotate(-10deg); } } /* Particles */ .particles { position: absolute; inset: 0; pointer-events: none; z-index: 6; } .particle { position: absolute; border-radius: 50%; background: radial-gradient(circle, #f4d03f 0%, rgba(212, 175, 55, 0.3) 50%, transparent 100%); pointer-events: none; animation: float linear infinite; } @keyframes float { 0% { transform: translateY(0) translateX(0); opacity: 0; } 10% { opacity: 1; } 90% { opacity: 1; } 100% { transform: translateY(-100vh) translateX(20px); opacity: 0; } } /* Vignette */ .vignette { position: absolute; inset: 0; background: radial-gradient(ellipse 100% 80% at 50% 50%, transparent 40%, rgba(0,0,0,0.6) 100%); pointer-events: none; z-index: 40; } /* Grain */ .grain { position: absolute; inset: 0; opacity: 0.06; mix-blend-mode: overlay; pointer-events: none; z-index: 41; background-image: url("data:image/svg+xml;utf8,"); animation: grainShift 0.5s steps(4) infinite; } @keyframes grainShift { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(-2%, 1%); } 50% { transform: translate(1%, -2%); } 75% { transform: translate(-1%, -1%); } } /* Brand mark (always visible, subtle) */ .brand-mark { position: absolute; top: 9vh; left: 50%; transform: translateX(-50%); z-index: 60; display: flex; align-items: center; gap: 10px; opacity: 0; transition: opacity 1s; } .brand-mark.show { opacity: 0.8; } .brand-mark .dot { width: 6px; height: 6px; background: var(--gold); border-radius: 50%; box-shadow: 0 0 12px var(--gold); } .brand-mark .text { font-family: 'Inter', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.3em; color: var(--cream); text-transform: uppercase; } /* Progress bar */ .progress { position: absolute; bottom: 7vh; left: 0; right: 0; height: 2px; background: rgba(255,255,255,0.08); z-index: 110; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--gold-dark), var(--gold-light)); box-shadow: 0 0 10px var(--gold); width: 0%; transition: width 0.1s linear; } /* Scenes common */ .scene { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; opacity: 0; z-index: 10; transition: opacity 0.8s cubic-bezier(0.65, 0, 0.35, 1); pointer-events: none; } .scene.active { opacity: 1; pointer-events: auto; } /* Scene 1: Hook */ .hook-container { text-align: center; max-width: 1400px; padding: 0 5vw; } .hook-pre { font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 500; letter-spacing: 0.4em; color: var(--gold); text-transform: uppercase; margin-bottom: 30px; opacity: 0; transform: translateY(20px); transition: all 1s cubic-bezier(0.16, 1, 0.3, 1) 0.2s; } .hook-pre.show { opacity: 1; transform: translateY(0); } .hook-line { font-family: 'Anton', sans-serif; font-size: clamp(48px, 9vw, 140px); line-height: 0.95; letter-spacing: -0.01em; text-transform: uppercase; color: var(--cream); margin-bottom: 10px; opacity: 0; transform: translateY(60px) scale(0.95); transition: all 1.2s cubic-bezier(0.16, 1, 0.3, 1); } .hook-line.show { opacity: 1; transform: translateY(0) scale(1); } .hook-line .word { display: inline-block; white-space: nowrap; } .hook-line .accent { background: linear-gradient(180deg, #f4d03f 0%, #d4af37 50%, #b8860b 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; position: relative; display: inline-block; } .hook-line .accent::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, transparent, var(--gold), transparent); animation: underline 1.5s ease-out 1s forwards; transform: scaleX(0); transform-origin: center; } @keyframes underline { to { transform: scaleX(1); } } /* Scene 2: Stats */ .stats-container { display: flex; align-items: center; justify-content: center; gap: 8vw; width: 100%; max-width: 1400px; padding: 0 5vw; } .stat { text-align: center; opacity: 0; transform: translateY(40px); transition: all 1s cubic-bezier(0.16, 1, 0.3, 1); } .stat.show { opacity: 1; transform: translateY(0); } .stat-number { font-family: 'Anton', sans-serif; font-size: clamp(70px, 11vw, 180px); line-height: 1; background: linear-gradient(180deg, #f4d03f 0%, #d4af37 50%, #b8860b 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.02em; } .stat-label { font-family: 'Inter', sans-serif; font-size: 14px; font-weight: 500; letter-spacing: 0.25em; text-transform: uppercase; color: var(--cream); margin-top: 16px; opacity: 0.8; } .stat-divider { width: 1px; height: 100px; background: linear-gradient(180deg, transparent, var(--gold), transparent); opacity: 0.3; } /* Scene 3: Cards */ .cards-title { position: absolute; top: 15vh; left: 50%; transform: translateX(-50%); font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 500; letter-spacing: 0.4em; color: var(--gold); text-transform: uppercase; opacity: 0; transition: opacity 1s; white-space: nowrap; } .cards-title.show { opacity: 1; } .cards-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; max-width: 900px; width: 90%; padding: 0 5vw; } .card { position: relative; background: linear-gradient(135deg, rgba(74, 26, 74, 0.3) 0%, rgba(20, 8, 30, 0.6) 100%); border: 1px solid rgba(212, 175, 55, 0.2); border-radius: 16px; padding: 32px 28px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(60px) rotateX(10deg); transition: all 0.9s cubic-bezier(0.16, 1, 0.3, 1); overflow: hidden; } .card.show { opacity: 1; transform: translateY(0) rotateX(0); } .card::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.1), transparent); transition: left 0.8s; } .card.show::before { left: 100%; } .card-icon { width: 48px; height: 48px; border-radius: 12px; background: linear-gradient(135deg, var(--gold) 0%, var(--gold-dark) 100%); display: flex; align-items: center; justify-content: center; margin-bottom: 20px; box-shadow: 0 8px 20px rgba(212, 175, 55, 0.3); } .card-icon svg { width: 24px; height: 24px; stroke: var(--bg); stroke-width: 2; fill: none; } .card-title { font-family: 'Anton', sans-serif; font-size: 28px; color: var(--cream); text-transform: uppercase; letter-spacing: 0.02em; margin-bottom: 8px; } .card-desc { font-family: 'Inter', sans-serif; font-size: 14px; color: rgba(245, 230, 211, 0.7); line-height: 1.5; } /* Scene 4: Testimonials */ .testimonials-container { width: 100%; max-width: 1100px; padding: 0 5vw; text-align: center; } .testimonials-title { font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 500; letter-spacing: 0.4em; color: var(--gold); text-transform: uppercase; margin-bottom: 50px; opacity: 0; transition: opacity 1s; } .testimonials-title.show { opacity: 1; } .testimonial { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 100%; max-width: 900px; padding: 0 5vw; opacity: 0; transition: opacity 1.2s, transform 1.2s; pointer-events: none; } .testimonial.active { opacity: 1; transform: translate(-50%, -50%); pointer-events: auto; } .testimonial.exit { opacity: 0; transform: translate(-50%, calc(-50% - 30px)); } .quote-mark { font-family: 'Cormorant Garamond', serif; font-size: 120px; color: var(--gold); line-height: 0.5; margin-bottom: -30px; opacity: 0.6; } .quote-text { font-family: 'Cormorant Garamond', serif; font-style: italic; font-size: clamp(24px, 3.5vw, 42px); line-height: 1.3; color: var(--cream); margin-bottom: 40px; } .quote-text .hl { color: var(--gold-light); font-style: normal; font-weight: 600; } .quote-author { display: flex; align-items: center; justify-content: center; gap: 16px; } .avatar { width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, var(--gold) 0%, var(--gold-dark) 100%); display: flex; align-items: center; justify-content: center; font-family: 'Inter', sans-serif; font-weight: 600; font-size: 18px; color: var(--bg); border: 2px solid var(--gold); box-shadow: 0 0 20px rgba(212, 175, 55, 0.3); } .author-info { text-align: left; } .author-name { font-family: 'Inter', sans-serif; font-weight: 600; font-size: 16px; color: var(--cream); } .author-role { font-family: 'Inter', sans-serif; font-size: 13px; color: rgba(245, 230, 211, 0.6); margin-top: 2px; } /* Scene 5: CTA */ .cta-container { text-align: center; } .cta-pre { font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 500; letter-spacing: 0.4em; color: var(--gold); text-transform: uppercase; margin-bottom: 30px; opacity: 0; transition: opacity 1s; } .cta-pre.show { opacity: 1; } .cta-line { font-family: 'Anton', sans-serif; font-size: clamp(60px, 11vw, 160px); line-height: 1; text-transform: uppercase; background: linear-gradient(180deg, #f4d03f 0%, #d4af37 50%, #b8860b 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -0.01em; margin-bottom: 24px; opacity: 0; transform: scale(0.9); transition: all 1.2s cubic-bezier(0.16, 1, 0.3, 1); } .cta-line.show { opacity: 1; transform: scale(1); } .cta-url { font-family: 'Inter', sans-serif; font-size: clamp(18px, 2vw, 28px); font-weight: 500; letter-spacing: 0.1em; color: var(--cream); position: relative; display: inline-block; padding: 16px 36px; border: 1px solid var(--gold); border-radius: 100px; background: linear-gradient(135deg, rgba(212, 175, 55, 0.1), rgba(212, 175, 55, 0.02)); opacity: 0; transform: translateY(20px); transition: all 1s cubic-bezier(0.16, 1, 0.3, 1) 0.3s; } .cta-url.show { opacity: 1; transform: translateY(0); } .cta-url::before { content: ''; position: absolute; inset: -1px; border-radius: 100px; background: linear-gradient(135deg, var(--gold-light), var(--gold-dark)); z-index: -1; opacity: 0; transition: opacity 0.4s; } .cta-url:hover::before { opacity: 1; } /* Background orbs for extra atmosphere */ .orb { position: absolute; border-radius: 50%; filter: blur(80px); opacity: 0.4; pointer-events: none; animation: orbFloat 20s ease-in-out infinite; } .orb-1 { width: 400px; height: 400px; background: radial-gradient(circle, var(--aubergine-light) 0%, transparent 70%); top: -100px; right: -100px; } .orb-2 { width: 500px; height: 500px; background: radial-gradient(circle, rgba(212, 175, 55, 0.3) 0%, transparent 70%); bottom: -150px; left: -150px; animation-delay: -10s; } @keyframes orbFloat { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(30px, -30px); } } /* Initial hint overlay (subtle) */ .hint { position: absolute; bottom: calc(7vh + 20px); left: 50%; transform: translateX(-50%); font-family: 'Inter', sans-serif; font-size: 11px; letter-spacing: 0.3em; color: rgba(245, 230, 211, 0.3); text-transform: uppercase; z-index: 105; opacity: 0; animation: hintFade 6s ease-out 1s forwards; pointer-events: none; } @keyframes hintFade { 0% { opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { opacity: 0; } } ``` OK that's a lot. Let me think about the JavaScript timeline now. ```js const TOTAL_DURATION = 30000; // 30s let startTime = null; let animationId = null; const timeline = { 0: () => { /* start scene 1 */ }, 4: () => { /* scene 1 out, scene 2 in */ }, 9: () => { /* scene 2 out, scene 3 in */ }, // etc }; ``` Or I'll use a state-based approach where the master loop checks time and triggers transitions. Let me think about the word-by-word reveal for scene 1. I'll split text into words and apply staggered animation delays. For the count-up, I'll write a function that interpolates a number with easing. ```js function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); } function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2; } function animateNumber(el, from, to, duration, prefix = '', suffix = '') { const start = performance.now(); function update(now) { const elapsed = now - start; const t = Math.min(elapsed / duration, 1); const eased = easeOutCubic(t); const value = Math.round(from + (to - from) * eased); el.textContent = prefix + value.toLocaleString() + suffix; if (t < 1) requestAnimationFrame(update); } requestAnimationFrame(update); } ``` OK let me put this all together and write the final code. Let me think about the avatar gradients - I want them to look distinct for each person: - Sarah Chen: warm gold/orange - Marcus Webb: cool blue/aubergine - Diana Voss: green/aubergine I'll use different gradient combinations. For the card icons, simple SVG paths: - Agent OS: chip/circuit - Weekly Calls: video/phone - Templates: grid/document - Community: people/network Let me write the full file now. I'll be thorough and aim for a really polished result. Actually, let me reconsider the structure. The animation needs to: 1. Start immediately 2. First frame should show something (not be empty) 3. Auto-loop after ~30s For the first frame, I'll have the brand mark and Scene 1's first elements already positioned (just before they animate in). The fade-in from 0 opacity happens in the first 200ms. Wait, but if the first frame should show visible content, I should have the hook already starting to appear. Let me have the scene 1 elements be visible by default, and the animation just enhances them. Actually, the cleanest approach: - All scenes start with opacity 0 - The master timeline immediately starts fading in scene 1 elements - By 100ms, something is visible - The animation continues OR: - Scene 1's first word is visible immediately - Then more words animate in I'll go with: Scene 1's pre-text "INTRODUCING" appears first, then the hook. The first frame shows the dark background with the brand mark. Actually, let me have the very first thing be the brand mark fading in, then the hook. This gives a clean opening. Hmm, but the request says "Render visible content on the first frame". So I need to have something visible. Let me have the scene 1 elements start at their final state and just animate IN over the first second. The first frame will show them at ~0% but with the brand mark visible. Or even simpler: I'll have the brand mark always visible (faded in at t=0), and scene 1's first elements appear within the first 200ms. So the first frame has the brand mark + dark background with letterbox. Let me also add a subtle hint at the bottom that fades out, like "auto-playing" or just the URL. OK let me write the final code. I'll be careful to make it production quality. One more consideration: the progress bar should be visible the whole time and fill from 0% to 100% over 30s. Let me also think about looping. After 30s, the timeline should restart. I can simply reset all states and call the start function again. Now let me code it. I want to make sure the visual design is REALLY premium. Let me think about: - A subtle grid pattern in the background (very faint, like a tech blueprint) - Animated lines or shapes - Glow effects - Smooth motion For the grid pattern: ```css .grid-bg { position: absolute; inset: 0; background-image: linear-gradient(rgba(212, 175, 55, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(212, 175, 55, 0.03) 1px, transparent 1px); background-size: 60px 60px; pointer-events: none; z-index: 3; } ``` OK let me write the full thing now. I want this to be exceptional. Let me draft the JS timeline: ```js // Timeline in seconds const SCENES = { hook: { start: 0, end: 5 }, stats: { start: 5, end: 10 }, cards: { start: 10, end: 17 }, testimonials: { start: 17, end: 25 }, cta: { start: 25, end: 30 } }; ``` Within each scene, sub-animations happen at specific times. For the hook: - 0.0s: brand mark fades in - 0.3s: pre-text "INTRODUCING" appears - 0.8s: "Your AI agents" appears (word by word) - 2.0s: "should be making you money" appears - 2.8s: underline animation - 4.5s: scene starts fading out For stats: - 5.0s: scene 2 in - 5.3s: stat 1 starts counting - 6.8s: stat 2 starts - 8.3s: stat 3 starts - 9.5s: scene out For cards: - 10.0s: scene in, title appears - 10.5s: card 1 slides in - 11.5s: card 2 slides in - 12.5s: card 3 slides in - 13.5s: card 4 slides in - 16.0s: hold - 16.5s: scene out For testimonials: - 17.0s: scene in, title appears - 17.5s: testimonial 1 fades in - 20.5s: testimonial 1 out, testimonial 2 in - 22.5s: testimonial 2 out, testimonial 3 in - 25.0s: scene out For CTA: - 25.0s: scene in - 25.5s: pre-text - 26.0s: "Join AIPB" scales in - 27.5s: URL pill appears - 29.0s: pulse effect on URL - 30.0s: loop OK let me write the code now. I'll be efficient. For the word-by-word reveal, I'll create the spans in HTML/JS with calculated delays. ```js function createWordSpans(text, className) { return text.split(' ').map(word => `${word}` ).join(' '); } ``` Then in CSS: ```css .word { display: inline-block; opacity: 0; transform: translateY(20px); transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1); transition-delay: calc(var(--i) * 0.08s); } .show .word { opacity: 1; transform: translateY(0); } ``` But I want to set the --i variable dynamically. I can do this in JS. OK let me just write the code. I'll structure it well. Actually, I realize I should also think about the "end CTA" being "Join AIPB โ€” aiprofitboardroom.com". So the scene 5 needs: - "Join AIPB" big - "aiprofitboardroom.com" as the URL Let me finalize and write. One more thing: I want a "scroll cue" or some indicator that the video is playing. The progress bar handles that. But maybe also a small "playing" indicator? Probably not needed. Let me write the full HTML file now. I'll structure it as: 1. HTML structure 2. CSS (organized by component) 3. JavaScript (timeline engine + utilities) Let me code: ```html AIPB โ€” AI Profit Boardroom
AI Profit Boardroom
โ€” INTRODUCING โ€”

Your AI agents

should be making you money

0
Founders
0
Countries
$0/mo
Per month
โ€” WHAT YOU GET โ€”
Agent OS
The operating system for profitable AI agents. Deploy, monitor, monetize.
Weekly Calls
Live mastermind sessions with operators shipping at scale every week.
Templates
Battle-tested agent templates, prompts, and SOPs you can clone instantly.
Community
3,600+ founders swapping wins, deals, and AI plays in real time.
โ€” FOUNDERS ARE SHIPPING โ€”
"

AIPB gave me the playbook to ship my first AI agent in a week. Now it's a $4k/mo business.

SC
Sarah Chen
Founder, ReplicaLabs
"

The weekly calls alone are worth 10x the price. I've made more here than in any mastermind.

MW
Marcus Webb
Solo Founder, Austin TX
"

Real operators, no fluff. I cloned a community member's agent and it's already profitable.

DV
Diana Voss
CEO, Helix AI
โ€” THE BOARDROOM IS OPEN โ€”
Join AIPB
aiprofitboardroom.com
โ–ถ AUTO-PLAYING
``` Now the JavaScript. Let me write a clean timeline engine. ```js (() => { const DURATION = 30000; // ms // Easing const easeOutCubic = t => 1 - Math.pow(1 - t, 3); const easeInOutCubic = t => t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2; // Helpers const $ = id => document.getElementById(id); const addClass = (el, cls) => el && el.classList.add(cls); const removeClass = (el, cls) => el && el.classList.remove(cls); // Set up word-by-word animations function splitIntoWords(el) { if (!el) return; const html = el.innerHTML; // Parse the HTML to extract text and accent spans // Simple approach: just split text content const text = el.textContent; el.innerHTML = ''; text.split(' ').forEach((word, i) => { const span = document.createElement('span'); span.className = 'word'; span.textContent = word; span.style.setProperty('--i', i); el.appendChild(span); el.appendChild(document.createTextNode(' ')); }); } // Hmm, this loses the accent spans. Let me handle it differently. // For the hook, I'll keep the structure with accent spans as-is // and animate the whole line. Word-by-word would require restructuring. // Actually, let me just have the whole line fade in, no word-by-word. // It's simpler and still looks good. // Count-up animation function countUp(el, target, duration, prefix = '', suffix = '') { const start = performance.now(); function update(now) { const elapsed = now - start; const t = Math.min(elapsed / duration, 1); const eased = easeOutCubic(t); const value = Math.floor(target * eased); el.textContent = prefix + value.toLocaleString() + suffix; if (t < 1) requestAnimationFrame(update); } requestAnimationFrame(update); } // Particles function createParticle() { const particles = $('particles'); if (!particles) return; const p = document.createElement('div'); p.className = 'particle'; const size = Math.random() * 3 + 1; p.style.width = size + 'px'; p.style.height = size + 'px'; p.style.left = Math.random() * 100 + '%'; p.style.top = Math.random() * 100 + '%'; p.style.animationDuration = (Math.random() * 10 + 8) + 's'; p.style.animationDelay = '-' + Math.random() * 10 + 's'; p.style.opacity = Math.random() * 0.6 + 0.2; particles.appendChild(p); } // Create some particles for (let i = 0; i < 40; i++) createParticle(); // Timeline events (in ms) const events = [ // Scene 1 { t: 0, fn: () => { addClass($('brand'), 'show'); addClass($('scene-hook'), 'active'); }}, { t: 300, fn: () => addClass($('hook-pre'), 'show') }, { t: 700, fn: () => addClass($('hook-1'), 'show') }, { t: 1800, fn: () => addClass($('hook-2'), 'show') }, // Scene 2 { t: 5000, fn: () => { removeClass($('scene-hook'), 'active'); removeClass($('hook-pre'), 'show'); removeClass($('hook-1'), 'show'); removeClass($('hook-2'), 'show'); addClass($('scene-stats'), 'active'); }}, { t: 5300, fn: () => { addClass($('stat-1'), 'show'); countUp($('stat-1').querySelector('.stat-number'), 3600, 1800, '', '+'); }}, { t: 6800, fn: () => { addClass($('stat-2'), 'show'); countUp($('stat-2').querySelector('.stat-number'), 38, 1200, '', ''); }}, { t: 8300, fn: () => { addClass($('stat-3'), 'show'); countUp($('stat-3').querySelector('.stat-number'), 59, 1200, '$', '/mo'); }}, // Scene 3 { t: 10000, fn: () => { removeClass($('scene-stats'), 'active'); document.querySelectorAll('#scene-stats .stat').forEach(s => removeClass(s, 'show')); addClass($('scene-cards'), 'active'); addClass($('cards-title'), 'show'); }}, { t: 10500, fn: () => addClass($('card-1'), 'show') }, { t: 11500, fn: () => addClass($('card-2'), 'show') }, { t: 12500, fn: () => addClass($('card-3'), 'show') }, { t: 13500, fn: () => addClass($('card-4'), 'show') }, // Scene 4 { t: 16500, fn: () => { removeClass($('scene-cards'), 'active'); removeClass($('cards-title'), 'show'); document.querySelectorAll('.card').forEach(c => removeClass(c, 'show')); addClass($('scene-testimonials'), 'active'); addClass($('testimonials-title'), 'show'); }}, { t: 17000, fn: () => addClass($('t-1'), 'active') }, { t: 20000, fn: () => { removeClass($('t-1'), 'active'); addClass($('t-2'), 'active'); }}, { t: 22500, fn: () => { removeClass($('t-2'), 'active'); addClass($('t-3'), 'active'); }}, // Scene 5 { t: 25000, fn: () => { removeClass($('scene-testimonials'), 'active'); removeClass($('testimonials-title'), 'show'); removeClass($('t-3'), 'active'); addClass($('scene-cta'), 'active'); }}, { t: 25500, fn: () => addClass($('cta-pre'), 'show') }, { t: 26000, fn: () => addClass($('cta-line'), 'show') }, { t: 27500, fn: () => addClass($('cta-url'), 'show') }, // Loop { t: 30000, fn: () => { // Reset all document.querySelectorAll('.scene').forEach(s => removeClass(s, 'active')); document.querySelectorAll('.show, .active').forEach(e => { if (e.id !== 'brand') e.classList.remove('show', 'active'); }); // Reset stat numbers document.querySelectorAll('.stat-number').forEach(s => { s.textContent = s.dataset.prefix || '0'; s.textContent = (s.dataset.prefix || '') + '0' + (s.dataset.suffix || ''); }); // Restart startTime = performance.now(); }} ]; let startTime = null; let executedEvents = new Set(); function tick(now) { if (!startTime) startTime = now; const elapsed = now - startTime; // Update progress const progress = Math.min(elapsed / DURATION, 1) * 100; $('progress').style.width = progress + '%'; // Execute events events.forEach((event, i) => { if (elapsed >= event.t && !executedEvents.has(i)) { event.fn(); executedEvents.add(i); } }); if (elapsed < DURATION + 500) { animationId = requestAnimationFrame(tick); } } // Start animationId = requestAnimationFrame(tick); })(); ``` Hmm, the reset logic is a bit messy. Let me clean it up. Also, I want to think about whether to use CSS animations triggered by classes, or JS-driven animations. CSS is more performant for simple transitions, JS gives more control. I'll use a hybrid: CSS for entrances/exits, JS for the count-up. Let me also think about the testimonials more. The current setup has them as positioned absolutely on top of each other. The transitions: - t-1 fades in at 17s - t-1 fades out, t-2 fades in at 20s - t-2 fades out, t-3 fades in at 22.5s - all fade out at 25s The CSS: ```css .testimonial { /* default: opacity 0, slightly translated up */ opacity: 0; transform: translate(-50%, calc(-50% + 30px)); transition: opacity 1s, transform 1s; } .testimonial.active { opacity: 1; transform: translate(-50%, -50%); } ``` For the exit, I can just remove the active class and the transition will fade it out. But wait, the way I've set it up, all three testimonials are in the same position. When I add 'active' to t-1, it fades in. When I remove 'active' from t-1 and add to t-2, t-1 fades out and t-2 fades in simultaneously. That should look good. Let me refine the CSS to handle the testiominal transitions better: ```css .testimonial { position: absolute; left: 50%; top: 50%; transform: translate(-50%, calc(-50% + 20px)); width: 100%; max-width: 900px; padding: 0 5vw; opacity: 0; transition: opacity 0.8s, transform 0.8s; pointer-events: none; } .testimonial.active { opacity: 1; transform: translate(-50%, -50%); } ``` This way, the active testimonial slides up to center, and when deactivated, it slides back down and fades. Good. Let me also make sure the title is positioned correctly. It should be at the top of the scene, above the testimonial. Hmm, the testimonials are positioned at top: 50% with transform translate(-50%, -50%). So they're centered. The title is positioned... where? I had it as `position: absolute; top: 15vh`. But the testimonials are also positioned absolute. Let me make sure they don't overlap. Actually, let me restructure. The testimonials scene has: - Title at top (positioned absolute, top: 15vh) - Testimonial centered (positioned absolute, top: 50%) This should work if the title doesn't overlap with the testimonial area. The testimonial is centered vertically, so it occupies the middle. The title at top: 15vh is above. Should be fine. Let me also add a subtle background to the testimonials scene, like radial spotlight. OK let me also think about the cards scene. I have: - Title at top (positioned absolute, top: 15vh) - Grid centered (display: grid) The grid has 2 columns, 2 rows. Each card is in a grid cell. The whole grid is centered by the flex parent. But wait, the cards have `transform: translateY(60px) rotateX(10deg)` initially, and `translateY(0) rotateX(0)` when shown. That's a 3D rotation. I need to make sure the perspective is set on the parent. ```css .scene#scene-cards { perspective: 1500px; } ``` Or set it on the cards themselves. Let me set it on the scene. Actually, the .stage already has perspective: 1500px. So it should work for all children. Let me think about whether the 3D rotation is too much. Maybe just a simple slide-up with slight scale is more elegant. Let me simplify: ```css .card { opacity: 0; transform: translateY(40px) scale(0.95); transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); } .card.show { opacity: 1; transform: translateY(0) scale(1); } ``` Cleaner. The 3D was maybe too much. OK let me finalize. I want to add a few more polish elements: - Subtle camera "breathing" - very slow scale on the whole stage - Subtle hue shift over time - Scan lines (very subtle, cinematic) Let me add a slow camera drift: ```css .stage { animation: cameraDrift 20s ease-in-out infinite; } @keyframes cameraDrift { 0%, 100% { transform: scale(1) translate(0, 0); } 50% { transform: scale(1.02) translate(-0.5%, 0.5%); } } ``` Hmm, that might be too much. Let me skip the camera drift and just have a subtle vignette pulse. OK let me write the final code. I'll keep it clean and well-organized. Wait, I also want to think about the initial state. The request says "Render visible content on the first frame โ€” no blocking start screens". So the first frame should have something visible. If the hook scene starts with all elements at opacity 0 and they fade in over 0-2s, then the first frame will be empty (just the background). To fix this, I can either: 1. Have the brand mark visible immediately (it is) 2. Start the hook with some elements already visible 3. Have a very short fade-in (200ms) so the first frame still shows something I'll do option 1 + 3. The brand mark fades in over 500ms but the dark background with letterbox is always visible. The hook starts appearing at 200ms with a quick fade-in. Actually, the brand mark starts at opacity 0 with a transition. Let me change it to be visible from the start, or have a very short transition. Let me have the brand mark already at opacity 0.8 on the first frame, no transition needed for the initial state. I'll just animate the show class. Wait, if it starts at opacity 0 and has a 1s transition, then at t=0 it's at 0. By t=0.5 it's at 0.4. The first frame (t=0) is opacity 0. To have something visible at t=0, I can: - Start the brand mark at opacity 0.8 by default - The animation just confirms it OR - Start with opacity 0.4 (subtle) - Animate to 0.8 I think starting with a subtle default visibility is best. Let me have the brand mark at opacity 0.5 by default, and the show class makes it 0.8. So the first frame shows the brand mark faintly. Or even simpler: the brand mark is always visible at 0.7 opacity, no animation. The other elements animate in. Let me go with: brand mark always visible (no animation), other elements animate in. Actually, I want the brand mark to be subtle but present. Let me have it at 0.6 opacity, no entrance animation. OK let me just write the code. I've been planning long enough. Let me also think about responsiveness. The video should work on different screen sizes. The clamp() font sizes will help. The grid will adapt. For mobile, the cards grid should probably be 1 column. Let me add a media query: ```css @media (max-width: 600px) { .cards-grid { grid-template-columns: 1fr; } .stats-container { flex-direction: column; gap: 30px; } .stat-divider { width: 100px; height: 1px; } } ``` Good. Let me write the final code now. I'll combine everything I've planned. One last thought: the testiominal avatar should have a gradient that looks like an actual avatar. I can use: - Linear gradients with multiple colors - Or even just initials with a nice color I'll use initials with a gold/aubergine gradient. Different combinations for each person. OK writing now. Let me be thorough and produce a great result. For the icons in cards, I'll use simple SVG paths: - Agent OS: chip/circuit icon - Weekly Calls: video/play icon - Templates: grid/layers icon - Community: people/users icon ```html ``` OK let me write the complete file now. I want to make it really shine. Let me also add a subtle "loop" indicator at the end, or just have it seamlessly loop. The seamless loop means: - At t=30s, everything resets - At t=0 (next loop), the brand mark and hook start fading in The transition between loops should be smooth. If I just remove all 'show' and 'active' classes at t=30s, then the next loop starts fresh. To make it seamless, I can: - At t=29.5s, start fading out the CTA - At t=30s, reset everything - The brand mark stays visible Actually, the easiest seamless loop: have the CTA fade out, then the brand mark and hook fade in. The transition through "black" or through the background is fine since the background is always there. Let me also add a subtle "next episode" or "replay" feel. Or just let it loop without indication. OK final code time. Let me write it all out. Wait, I realize I should also think about the "smooth scene transitions" requirement. I want each scene to crossfade smoothly. Currently, I'm doing: - Scene N out: remove 'active' class - Scene N+1 in: add 'active' class These happen at the same time (same event), so the crossfade happens. Good. But within a scene, elements appear sequentially. The whole scene's opacity is 1, and the individual elements have their own opacity transitions. So the crossfade is clean. OK let me write the final code. Actually, I realize there's an issue with my timeline. When I switch from scene 1 to scene 2, I do: ```js { t: 5000, fn: () => { removeClass($('scene-hook'), 'active'); addClass($('scene-stats'), 'active'); }} ``` But scene-hook has its own elements that are animated. When I remove 'active' from scene-hook, the scene fades out (opacity 0). But the internal elements (hook-pre, hook-1, hook-2) are still in 'show' state. That's fine for now, but on the next loop, I need to reset them. In my reset function, I'm removing all 'show' and 'active' classes (except brand). That should work. But wait, the reset happens at t=30s. By that time, the CTA is showing. I need to fade it out before resetting, or the reset will be abrupt. Let me add: ```js { t: 29500, fn: () => { // Start fading out CTA removeClass($('cta-url'), 'show'); }}, { t: 30000, fn: () => { // Full reset // ... }} ``` And the transition is fast enough that by t=30s, the CTA is mostly faded out. Then the reset happens and the brand mark and hook start fading in. Actually, I want the loop to feel continuous. Let me have: - t=29.0s: start fading CTA out - t=30.0s: CTA is mostly invisible - t=30.0s: reset all and start loop The reset itself happens instantly (classes removed), but the visual elements have already faded out. So the loop is seamless. Wait, the reset removes 'show' from cta-url, cta-line, cta-pre. If they were already fading out via the t=29.0s event, then by t=30s they're at opacity 0. The reset doesn't visually change anything. Then the next loop starts: brand mark stays, scene-hook gets 'active' class, hook elements start fading in from 0. This is the beginning of the loop. The issue: when the loop restarts, the CTA elements (cta-line, cta-url) are at opacity 0. Then when scene-cta gets 'active' again later, they fade in. But the show class needs to be re-added. The reset removes it, so the events at t=25.5s, 26s, 27.5s in the next loop will re-add them. Good. Actually, the events use an `executedEvents` Set to track which have run. When I reset, I should clear this Set. Otherwise, the events won't re-execute in the next loop. Let me add that to the reset: ```js { t: 30000, fn: () => { // ... reset DOM ... executedEvents.clear(); startTime = performance.now(); }} ``` Good. OK let me write the complete code now. I've planned enough. Hmm wait, I want to also think about the word-by-word animation. Let me handle the hook text properly. For "Your AI agents" with "AI agents" as accent, I can do: ```html

Your AI agents

``` But "AI agents" should be one accent group. Let me use a different approach: have the whole line fade in, and the accent part has a special gradient effect. Or, I can split into words and apply the accent class to specific words: ```html

Your AI agents

``` Each word is a span, and the accent words have the gold gradient. The whole line can have staggered word animations. ```css .hook-line .word { display: inline-block; opacity: 0; transform: translateY(40px); transition: all 0.7s cubic-bezier(0.16, 1, 0.3, 1); } .hook-line.show .word { opacity: 1; transform: translateY(0); } /* Stagger */ .hook-line.show .word:nth-child(1) { transition-delay: 0s; } .hook-line.show .word:nth-child(2) { transition-delay: 0.1s; } .hook-line.show .word:nth-child(3) { transition-delay: 0.2s; } ``` Or use CSS custom property for delay, set in HTML. Actually, let me just use a simple approach: the whole line fades in, no word-by-word. The accent words have the gold gradient. This is simpler and still looks good. ```css .hook-line { opacity: 0; transform: translateY(50px); transition: all 1s cubic-bezier(0.16, 1, 0.3, 1); } .hook-line.show { opacity: 1; transform: translateY(0); } ``` Two lines, two fades. Clean. But the request says "kinetic typography" which implies more dynamic text animation. Let me add some kinetic effects: - Slight rotation - Letter spacing animation - Scale on entrance ```css .hook-line { opacity: 0; transform: translateY(50px) scale(0.95); letter-spacing: -0.05em; transition: opacity 1s, transform 1s, letter-spacing 1.2s; } .hook-line.show { opacity: 1; transform: translateY(0) scale(1); letter-spacing: -0.01em; } ``` Good. The letter-spacing expanding gives a nice kinetic feel. Let me also add a slight blur transition: ```css .hook-line { filter: blur(10px); } .hook-line.show { filter: blur(0); } ``` OK let me finalize. I'll write the complete code now. One more thing: I want to add some depth to the scenes. Maybe each scene has a different background tint or particle density. Or a subtle parallax effect. Actually, let me keep it simple. The background is consistent throughout. The scenes fade in and out with their content. OK writing the final code now. Let me be comprehensive and produce a great result. ```html AIPB โ€” AI Profit Boardroom
``` OK I'm going to just write the whole thing now without more planning. Let me produce a high-quality file. Actually, let me reconsider the count-up format. For "3,600+ founders", the "+" is a suffix. So the number counts from 0 to 3600, and the final display is "3,600+". The formatting with comma is nice. For "38 countries", just 0 to 38. For "$59/mo", prefix "$", target 59, suffix "/mo". So it goes from "$0/mo" to "$59/mo". The countUp function handles prefix and suffix. ```js function countUp(el, target, duration, prefix = '', suffix = '') { const start = performance.now(); function update(now) { const elapsed = now - start; const t = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - t, 3); const value = Math.floor(target * eased); el.textContent = prefix + value.toLocaleString() + suffix; if (t < 1) requestAnimationFrame(update); } requestAnimationFrame(update); } ``` Good. Now for the reset, I need to reset the stat numbers to their initial state. The initial state is: - stat-1: "0" - stat-2: "0" - stat-3: "$0/mo" In HTML, I have them as data attributes for the target, prefix, suffix. Let me set the initial text in HTML: ```html
0
0
$0/mo
``` And in the reset: ```js document.querySelectorAll('.stat-number').forEach(el => { const prefix = el.dataset.prefix || ''; const suffix = el.dataset.suffix || ''; el.textContent = prefix + '0' + suffix; }); ``` Good. OK final code. Let me write it all out now. I also want to add a "play" overlay hint that disappears after a few seconds. The request says "a small overlay hint is fine, game visible behind it". So I can have a small "โ–ถ Auto-playing" text at the bottom that fades out. Actually, the request is for a video advert, not a game. So the hint is optional. Let me just have a subtle "now playing" or just nothing. The progress bar serves as the playback indicator. OK writing now. Final version. Let me also consider: should the scenes have a slight zoom effect? Like, each scene starts at scale 1.05 and zooms to 1? That adds dynamism. ```css .scene { transform: scale(1.05); transition: opacity 0.8s, transform 1.2s; } .scene.active { transform: scale(1); } ``` Nice subtle effect. Let me include it. OK writing the final code now. For real this time. Let me also think about the "loop" feel. After the CTA scene, the video loops. To make it feel intentional, I can have a brief moment of "black" or "logo" before the hook reappears. Like: - t=29.0s: CTA fades out - t=30.0s: brief black - t=30.5s: brand mark pulses - t=31.0s: hook starts (but this is now in the 2nd loop) Wait, the total duration is 30s. So at t=30s, the loop restarts at t=0. The brand mark is already visible (it was visible the whole time). The hook scene starts at t=0 with the elements fading in. So the transition is: CTA fades out โ†’ brand mark visible on dark background โ†’ hook elements fade in. That should be smooth. Let me ensure the CTA fade-out is timed well. ```js { t: 28000, fn: () => removeClass($('cta-url'), 'show') }, { t: 29000, fn: () => removeClass($('cta-line'), 'show') }, { t: 29500, fn: () => removeClass($('cta-pre'), 'show') }, { t: 30000, fn: () => { // Full reset removeClass($('scene-cta'), 'active'); executedEvents.clear(); startTime = performance.now(); }} ``` The reset happens at t=30s. By then, all CTA elements are faded out (the transitions are 0.8-1s, so by t=30s they're done). The scene-cta is removed. Then the next loop starts. In the next loop: - t=0: scene-hook gets active, brand mark is already visible - t=0.3s: hook-pre starts showing - etc. Good. One issue: the executedEvents Set is cleared, so the events at t=0, 0.3, 0.7, etc. will re-execute. But the brand mark already has the 'show' class. Adding it again is a no-op. The scene-hook doesn't have 'active' (we removed it in the reset... wait, do we?). In my reset, I remove 'show' and 'active' from all elements except brand. So scene-hook loses 'active'. Then at t=0 in the next loop, it gets 'active' again. Good. But wait, I also need to remove 'active' from the other scenes. The reset should handle that. Let me make the reset comprehensive: ```js function resetAll() { // Remove all show/active classes except brand document.querySelectorAll('.show, .active').forEach(e => { if (e.id !== 'brand') { e.classList.remove('show', 'active'); } }); // Reset stat numbers document.querySelectorAll('.stat-number').forEach(el => { const prefix = el.dataset.prefix || ''; const suffix = el.dataset.suffix || ''; el.textContent = prefix + '0' + suffix; }); } ``` This should reset everything. Then executedEvents.clear() and startTime = performance.now(). OK let me write the complete file. Final version, for real. Let me also add some micro-interactions: - Hover effects on cards (subtle lift) - Hover effect on the URL pill (glow) - Cursor pointer on interactive elements These add polish. OK here's the final code. Let me write it all out. I realize I've been over-planning. Let me just write the file and iterate if needed. For the word-by-word animation on the hook, I'll use a simple approach: the whole line fades in, but the accent part has a special animation (slight scale or color shift). Actually, let me do word-by-word. It looks more kinetic. I'll split the text into words with spans. For "Your AI agents": - "Your" - white - "AI agents" - gold accent For "should be making you money": - "should be" - white - "making you money" - gold accent I'll structure the HTML with the accent spans, and use JS to split into word spans. Or I can write the word spans directly in HTML. Let me write it directly in HTML for simplicity: ```html

Your AI agents

``` ```css .hook-line .word { display: inline-block; opacity: 0; transform: translateY(40px); transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1); } .hook-line.show .word { opacity: 1; transform: translateY(0); transition-delay: calc(var(--i) * 0.1s); } .hook-line .accent { background: linear-gradient(180deg, #f4d03f 0%, #d4af37 50%, #b8860b 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } ``` This gives a nice staggered word reveal. The accent words have the gold gradient. But there's an issue: when the accent is on multiple consecutive words, the gradient should be continuous. With separate spans, each word has its own gradient. It might look slightly off but should be fine. Actually, a better approach for the accent: have the accent words be inline-block but with negative margin or something. Or just accept the slight visual break. Let me just go with it. The gradient on each accent word will look fine. OK FINAL CODE TIME. Let me write it all out in one go. Hmm, one more thought: the request mentions "kinetic typography" which is a specific style. It involves: - Dynamic text movement - Words flying in - Letters animating - Staggered timing - Multiple text layers I should make sure my typography animations feel "kinetic". Let me add: - Slight rotation on word entrance - Scale animation - Staggered delays - Maybe a slight blur-to-clear For the hook lines: - Words fly in from below with slight scale and blur - Staggered by 0.1s - Easing: cubic-bezier(0.16, 1, 0.3, 1) (smooth out) For the stats: - Numbers scale up with a bounce - Maybe a slight rotation For the cards: - Cards slide in from bottom with slight scale - Staggered For the CTA: - "Join AIPB" scales in with a big impact - URL pill slides up Let me make sure all these have good easing and timing. OK writing the final code now. No more planning. Let me draft the complete HTML file: ```html AIPB โ€” AI Profit Boardroom
``` OK let me write it all. I'll be comprehensive. One thing I want to add: a subtle "scan line" effect that's very faint, like a CRT monitor. This adds to the cinematic feel. ```css .scanlines { position: absolute; inset: 0; background: repeating-linear-gradient( 0deg, transparent 0px, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px ); pointer-events: none; z-index: 42; opacity: 0.3; } ``` Hmm, this might be too much. Let me skip it. The grain and vignette are enough. OK writing the final code. Here we go. Let me also think about the color of the brand mark. The request says "gold + aubergine palette". So the brand mark could be in gold. Or white. Let me use a gold dot with white text. ```html
AI PROFIT BOARDROOM
``` ```css .brand-mark { position: absolute; top: 9vh; left: 50%; transform: translateX(-50%); z-index: 60; display: flex; align-items: center; gap: 12px; opacity: 0.7; } .mark-dot { width: 6px; height: 6px; background: var(--gold); border-radius: 50%; box-shadow: 0 0 12px var(--gold), 0 0 24px rgba(212, 175, 55, 0.3); animation: pulse 2s ease-in-out infinite; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .mark-text { font-family: 'Inter', sans-serif; font-size: 11px; font-weight: 500; letter-spacing: 0.3em; color: var(--cream); text-transform: uppercase; } ``` Good. The brand mark is always visible (opacity 0.7), with a pulsing dot. OK writing the final code now. For real. Let me organize the CSS by section: 1. Reset & Variables 2. Stage & Background 3. Atmosphere (particles, beams, orbs, grain, vignette) 4. Brand mark 5. Letterbox & Progress 6. Scene common 7. Scene 1: Hook 8. Scene 2: Stats 9. Scene 3: Cards 10. Scene 4: Testimonials 11. Scene 5: CTA 12. Responsive And the JS: 1. Utilities (easing, $, addClass, etc.) 2. Particle system 3. Count-up 4. Timeline events 5. Master loop 6. Initialization OK let me write it. For the testimonials, I want to add a subtle background. Maybe a radial gradient spotlight that follows the active testimonial. Or just a consistent dark background. Let me add a subtle spotlight effect to the testimonials scene: ```css .scene#scene-testimonials::before { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse 60% 40% at 50% 50%, rgba(74, 26, 74, 0.3) 0%, transparent 70%); pointer-events: none; } ``` Good. For the cards, I want to add a subtle grid background or pattern. Maybe just the existing grid-bg is enough. OK writing the final code. Let me do it. I'll write the file now. Here it is: ```html AIPB โ€” AI Profit Boardroom
AI Profit Boardroom
โ€” INTRODUCING โ€”

Your AI agents

should be making you money

0+
Founders
0
Countries
$0/mo
Per Month
โ€” WHAT YOU GET โ€”
Agent OS
The operating system for profitable AI agents. Deploy, monitor, monetize.
Weekly Calls
Live mastermind sessions with operators shipping AI agents at scale every week.
Templates
Battle-tested agent templates, prompts, and SOPs you can clone and deploy instantly.
Community
3,600+ founders swapping wins, deals, and AI plays in real time, 24/7.
โ€” FOUNDERS ARE SHIPPING โ€”
"

AIPB gave me the playbook to ship my first AI agent in a week. Now it's a $4k/mo business.

SC
Sarah Chen
Founder, ReplicaLabs
"

The weekly calls alone are worth 10x the price. I've made more here than in any mastermind I've paid for.

MW
Marcus Webb
Solo Founder, Austin TX
"

Real operators, zero fluff. I cloned a community member's agent and it's profitable in week one.

DV
Diana Voss
CEO, Helix AI
โ€” THE BOARDROOM IS OPEN โ€”
Join AIPB
aiprofitboardroom.com
NOW PLAYING ยท 30s LOOP