Files
breakpilot-core/pitch-deck/components/slides/MilestonesSlide.tsx
Benjamin Admin 56da89fb0e
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m8s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 29s
CI / test-python-voice (push) Successful in 30s
CI / test-bqas (push) Successful in 29s
feat(pitch-deck): add subtitles to USP + Milestones, EUR hint, re-secure
- USP: "Compliance und Code — in einer Plattform, immer synchron"
- Milestones: "Von der Idee zur GmbH — was wir bereits erreicht haben"
- Finanzplan subtitle: "Alle Werte in EUR"
- 2. Finanzierungsrunde (optional)
- Re-secure fp-patch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 23:38:51 +02:00

818 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect, useRef, useMemo, useCallback, Fragment } from 'react'
import { Language } from '@/lib/types'
import GradientText from '../ui/GradientText'
import FadeInView from '../ui/FadeInView'
interface MilestonesSlideProps { lang: Language }
const MONO: React.CSSProperties = {
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
fontVariantNumeric: 'tabular-nums',
}
const CSS_KF = `
@keyframes msFlow { 0%{stroke-dashoffset:0} 100%{stroke-dashoffset:-18} }
@keyframes msFadeIn { from{opacity:0} to{opacity:1} }
@keyframes msScaleIn { from{opacity:0;transform:scale(.94)} to{opacity:1;transform:scale(1)} }
@keyframes msHeadingDark {
0%,100%{text-shadow:0 0 22px rgba(167,139,250,.3)}
50% {text-shadow:0 0 40px rgba(167,139,250,.6)}
}
@keyframes msHeadingLight {
0%,100%{text-shadow:0 0 22px rgba(124,58,237,.15)}
50% {text-shadow:0 0 36px rgba(124,58,237,.30)}
}
@keyframes msPulse {
0%,100%{r:9;opacity:.4}
50% {r:14;opacity:.05}
}
`
// ── Light mode hook ───────────────────────────────────────────────────────────
function useIsLight() {
const [isLight, setIsLight] = useState(false)
useEffect(() => {
const check = () => setIsLight(document.documentElement.classList.contains('theme-light'))
check()
const obs = new MutationObserver(check)
obs.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
return () => obs.disconnect()
}, [])
return isLight
}
// ── Themes ────────────────────────────────────────────────────────────────────
const THEMES = {
dark: {
key: 'dark' as const,
bg: 'radial-gradient(ellipse at 50% 25%, #1a0f34 0%, #0e0720 55%, #050210 100%)',
ambient: 'radial-gradient(ellipse, rgba(167,139,250,.18), transparent 65%)',
stars: true,
fg: '#f7f5fc',
fgSoft: 'rgba(236,233,247,.82)',
fgMid: 'rgba(236,233,247,.72)',
fgMuted: 'rgba(236,233,247,.62)',
fgFaint: 'rgba(236,233,247,.55)',
fgGhost: 'rgba(236,233,247,.45)',
fgWhisper: 'rgba(236,233,247,.4)',
accent: '#a78bfa',
accent80: 'rgba(167,139,250,.8)',
accent70: 'rgba(167,139,250,.7)',
accent50: 'rgba(167,139,250,.5)',
accent40: 'rgba(167,139,250,.4)',
accent20: 'rgba(167,139,250,.2)',
headingGrad: 'linear-gradient(90deg, #e9e2ff, #a78bfa 50%, #e9e2ff)',
headingAnim: 'msHeadingDark 4s ease-in-out infinite',
heuteText: '#e4d4ff',
heutePillBg: 'rgba(14,8,28,.95)',
heuteCore: '#f0e9ff',
done: '#4ade80',
doneBright: '#86efac',
doneDeep: '#166534',
doneSolid: '#22c55e',
cardBase: 'rgba(14,8,28,',
cardBaseA: '.9',
cardBaseAH: '.95',
cardTintTop: '18', cardTintTopH: '2e',
cardTintMid: '08', cardTintMidH: '14',
cardShadowSoft: '0 10px 24px rgba(0,0,0,.45)',
cardShadowLift: (t: string) => `0 20px 44px ${t}33, 0 0 0 1px ${t}66, inset 0 1px 0 ${t}66`,
statTintTop: '18', statTintTopH: '2a',
statTintMid: '06',
statShadowSoft: '0 10px 24px rgba(0,0,0,.45)',
statShadowLift: (t: string) => `0 18px 40px ${t}33, 0 0 0 1px ${t}55, inset 0 1px 0 ${t}55`,
modalScrim: 'rgba(5,2,16,.75)',
modalBgMid: 'rgba(20,10,40,.97)',
modalBgLow: 'rgba(14,8,28,.98)',
modalShadow: (t: string) => `0 30px 80px rgba(0,0,0,.65), 0 0 60px ${t}33, inset 0 1px 0 ${t}55`,
bulletBg: 'rgba(0,0,0,.3)',
progressTrackBg: 'rgba(255,255,255,.08)',
progressTrackBorder: 'rgba(167,139,250,.2)',
dotTodoDeep: '#1a0f34',
dotLitHi: 'rgba(255,255,255,.5)',
dotSoftHi: 'rgba(255,255,255,.3)',
sparkOp: 0.45,
},
light: {
key: 'light' as const,
bg: 'radial-gradient(ellipse at 50% 12%, #ffffff 0%, #f5efff 55%, #ebdfff 100%)',
ambient: 'radial-gradient(ellipse, rgba(124,58,237,.14), transparent 65%)',
stars: false,
fg: '#1a0f34',
fgSoft: 'rgba(26,15,52,.85)',
fgMid: 'rgba(26,15,52,.72)',
fgMuted: 'rgba(26,15,52,.62)',
fgFaint: 'rgba(26,15,52,.50)',
fgGhost: 'rgba(26,15,52,.40)',
fgWhisper: 'rgba(26,15,52,.32)',
accent: '#7c3aed',
accent80: 'rgba(124,58,237,.8)',
accent70: 'rgba(124,58,237,.75)',
accent50: 'rgba(124,58,237,.55)',
accent40: 'rgba(124,58,237,.4)',
accent20: 'rgba(124,58,237,.18)',
headingGrad: 'linear-gradient(90deg, #3b0e7a, #7c3aed 50%, #3b0e7a)',
headingAnim: 'msHeadingLight 4s ease-in-out infinite',
heuteText: '#4c1d95',
heutePillBg: 'rgba(255,255,255,.98)',
heuteCore: '#7c3aed',
done: '#16a34a',
doneBright: '#4ade80',
doneDeep: '#14532d',
doneSolid: '#22c55e',
cardBase: 'rgba(255,255,255,',
cardBaseA: '.92',
cardBaseAH: '.98',
cardTintTop: '22', cardTintTopH: '3a',
cardTintMid: '10', cardTintMidH: '1c',
cardShadowSoft: '0 10px 24px rgba(59,26,122,.10), 0 2px 6px rgba(59,26,122,.06)',
cardShadowLift: (t: string) => `0 20px 44px ${t}38, 0 0 0 1px ${t}77, inset 0 1px 0 rgba(255,255,255,.9)`,
statTintTop: '1e', statTintTopH: '34',
statTintMid: '08',
statShadowSoft: '0 10px 24px rgba(59,26,122,.10), 0 2px 6px rgba(59,26,122,.06)',
statShadowLift: (t: string) => `0 18px 40px ${t}38, 0 0 0 1px ${t}77, inset 0 1px 0 rgba(255,255,255,.9)`,
modalScrim: 'rgba(40,20,80,.28)',
modalBgMid: 'rgba(255,255,255,.98)',
modalBgLow: 'rgba(250,247,255,.98)',
modalShadow: (t: string) => `0 30px 80px rgba(59,26,122,.25), 0 0 60px ${t}33, inset 0 1px 0 rgba(255,255,255,.9)`,
bulletBg: 'rgba(124,58,237,.06)',
progressTrackBg: 'rgba(124,58,237,.12)',
progressTrackBorder: 'rgba(124,58,237,.25)',
dotTodoDeep: '#faf5ff',
dotLitHi: 'rgba(255,255,255,.85)',
dotSoftHi: 'rgba(255,255,255,.55)',
sparkOp: 0.55,
},
}
type Theme = typeof THEMES.dark
// ── Data ──────────────────────────────────────────────────────────────────────
const TODAY_POSITION = 0.56
interface Milestone {
id: string
when: string
tick: string
title: { de: string; en: string }
short: { de: string; en: string }
body: { de: string; en: string }
bullets: { de: string[]; en: string[] }
tint: string
done: boolean
next?: boolean
}
const MILESTONES: Milestone[] = [
{
id: 'ihk',
when: 'Okt. 2025', tick: '10 · 25',
title: { de: 'Gründerzuschuss & IHK', en: 'Founder Grant & IHK' },
short: { de: 'Abstimmung mit Agentur für Arbeit und IHK Konstanz.', en: 'Coordination with Employment Agency and IHK Konstanz.' },
body: {
de: 'Seit Oktober 2025 Gründerzuschussantrag in Abstimmung mit der Agentur für Arbeit und der IHK Konstanz. Grundlage für die Unternehmensgründung.',
en: 'Since October 2025, founder grant application in coordination with the Employment Agency and IHK Konstanz. Foundation for company formation.',
},
bullets: {
de: ['Gründerzuschuss beantragt', 'Beratung IHK Konstanz', 'Businessplan finalisiert'],
en: ['Founder grant applied', 'IHK Konstanz advisory', 'Business plan finalized'],
},
tint: '#a78bfa', done: true,
},
{
id: 'brand',
when: '11. Nov. 2025', tick: '11 · 25',
title: { de: 'Markenanmeldung & Domains', en: 'Trademark Filing & Domains' },
short: { de: 'DPMA-Anmeldung BreakPilot + Domain-Portfolio.', en: 'DPMA filing BreakPilot + domain portfolio.' },
body: {
de: 'Markenanmeldung BreakPilot beim DPMA am 11.11.2025. Domain-Kauf breakpilot.com, .de, .ai und brakepilot.com, .de, .ai am 21.11.2025.',
en: 'BreakPilot trademark filed with DPMA on 11.11.2025. Domain purchase breakpilot.com, .de, .ai and brakepilot.com, .de, .ai on 21.11.2025.',
},
bullets: {
de: ['DPMA-Markenanmeldung 11.11.2025', 'Domains .com .de .ai gesichert', 'Typo-Domains (.brakepilot) gesichert'],
en: ['DPMA trademark filed 11.11.2025', 'Domains .com .de .ai secured', 'Typo domains (.brakepilot) secured'],
},
tint: '#a78bfa', done: true,
},
{
id: 'dev',
when: 'Jan. 2026', tick: '01 · 26',
title: { de: 'Plattform-Entwicklung gestartet', en: 'Platform Development Started' },
short: { de: '500.000+ Lines of Code, vollständige Architektur.', en: '500,000+ lines of code, full architecture.' },
body: {
de: 'Start der Plattform-Entwicklung mit 500.000+ Lines of Code. Vollständige Microservice-Architektur mit Go, Python und TypeScript.',
en: 'Platform development started with 500,000+ lines of code. Full microservice architecture with Go, Python and TypeScript.',
},
bullets: {
de: ['500K+ Lines of Code', 'Go + Python + TypeScript', 'Vollständige Architektur'],
en: ['500K+ lines of code', 'Go + Python + TypeScript', 'Full architecture'],
},
tint: '#c084fc', done: true,
},
{
id: 'dpma',
when: '27. Mär. 2026', tick: '03 · 26',
title: { de: 'Markeneintragung DPMA', en: 'DPMA Trademark Registration' },
short: { de: 'BreakPilot offiziell eingetragen.', en: 'BreakPilot officially registered.' },
body: {
de: 'Markeneintragung BreakPilot beim Deutschen Patent- und Markenamt (DPMA) am 27.03.2026.',
en: 'BreakPilot trademark registration at the German Patent and Trademark Office (DPMA) on 27.03.2026.',
},
bullets: {
de: ['DPMA-Eintragung 27.03.2026', 'Markenschutz Deutschland'],
en: ['DPMA registration 27.03.2026', 'Trademark protection Germany'],
},
tint: '#c084fc', done: true,
},
{
id: 'rag',
when: 'Apr. 2026', tick: '04 · 26',
title: { de: 'RAG mit 375+ Dokumenten', en: 'RAG with 375+ Documents' },
short: { de: 'EU + DACH Regularien & Normen indexiert.', en: 'EU + DACH regulations & standards indexed.' },
body: {
de: '375+ Gesetze, Verordnungen, Normen und Urteile in die RAG-Pipeline ingestiert. 25.000+ Prüfaspekte generiert.',
en: '375+ laws, regulations, standards and rulings ingested into the RAG pipeline. 25,000+ audit controls generated.',
},
bullets: {
de: ['375+ Dokumente im RAG', '25.000+ Prüfaspekte', 'EU + DACH Abdeckung'],
en: ['375+ documents in RAG', '25,000+ audit controls', 'EU + DACH coverage'],
},
tint: '#c084fc', done: true,
},
{
id: 'euipo',
when: '1. Mai 2026', tick: '05 · 26',
title: { de: 'Markenanmeldung EUIPO', en: 'EUIPO Trademark Filing' },
short: { de: 'EU-weiter Markenschutz beantragt.', en: 'EU-wide trademark protection filed.' },
body: {
de: 'Markenanmeldung BreakPilot beim EUIPO (Amt der Europäischen Union für geistiges Eigentum) am 01.05.2026 für EU-weiten Markenschutz.',
en: 'BreakPilot trademark filing with EUIPO (European Union Intellectual Property Office) on 01.05.2026 for EU-wide trademark protection.',
},
bullets: {
de: ['EUIPO-Anmeldung 01.05.2026', 'EU-weiter Markenschutz'],
en: ['EUIPO filing 01.05.2026', 'EU-wide trademark protection'],
},
tint: '#fbbf24', done: false, next: true,
},
{
id: 'gmbh',
when: 'Aug. 2026', tick: '08 · 26',
title: { de: 'GmbH-Gründung', en: 'GmbH Incorporation' },
short: { de: 'Breakpilot COMPLAI GmbH gegründet.', en: 'Breakpilot COMPLAI GmbH incorporated.' },
body: {
de: 'Gründung der Breakpilot COMPLAI GmbH im August 2026. Notartermin, Handelsregistereintrag, operative Aufnahme.',
en: 'Incorporation of Breakpilot COMPLAI GmbH in August 2026. Notary appointment, commercial register entry, start of operations.',
},
bullets: {
de: ['GmbH-Gründung August 2026', 'Handelsregistereintrag', 'Operativer Start'],
en: ['GmbH incorporation August 2026', 'Commercial register entry', 'Start of operations'],
},
tint: '#fbbf24', done: false,
},
{
id: 'customers',
when: 'Aug. 2026', tick: '08 · 26',
title: { de: '2 zahlende Kunden', en: '2 Paying Customers' },
short: { de: 'Erste Umsätze ab Gründung.', en: 'First revenue from incorporation.' },
body: {
de: 'Zwei zahlende Kunden ab August 2026 — Validierung des Produkts im Maschinenbau-Umfeld mit echten Compliance-Anforderungen.',
en: 'Two paying customers from August 2026 — product validation in manufacturing with real compliance requirements.',
},
bullets: {
de: ['2 zahlende Kunden', 'Maschinenbau-Validierung', 'Erste Umsätze'],
en: ['2 paying customers', 'Manufacturing validation', 'First revenue'],
},
tint: '#fbbf24', done: false,
},
{
id: 'beta',
when: 'Q3 2026', tick: 'Q3 · 26',
title: { de: 'Öffentliches Beta', en: 'Public Beta' },
short: { de: 'Beta-Launch mit ersten zahlenden Kunden.', en: 'Beta launch with first paying customers.' },
body: {
de: 'Öffentliches Beta-Release der Plattform. Erste zahlende Kunden aus dem Pilotprogramm gehen live.',
en: 'Public beta release of the platform. First paying customers from the pilot program go live.',
},
bullets: {
de: ['Public Beta verfügbar', 'Onboarding-Prozess live', 'Feedback-Loop etabliert'],
en: ['Public beta available', 'Onboarding process live', 'Feedback loop established'],
},
tint: '#f59e0b', done: false,
},
]
interface StatItem { k: { de: string; en: string }; v: string; tint: string }
const STATS: StatItem[] = [
{ k: { de: 'Gesetze & Dokumente im RAG', en: 'Laws & Docs in RAG' }, v: '385', tint: '#a78bfa' },
{ k: { de: 'Atomare Controls', en: 'Atomic Controls' }, v: '25.000+', tint: '#c084fc' },
{ k: { de: 'Compliance-Module', en: 'Compliance Modules' }, v: '12', tint: '#fbbf24' },
{ k: { de: 'Pilotkunden', en: 'Pilot Customers' }, v: '2', tint: '#f59e0b' },
{ k: { de: 'Lines of Code', en: 'Lines of Code' }, v: '500.000+', tint: '#8b5cf6' },
]
// ── Star Field ────────────────────────────────────────────────────────────────
function StarField() {
const stars = useMemo(() => {
let s = 77
const r = () => { s = (s * 9301 + 49297) % 233280; return s / 233280 }
return Array.from({ length: 95 }, () => ({ x: r() * 100, y: r() * 100, size: r() * 1.4 + 0.3, op: r() * 0.5 + 0.15 }))
}, [])
return (
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
{stars.map((st, i) => (
<div key={i} style={{
position: 'absolute', left: `${st.x}%`, top: `${st.y}%`,
width: st.size, height: st.size, borderRadius: '50%',
background: '#fff', opacity: st.op,
boxShadow: `0 0 ${st.size * 3}px rgba(180,160,255,.7)`,
}} />
))}
</div>
)
}
function SoftGrid({ t }: { t: Theme }) {
return (
<div style={{
position: 'absolute', inset: 0, pointerEvents: 'none',
backgroundImage: `radial-gradient(${t.accent20} 1px, transparent 1px)`,
backgroundSize: '28px 28px',
maskImage: 'radial-gradient(ellipse at center, #000 40%, transparent 85%)',
WebkitMaskImage: 'radial-gradient(ellipse at center, #000 40%, transparent 85%)',
opacity: 0.8,
}} />
)
}
// ── Timeline ──────────────────────────────────────────────────────────────────
interface MilestoneWithPos extends Milestone { x: number; row: 'top' | 'bottom' }
function Timeline({ onSelect, selectedId, t, de }: {
onSelect: (m: Milestone) => void
selectedId: string | null
t: Theme
de: boolean
}) {
const trackW = 1160
const innerPad = 120
const usableW = trackW - innerPad * 2
const positions = MILESTONES.map((_, i) => innerPad + (usableW * i) / (MILESTONES.length - 1))
const todayX = innerPad + usableW * TODAY_POSITION
const layout: MilestoneWithPos[] = MILESTONES.map((m, i) => ({
...m, x: positions[i],
row: i % 2 === 0 ? 'top' : 'bottom',
}))
const railColor = t.key === 'dark' ? '#a78bfa' : '#7c3aed'
return (
<div style={{ position: 'relative', width: trackW, height: 360, margin: '0 auto' }}>
<svg viewBox={`0 0 ${trackW} 360`} preserveAspectRatio="none"
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<defs>
<linearGradient id="msTrackBg" x1="0" x2="1">
<stop offset="0" stopColor={railColor} stopOpacity={t.key === 'dark' ? .18 : .28} />
<stop offset=".5" stopColor={railColor} stopOpacity={t.key === 'dark' ? .28 : .38} />
<stop offset="1" stopColor={railColor} stopOpacity={t.key === 'dark' ? .18 : .28} />
</linearGradient>
<filter id="msGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="b"/>
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
</defs>
{/* rail background */}
<line x1={innerPad} y1={180} x2={trackW - innerPad} y2={180}
stroke="url(#msTrackBg)" strokeWidth="2.5" />
{/* past progress */}
<line x1={innerPad} y1={180} x2={todayX} y2={180}
stroke={t.done} strokeWidth="3" opacity={t.key === 'dark' ? .85 : .9} />
{/* future dashed */}
<line x1={todayX} y1={180} x2={trackW - innerPad} y2={180}
stroke="#f59e0b" strokeWidth="1.75" strokeDasharray="4 5"
opacity={t.key === 'dark' ? .6 : .75}
style={{ animation: 'msFlow 1.8s linear infinite' }} />
{/* connector stubs */}
{layout.map((m) => (
<line key={m.id}
x1={m.x} y1={180}
x2={m.x} y2={m.row === 'top' ? 154 : 200}
stroke={m.done ? t.done : m.tint}
strokeOpacity={t.key === 'dark' ? (m.done ? .6 : .55) : (m.done ? .7 : .65)}
strokeWidth="1"
strokeDasharray={m.done ? '0' : '3 3'} />
))}
{/* HEUTE marker — circles only; pill is HTML below */}
<g transform={`translate(${todayX} 180)`}>
<circle r="14" fill={t.accent} opacity=".15" />
<circle r="9" fill={t.accent} opacity=".4">
<animate attributeName="r" values="9;14;9" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values=".4;.05;.4" dur="2s" repeatCount="indefinite" />
</circle>
<circle r="6" fill={t.heuteCore} stroke={t.accent} strokeWidth="2" filter="url(#msGlow)" />
</g>
</svg>
{/* HEUTE pill — HTML so it sits above milestone cards */}
<div style={{
position: 'absolute',
left: todayX - 30, top: 146,
width: 60, height: 18,
borderRadius: 9,
background: t.heutePillBg,
border: `1px solid ${t.accent}99`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
zIndex: 10, pointerEvents: 'none',
...MONO, fontSize: 9.5, letterSpacing: 2.5, fontWeight: 700,
color: t.heuteText,
}}>HEUTE</div>
{layout.map((m) => (
<MilestoneNode key={m.id} m={m} t={t} de={de}
onClick={() => onSelect(m)}
active={selectedId === m.id} />
))}
</div>
)
}
function MilestoneNode({ m, onClick, active, t, de }: {
m: MilestoneWithPos; onClick: () => void; active: boolean; t: Theme; de: boolean
}) {
const [hover, setHover] = useState(false)
const lit = hover || active
const isTop = m.row === 'top'
const cardY = isTop ? 4 : 200
const nodeColor = m.done ? t.done : m.tint
const bgTopA = lit ? m.tint + t.cardTintTopH : m.tint + t.cardTintTop
const bgMidA = lit ? m.tint + t.cardTintMidH : m.tint + t.cardTintMid
const cardBg = `linear-gradient(180deg, ${bgTopA} 0%, ${bgMidA} 55%, ${t.cardBase}${lit ? t.cardBaseAH : t.cardBaseA})`
const badge = m.done ? (de ? 'erledigt' : 'done') : (m.next ? (de ? 'als nächstes' : 'next') : (de ? 'geplant' : 'plan'))
return (
<>
{/* dot */}
<div
onClick={onClick}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
position: 'absolute', left: m.x - 14, top: 180 - 14,
width: 28, height: 28, borderRadius: '50%',
background: m.done
? `radial-gradient(circle at 35% 30%, ${t.doneBright}, ${t.doneSolid} 60%, ${t.doneDeep})`
: `radial-gradient(circle at 35% 30%, ${m.tint}dd, ${m.tint}66 60%, ${t.dotTodoDeep})`,
border: `2px solid ${lit ? '#fff' : nodeColor}`,
boxShadow: lit
? `0 0 22px ${nodeColor}, 0 0 44px ${nodeColor}66, inset 0 1px 0 ${t.dotLitHi}`
: `0 0 10px ${nodeColor}88, inset 0 1px 0 ${t.dotSoftHi}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 11, fontWeight: 700,
cursor: 'pointer', zIndex: 5,
transition: 'all .25s',
transform: lit ? 'scale(1.15)' : 'scale(1)',
}}>
{m.done ? '✓' : (m.next ? '◉' : '○')}
</div>
{/* card */}
<div
onClick={onClick}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
position: 'absolute', left: m.x - 112, top: cardY,
width: 224, height: 150, padding: '12px 14px',
borderRadius: 12,
background: cardBg,
border: `1px solid ${lit ? m.tint : m.tint + '55'}`,
boxShadow: lit ? t.cardShadowLift(m.tint) : t.cardShadowSoft,
cursor: 'pointer', zIndex: 4,
transition: 'all .25s',
transform: lit ? `translateY(${isTop ? -2 : 2}px)` : 'translateY(0)',
display: 'flex', flexDirection: 'column', gap: 6,
backdropFilter: t.key === 'light' ? 'blur(6px)' : 'none',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{
...MONO, fontSize: 10, letterSpacing: 1.5, fontWeight: 700,
color: m.done ? t.done : m.tint, textTransform: 'uppercase' as const,
}}>{m.tick}</span>
<span style={{ flex: 1, height: 1, background: `${m.tint}44` }} />
<span style={{
...MONO, fontSize: 9, letterSpacing: 2, fontWeight: 700,
color: m.done ? t.done : m.tint, textTransform: 'uppercase' as const, opacity: .85,
}}>{badge}</span>
</div>
<div style={{ fontSize: 13, fontWeight: 700, color: t.fg, letterSpacing: -0.2, lineHeight: 1.25 }}>
{de ? m.title.de : m.title.en}
</div>
<div style={{ fontSize: 10.5, lineHeight: 1.45, color: lit ? t.fgSoft : t.fgMuted, transition: 'color .25s' }}>
{de ? m.short.de : m.short.en}
</div>
<div style={{
marginTop: 'auto',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
paddingTop: 6, borderTop: `1px dashed ${m.tint}44`,
}}>
<span style={{ fontSize: 10, color: t.fgFaint }}>{m.when}</span>
<span style={{
fontSize: 10, color: m.tint, fontWeight: 700,
opacity: lit ? 1 : 0.55,
transform: `translateX(${lit ? 0 : -4}px)`,
transition: 'all .25s',
}}>{de ? 'Details →' : 'Details →'}</span>
</div>
</div>
</>
)
}
// ── Stat Card ─────────────────────────────────────────────────────────────────
function StatCard({ item, t, de }: { item: StatItem; t: Theme; de: boolean }) {
const [hover, setHover] = useState(false)
const bgTop = hover ? item.tint + t.statTintTopH : item.tint + t.statTintTop
const bgMid = item.tint + t.statTintMid
return (
<div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
position: 'relative', padding: '14px 18px', borderRadius: 12,
background: `linear-gradient(180deg, ${bgTop} 0%, ${bgMid} 60%, ${t.cardBase}${t.cardBaseA})`,
border: `1px solid ${hover ? item.tint : item.tint + '55'}`,
boxShadow: hover ? t.statShadowLift(item.tint) : t.statShadowSoft,
transform: hover ? 'translateY(-3px)' : 'translateY(0)',
transition: 'all .25s',
overflow: 'hidden',
backdropFilter: t.key === 'light' ? 'blur(6px)' : 'none',
}}>
<div style={{
position: 'absolute', right: 10, top: 10, width: 6, height: 6,
borderRadius: '50%', background: item.tint, opacity: .9,
boxShadow: `0 0 10px ${item.tint}`,
}} />
<div style={{ ...MONO, fontSize: 9.5, letterSpacing: 2, color: item.tint, textTransform: 'uppercase' as const, fontWeight: 700, marginBottom: 6 }}>
{de ? item.k.de : item.k.en}
</div>
<div style={{ fontSize: 32, fontWeight: 700, color: t.fg, letterSpacing: -0.8, lineHeight: 1 }}>
{item.v}
</div>
<svg viewBox="0 0 100 16" preserveAspectRatio="none"
style={{ width: '100%', height: 14, marginTop: 8, opacity: hover ? 1 : t.sparkOp, transition: 'opacity .25s' }}>
<defs>
<linearGradient id={`spark-${item.tint.replace('#', '')}`} x1="0" x2="1">
<stop offset="0" stopColor={item.tint} stopOpacity="0" />
<stop offset=".5" stopColor={item.tint} stopOpacity=".9" />
<stop offset="1" stopColor={item.tint} stopOpacity="0" />
</linearGradient>
</defs>
<path d="M 0 10 L 15 8 L 30 11 L 48 6 L 62 9 L 78 4 L 100 2"
stroke={`url(#spark-${item.tint.replace('#', '')})`} strokeWidth="1.5" fill="none" />
</svg>
</div>
)
}
// ── Detail modal ──────────────────────────────────────────────────────────────
function DetailModal({ item, onClose, t, de }: {
item: Milestone | null; onClose: () => void; t: Theme; de: boolean
}) {
useEffect(() => {
if (!item) return
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [item, onClose])
if (!item) return null
const tint = item.tint
const badge = item.done
? (de ? 'ABGESCHLOSSEN' : 'COMPLETED')
: (item.next ? (de ? 'ALS NÄCHSTES' : 'NEXT UP') : (de ? 'GEPLANT' : 'PLANNED'))
const badgeColor = item.done ? t.done : tint
return (
<div onClick={onClose} style={{
position: 'absolute', inset: 0, zIndex: 50,
background: t.modalScrim, backdropFilter: 'blur(8px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
animation: 'msFadeIn .2s ease-out',
}}>
<div onClick={e => e.stopPropagation()} style={{
width: 580, maxWidth: '88%',
background: `linear-gradient(180deg, ${tint}22 0%, ${t.modalBgMid} 50%, ${t.modalBgLow} 100%)`,
border: `1px solid ${tint}77`,
borderRadius: 16,
boxShadow: t.modalShadow(tint),
padding: '24px 28px', color: t.fg,
animation: 'msScaleIn .22s ease-out',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<div style={{
width: 42, height: 42, borderRadius: 11,
background: `linear-gradient(135deg, ${tint}66, ${tint}22)`,
border: `1px solid ${tint}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: t.key === 'light' ? tint : '#fff', fontSize: 17, fontWeight: 700,
boxShadow: `0 0 20px ${tint}66`,
}}>{item.done ? '✓' : (item.next ? '◉' : '○')}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
<span style={{
...MONO, fontSize: 9.5, letterSpacing: 2.5, color: badgeColor,
textTransform: 'uppercase' as const, fontWeight: 700,
padding: '2px 8px', borderRadius: 4,
background: `${badgeColor}22`, border: `1px solid ${badgeColor}66`,
}}>{badge}</span>
<span style={{ ...MONO, fontSize: 10, color: t.fgFaint }}>{item.when}</span>
</div>
<div style={{ fontSize: 20, fontWeight: 700, color: t.fg, letterSpacing: -0.3 }}>
{de ? item.title.de : item.title.en}
</div>
</div>
<button onClick={onClose} style={{
background: 'transparent', border: `1px solid ${tint}66`, color: t.fg,
width: 32, height: 32, borderRadius: 8, cursor: 'pointer', fontSize: 14,
}}></button>
</div>
<div style={{ fontSize: 13, lineHeight: 1.6, color: t.fgSoft, marginBottom: 16 }}>
{de ? item.body.de : item.body.en}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{(de ? item.bullets.de : item.bullets.en).map((b, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'flex-start', gap: 10,
padding: '9px 13px', borderRadius: 8,
background: t.bulletBg, border: `1px solid ${tint}44`,
}}>
<span style={{ color: item.done ? t.done : tint, fontSize: 12, marginTop: 1 }}>
{item.done ? '✓' : '▸'}
</span>
<span style={{ fontSize: 12, lineHeight: 1.5, color: t.fgSoft }}>{b}</span>
</div>
))}
</div>
</div>
</div>
)
}
// ── Inner slide (fixed 1280×680) ──────────────────────────────────────────────
function MilestonesInner({ t, de, sel, setSel }: {
t: Theme; de: boolean
sel: Milestone | null
setSel: (m: Milestone | null) => void
}) {
const doneCnt = useMemo(() => MILESTONES.filter(m => m.done).length, [])
const total = MILESTONES.length
return (
<div style={{
position: 'relative', width: 1280, height: 600, overflow: 'hidden',
background: t.bg, color: t.fg,
fontFamily: '"Inter", system-ui, sans-serif', WebkitFontSmoothing: 'antialiased',
}}>
{/* Ambient glow */}
<div style={{
position: 'absolute', top: -120, left: '50%', transform: 'translateX(-50%)',
width: 800, height: 500, borderRadius: '50%',
background: t.ambient, filter: 'blur(50px)', pointerEvents: 'none',
}} />
{t.stars ? <StarField /> : <SoftGrid t={t} />}
{/* Progress indicator */}
<div style={{
position: 'absolute', top: 36, right: 52, display: 'flex', alignItems: 'center', gap: 10, zIndex: 3,
}}>
<div style={{ ...MONO, fontSize: 10, letterSpacing: 2, color: t.fgMuted, textTransform: 'uppercase' as const, fontWeight: 700 }}>
{de ? 'Fortschritt' : 'Progress'}
</div>
<div style={{
width: 120, height: 6, background: t.progressTrackBg, borderRadius: 3, overflow: 'hidden',
border: `1px solid ${t.progressTrackBorder}`,
}}>
<div style={{
width: `${(doneCnt / total) * 100}%`, height: '100%',
background: `linear-gradient(90deg, ${t.done}, ${t.accent})`,
boxShadow: `0 0 12px ${t.done}99`,
}} />
</div>
<div style={{ ...MONO, fontSize: 11, color: t.fg, fontWeight: 700 }}>
<span style={{ color: t.done }}>{doneCnt}</span>
<span style={{ color: t.fgWhisper }}> / {total}</span>
</div>
</div>
{/* Tip */}
<div style={{
position: 'absolute', top: 36, left: 52, ...MONO, fontSize: 10,
letterSpacing: 2, color: t.fgGhost, textTransform: 'uppercase' as const, fontWeight: 700,
display: 'flex', alignItems: 'center', gap: 8, zIndex: 3,
}}>
<span>{de ? 'Tipp:' : 'Tip:'}</span>
<span style={{ color: t.accent70 }}>{de ? 'Klick auf einen Meilenstein' : 'Click any milestone'}</span>
</div>
{/* Timeline */}
<div style={{ position: 'relative', marginTop: 68 }}>
<Timeline onSelect={setSel} selectedId={sel?.id ?? null} t={t} de={de} />
</div>
{/* Stats */}
<div style={{
position: 'absolute', left: 40, right: 40, bottom: 36,
display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 14,
}}>
{STATS.map(s => <StatCard key={s.tint} item={s} t={t} de={de} />)}
</div>
{/* Footer */}
<div style={{
position: 'absolute', left: 0, right: 0, bottom: 14, textAlign: 'center',
...MONO, fontSize: 9, letterSpacing: 3, color: t.accent40,
textTransform: 'uppercase' as const, fontWeight: 700,
}}>
{de ? 'Stand heute · live-Metriken aus der Plattform' : 'As of today · live metrics from the platform'}
</div>
<DetailModal item={sel} onClose={() => setSel(null)} t={t} de={de} />
</div>
)
}
// ── Main slide ────────────────────────────────────────────────────────────────
const INNER_W = 1280
const INNER_H = 600
export default function MilestonesSlide({ lang }: MilestonesSlideProps) {
const de = lang === 'de'
const isLight = useIsLight()
const t = isLight ? THEMES.light : THEMES.dark
const [sel, setSel] = useState<Milestone | null>(null)
const [scale, setScale] = useState(1)
const containerRef = useRef<HTMLDivElement>(null)
const calcScale = useCallback(() => {
if (containerRef.current) {
const w = containerRef.current.offsetWidth
setScale(Math.min(w / INNER_W, 1))
}
}, [])
useEffect(() => {
calcScale()
const obs = new ResizeObserver(calcScale)
if (containerRef.current) obs.observe(containerRef.current)
return () => obs.disconnect()
}, [calcScale])
return (
<div>
<style>{CSS_KF}</style>
<FadeInView className="text-center mb-1">
<h2 className="text-5xl md:text-6xl font-bold mb-1">
<GradientText>{de ? 'Meilensteine' : 'Milestones'}</GradientText>
</h2>
<p className="text-lg text-white/50 max-w-2xl mx-auto">
{de ? 'Von der Idee zur GmbH — was wir bereits erreicht haben' : 'From idea to GmbH — what we have already achieved'}
</p>
</FadeInView>
<FadeInView delay={0.1}>
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: INNER_H * scale,
overflow: 'hidden',
borderRadius: 16,
transform: 'scale(1.12)',
transformOrigin: 'top center',
marginBottom: -40,
}}
>
<div style={{
position: 'absolute', top: 0, left: 0,
width: INNER_W, height: INNER_H,
transform: `scale(${scale})`,
transformOrigin: 'top left',
}}>
<MilestonesInner t={t} de={de} sel={sel} setSel={setSel} />
</div>
</div>
</FadeInView>
</div>
)
}