pitch-deck: light mode support + MilestonesSlide redesign
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m12s
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 32s
CI / test-python-voice (push) Successful in 28s
CI / test-bqas (push) Successful in 32s

- ArchitectureSlide: full light mode via useIsLight() hook, all inline styles adapt
- USPSlide: full light mode via useIsLight() hook, all inline styles adapt
- MilestonesSlide: new component — horizontal timeline with past/HEUTE/future,
  THEMES object (dark + light), clickable milestone nodes and stat cards with
  detail modal, bilingual (de/en), scaling via ResizeObserver
- PitchDeck: register new 'milestones' slide case

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-21 10:34:36 +02:00
parent 8a4e196864
commit c0c44adaaa
4 changed files with 1138 additions and 239 deletions

View File

@@ -0,0 +1,814 @@
'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: 'start',
when: 'Mär. 2025', tick: '03 · 25',
title: { de: 'Idee & Team-Start', en: 'Idea & Team Start' },
short: { de: 'Gründerteam formiert sich, erste Konzeption.', en: 'Founding team forms, first product concept.' },
body: {
de: 'Zwei Gründer, ein klares Problem: Compliance-Doks und Code leben in getrennten Welten. Start der Konzeption für eine Plattform, die beide Welten verbindet.',
en: 'Two founders, one clear problem: compliance docs and code live in separate worlds. Started designing a platform that bridges both.',
},
bullets: {
de: ['Team von 2 Gründern', 'Markt-Research DACH + EU', 'Erste Architektur-Skizze'],
en: ['Team of 2 founders', 'Market research DACH + EU', 'First architecture sketch'],
},
tint: '#a78bfa', done: true,
},
{
id: 'ihk',
when: 'Okt. 2025', tick: '10 · 25',
title: { de: 'IHK & Agentur für Arbeit', en: 'IHK & Employment Agency' },
short: { de: 'Gründerzuschuss beantragt & gesichert.', en: 'Founder grant applied for & secured.' },
body: {
de: 'Information und Austausch mit Agentur für Arbeit und IHK Konstanz für den Gründerzuschuss — seit Oktober 2025 in Bearbeitung und Aufbau.',
en: 'Collaboration with Employment Agency and IHK Konstanz for the founder grant — in processing since October 2025.',
},
bullets: {
de: ['Gründerzuschuss genehmigt', 'Mentorship-Programm IHK', 'Erste öffentliche Sichtbarkeit'],
en: ['Founder grant approved', 'IHK mentorship program', 'First public visibility'],
},
tint: '#a78bfa', done: true,
},
{
id: 'proto',
when: 'Dez. 2025', tick: '12 · 25',
title: { de: 'Prototyp Compliance SDK', en: 'Compliance SDK Prototype' },
short: { de: 'Compliance SDK & Security Cloud laufen.', en: 'Compliance SDK & Security Cloud running.' },
body: {
de: 'Entwicklung eines funktionsfähigen Prototypen der Compliance SDK und der Security-Cloud-Lösung — erste End-to-End-Demo läuft seit Dezember 2025.',
en: 'Built a working prototype of the Compliance SDK and Security Cloud solution — first end-to-end demo running since December 2025.',
},
bullets: {
de: ['SDK: policy → code mapping', 'Security Cloud MVP', 'Interne Demo-Audits erfolgreich'],
en: ['SDK: policy → code mapping', 'Security Cloud MVP', 'Internal demo audits successful'],
},
tint: '#c084fc', done: true,
},
{
id: 'pilot',
when: 'Dez. 2025', tick: '12 · 25',
title: { de: '2 Pilotkunden im Gespräch', en: '2 Pilot Customers in Talks' },
short: { de: 'Schwarzwald + Mobilitäts-Sektor.', en: 'Black Forest + Mobility Sector.' },
body: {
de: 'Kommunikation seit Dezember 2025 mit Kunden aus dem Schwarzwald (CE-Software-Risikobeurteilung) und einem globalen Maschinen- und Anlagenbauer aus dem Mobilitätssektor (KI-Roadmap).',
en: 'Since December 2025 in talks with a Black Forest CE-software customer (risk assessment) and a global mobility-sector machine builder (AI roadmap).',
},
bullets: {
de: ['CE-Software-Risikobeurteilung', 'KI-Roadmap für Mobilitätssektor', 'LOIs in Vorbereitung'],
en: ['CE software risk assessment', 'AI roadmap for mobility sector', 'LOIs in preparation'],
},
tint: '#c084fc', done: true,
},
{
id: 'reg',
when: '27. Mär. 2026', tick: '03 · 26',
title: { de: 'Eintragung GmbH', en: 'GmbH Registration' },
short: { de: 'Offizielle Gründung im Handelsregister.', en: 'Official incorporation in commercial register.' },
body: {
de: 'Notartermin und Eintragung ins Handelsregister am 27.03.2026. Ab diesem Datum voll operative GmbH mit klaren Governance-Strukturen.',
en: 'Notary appointment and commercial register entry on 27.03.2026. Fully operative GmbH with clear governance structures from this date.',
},
bullets: {
de: ['Gesellschaftsvertrag unterzeichnet', 'HRB-Eintrag Konstanz', 'Erste Rechnung ausgestellt'],
en: ['Articles of association signed', 'HRB entry Constance', 'First invoice issued'],
},
tint: '#fbbf24', done: false, next: true,
},
{
id: 'seed',
when: 'Q2 2026', tick: 'Q2 · 26',
title: { de: 'Seed-Runde', en: 'Seed Round' },
short: { de: '1,5 Mio € für 18 Monate Runway.', en: '€1.5M for 18 months runway.' },
body: {
de: 'Pre-Seed / Seed-Runde zur Finanzierung des ersten Kundensegments, Ausbau des Teams und Zertifizierung (ISO 27001, BSI C5).',
en: 'Pre-Seed / Seed round to fund first customer segment, team growth and certification (ISO 27001, BSI C5).',
},
bullets: {
de: ['Ziel: 1,5 Mio € Seed', 'Ausbau auf 8 FTE', 'Zertifizierungs-Track startet'],
en: ['Target: €1.5M seed', 'Scale to 8 FTE', 'Certification track starts'],
},
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. Integration in Gitlab + GitHub Cloud.',
en: 'Public beta release of the platform. First paying customers from the pilot program go live. GitLab + GitHub Cloud integration.',
},
bullets: {
de: ['35 zahlende Pilot-Kunden', 'Public Beta verfügbar', 'Git-Integration live'],
en: ['35 paying pilot customers', 'Public beta available', 'Git integration live'],
},
tint: '#f59e0b', done: false,
},
{
id: 'v1',
when: 'Q4 2026', tick: 'Q4 · 26',
title: { de: 'EU Trust Stack v1.0', en: 'EU Trust Stack v1.0' },
short: { de: 'DSGVO · NIS-2 · DORA · EU AI Act.', en: 'GDPR · NIS-2 · DORA · EU AI Act.' },
body: {
de: 'Alle vier zentralen EU-Frameworks voll abgedeckt. EU-souveränes Hosting, vollständige Audit-Trail-Unterstützung, Zertifizierung ISO 27001 abgeschlossen.',
en: 'All four central EU frameworks fully covered. EU-sovereign hosting, complete audit trail support, ISO 27001 certification completed.',
},
bullets: {
de: ['4 EU-Frameworks live', 'EU-souveränes Hosting', 'ISO 27001 zertifiziert'],
en: ['4 EU frameworks live', 'EU-sovereign hosting', 'ISO 27001 certified'],
},
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 */}
<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)" />
<rect x="-30" y="-34" width="60" height="16" rx="8"
fill={t.heutePillBg} stroke={t.accent} strokeOpacity=".6" strokeWidth="1" />
<text y="-22" textAnchor="middle" fill={t.heuteText}
style={{ ...MONO, fontSize: 9.5, letterSpacing: 2.5, fontWeight: 700 }}>
HEUTE
</text>
</g>
</svg>
{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: 680, 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>
{/* Heading */}
<div style={{ position: 'relative', padding: '22px 60px 0', textAlign: 'center', zIndex: 2 }}>
<div style={{
...MONO, fontSize: 10, letterSpacing: 4, color: t.accent70,
textTransform: 'uppercase' as const, fontWeight: 700, marginBottom: 6,
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10,
}}>
<span style={{ width: 60, height: 1, background: `linear-gradient(90deg, transparent, ${t.accent50})` }} />
{de ? 'Roadmap' : 'Roadmap'}
<span style={{ width: 60, height: 1, background: `linear-gradient(270deg, transparent, ${t.accent50})` }} />
</div>
<h1 style={{
fontSize: 40, fontWeight: 700, letterSpacing: -1, margin: 0, lineHeight: 1.1,
background: t.headingGrad,
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text',
animation: t.headingAnim,
}}>
{de ? 'Meilensteine' : 'Milestones'}
</h1>
<div style={{ fontSize: 14, color: t.fgMuted, marginTop: 6 }}>
{de
? 'Was wir bereits erreicht haben — und was als Nächstes kommt'
: 'What we\'ve already achieved — and what\'s coming next'}
</div>
</div>
{/* Timeline */}
<div style={{ position: 'relative', marginTop: 14 }}>
<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 = 680
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-4">
<h2 className="text-4xl md:text-5xl font-bold mb-2">
<GradientText>{de ? 'Meilensteine' : 'Milestones'}</GradientText>
</h2>
</FadeInView>
<FadeInView delay={0.1}>
<div
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: INNER_H * scale,
overflow: 'hidden',
borderRadius: 16,
}}
>
<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>
)
}