2026-01-28 23:00:58 -05:00

843 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1920, height=1080">
<title>Stripe MCP - Autonomous Agent Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--bg-tertiary: #252542;
--text-primary: #FFFFFF;
--text-secondary: #A0A0B8;
--text-muted: #6B6B80;
--border-default: rgba(255,255,255,0.1);
--border-subtle: rgba(255,255,255,0.05);
--accent-gradient: linear-gradient(135deg, #667eea, #764ba2);
--stripe-purple: #635BFF;
--success-green: #30B566;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
width: 1920px;
height: 1080px;
overflow: hidden;
}
.camera {
width: 1920px;
height: 1080px;
position: relative;
overflow: hidden;
}
.scene {
position: absolute;
width: 3200px;
height: 2200px;
left: -640px;
top: -560px;
background:
radial-gradient(ellipse at 30% 30%, rgba(99, 91, 255, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at 70% 70%, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
var(--bg-primary);
}
.chat-window {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1500px;
height: 950px;
background: var(--bg-secondary);
border-radius: 24px;
box-shadow: 0 30px 100px rgba(0,0,0,0.6);
display: flex;
flex-direction: column;
border: 1px solid var(--border-default);
overflow: hidden;
}
.chat-header {
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: linear-gradient(180deg, rgba(255,255,255,0.03) 0%, transparent 100%);
border-bottom: 1px solid var(--border-subtle);
}
.window-controls { display: flex; gap: 8px; }
.window-dot { width: 14px; height: 14px; border-radius: 50%; }
.window-dot-red { background: #ff5f56; }
.window-dot-yellow { background: #ffbd2e; }
.window-dot-green { background: #27ca40; }
.chat-header-title { font-size: 15px; font-weight: 500; color: var(--text-secondary); }
.chat-messages {
flex: 1;
padding: 32px;
display: flex;
flex-direction: column;
gap: 24px;
overflow: hidden;
}
.message-user {
align-self: flex-end;
max-width: 60%;
padding: 18px 26px;
background: var(--accent-gradient);
color: var(--text-primary);
border-radius: 22px;
border-bottom-right-radius: 6px;
font-size: 16px;
line-height: 1.5;
opacity: 0;
transform: translateX(30px);
}
.message-ai-container {
display: flex;
gap: 16px;
align-items: flex-start;
opacity: 0;
transform: translateY(20px);
}
.ai-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--accent-gradient);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ai-avatar svg { width: 24px; height: 24px; }
.ai-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
max-width: 800px;
}
.message-ai-text {
background: rgba(255,255,255,0.05);
color: var(--text-primary);
padding: 18px 26px;
border-radius: 22px;
border-bottom-left-radius: 6px;
font-size: 16px;
line-height: 1.6;
}
.ai-typing {
display: flex;
gap: 6px;
padding: 18px 26px;
background: rgba(255,255,255,0.05);
border-radius: 22px;
border-bottom-left-radius: 6px;
width: fit-content;
}
.typing-dot {
width: 8px;
height: 8px;
background: var(--text-muted);
border-radius: 50%;
animation: typingBounce 1.4s ease-in-out infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-8px); }
}
/* Stripe Embed */
.stripe-embed {
background: #FFFFFF;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(99, 91, 255, 0.2), 0 0 0 1px rgba(99, 91, 255, 0.15);
width: 640px;
opacity: 0;
transform: scale(0.9) translateY(20px);
}
.stripe-header {
padding: 16px 20px;
border-bottom: 1px solid #E3E8EE;
display: flex;
align-items: center;
gap: 14px;
}
.stripe-logo {
background: #635BFF;
color: white;
font-weight: 700;
font-size: 13px;
padding: 6px 12px;
border-radius: 6px;
}
.stripe-title { font-size: 15px; font-weight: 600; color: #1A1F36; }
.stripe-subtitle { font-size: 12px; color: #697386; margin-top: 2px; }
.stripe-stat-row {
display: flex;
gap: 32px;
padding: 16px 20px;
background: #F6F9FC;
border-bottom: 1px solid #E3E8EE;
}
.stripe-stat { display: flex; flex-direction: column; gap: 4px; }
.stripe-stat-value { font-size: 22px; font-weight: 600; color: #1A1F36; }
.stripe-stat-value.warning { color: #D97706; }
.stripe-stat-label { font-size: 11px; color: #697386; text-transform: uppercase; letter-spacing: 0.5px; }
.stripe-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.stripe-table th {
text-align: left;
padding: 12px 20px;
font-size: 11px;
font-weight: 500;
color: #697386;
text-transform: uppercase;
background: #F6F9FC;
border-bottom: 1px solid #E3E8EE;
}
.stripe-table td {
padding: 14px 20px;
border-bottom: 1px solid #E3E8EE;
color: #1A1F36;
}
.stripe-table tr:last-child td { border-bottom: none; }
.stripe-status {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 5px;
font-size: 11px;
font-weight: 500;
}
.stripe-status.failed { background: #FEE2E2; color: #DC2626; }
.stripe-status-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.stripe-customer { display: flex; flex-direction: column; gap: 2px; }
.stripe-customer-email { color: #1A1F36; font-weight: 500; }
.stripe-customer-id { font-size: 11px; color: #697386; font-family: monospace; }
/* Recommendation */
.recommendation {
background: rgba(217, 119, 6, 0.1);
border: 1px solid rgba(217, 119, 6, 0.3);
border-radius: 14px;
padding: 20px;
opacity: 0;
transform: translateY(10px);
}
.recommendation-title {
font-size: 14px;
font-weight: 600;
color: #D97706;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.recommendation-text {
font-size: 15px;
color: var(--text-primary);
line-height: 1.6;
margin-bottom: 16px;
}
/* Yes/No Buttons */
.action-buttons {
display: flex;
gap: 12px;
opacity: 0;
transform: translateY(10px);
}
.action-btn {
padding: 12px 28px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.action-btn.yes {
background: var(--success-green);
color: white;
}
.action-btn.yes.clicked {
transform: scale(0.95);
box-shadow: 0 0 0 4px rgba(48, 181, 102, 0.3);
}
.action-btn.no {
background: var(--bg-tertiary);
color: var(--text-secondary);
border: 1px solid var(--border-default);
}
/* Success Notifications */
.notification {
display: flex;
align-items: center;
gap: 12px;
background: rgba(48, 181, 102, 0.15);
border: 1px solid rgba(48, 181, 102, 0.4);
border-radius: 12px;
padding: 14px 18px;
opacity: 0;
transform: translateX(-20px);
}
.notification-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--success-green);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.notification-icon svg {
width: 20px;
height: 20px;
fill: white;
}
.notification-content {
flex: 1;
}
.notification-title {
font-size: 14px;
font-weight: 600;
color: var(--success-green);
}
.notification-text {
font-size: 13px;
color: var(--text-secondary);
margin-top: 2px;
}
/* Input area */
.chat-input-container {
padding: 20px 28px 28px;
border-top: 1px solid var(--border-subtle);
}
.chat-input {
display: flex;
align-items: center;
gap: 14px;
background: var(--bg-primary);
border: 2px solid var(--border-default);
border-radius: 18px;
padding: 14px 18px;
transition: all 0.3s ease;
}
.chat-input.active {
border-color: var(--stripe-purple);
box-shadow: 0 0 0 4px rgba(99, 91, 255, 0.15);
}
.chat-input-text {
flex: 1;
color: var(--text-primary);
font-size: 15px;
min-height: 22px;
white-space: nowrap;
}
.chat-input-text .cursor {
display: inline-block;
width: 2px;
height: 20px;
background: var(--stripe-purple);
margin-left: 2px;
vertical-align: text-bottom;
animation: blink 0.8s step-end infinite;
}
.chat-input-text .placeholder {
color: var(--text-muted);
}
@keyframes blink { 50% { opacity: 0; } }
.chat-input-send {
width: 40px;
height: 40px;
border-radius: 12px;
background: var(--bg-tertiary);
border: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.chat-input-send.ready {
background: var(--accent-gradient);
transform: scale(1.05);
}
.chat-input-send svg { width: 20px; height: 20px; fill: white; }
.embed-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: rgba(255,255,255,0.03);
border-radius: 16px;
border: 1px dashed var(--border-default);
}
.embed-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border-default);
border-top-color: var(--stripe-purple);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="camera">
<div class="scene" id="scene">
<div class="chat-window">
<div class="chat-header">
<div class="window-controls">
<span class="window-dot window-dot-red"></span>
<span class="window-dot window-dot-yellow"></span>
<span class="window-dot window-dot-green"></span>
</div>
<span class="chat-header-title">AI Assistant</span>
<div style="width: 50px;"></div>
</div>
<div class="chat-messages">
<div class="message-user" id="userMessage">
How can we recover some failed payments?
</div>
<div class="message-ai-container" id="aiContainer">
<div class="ai-avatar">
<svg viewBox="0 0 24 24" fill="white">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
</div>
<div class="ai-content">
<div class="ai-typing" id="aiTyping">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
<div class="message-ai-text" id="aiText" style="display: none;">
Found 2 failed payments. Let me analyze the best recovery strategy...
</div>
<div class="embed-loading" id="embedLoading" style="display: none;">
<div class="embed-spinner"></div>
</div>
<div class="stripe-embed" id="stripeEmbed">
<div class="stripe-header">
<span class="stripe-logo">stripe</span>
<div>
<div class="stripe-title">Failed Payments</div>
<div class="stripe-subtitle">2 customers with declined cards</div>
</div>
</div>
<div class="stripe-stat-row">
<div class="stripe-stat">
<span class="stripe-stat-value warning">$948</span>
<span class="stripe-stat-label">At Risk</span>
</div>
<div class="stripe-stat">
<span class="stripe-stat-value">2</span>
<span class="stripe-stat-label">Customers</span>
</div>
<div class="stripe-stat">
<span class="stripe-stat-value">14 days</span>
<span class="stripe-stat-label">Avg Overdue</span>
</div>
</div>
<table class="stripe-table">
<thead>
<tr>
<th>Customer</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="stripe-customer">
<span class="stripe-customer-email">john@example.com</span>
<span class="stripe-customer-id">cus_Nk4x8J</span>
</div>
</td>
<td><strong>$449.00</strong></td>
<td>
<span class="stripe-status failed">
<span class="stripe-status-dot"></span>
Card declined
</span>
</td>
</tr>
<tr>
<td>
<div class="stripe-customer">
<span class="stripe-customer-email">mike@startup.io</span>
<span class="stripe-customer-id">cus_Qj3p7K</span>
</div>
</td>
<td><strong>$499.00</strong></td>
<td>
<span class="stripe-status failed">
<span class="stripe-status-dot"></span>
Card declined
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="recommendation" id="recommendation">
<div class="recommendation-title">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-2h2v2h-1zm0-4V7h2v6h-2z"/>
</svg>
Smart Recovery Strategy
</div>
<div class="recommendation-text">
Based on payment history, I recommend offering a <strong>payment plan</strong> — collect 50% now, remainder in 1 week. This approach has a 92% success rate with overdue accounts.
</div>
<div class="action-buttons" id="actionButtons">
<button class="action-btn yes" id="yesBtn">Yes, reach out</button>
<button class="action-btn no">No thanks</button>
</div>
</div>
<div class="notification" id="notification1">
<div class="notification-icon">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="notification-content">
<div class="notification-title">Payment Received</div>
<div class="notification-text">john@example.com paid <strong>$225</strong> — remaining $224 due in 1 week</div>
</div>
</div>
<div class="notification" id="notification2">
<div class="notification-icon">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="notification-content">
<div class="notification-title">Payment Received</div>
<div class="notification-text">mike@startup.io paid <strong>$250</strong> — remaining $249 due in 1 week</div>
</div>
</div>
</div>
</div>
</div>
<div class="chat-input-container">
<div class="chat-input" id="chatInput">
<div class="chat-input-text" id="inputText"><span class="placeholder">Type a message...</span></div>
<button class="chat-input-send" id="sendBtn">
<svg viewBox="0 0 24 24">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
<script>
const userQuestion = "How can we recover some failed payments?";
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 easeOutBack(t) { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }
function getPhaseProgress(scroll, start, end) {
if (scroll < start) return 0;
if (scroll > end) return 1;
return (scroll - start) / (end - start);
}
// Camera positions
const CAMERA = {
inputLeft: { scale: 2.2, x: -320, y: 340, rotation: 0 }, // Zoomed on LEFT of input
inputRight: { scale: 2.2, x: 200, y: 340, rotation: 0 }, // Follow text to right
userMsg: { scale: 1.8, x: 320, y: -140, rotation: 0 }, // User message top-right
aiResponse: { scale: 1.5, x: -180, y: -20, rotation: 0 }, // AI area left
embed: { scale: 1.2, x: -120, y: 80, rotation: 0 }, // Embed view
recommendation: { scale: 1.3, x: -100, y: 200, rotation: 0 }, // Recommendation
notifications: { scale: 1.2, x: -80, y: 280, rotation: 0 }, // Notifications
final: { scale: 1, x: 0, y: 100, rotation: 0 } // Final wide
};
function updateScene(scrollProgress) {
const scene = document.getElementById('scene');
const inputText = document.getElementById('inputText');
const chatInput = document.getElementById('chatInput');
const sendBtn = document.getElementById('sendBtn');
const userMessage = document.getElementById('userMessage');
const aiContainer = document.getElementById('aiContainer');
const aiTyping = document.getElementById('aiTyping');
const aiText = document.getElementById('aiText');
const embedLoading = document.getElementById('embedLoading');
const stripeEmbed = document.getElementById('stripeEmbed');
const recommendation = document.getElementById('recommendation');
const actionButtons = document.getElementById('actionButtons');
const yesBtn = document.getElementById('yesBtn');
const notification1 = document.getElementById('notification1');
const notification2 = document.getElementById('notification2');
// TIMELINE (slowed down Stripe section):
// 0-5%: Fade in
// 5-25%: Type in input (camera follows left to right)
// 25-30%: Ready to send
// 30-40%: Pan to user message (top right) with see-saw
// 40-48%: Pan to AI, show typing
// 48-55%: AI text + loading spinner
// 55-68%: Stripe embed loads (SLOWER)
// 68-78%: Recommendation appears (SLOWER)
// 78-82%: Yes button clicked
// 82-90%: First notification
// 90-97%: Second notification
// 97-100%: Final wide shot
// ========== FADE IN ==========
const fadeIn = easeOutCubic(getPhaseProgress(scrollProgress, 0, 0.05));
// ========== TYPING (5-25%) ==========
const typeProgress = getPhaseProgress(scrollProgress, 0.05, 0.25);
const charsToShow = Math.floor(easeOutCubic(typeProgress) * userQuestion.length);
if (scrollProgress < 0.05) {
inputText.innerHTML = '<span class="placeholder">Type a message...</span>';
chatInput.classList.remove('active');
} else if (scrollProgress < 0.25) {
chatInput.classList.add('active');
inputText.innerHTML = (charsToShow > 0 ? userQuestion.substring(0, charsToShow) : '') + '<span class="cursor"></span>';
sendBtn.classList.toggle('ready', charsToShow === userQuestion.length);
} else if (scrollProgress < 0.30) {
inputText.innerHTML = userQuestion + '<span class="cursor"></span>';
chatInput.classList.add('active');
sendBtn.classList.add('ready');
} else {
inputText.innerHTML = '<span class="placeholder">Type a message...</span>';
chatInput.classList.remove('active');
sendBtn.classList.remove('ready');
}
// ========== USER MESSAGE (30-40%) ==========
const userMsgProgress = getPhaseProgress(scrollProgress, 0.30, 0.40);
if (scrollProgress >= 0.30) {
const appear = easeOutBack(Math.min(userMsgProgress * 2, 1));
userMessage.style.opacity = appear;
userMessage.style.transform = `translateX(${30 * (1 - appear)}px)`;
} else {
userMessage.style.opacity = 0;
userMessage.style.transform = 'translateX(30px)';
}
// ========== AI TYPING (40-48%) ==========
if (scrollProgress >= 0.40 && scrollProgress < 0.48) {
aiContainer.style.opacity = 1;
aiContainer.style.transform = 'translateY(0)';
aiTyping.style.display = 'flex';
aiText.style.display = 'none';
embedLoading.style.display = 'none';
stripeEmbed.style.opacity = 0;
} else if (scrollProgress < 0.40) {
aiContainer.style.opacity = 0;
aiContainer.style.transform = 'translateY(20px)';
}
// ========== AI TEXT + LOADING (48-55%) ==========
if (scrollProgress >= 0.48 && scrollProgress < 0.55) {
aiTyping.style.display = 'none';
aiText.style.display = 'block';
embedLoading.style.display = 'flex';
stripeEmbed.style.opacity = 0;
}
// ========== STRIPE EMBED (55-68%) ==========
const embedProgress = getPhaseProgress(scrollProgress, 0.55, 0.68);
if (scrollProgress >= 0.55) {
embedLoading.style.display = 'none';
stripeEmbed.style.opacity = easeOutCubic(embedProgress);
stripeEmbed.style.transform = `scale(${0.9 + 0.1 * easeOutCubic(embedProgress)}) translateY(${20 * (1 - easeOutCubic(embedProgress))}px)`;
}
// ========== RECOMMENDATION (68-78%) ==========
const recoProgress = getPhaseProgress(scrollProgress, 0.68, 0.78);
if (scrollProgress >= 0.68) {
recommendation.style.opacity = easeOutCubic(recoProgress);
recommendation.style.transform = `translateY(${10 * (1 - easeOutCubic(recoProgress))}px)`;
const btnProgress = getPhaseProgress(scrollProgress, 0.72, 0.78);
actionButtons.style.opacity = easeOutCubic(btnProgress);
actionButtons.style.transform = `translateY(${10 * (1 - easeOutCubic(btnProgress))}px)`;
} else {
recommendation.style.opacity = 0;
recommendation.style.transform = 'translateY(10px)';
actionButtons.style.opacity = 0;
}
// ========== YES CLICKED (78-82%) ==========
if (scrollProgress >= 0.78) {
yesBtn.classList.add('clicked');
} else {
yesBtn.classList.remove('clicked');
}
// ========== NOTIFICATION 1 (82-90%) ==========
const notif1Progress = getPhaseProgress(scrollProgress, 0.82, 0.88);
if (scrollProgress >= 0.82) {
notification1.style.opacity = easeOutCubic(notif1Progress);
notification1.style.transform = `translateX(${-20 * (1 - easeOutCubic(notif1Progress))}px)`;
} else {
notification1.style.opacity = 0;
notification1.style.transform = 'translateX(-20px)';
}
// ========== NOTIFICATION 2 (90-97%) ==========
const notif2Progress = getPhaseProgress(scrollProgress, 0.90, 0.95);
if (scrollProgress >= 0.90) {
notification2.style.opacity = easeOutCubic(notif2Progress);
notification2.style.transform = `translateX(${-20 * (1 - easeOutCubic(notif2Progress))}px)`;
} else {
notification2.style.opacity = 0;
notification2.style.transform = 'translateX(-20px)';
}
// ========== CAMERA ==========
let cam = { scale: 1, x: 0, y: 0, rotation: 0 };
function lerpCam(from, to, t) {
return {
scale: from.scale + (to.scale - from.scale) * t,
x: from.x + (to.x - from.x) * t,
y: from.y + (to.y - from.y) * t,
rotation: from.rotation + (to.rotation - from.rotation) * t
};
}
if (scrollProgress < 0.05) {
cam = CAMERA.inputLeft;
} else if (scrollProgress < 0.25) {
// Follow typing left to right
const p = easeOutCubic(getPhaseProgress(scrollProgress, 0.05, 0.25));
cam = lerpCam(CAMERA.inputLeft, CAMERA.inputRight, p);
} else if (scrollProgress < 0.30) {
cam = CAMERA.inputRight;
} else if (scrollProgress < 0.40) {
// Pan to user message with see-saw
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.30, 0.40));
cam = lerpCam(CAMERA.inputRight, CAMERA.userMsg, p);
const seesawT = getPhaseProgress(scrollProgress, 0.30, 0.40);
cam.rotation = Math.sin(seesawT * Math.PI * 4) * 2 * (1 - seesawT);
} else if (scrollProgress < 0.48) {
// Pan to AI
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.40, 0.48));
cam = lerpCam(CAMERA.userMsg, CAMERA.aiResponse, p);
} else if (scrollProgress < 0.55) {
cam = CAMERA.aiResponse;
} else if (scrollProgress < 0.68) {
// Move to embed
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.55, 0.68));
cam = lerpCam(CAMERA.aiResponse, CAMERA.embed, p);
} else if (scrollProgress < 0.78) {
// Move to recommendation
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.68, 0.78));
cam = lerpCam(CAMERA.embed, CAMERA.recommendation, p);
} else if (scrollProgress < 0.90) {
// Move to notifications
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.78, 0.90));
cam = lerpCam(CAMERA.recommendation, CAMERA.notifications, p);
} else {
// Final wide
const p = easeInOutCubic(getPhaseProgress(scrollProgress, 0.90, 1.0));
cam = lerpCam(CAMERA.notifications, CAMERA.final, p);
}
scene.style.transform = `scale(${cam.scale}) translate(${-cam.x}px, ${-cam.y}px) rotate(${cam.rotation}deg)`;
scene.style.opacity = fadeIn;
}
window.updateScene = updateScene;
updateScene(0);
</script>
</body>
</html>