Add full application: receipt scanning, auth, billing, and account deletion

Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Timo
2026-08-19 20:59:04 +02:00
parent 650a74da97
commit 84b9987c49
415 changed files with 96619 additions and 0 deletions

73
marketing-video/README.md Normal file
View File

@@ -0,0 +1,73 @@
# ScanReceipts — Marketing Video
A cinematic 35-second hype/marketing video built with [Remotion](https://remotion.dev), showcasing the ScanReceipts AI receipt-to-Excel app.
## 🎬 Video Structure
| Scene | Frames | Duration | Description |
|-------|--------|----------|-------------|
| 01 — Intro | 0215 | 7s | Logo reveal + animated headlines + receipts flying in + live AI scan |
| 02 — Problem | 210405 | 6.5s | Dark panel with stat counters (hours/€ wasted) + pain-point cards |
| 03 — Magic | 390605 | 7s | 3-panel flow: Receipt → AI Extraction fields → Excel export |
| 04 — Features | 600815 | 7s | 6-card feature grid with staggered spring animations |
| 05 — Social Proof | 810965 | 5s | Dark scene with KPI stats + testimonial cards |
| 06 — CTA | 9601050 | 3s | Punchy CTA with typewriter URL + pulsing glow button |
## 🚀 Commands
```bash
# Open Remotion Studio (live preview at http://localhost:3030)
npm start
# Render the video (standard quality)
npm run render
# Render high-quality (CRF 14, quality 100)
npm run render:hq
# Export a single frame as PNG
npm run still
```
## 📐 Specs
- **Resolution**: 1920 × 1080 (16:9 Full HD)
- **FPS**: 30
- **Duration**: 35 seconds (1050 frames)
- **Fonts**: Hanken Grotesk · Inter · JetBrains Mono (via @remotion/google-fonts)
- **Design System**: Zenith Silver (matches the ScanReceipts brand)
## 🎨 Design Tokens
| Token | Value | Use |
|-------|-------|-----|
| Background | `#F6F9FF` | Page background |
| Surface | `#FFFFFF` | Cards / modals |
| Border | `#E2E8F0` | Dividers |
| Ink | `#161C22` | Body text |
| Accent | `#000000` | Headlines, buttons |
| Success | `#059669` | Status badges, scan line |
| Ink-2 | `#475569` | Subtext |
## 📁 Structure
```
src/
├── index.ts # Remotion entry point
├── Root.tsx # Composition registry
├── MarketingVideo.tsx # Main video with all Sequences
├── components/
│ ├── AnimatedHeadline.tsx # Word-by-word stagger animation
│ ├── Background.tsx # Scrolling grid background
│ ├── ExcelMock.tsx # Spreadsheet UI mock
│ ├── FontLoader.tsx # Google Fonts preloader
│ ├── ReceiptMock.tsx # Animated receipt with scanline
│ └── Reveal.tsx # Spring-based entrance animation
└── scenes/
├── Scene01Intro.tsx # Hero scene
├── Scene02Problem.tsx # Pain points + counters
├── Scene03Magic.tsx # AI demo flow
├── Scene04Features.tsx # Feature grid
├── Scene05Social.tsx # Social proof
└── Scene06CTA.tsx # Call to action
```

3759
marketing-video/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
{
"name": "marketing-video",
"version": "1.0.0",
"description": "ScanReceipts hype marketing video built with Remotion",
"main": "src/index.ts",
"scripts": {
"start": "npx remotion studio",
"render": "npx remotion render ScanReceiptsMarketing out/marketing.mp4",
"render:hq": "npx remotion render ScanReceiptsMarketing out/marketing-hq.mp4 --quality 100 --crf 14",
"still": "npx remotion still ScanReceiptsMarketing out/still.png",
"build": "npx tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@remotion/cli": "^4.0.0",
"@remotion/google-fonts": "^4.0.512",
"@remotion/renderer": "^4.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"remotion": "^4.0.0"
}
}

View File

@@ -0,0 +1,9 @@
import { Config } from '@remotion/cli/config';
Config.setEntryPoint('./src/index.ts');
// High quality output defaults
Config.setVideoImageFormat('jpeg');
Config.setJpegQuality(95);
Config.setScale(1);
Config.setChromiumOpenGlRenderer('angle');

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { AbsoluteFill, Sequence } from 'remotion';
import { Scene01Intro } from './scenes/Scene01Intro';
import { Scene02Problem } from './scenes/Scene02Problem';
import { Scene03Magic } from './scenes/Scene03Magic';
import { Scene04Features } from './scenes/Scene04Features';
import { Scene05Social } from './scenes/Scene05Social';
import { Scene06CTA } from './scenes/Scene06CTA';
import { Background } from './components/Background';
import { FontLoader } from './components/FontLoader';
// 35 seconds @ 30fps = 1050 frames
// Scene timing:
// 0 215 (7.2s): Intro / Hero
// 210 405 (6.5s): The Problem
// 390 605 (7.2s): The Magic (AI scanning)
// 600 815 (7.2s): Features showcase
// 810 965 (5.2s): Social proof
// 960 1050 (3s) : CTA
export const MarketingVideo: React.FC = () => {
return (
<AbsoluteFill style={{ background: '#F6F9FF', fontFamily: "'Hanken Grotesk', sans-serif" }}>
{/* Preload Google Fonts for pixel-perfect rendering */}
<FontLoader />
{/* Global background grid always present */}
<Background />
<Sequence from={0} durationInFrames={215}>
<Scene01Intro />
</Sequence>
<Sequence from={210} durationInFrames={195}>
<Scene02Problem />
</Sequence>
<Sequence from={390} durationInFrames={215}>
<Scene03Magic />
</Sequence>
<Sequence from={600} durationInFrames={215}>
<Scene04Features />
</Sequence>
<Sequence from={810} durationInFrames={155}>
<Scene05Social />
</Sequence>
<Sequence from={960} durationInFrames={90}>
<Scene06CTA />
</Sequence>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { Composition } from 'remotion';
import { MarketingVideo } from './MarketingVideo';
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="ScanReceiptsMarketing"
component={MarketingVideo}
durationInFrames={1050}
fps={30}
width={1920}
height={1080}
defaultProps={{}}
/>
</>
);
};

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
interface Props {
text: string;
delay?: number;
style?: React.CSSProperties;
staggerMs?: number;
}
// Animates each word in individually
export const AnimatedHeadline: React.FC<Props> = ({ text, delay = 0, style = {}, staggerMs = 3 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const words = text.split(' ');
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25em 0.2em', ...style }}>
{words.map((word, i) => {
const wordDelay = delay + i * staggerMs;
const progress = spring({
frame: frame - wordDelay,
fps,
config: { damping: 16, stiffness: 150, mass: 0.6 },
});
const opacity = interpolate(frame - wordDelay, [0, 10], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const translateY = interpolate(progress, [0, 1], [50, 0]);
return (
<span
key={i}
style={{
display: 'inline-block',
opacity,
transform: `translateY(${translateY}px)`,
overflow: 'hidden',
}}
>
{word}
</span>
);
})}
</div>
);
};

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
export const Background: React.FC = () => {
const frame = useCurrentFrame();
const drift = interpolate(frame, [0, 1050], [0, -40], { extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ pointerEvents: 'none', overflow: 'hidden' }}>
{/* Subtle grid */}
<div
style={{
position: 'absolute',
inset: '-10%',
backgroundImage:
'linear-gradient(#E2E8F0 1px, transparent 1px), linear-gradient(90deg, #E2E8F0 1px, transparent 1px)',
backgroundSize: '80px 80px',
opacity: 0.28,
transform: `translateY(${drift}px)`,
}}
/>
{/* Radial vignette */}
<div
style={{
position: 'absolute',
inset: 0,
background:
'radial-gradient(ellipse 80% 80% at 50% 50%, transparent 40%, rgba(246,249,255,0.7) 100%)',
}}
/>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,156 @@
import React from 'react';
interface Props {
rows: { vendor: string; date: string; category: string; amount: string; status: 'confirmed' | 'pending' | 'scanned' }[];
highlightRow?: number;
scale?: number;
}
const STATUS_COLORS = {
confirmed: { bg: '#F0FDF4', text: '#15803D', border: '#BBF7D0' },
pending: { bg: '#FFFBEB', text: '#B45309', border: '#FDE68A' },
scanned: { bg: '#EFF6FF', text: '#1D4ED8', border: '#BFDBFE' },
};
export const ExcelMock: React.FC<Props> = ({ rows, highlightRow, scale = 1 }) => {
const cols = ['#', 'VENDOR', 'DATE', 'CATEGORY', 'AMOUNT', 'STATUS'];
return (
<div
style={{
background: '#FFFFFF',
border: '1px solid #E2E8F0',
overflow: 'hidden',
boxShadow: '0 20px 60px -20px rgba(22,28,34,0.2)',
fontFamily: "'Inter', sans-serif",
}}
>
{/* Toolbar */}
<div
style={{
background: '#F8FAFC',
borderBottom: '1px solid #E2E8F0',
padding: `${7 * scale}px ${12 * scale}px`,
display: 'flex',
alignItems: 'center',
gap: 8 * scale,
fontFamily: 'monospace',
fontSize: 9 * scale,
letterSpacing: '0.1em',
textTransform: 'uppercase' as const,
color: '#64748B',
}}
>
<div
style={{
background: '#FFFFFF',
border: '1px solid #E2E8F0',
borderBottom: 'none',
padding: `${3 * scale}px ${8 * scale}px`,
color: '#000000',
fontSize: 9 * scale,
}}
>
Sheet1
</div>
<div style={{ padding: `${3 * scale}px ${8 * scale}px` }}>DATEV</div>
<div style={{ marginLeft: 'auto', color: '#059669', display: 'flex', alignItems: 'center', gap: 4 * scale }}>
<div style={{ width: 6 * scale, height: 6 * scale, borderRadius: '50%', background: '#059669' }} />
AUTO-SAVED
</div>
</div>
{/* Table */}
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
{cols.map((col) => (
<th
key={col}
style={{
textAlign: 'left',
padding: `${6 * scale}px ${10 * scale}px`,
borderBottom: '1px solid #E2E8F0',
borderRight: '1px solid #E2E8F0',
background: '#F8FAFC',
fontFamily: 'monospace',
fontSize: 8 * scale,
letterSpacing: '0.1em',
color: '#64748B',
fontWeight: 500,
}}
>
{col}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => {
const isHighlighted = highlightRow === i;
const s = STATUS_COLORS[row.status];
return (
<tr
key={i}
style={{
background: isHighlighted ? '#F0F9FF' : i % 2 === 0 ? '#FFFFFF' : '#FAFBFC',
borderLeft: isHighlighted ? `3px solid #3B82F6` : '3px solid transparent',
transition: 'all 0.3s ease',
}}
>
<td style={cellStyle(scale)}>
<span style={{ fontFamily: 'monospace', fontSize: 9 * scale, color: '#94A3B8' }}>
{String(i + 1).padStart(2, '0')}
</span>
</td>
<td style={{ ...cellStyle(scale), fontWeight: 600, color: '#161C22', fontSize: 11 * scale }}>
{row.vendor}
</td>
<td style={{ ...cellStyle(scale), fontFamily: 'monospace', fontSize: 9 * scale, color: '#475569' }}>
{row.date}
</td>
<td style={{ ...cellStyle(scale), fontSize: 10 * scale, color: '#475569' }}>{row.category}</td>
<td
style={{
...cellStyle(scale),
fontFamily: 'monospace',
fontSize: 11 * scale,
fontWeight: 700,
color: '#161C22',
textAlign: 'right',
}}
>
{row.amount}
</td>
<td style={cellStyle(scale)}>
<span
style={{
background: s.bg,
color: s.text,
border: `1px solid ${s.border}`,
padding: `${2 * scale}px ${6 * scale}px`,
fontSize: 8 * scale,
fontFamily: 'monospace',
letterSpacing: '0.08em',
textTransform: 'uppercase' as const,
}}
>
{row.status}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
const cellStyle = (scale: number): React.CSSProperties => ({
padding: `${7 * scale}px ${10 * scale}px`,
borderBottom: '1px solid #EDF0F4',
borderRight: '1px solid #EDF0F4',
fontSize: 11 * scale,
color: '#475569',
});

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { AbsoluteFill } from 'remotion';
import { loadFont as loadHanken } from '@remotion/google-fonts/HankenGrotesk';
import { loadFont as loadInter } from '@remotion/google-fonts/Inter';
import { loadFont as loadJetBrains } from '@remotion/google-fonts/JetBrainsMono';
loadHanken();
loadInter();
loadJetBrains();
export const FontLoader: React.FC = () => null;

View File

@@ -0,0 +1,129 @@
import React from 'react';
import { useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
interface ReceiptProps {
vendor: string;
date: string;
items: { name: string; amount: string }[];
total: string;
scanning?: boolean;
scanProgress?: number; // 01
style?: React.CSSProperties;
scale?: number;
}
export const ReceiptMock: React.FC<ReceiptProps> = ({
vendor,
date,
items,
total,
scanning = false,
scanProgress = 0,
style = {},
scale = 1,
}) => {
const scanY = `${scanProgress * 90 + 5}%`;
return (
<div
style={{
background: '#FFFFFF',
border: '1px solid #E2E8F0',
fontFamily: "'Inter', sans-serif",
fontSize: 13 * scale,
width: 220 * scale,
position: 'relative',
overflow: 'hidden',
boxShadow: '0 20px 60px -20px rgba(22,28,34,0.25)',
transform: scanning ? `translateY(${-6 * scale}px)` : 'translateY(0)',
transition: 'transform 0.4s ease',
...style,
}}
>
{/* Scan line */}
{scanning && (
<div
style={{
position: 'absolute',
left: 0,
right: 0,
top: scanY,
height: 2,
background: '#059669',
boxShadow: '0 0 14px 3px rgba(5,150,105,0.55)',
zIndex: 10,
transition: 'top 0.05s linear',
}}
/>
)}
{/* Header */}
<div
style={{
borderBottom: '1px solid #E2E8F0',
padding: `${12 * scale}px ${14 * scale}px`,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 700,
fontSize: 16 * scale,
letterSpacing: '-0.01em',
color: '#161C22',
}}
>
{vendor}
</div>
<div style={{ fontFamily: 'monospace', fontSize: 9 * scale, color: '#64748B' }}>{date}</div>
</div>
{/* Items */}
<div style={{ padding: `${10 * scale}px ${14 * scale}px` }}>
{items.map((item, i) => (
<div
key={i}
style={{
display: 'flex',
justifyContent: 'space-between',
padding: `${4 * scale}px 0`,
borderBottom: '1px dotted #EEF1F5',
color: '#161C22',
fontSize: 11 * scale,
}}
>
<span>{item.name}</span>
<span style={{ fontFamily: 'monospace', fontWeight: 600 }}>{item.amount}</span>
</div>
))}
</div>
{/* Total */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
padding: `${10 * scale}px ${14 * scale}px`,
borderTop: '2px solid #161C22',
fontFamily: 'monospace',
fontWeight: 700,
fontSize: 13 * scale,
color: '#161C22',
}}
>
<span>TOTAL</span>
<span>{total}</span>
</div>
{/* Receipt tape perforations */}
<div style={{ display: 'flex', gap: 4 * scale, padding: `0 ${14 * scale}px ${8 * scale}px`, opacity: 0.3 }}>
{Array.from({ length: 14 }).map((_, i) => (
<div key={i} style={{ width: 4 * scale, height: 4 * scale, borderRadius: '50%', background: '#94A3B8' }} />
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { useCurrentFrame, useVideoConfig, interpolate, spring, Easing } from 'remotion';
interface Props {
children: React.ReactNode;
delay?: number;
direction?: 'up' | 'down' | 'left' | 'right' | 'scale' | 'fade';
duration?: number;
}
export const Reveal: React.FC<Props> = ({
children,
delay = 0,
direction = 'up',
duration = 20,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame: frame - delay,
fps,
config: {
damping: 18,
stiffness: 120,
mass: 0.8,
},
});
const opacity = interpolate(frame - delay, [0, 12], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
let transform = '';
if (direction === 'up') transform = `translateY(${interpolate(progress, [0, 1], [40, 0])}px)`;
if (direction === 'down') transform = `translateY(${interpolate(progress, [0, 1], [-40, 0])}px)`;
if (direction === 'left') transform = `translateX(${interpolate(progress, [0, 1], [60, 0])}px)`;
if (direction === 'right') transform = `translateX(${interpolate(progress, [0, 1], [-60, 0])}px)`;
if (direction === 'scale') transform = `scale(${interpolate(progress, [0, 1], [0.85, 1])})`;
if (direction === 'fade') transform = '';
return (
<div style={{ opacity, transform }}>
{children}
</div>
);
};

View File

@@ -0,0 +1,4 @@
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);

View File

@@ -0,0 +1,360 @@
import React from 'react';
import {
AbsoluteFill,
useCurrentFrame,
useVideoConfig,
interpolate,
spring,
} from 'remotion';
import { AnimatedHeadline } from '../components/AnimatedHeadline';
import { Reveal } from '../components/Reveal';
import { ReceiptMock } from '../components/ReceiptMock';
// Scene 01: Cinematic Intro — 0210 frames (7s)
// Big logo reveal + headline + animated receipts flying in
export const Scene01Intro: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Scene fade-in
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
// Logo spring
const logoSpring = spring({ frame: frame - 5, fps, config: { damping: 14, stiffness: 100, mass: 0.8 } });
const logoScale = interpolate(logoSpring, [0, 1], [0.5, 1]);
const logoOpacity = interpolate(frame - 5, [0, 18], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Receipt 1 — flies in from left
const r1Spring = spring({ frame: frame - 30, fps, config: { damping: 18, stiffness: 90, mass: 1 } });
const r1x = interpolate(r1Spring, [0, 1], [-350, 0]);
const r1rot = interpolate(r1Spring, [0, 1], [-15, -8]);
const r1opacity = interpolate(frame - 30, [0, 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Receipt 2 — flies in from right
const r2Spring = spring({ frame: frame - 50, fps, config: { damping: 18, stiffness: 90, mass: 1 } });
const r2x = interpolate(r2Spring, [0, 1], [350, 0]);
const r2rot = interpolate(r2Spring, [0, 1], [12, 6]);
const r2opacity = interpolate(frame - 50, [0, 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Receipt 3 — flies in from bottom
const r3Spring = spring({ frame: frame - 70, fps, config: { damping: 18, stiffness: 90, mass: 1 } });
const r3y = interpolate(r3Spring, [0, 1], [300, 0]);
const r3rot = interpolate(r3Spring, [0, 1], [20, 3]);
const r3opacity = interpolate(frame - 70, [0, 20], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Scanline over stacked receipts
const scanStart = 90;
const scanProgress = interpolate(frame - scanStart, [0, 60], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const scanning = frame > scanStart;
// Scene exit fade
const exitFade = interpolate(frame, [195, 215], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
{/* Left half: Text content */}
<div
style={{
position: 'absolute',
left: 0,
top: 0,
width: '50%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 80px',
}}
>
{/* Logo / Brand */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 14,
marginBottom: 40,
opacity: logoOpacity,
transform: `scale(${logoScale})`,
transformOrigin: 'left center',
}}
>
<div
style={{
width: 54,
height: 54,
border: '2.5px solid #000000',
background: '#FFFFFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 18,
letterSpacing: '-0.02em',
}}
>
SR
</div>
<div>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 22,
letterSpacing: '-0.03em',
color: '#000000',
lineHeight: 1,
}}
>
ScanReceipts
</div>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#64748B',
marginTop: 3,
}}
>
AI-Powered Bookkeeping
</div>
</div>
</div>
{/* Badge */}
<Reveal delay={20}>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
border: '1px solid #E2E8F0',
background: '#EEF4FC',
padding: '7px 12px',
marginBottom: 22,
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#161C22',
width: 'fit-content',
}}
>
<div style={{ width: 7, height: 7, background: '#059669' }} />
New · AI Receipt Scanner
</div>
</Reveal>
{/* Main headline */}
<AnimatedHeadline
text="One Photo."
delay={28}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 82,
letterSpacing: '-0.04em',
lineHeight: 1.0,
color: '#000000',
marginBottom: 4,
}}
/>
<AnimatedHeadline
text="Perfect Excel."
delay={40}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 82,
letterSpacing: '-0.04em',
lineHeight: 1.0,
color: '#475569',
marginBottom: 32,
}}
/>
{/* Subline */}
<Reveal delay={65}>
<div
style={{
fontSize: 18,
color: '#475569',
lineHeight: 1.6,
maxWidth: 420,
marginBottom: 40,
fontFamily: "'Inter', sans-serif",
}}
>
Snap a receipt. AI reads vendor, date, items, and tax then exports a flawless dual-sheet Excel in seconds.
</div>
</Reveal>
{/* Proof chips */}
<Reveal delay={80}>
<div
style={{
display: 'flex',
gap: 16,
flexWrap: 'wrap',
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#475569',
borderTop: '1px solid #E2E8F0',
paddingTop: 22,
}}
>
{['✓ DATEV-Compatible', '✓ 99% AI Accuracy', '✓ Instant Export', '✓ Zero Manual Work'].map((t, i) => (
<span key={i} style={{ color: i === 0 ? '#059669' : '#475569', fontWeight: i === 0 ? 700 : 400 }}>
{t}
</span>
))}
</div>
</Reveal>
</div>
{/* Right half: Animated receipts stack */}
<div
style={{
position: 'absolute',
right: 0,
top: 0,
width: '50%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{ position: 'relative', width: 340, height: 460 }}>
{/* Receipt 3 — back */}
<div
style={{
position: 'absolute',
top: 60,
left: 80,
opacity: r3opacity,
transform: `translateY(${r3y}px) rotate(${r3rot}deg)`,
}}
>
<ReceiptMock
vendor="REWE"
date="17.08.2026"
items={[
{ name: 'Milch 1L', amount: '€1.29' },
{ name: 'Brot', amount: '€2.49' },
{ name: 'MwSt. 7%', amount: '€0.27' },
]}
total="€3.78"
scale={0.88}
/>
</div>
{/* Receipt 1 — left */}
<div
style={{
position: 'absolute',
top: 20,
left: -20,
opacity: r1opacity,
transform: `translateX(${r1x}px) rotate(${r1rot}deg)`,
}}
>
<ReceiptMock
vendor="Trattoria Roma"
date="15.08.2026"
items={[
{ name: 'Pasta Carbonara', amount: '€14.90' },
{ name: 'Vino Rosso', amount: '€8.50' },
{ name: 'MwSt. 19%', amount: '€4.44' },
]}
total="€27.84"
scale={0.85}
/>
</div>
{/* Receipt 2 — right, SCANNING */}
<div
style={{
position: 'absolute',
top: 80,
right: -30,
opacity: r2opacity,
transform: `translateX(${r2x}px) rotate(${r2rot}deg)`,
zIndex: 10,
}}
>
<ReceiptMock
vendor="Aral Tankstelle"
date="16.08.2026"
items={[
{ name: 'Super E10 — 48L', amount: '€82.08' },
{ name: 'MwSt. 19%', amount: '€13.10' },
]}
total="€95.18"
scanning={scanning}
scanProgress={scanProgress}
scale={0.92}
/>
</div>
{/* AI Processing badge */}
{frame > 110 && (
<Reveal delay={0}>
<div
style={{
position: 'absolute',
bottom: 0,
left: '50%',
transform: 'translateX(-50%)',
background: '#000000',
color: '#FFFFFF',
padding: '10px 18px',
display: 'flex',
alignItems: 'center',
gap: 10,
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.1em',
textTransform: 'uppercase',
whiteSpace: 'nowrap',
}}
>
<div
style={{
width: 7,
height: 7,
borderRadius: '50%',
background: '#059669',
animation: 'pulse 1.5s ease-in-out infinite',
}}
/>
AI Extracting Data...
</div>
</Reveal>
)}
</div>
</div>
{/* Vertical divider line */}
<div
style={{
position: 'absolute',
left: '50%',
top: '10%',
width: 1,
height: '80%',
background: '#E2E8F0',
opacity: interpolate(frame, [40, 70], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
}}
/>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,243 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
import { AnimatedHeadline } from '../components/AnimatedHeadline';
import { Reveal } from '../components/Reveal';
// Scene 02: The Problem — 210390 frames (6s)
// Show the pain: manual receipt tracking is a nightmare
const PAIN_POINTS = [
{ icon: '📁', text: 'Receipts piling up in shoeboxes', sub: 'Average accountant wastes 4h/week sorting' },
{ icon: '⌨️', text: 'Manual data entry — typo-prone', sub: 'One error = rejected expense claim' },
{ icon: '🗓️', text: 'Tax season panic & missing docs', sub: '43% of SMBs pay late-filing penalties' },
{ icon: '💸', text: 'Accountant bills through the roof', sub: 'Up to €180/hour for data entry' },
];
export const Scene02Problem: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
const exitFade = interpolate(frame, [175, 195], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Headline counter animation
const counterProgress = interpolate(frame, [40, 120], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
easing: (t) => 1 - Math.pow(1 - t, 3),
});
const hoursWasted = Math.round(counterProgress * 4);
const costWasted = Math.round(counterProgress * 2880);
return (
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
{/* Dark panel left */}
<div
style={{
position: 'absolute',
left: 0,
top: 0,
width: interpolate(frame, [0, 25], [0, 44], { extrapolateRight: 'clamp' }) + '%',
height: '100%',
background: '#000000',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 64px',
overflow: 'hidden',
}}
>
<Reveal delay={18}>
<div
style={{
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.14em',
textTransform: 'uppercase',
color: '#EF4444',
marginBottom: 14,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ width: 26, height: 1, background: '#EF4444' }} />
The Problem
</div>
</Reveal>
<AnimatedHeadline
text="Manual receipts are killing your time."
delay={25}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 44,
letterSpacing: '-0.03em',
lineHeight: 1.1,
color: '#FFFFFF',
marginBottom: 40,
}}
/>
{/* Stats counters */}
<Reveal delay={45}>
<div style={{ display: 'flex', gap: 32, marginBottom: 40 }}>
<div>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 56,
letterSpacing: '-0.04em',
color: '#EF4444',
lineHeight: 1,
}}
>
{hoursWasted}h
</div>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#94A3B8',
marginTop: 6,
}}
>
Wasted per week
</div>
</div>
<div style={{ width: 1, background: '#1E293B' }} />
<div>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 56,
letterSpacing: '-0.04em',
color: '#EF4444',
lineHeight: 1,
}}
>
{costWasted.toLocaleString()}
</div>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#94A3B8',
marginTop: 6,
}}
>
Avg. annual cost
</div>
</div>
</div>
</Reveal>
<Reveal delay={60}>
<div
style={{
fontFamily: 'monospace',
fontSize: 11,
color: '#475569',
letterSpacing: '0.05em',
lineHeight: 1.7,
}}
>
Sound familiar? You're not alone.
<br />
<span style={{ color: '#64748B' }}>Over 2 million SMBs still do this manually.</span>
</div>
</Reveal>
</div>
{/* Right side: pain point cards */}
<div
style={{
position: 'absolute',
right: 0,
top: 0,
width: '56%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 72px 0 48px',
gap: 16,
}}
>
<Reveal delay={8}>
<div
style={{
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.14em',
textTransform: 'uppercase',
color: '#94A3B8',
marginBottom: 8,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ width: 26, height: 1, background: '#E2E8F0' }} />
Sound familiar?
</div>
</Reveal>
{PAIN_POINTS.map((point, i) => {
const cardSpring = spring({
frame: frame - (30 + i * 18),
fps,
config: { damping: 16, stiffness: 100, mass: 0.8 },
});
const cardOpacity = interpolate(frame - (30 + i * 18), [0, 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const cardX = interpolate(cardSpring, [0, 1], [80, 0]);
return (
<div
key={i}
style={{
opacity: cardOpacity,
transform: `translateX(${cardX}px)`,
background: '#FFFFFF',
border: '1px solid #E2E8F0',
padding: '20px 24px',
display: 'flex',
alignItems: 'center',
gap: 18,
}}
>
<div style={{ fontSize: 28, lineHeight: 1 }}>{point.icon}</div>
<div>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 600,
fontSize: 16,
color: '#161C22',
letterSpacing: '-0.01em',
marginBottom: 4,
}}
>
{point.text}
</div>
<div style={{ fontFamily: 'monospace', fontSize: 10, color: '#EF4444', letterSpacing: '0.06em' }}>
{point.sub}
</div>
</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,382 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
import { AnimatedHeadline } from '../components/AnimatedHeadline';
import { Reveal } from '../components/Reveal';
import { ReceiptMock } from '../components/ReceiptMock';
import { ExcelMock } from '../components/ExcelMock';
// Scene 03: The Magic — 390600 (7s)
// Shows the AI scanning process: receipt in → data extracted → Excel out
const EXCEL_ROWS = [
{ vendor: 'Aral Tankstelle', date: '16.08.26', category: 'Travel', amount: '€95.18', status: 'confirmed' as const },
{ vendor: 'Trattoria Roma', date: '15.08.26', category: 'Dining', amount: '€27.84', status: 'confirmed' as const },
{ vendor: 'REWE', date: '17.08.26', category: 'Office', amount: '€3.78', status: 'scanned' as const },
{ vendor: 'Media Markt', date: '12.08.26', category: 'Equipment', amount: '€349.00', status: 'pending' as const },
];
const EXTRACTED_FIELDS = [
{ label: 'VENDOR', value: 'Aral Tankstelle München', confirmed: true },
{ label: 'DATE', value: '16.08.2026', confirmed: true },
{ label: 'AMOUNT', value: '€95.18', confirmed: true },
{ label: 'TAX (19%)', value: '€13.10', confirmed: true },
{ label: 'CATEGORY', value: 'Travel / Fuel', confirmed: true },
{ label: 'PAYMENT', value: 'Credit Card', confirmed: true },
];
export const Scene03Magic: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
const exitFade = interpolate(frame, [195, 215], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
// Scan progress (receipt scan line)
const scanProgress = interpolate(frame, [40, 100], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Arrow pulse animation
const arrowScale = interpolate(
Math.sin((frame / 8) * Math.PI),
[-1, 1],
[0.92, 1.08]
);
// Excel appears at frame 110 with row highlighting cycling
const excelOpacity = interpolate(frame, [110, 130], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const highlightRow = frame > 130 ? Math.floor(((frame - 130) / 30) % 4) : undefined;
// Fields appear one by one
const fieldsVisible = Math.min(6, Math.floor((frame - 50) / 15));
return (
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
{/* Header section */}
<div
style={{
position: 'absolute',
top: 60,
left: 80,
right: 80,
}}
>
<Reveal delay={5}>
<div
style={{
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.14em',
textTransform: 'uppercase',
color: '#059669',
marginBottom: 10,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ width: 26, height: 1, background: '#059669' }} />
The Solution
</div>
</Reveal>
<AnimatedHeadline
text="Watch the magic happen."
delay={10}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 64,
letterSpacing: '-0.04em',
lineHeight: 1.05,
color: '#000000',
}}
/>
</div>
{/* Three-panel demo layout */}
<div
style={{
position: 'absolute',
top: 220,
left: 80,
right: 80,
display: 'flex',
alignItems: 'center',
gap: 40,
}}
>
{/* Panel 1: Receipt */}
<div style={{ flex: '0 0 auto' }}>
<Reveal delay={20} direction="left">
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#64748B',
marginBottom: 12,
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
<span style={{ background: '#000', color: '#fff', padding: '2px 6px', fontSize: 8 }}>01</span>
Snap a receipt
</div>
<ReceiptMock
vendor="Aral Tankstelle"
date="16.08.2026"
items={[
{ name: 'Super E10 — 48L', amount: '€82.08' },
{ name: 'MwSt. 19%', amount: '€13.10' },
]}
total="€95.18"
scanning={frame > 30}
scanProgress={scanProgress}
scale={1.05}
/>
</Reveal>
</div>
{/* Arrow 1 */}
<div
style={{
flex: '0 0 auto',
opacity: interpolate(frame, [35, 55], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
transform: `scale(${arrowScale})`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 6,
}}
>
<div style={{ width: 80, height: 1, background: '#000000' }} />
<div
style={{
width: 0,
height: 0,
borderLeft: '10px solid #000',
borderTop: '6px solid transparent',
borderBottom: '6px solid transparent',
marginLeft: 80,
marginTop: -6,
}}
/>
<div
style={{
fontFamily: 'monospace',
fontSize: 8,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#059669',
marginTop: -4,
background: '#F0FDF4',
border: '1px solid #BBF7D0',
padding: '3px 8px',
}}
>
AI Processing
</div>
</div>
{/* Panel 2: Extracted fields */}
<div style={{ flex: '0 0 auto' }}>
<Reveal delay={45}>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#64748B',
marginBottom: 12,
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
<span style={{ background: '#059669', color: '#fff', padding: '2px 6px', fontSize: 8 }}>02</span>
AI Extracts Data
</div>
<div
style={{
background: '#FFFFFF',
border: '1px solid #E2E8F0',
width: 260,
overflow: 'hidden',
boxShadow: '0 20px 60px -20px rgba(22,28,34,0.18)',
}}
>
{/* AI header */}
<div
style={{
background: '#000000',
padding: '10px 16px',
display: 'flex',
alignItems: 'center',
gap: 8,
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#FFFFFF',
}}
>
<div
style={{
width: 7,
height: 7,
borderRadius: '50%',
background: '#059669',
boxShadow: '0 0 8px rgba(5,150,105,0.7)',
}}
/>
Extracted Fields
</div>
{EXTRACTED_FIELDS.map((field, i) => {
const visible = i < fieldsVisible;
const fieldProgress = interpolate(frame - (50 + i * 15), [0, 14], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<div
key={i}
style={{
opacity: visible ? 1 : 0,
transform: visible ? 'translateX(0)' : 'translateX(20px)',
transition: 'all 0.3s ease',
padding: '9px 16px',
borderBottom: '1px solid #EDF0F4',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span
style={{
fontFamily: 'monospace',
fontSize: 8,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#94A3B8',
}}
>
{field.label}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 12,
fontWeight: 600,
color: '#161C22',
}}
>
{field.value}
</span>
{visible && (
<span style={{ color: '#059669', fontSize: 11, fontWeight: 700 }}></span>
)}
</div>
</div>
);
})}
</div>
</Reveal>
</div>
{/* Arrow 2 */}
<div
style={{
flex: '0 0 auto',
opacity: interpolate(frame, [115, 135], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }),
transform: `scale(${arrowScale})`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 6,
}}
>
<div style={{ width: 80, height: 1, background: '#000000' }} />
<div
style={{
width: 0,
height: 0,
borderLeft: '10px solid #000',
borderTop: '6px solid transparent',
borderBottom: '6px solid transparent',
marginLeft: 80,
marginTop: -6,
}}
/>
<div
style={{
fontFamily: 'monospace',
fontSize: 8,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#059669',
marginTop: -4,
background: '#F0FDF4',
border: '1px solid #BBF7D0',
padding: '3px 8px',
}}
>
Auto-Export
</div>
</div>
{/* Panel 3: Excel */}
<div style={{ flex: 1, opacity: excelOpacity, transform: `translateX(${interpolate(excelOpacity, [0, 1], [40, 0])}px)` }}>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: '#64748B',
marginBottom: 12,
display: 'flex',
alignItems: 'center',
gap: 6,
}}
>
<span style={{ background: '#1D4ED8', color: '#fff', padding: '2px 6px', fontSize: 8 }}>03</span>
Perfect Excel
</div>
<ExcelMock rows={EXCEL_ROWS} highlightRow={highlightRow} scale={0.88} />
</div>
</div>
{/* Bottom timing badge */}
<Reveal delay={150}>
<div
style={{
position: 'absolute',
bottom: 60,
left: '50%',
transform: 'translateX(-50%)',
background: '#000000',
color: '#FFFFFF',
padding: '12px 28px',
fontFamily: 'monospace',
fontSize: 13,
letterSpacing: '0.08em',
textTransform: 'uppercase',
whiteSpace: 'nowrap',
display: 'flex',
alignItems: 'center',
gap: 14,
}}
>
<span style={{ color: '#059669', fontSize: 20, lineHeight: 1 }}></span>
Total time: Under 3 seconds
<span style={{ color: '#059669', fontSize: 20, lineHeight: 1 }}></span>
</div>
</Reveal>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,216 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
import { AnimatedHeadline } from '../components/AnimatedHeadline';
import { Reveal } from '../components/Reveal';
// Scene 04: Features Showcase — 600810 (7s)
// Grid of features, each reveals with stagger
const FEATURES = [
{
number: '01',
title: 'AI Accuracy Engine',
desc: 'Reads vendor, date, line items, VAT, and totals with 99% accuracy across all receipt types.',
tag: 'CORE AI',
color: '#000000',
bg: '#FFFFFF',
},
{
number: '02',
title: 'Batch Upload Queue',
desc: 'Drop 50 receipts at once. Live progress bars, instant thumbnails, one-click retry on errors.',
tag: 'PRODUCTIVITY',
color: '#1D4ED8',
bg: '#EFF6FF',
},
{
number: '03',
title: 'Dual-Sheet Excel Export',
desc: 'Summary sheet + raw data. DATEV-compatible CSV also included — ready for your accountant.',
tag: 'EXPORT',
color: '#059669',
bg: '#F0FDF4',
},
{
number: '04',
title: 'Side-by-Side Inspector',
desc: 'Zoom, pan, rotate the document while editing extracted fields with 2-way bounding box sync.',
tag: 'REVIEW',
color: '#7C3AED',
bg: '#F5F3FF',
},
{
number: '05',
title: 'Smart Filter & Search',
desc: 'Filter by date range, category, amount, or status. Instant chip filters. Powerful full-text search.',
tag: 'SEARCH',
color: '#B45309',
bg: '#FFFBEB',
},
{
number: '06',
title: 'Offline-First Storage',
desc: 'Your data stays on-device in IndexedDB. No cloud dependency. Privacy-first by design.',
tag: 'PRIVACY',
color: '#475569',
bg: '#F8FAFC',
},
];
export const Scene04Features: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
const exitFade = interpolate(frame, [195, 215], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ opacity: sceneFade * exitFade }}>
{/* Header */}
<div style={{ position: 'absolute', top: 64, left: 80, right: 80 }}>
<Reveal delay={5}>
<div
style={{
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.14em',
textTransform: 'uppercase',
color: '#475569',
marginBottom: 10,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ width: 26, height: 1, background: '#000' }} />
Everything you need
</div>
</Reveal>
<AnimatedHeadline
text="Built for real-world accounting."
delay={12}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 60,
letterSpacing: '-0.04em',
lineHeight: 1.05,
color: '#000000',
}}
/>
</div>
{/* Feature grid */}
<div
style={{
position: 'absolute',
top: 220,
left: 80,
right: 80,
bottom: 60,
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridTemplateRows: 'repeat(2, 1fr)',
gap: 16,
}}
>
{FEATURES.map((feat, i) => {
const row = Math.floor(i / 3);
const col = i % 3;
const cardDelay = 20 + row * 20 + col * 12;
const cardSpring = spring({
frame: frame - cardDelay,
fps,
config: { damping: 16, stiffness: 100, mass: 0.7 },
});
const cardOpacity = interpolate(frame - cardDelay, [0, 12], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const cardY = interpolate(cardSpring, [0, 1], [30, 0]);
return (
<div
key={i}
style={{
background: feat.bg,
border: '1px solid #E2E8F0',
padding: '24px 26px',
opacity: cardOpacity,
transform: `translateY(${cardY}px)`,
display: 'flex',
flexDirection: 'column',
gap: 10,
position: 'relative',
overflow: 'hidden',
}}
>
{/* Number */}
<div
style={{
position: 'absolute',
top: 18,
right: 20,
fontFamily: 'monospace',
fontSize: 48,
fontWeight: 700,
color: feat.color,
opacity: 0.08,
lineHeight: 1,
letterSpacing: '-0.04em',
}}
>
{feat.number}
</div>
{/* Tag */}
<div
style={{
fontFamily: 'monospace',
fontSize: 8,
letterSpacing: '0.12em',
textTransform: 'uppercase',
color: feat.color,
background: 'transparent',
border: `1px solid ${feat.color}`,
padding: '2px 7px',
width: 'fit-content',
opacity: 0.75,
}}
>
{feat.tag}
</div>
{/* Title */}
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 700,
fontSize: 18,
letterSpacing: '-0.02em',
color: '#161C22',
lineHeight: 1.2,
}}
>
{feat.title}
</div>
{/* Desc */}
<div
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 13,
color: '#475569',
lineHeight: 1.55,
}}
>
{feat.desc}
</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,231 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
import { AnimatedHeadline } from '../components/AnimatedHeadline';
import { Reveal } from '../components/Reveal';
// Scene 05: Social Proof — 810960 (5s)
// Testimonials + KPI stats
const TESTIMONIALS = [
{
quote: 'Cut our expense reporting from 4 hours to 8 minutes. Absolute game-changer.',
name: 'Sarah K.',
role: 'CFO, TechStart GmbH',
rating: 5,
},
{
quote: 'My accountant was blown away. DATEV export works flawlessly, zero corrections needed.',
name: 'Marcus T.',
role: 'Freelance Designer',
rating: 5,
},
{
quote: 'Finally — a tool that handles German receipts perfectly. MwSt. parsing is spot-on.',
name: 'Jana B.',
role: 'Steuerberaterin',
rating: 5,
},
];
const STATS = [
{ value: '99%', label: 'AI Accuracy Rate' },
{ value: '3s', label: 'Avg. Processing Time' },
{ value: '50k+', label: 'Receipts Processed' },
{ value: '€0', label: 'Manual Entry Cost' },
];
export const Scene05Social: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const sceneFade = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: 'clamp' });
const exitFade = interpolate(frame, [135, 155], [1, 0], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
return (
<AbsoluteFill style={{ background: '#000000', opacity: sceneFade * exitFade }}>
{/* Subtle grid on dark bg */}
<div
style={{
position: 'absolute',
inset: 0,
backgroundImage:
'linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px)',
backgroundSize: '80px 80px',
}}
/>
{/* Header */}
<div style={{ position: 'absolute', top: 60, left: 80, right: 80 }}>
<Reveal delay={5}>
<div
style={{
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.14em',
textTransform: 'uppercase',
color: '#059669',
marginBottom: 10,
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<div style={{ width: 26, height: 1, background: '#059669' }} />
Trusted by thousands
</div>
</Reveal>
<AnimatedHeadline
text="Don't take our word for it."
delay={12}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 58,
letterSpacing: '-0.04em',
lineHeight: 1.05,
color: '#FFFFFF',
}}
/>
</div>
{/* Stats row */}
<div
style={{
position: 'absolute',
top: 230,
left: 80,
right: 80,
display: 'flex',
gap: 0,
}}
>
{STATS.map((stat, i) => {
const statSpring = spring({ frame: frame - (20 + i * 12), fps, config: { damping: 16, stiffness: 110, mass: 0.7 } });
const statOpacity = interpolate(frame - (20 + i * 12), [0, 14], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const statY = interpolate(statSpring, [0, 1], [24, 0]);
return (
<div
key={i}
style={{
flex: 1,
padding: '28px 32px',
borderRight: i < 3 ? '1px solid rgba(255,255,255,0.08)' : 'none',
borderBottom: '1px solid rgba(255,255,255,0.08)',
opacity: statOpacity,
transform: `translateY(${statY}px)`,
}}
>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 52,
letterSpacing: '-0.04em',
color: '#FFFFFF',
lineHeight: 1,
marginBottom: 8,
}}
>
{stat.value}
</div>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#475569',
}}
>
{stat.label}
</div>
</div>
);
})}
</div>
{/* Testimonials row */}
<div
style={{
position: 'absolute',
top: 430,
left: 80,
right: 80,
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 16,
}}
>
{TESTIMONIALS.map((t, i) => {
const cardSpring = spring({ frame: frame - (55 + i * 18), fps, config: { damping: 16, stiffness: 100, mass: 0.8 } });
const cardOpacity = interpolate(frame - (55 + i * 18), [0, 14], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const cardY = interpolate(cardSpring, [0, 1], [30, 0]);
return (
<div
key={i}
style={{
opacity: cardOpacity,
transform: `translateY(${cardY}px)`,
border: '1px solid rgba(255,255,255,0.10)',
padding: '24px 26px',
background: 'rgba(255,255,255,0.04)',
display: 'flex',
flexDirection: 'column',
gap: 16,
}}
>
{/* Stars */}
<div style={{ display: 'flex', gap: 3 }}>
{Array.from({ length: t.rating }).map((_, si) => (
<span key={si} style={{ color: '#FBBF24', fontSize: 14 }}></span>
))}
</div>
{/* Quote */}
<div
style={{
fontFamily: "'Inter', sans-serif",
fontSize: 15,
color: '#E2E8F0',
lineHeight: 1.6,
fontStyle: 'italic',
}}
>
"{t.quote}"
</div>
{/* Author */}
<div style={{ borderTop: '1px solid rgba(255,255,255,0.08)', paddingTop: 14 }}>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 600,
fontSize: 14,
color: '#FFFFFF',
letterSpacing: '-0.01em',
}}
>
{t.name}
</div>
<div
style={{
fontFamily: 'monospace',
fontSize: 9,
color: '#475569',
letterSpacing: '0.08em',
textTransform: 'uppercase',
marginTop: 3,
}}
>
{t.role}
</div>
</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,230 @@
import React from 'react';
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from 'remotion';
import { AnimatedHeadline } from '../components/AnimatedHeadline';
import { Reveal } from '../components/Reveal';
// Scene 06: CTA — 9601050 (3s)
// Big, punchy final call to action
export const Scene06CTA: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const sceneFade = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
// Pulsing button glow
const glowIntensity = interpolate(
Math.sin((frame / 15) * Math.PI),
[-1, 1],
[0.5, 1]
);
// URL reveal progress
const urlProgress = interpolate(frame, [50, 80], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Background white sweep from left
const sweepWidth = interpolate(frame, [0, 25], [0, 100], { extrapolateRight: 'clamp' });
// Final logo fade
const logoOpacity = interpolate(frame, [20, 45], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const url = 'scanreceipts.app';
const visibleChars = Math.floor(urlProgress * url.length);
return (
<AbsoluteFill
style={{
opacity: sceneFade,
background: '#FFFFFF',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
}}
>
{/* Grid bg */}
<div
style={{
position: 'absolute',
inset: '-10%',
backgroundImage:
'linear-gradient(#E2E8F0 1px, transparent 1px), linear-gradient(90deg, #E2E8F0 1px, transparent 1px)',
backgroundSize: '80px 80px',
opacity: 0.3,
}}
/>
{/* Radial highlight */}
<div
style={{
position: 'absolute',
inset: 0,
background: 'radial-gradient(ellipse 70% 60% at 50% 50%, rgba(5,150,105,0.06) 0%, transparent 70%)',
}}
/>
{/* Content */}
<div style={{ position: 'relative', zIndex: 1, textAlign: 'center', maxWidth: 900 }}>
{/* Logo */}
<div
style={{
opacity: logoOpacity,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 14,
marginBottom: 40,
}}
>
<div
style={{
width: 52,
height: 52,
border: '2.5px solid #000000',
background: '#FFFFFF',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 17,
letterSpacing: '-0.02em',
}}
>
SR
</div>
<div
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 24,
letterSpacing: '-0.03em',
color: '#000000',
}}
>
ScanReceipts
</div>
</div>
{/* Main CTA headline */}
<AnimatedHeadline
text="Stop wasting hours."
delay={18}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 90,
letterSpacing: '-0.045em',
lineHeight: 1.0,
color: '#000000',
justifyContent: 'center',
marginBottom: 6,
}}
/>
<AnimatedHeadline
text="Start in 30 seconds."
delay={28}
style={{
fontFamily: "'Hanken Grotesk', sans-serif",
fontWeight: 800,
fontSize: 90,
letterSpacing: '-0.045em',
lineHeight: 1.0,
color: '#059669',
justifyContent: 'center',
marginBottom: 44,
}}
/>
{/* CTA Button */}
<Reveal delay={42} direction="scale">
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
gap: 20,
}}
>
<div
style={{
background: '#000000',
color: '#FFFFFF',
padding: '20px 52px',
fontFamily: 'monospace',
fontSize: 15,
letterSpacing: '0.1em',
textTransform: 'uppercase',
display: 'flex',
alignItems: 'center',
gap: 12,
boxShadow: `0 0 ${40 * glowIntensity}px ${12 * glowIntensity}px rgba(5,150,105,${0.15 * glowIntensity})`,
}}
>
<span style={{ fontSize: 18 }}></span>
Try Free No Credit Card
</div>
{/* URL typewriter */}
<div
style={{
fontFamily: 'monospace',
fontSize: 14,
color: '#64748B',
letterSpacing: '0.06em',
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
<span style={{ opacity: 0.4 }}>https://</span>
<span style={{ color: '#000000', fontWeight: 600 }}>
{url.slice(0, visibleChars)}
</span>
{urlProgress < 1 && (
<span
style={{
display: 'inline-block',
width: 2,
height: 14,
background: '#000000',
opacity: Math.sin(frame * 0.3) > 0 ? 1 : 0,
}}
/>
)}
</div>
</div>
</Reveal>
{/* Badges row */}
<Reveal delay={60}>
<div
style={{
display: 'flex',
justifyContent: 'center',
gap: 28,
marginTop: 36,
fontFamily: 'monospace',
fontSize: 10,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: '#64748B',
borderTop: '1px solid #E2E8F0',
paddingTop: 24,
}}
>
{['✓ Free Forever Plan', '✓ GDPR Compliant', '✓ DATEV Export', '✓ No Setup Required'].map((b, i) => (
<span key={i}>{b}</span>
))}
</div>
</Reveal>
</div>
</AbsoluteFill>
);
};

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react",
"strict": true,
"allowJs": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"outDir": "dist"
},
"include": ["src"]
}