Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-core
Some checks failed
Build pitch-deck / build-push-deploy (push) Successful in 1m28s
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 41s
CI / test-python-voice (push) Successful in 34s
CI / test-bqas (push) Has been cancelled
Some checks failed
Build pitch-deck / build-push-deploy (push) Successful in 1m28s
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 41s
CI / test-python-voice (push) Successful in 34s
CI / test-bqas (push) Has been cancelled
This commit is contained in:
@@ -78,6 +78,16 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Database query error:', error)
|
console.error('Database query error:', error)
|
||||||
|
// Return minimal stub in dev so the pitch renders without a DB connection
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return NextResponse.json({
|
||||||
|
company: { name: 'BreakPilot', tagline: '[dev mode — no DB]' },
|
||||||
|
team: [], financials: [], market: [], competitors: [],
|
||||||
|
features: [], milestones: [], metrics: [],
|
||||||
|
funding: { instrument: 'Wandeldarlehen', amount: 500000, valuation_cap: 3000000, currency: 'EUR' },
|
||||||
|
products: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
return NextResponse.json({ error: 'Failed to load pitch data' }, { status: 500 })
|
return NextResponse.json({ error: 'Failed to load pitch data' }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap');
|
|
||||||
|
|
||||||
/* === Dark Mode (default) === */
|
/* === Dark Mode (default) === */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0a0a1a;
|
--bg-primary: #0a0a1a;
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import ProductSlide from './slides/ProductSlide'
|
|||||||
import HowItWorksSlide from './slides/HowItWorksSlide'
|
import HowItWorksSlide from './slides/HowItWorksSlide'
|
||||||
import MarketSlide from './slides/MarketSlide'
|
import MarketSlide from './slides/MarketSlide'
|
||||||
import BusinessModelSlide from './slides/BusinessModelSlide'
|
import BusinessModelSlide from './slides/BusinessModelSlide'
|
||||||
import TractionSlide from './slides/TractionSlide'
|
|
||||||
import CompetitionSlide from './slides/CompetitionSlide'
|
import CompetitionSlide from './slides/CompetitionSlide'
|
||||||
import TeamSlide from './slides/TeamSlide'
|
import TeamSlide from './slides/TeamSlide'
|
||||||
import FinancialsSlide from './slides/FinancialsSlide'
|
import FinancialsSlide from './slides/FinancialsSlide'
|
||||||
@@ -52,6 +51,7 @@ import StrategySlide from './slides/StrategySlide'
|
|||||||
import FinanzplanSlide from './slides/FinanzplanSlide'
|
import FinanzplanSlide from './slides/FinanzplanSlide'
|
||||||
import GlossarySlide from './slides/GlossarySlide'
|
import GlossarySlide from './slides/GlossarySlide'
|
||||||
import RiskSlide from './slides/RiskSlide'
|
import RiskSlide from './slides/RiskSlide'
|
||||||
|
import MilestonesSlide from './slides/MilestonesSlide'
|
||||||
|
|
||||||
interface PitchDeckProps {
|
interface PitchDeckProps {
|
||||||
lang: Language
|
lang: Language
|
||||||
@@ -181,7 +181,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
case 'business-model':
|
case 'business-model':
|
||||||
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
||||||
case 'traction':
|
case 'traction':
|
||||||
return <TractionSlide lang={lang} milestones={data.milestones} metrics={data.metrics} />
|
return <MilestonesSlide lang={lang} />
|
||||||
case 'competition':
|
case 'competition':
|
||||||
return <CompetitionSlide lang={lang} features={data.features} competitors={data.competitors} />
|
return <CompetitionSlide lang={lang} features={data.features} competitors={data.competitors} />
|
||||||
case 'team':
|
case 'team':
|
||||||
|
|||||||
@@ -1,129 +1,733 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef, Fragment } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { Language } from '@/lib/types'
|
import { Language } from '@/lib/types'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import GradientText from '../ui/GradientText'
|
import GradientText from '../ui/GradientText'
|
||||||
import FadeInView from '../ui/FadeInView'
|
import FadeInView from '../ui/FadeInView'
|
||||||
import GlassCard from '../ui/GlassCard'
|
import {
|
||||||
import { Server, Cpu, Shield, Database, Globe, Lock, Layers, Workflow } from 'lucide-react'
|
Brain, Shield, ScanLine, Zap, Cpu,
|
||||||
|
Layers, Wrench, X, Users, Lock,
|
||||||
|
Server, BadgeCheck,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
interface ArchitectureSlideProps {
|
interface ArchitectureSlideProps { lang: Language }
|
||||||
lang: Language
|
type NodeId = 'certifai' | 'complai' | 'scanner' | 'litellm' | 'llm' | 'embeddings' | 'tools'
|
||||||
|
|
||||||
|
interface NodeDef {
|
||||||
|
id: NodeId
|
||||||
|
icon: React.ElementType
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
color: string
|
||||||
|
tech: string[]
|
||||||
|
services: { name: string; desc: string }[]
|
||||||
|
primary?: boolean
|
||||||
|
tier: 'product' | 'proxy' | 'inference'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodes(de: boolean): NodeDef[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'certifai', icon: Brain,
|
||||||
|
title: 'CERTifAI',
|
||||||
|
subtitle: de ? 'GenAI Mandantenportal' : 'GenAI Tenant Portal',
|
||||||
|
color: '#c084fc', tier: 'product',
|
||||||
|
tech: ['Rust', 'Dioxus', 'MongoDB', 'Keycloak', 'SearXNG', 'LangGraph'],
|
||||||
|
services: [
|
||||||
|
{ name: 'LiteLLM Dashboard', desc: de ? 'Modellverwaltung & Kostentracking' : 'Model mgmt & cost tracking' },
|
||||||
|
{ name: 'LibreChat + SSO', desc: de ? 'Mandanten-Chat mit Keycloak' : 'Tenant chat with Keycloak' },
|
||||||
|
{ name: 'LangGraph Agents', desc: de ? 'Agent-Orchestrierung' : 'Agent orchestration' },
|
||||||
|
{ name: 'MCP Hub', desc: de ? 'Tool-Integration für KI-Clients' : 'Tool integration for AI clients' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'complai', icon: Shield,
|
||||||
|
title: 'COMPLAI',
|
||||||
|
subtitle: de ? 'Compliance & Audit' : 'Compliance & Audit',
|
||||||
|
color: '#818cf8', tier: 'product',
|
||||||
|
tech: ['Next.js 15', 'FastAPI', 'Go/Gin', 'PostgreSQL', 'Qdrant', 'Valkey'],
|
||||||
|
services: [
|
||||||
|
{ name: de ? 'DSGVO / AI Act / NIS2' : 'GDPR / AI Act / NIS2', desc: de ? '70k+ auditierbare Controls' : '70k+ auditable controls' },
|
||||||
|
{ name: 'RAG Pipeline', desc: de ? '75+ Rechtsquellen, semantische Suche' : '75+ legal sources, semantic search' },
|
||||||
|
{ name: 'Control Pipeline', desc: de ? 'Gesetzestextanalyse via LLM' : 'Legal text analysis via LLM' },
|
||||||
|
{ name: 'MCP Client', desc: de ? 'Echtzeit-Findings vom Scanner' : 'Real-time findings from Scanner' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scanner', icon: ScanLine,
|
||||||
|
title: 'Compliance Scanner',
|
||||||
|
subtitle: de ? 'Code-Sicherheit' : 'Code Security',
|
||||||
|
color: '#34d399', tier: 'product',
|
||||||
|
tech: ['Rust', 'Axum', 'MongoDB', 'Semgrep', 'Gitleaks', 'Syft'],
|
||||||
|
services: [
|
||||||
|
{ name: 'SAST / SBOM / CVE', desc: de ? 'Vollautomatische Pipeline' : 'Fully automated pipeline' },
|
||||||
|
{ name: de ? 'KI-Triage' : 'AI Triage', desc: de ? 'LLM filtert False Positives' : 'LLM filters false positives' },
|
||||||
|
{ name: de ? 'KI-Pentest' : 'AI Pentest', desc: de ? 'Autonome Angriffsketten' : 'Autonomous attack chains' },
|
||||||
|
{ name: 'MCP Server', desc: de ? 'Live-Findings für COMPLAI' : 'Live findings for COMPLAI' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'litellm', icon: Zap,
|
||||||
|
title: 'LiteLLM Proxy',
|
||||||
|
subtitle: de ? 'KI-Gateway & Guardrails' : 'AI Gateway & Guardrails',
|
||||||
|
color: '#fbbf24', tier: 'proxy', primary: true,
|
||||||
|
tech: ['OpenAI-kompatible API', 'Bearer Auth', 'Rate Limiting', 'PII-Filter', 'Spend Tracking'],
|
||||||
|
services: [
|
||||||
|
{ name: de ? 'Token-Budget' : 'Token Budget', desc: de ? 'Pro-Mandant Kontingente & Abrechnung' : 'Per-tenant quotas & billing' },
|
||||||
|
{ name: 'PII Guardrails', desc: de ? 'Datenschutz-Filter für alle Anfragen' : 'Privacy filter on all requests' },
|
||||||
|
{ name: de ? 'Web-Suche (anonym)' : 'Web Search (anon)', desc: de ? 'SearXNG-Proxy, kein US-Anbieter' : 'SearXNG proxy, no US providers' },
|
||||||
|
{ name: de ? 'Namespace-Isolierung' : 'Namespace Isolation', desc: de ? 'Mandantentrennung per API-Key' : 'Tenant isolation per API key' },
|
||||||
|
{ name: de ? 'Failover-Routing' : 'Failover Routing', desc: de ? 'Automatisches Fallback' : 'Automatic fallback between models' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'llm', icon: Cpu,
|
||||||
|
title: de ? 'LLM Inferenz' : 'LLM Inference',
|
||||||
|
subtitle: de ? 'Lokale Sprachmodelle' : 'Local Language Models',
|
||||||
|
color: '#60a5fa', tier: 'inference',
|
||||||
|
tech: ['Qwen3-32B', 'Qwen3-Coder-30B', 'DeepSeek-R1-8B', 'Ollama'],
|
||||||
|
services: [
|
||||||
|
{ name: de ? 'Vollständig lokal' : 'Fully local', desc: de ? 'Daten verlassen nie den Server' : 'Data never leaves the server' },
|
||||||
|
{ name: de ? 'Air-Gap fähig' : 'Air-Gap Capable', desc: de ? 'Kein Internet erforderlich' : 'No internet required' },
|
||||||
|
{ name: de ? 'GPU-optimiert' : 'GPU-optimized', desc: de ? 'Dedizierte Inferenz-Hardware' : 'Dedicated inference hardware' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'embeddings', icon: Layers,
|
||||||
|
title: 'Embeddings',
|
||||||
|
subtitle: de ? 'Semantische Suche' : 'Semantic Search',
|
||||||
|
color: '#a78bfa', tier: 'inference',
|
||||||
|
tech: ['bge-m3', 'Qdrant Vector DB', 'Sentence-Transformers'],
|
||||||
|
services: [
|
||||||
|
{ name: 'RAG Pipeline', desc: de ? '75+ Rechtsquellen indexiert' : '75+ legal sources indexed' },
|
||||||
|
{ name: de ? 'Semantische Suche' : 'Semantic Search', desc: de ? 'Multi-linguale Einbettungen' : 'Multi-lingual embeddings' },
|
||||||
|
{ name: de ? 'Lokal' : 'Fully local', desc: de ? 'Keine externen APIs' : 'No external APIs' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tools', icon: Wrench,
|
||||||
|
title: de ? 'KI-Tools' : 'AI Tools',
|
||||||
|
subtitle: de ? 'Web-Suche & MCP' : 'Web Search & MCP',
|
||||||
|
color: '#2dd4bf', tier: 'inference',
|
||||||
|
tech: ['SearXNG', 'MCP Protocol', 'Semgrep API', 'Gitleaks API'],
|
||||||
|
services: [
|
||||||
|
{ name: 'SearXNG', desc: de ? 'Anonymisierte EU-Websuche' : 'Anonymized EU web search' },
|
||||||
|
{ name: 'MCP Tools', desc: de ? 'Auditdokumente & Code-Findings' : 'Audit docs & code findings' },
|
||||||
|
{ name: de ? 'Kein US-Anbieter' : 'No US providers', desc: de ? '100% DSGVO-konform' : '100% GDPR-compliant' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAYERS: { id: string; nodeIds: NodeId[]; tint: string; depth: number }[] = [
|
||||||
|
{ id: 'product', nodeIds: ['certifai', 'complai', 'scanner'], tint: '#a78bfa', depth: 24 },
|
||||||
|
{ id: 'proxy', nodeIds: ['litellm'], tint: '#fbbf24', depth: 12 },
|
||||||
|
{ id: 'inference', nodeIds: ['llm', 'embeddings', 'tools'], tint: '#8b5cf6', depth: 0 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const CSS_KF = `
|
||||||
|
@keyframes v4FlowDown { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -18px } }
|
||||||
|
@keyframes v4Pulse { 0%,100% { opacity:1;transform:scale(1) } 50% { opacity:.4;transform:scale(1.4) } }
|
||||||
|
@keyframes v4Caret { 0%,50% { opacity:1 } 51%,100% { opacity:0 } }
|
||||||
|
@keyframes v4DotFall {
|
||||||
|
0% { transform: translateY(-5px); opacity: 0; }
|
||||||
|
12% { opacity: 1; }
|
||||||
|
88% { opacity: 1; }
|
||||||
|
100% { transform: translateY(38px); opacity: 0; }
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MONO: React.CSSProperties = {
|
||||||
|
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
|
||||||
|
fontVariantNumeric: 'tabular-nums',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Theme detection ───────────────────────────────────────────────────────────
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ticker primitives ─────────────────────────────────────────────────────────
|
||||||
|
function useTicker(fn: () => void, min = 140, max = 360, skipChance = 0.1) {
|
||||||
|
const ref = useRef(fn)
|
||||||
|
ref.current = fn
|
||||||
|
useEffect(() => {
|
||||||
|
let tid: ReturnType<typeof setTimeout>
|
||||||
|
const loop = () => {
|
||||||
|
if (Math.random() > skipChance) ref.current()
|
||||||
|
tid = setTimeout(loop, min + Math.random() * (max - min))
|
||||||
|
}
|
||||||
|
loop()
|
||||||
|
return () => clearTimeout(tid)
|
||||||
|
}, [min, max, skipChance])
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickerShell({ color, children, isLight }: { color: string; children: React.ReactNode; isLight: boolean }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
...MONO,
|
||||||
|
marginTop: 7, padding: '5px 9px',
|
||||||
|
background: isLight ? '#f1f5f9' : 'rgba(0,0,0,.38)',
|
||||||
|
border: `1px solid ${color}${isLight ? '55' : '55'}`, borderRadius: 6,
|
||||||
|
fontSize: 10, color: isLight ? '#475569' : 'rgba(236,233,247,.88)',
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
whiteSpace: 'nowrap', overflow: 'hidden', height: 22,
|
||||||
|
}}>{children}</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Caret({ color }: { color: string }) {
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block', width: 5, height: 9, marginLeft: -2,
|
||||||
|
background: color, animation: 'v4Caret 1s step-end infinite',
|
||||||
|
}} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Per-node tickers ──────────────────────────────────────────────────────────
|
||||||
|
function TickCertifAI({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const [n, setN] = useState(8421)
|
||||||
|
const [hash, setHash] = useState('9f3a…e10b')
|
||||||
|
const pool = 'abcdef0123456789'
|
||||||
|
const r = (k: number) => Array.from({ length: k }, () => pool[Math.floor(Math.random() * pool.length)]).join('')
|
||||||
|
useTicker(() => { setN(v => v + 1); setHash(`${r(4)}…${r(4)}`) }, 1000, 2000, 0.1)
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>✓</span>
|
||||||
|
<span style={{ color, opacity: .85 }}>sig</span>
|
||||||
|
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{n.toLocaleString()}</span>
|
||||||
|
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.55)' }}>{hash}</span>
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickComplAI({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const [evals, setEvals] = useState(1284)
|
||||||
|
const [rate, setRate] = useState(99.2)
|
||||||
|
useTicker(() => {
|
||||||
|
setEvals(v => v + 1 + Math.floor(Math.random() * 3))
|
||||||
|
setRate(r => Math.max(97, Math.min(99.9, r + (Math.random() - 0.5) * 0.4)))
|
||||||
|
}, 200, 500, 0.1)
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||||
|
<span style={{ color, opacity: .85 }}>eval</span>
|
||||||
|
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{evals.toLocaleString()}</span>
|
||||||
|
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.45)' }}>pass</span>
|
||||||
|
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>{rate.toFixed(1)}%</span>
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickScanner({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const lines = [
|
||||||
|
{ k: 'PASS', c: '#16a34a', cd: '#4ade80', t: 'CWE-79 xss check' },
|
||||||
|
{ k: 'WARN', c: '#d97706', cd: '#fbbf24', t: 'drift: model v2.1→2.2' },
|
||||||
|
{ k: 'PASS', c: '#16a34a', cd: '#4ade80', t: 'bias: demographic parity' },
|
||||||
|
{ k: 'FAIL', c: '#dc2626', cd: '#f87171', t: 'license: GPL-3 detected' },
|
||||||
|
{ k: 'PASS', c: '#16a34a', cd: '#4ade80', t: 'prompt-inject: 214 vectors' },
|
||||||
|
{ k: 'SCAN', c: '#7c3aed', cd: '#a78bfa', t: 'artifact model-card.json' },
|
||||||
|
]
|
||||||
|
const [i, setI] = useState(0)
|
||||||
|
useTicker(() => setI(x => (x + 1) % lines.length), 700, 1200, 0.05)
|
||||||
|
const l = lines[i]
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: isLight ? l.c : l.cd, fontWeight: 600, minWidth: 30 }}>{l.k}</span>
|
||||||
|
<span style={{ color: isLight ? '#334155' : 'rgba(236,233,247,.85)', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{l.t}</span>
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickLiteLLM({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const [rps, setRps] = useState(428)
|
||||||
|
const [p50, setP50] = useState(84)
|
||||||
|
useTicker(() => {
|
||||||
|
setRps(v => Math.max(200, Math.min(800, v + (Math.random() - 0.5) * 60)))
|
||||||
|
setP50(v => Math.max(40, Math.min(160, v + (Math.random() - 0.5) * 20)))
|
||||||
|
}, 250, 500, 0.05)
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: '#d97706' }}>⚡</span>
|
||||||
|
<span style={{ color, opacity: .9 }}>req/s</span>
|
||||||
|
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{Math.round(rps)}</span>
|
||||||
|
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.4)' }}>·</span>
|
||||||
|
<span style={{ color, opacity: .9 }}>p50</span>
|
||||||
|
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{Math.round(p50)}ms</span>
|
||||||
|
<Caret color={color} />
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickLLM({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const [tokens, setTokens] = useState(14832)
|
||||||
|
const [stream, setStream] = useState('t_a91f')
|
||||||
|
const pool = 'abcdef0123456789'
|
||||||
|
useTicker(() => {
|
||||||
|
setTokens(v => v + 1 + Math.floor(Math.random() * 5))
|
||||||
|
setStream('t_' + Array.from({ length: 4 }, () => pool[Math.floor(Math.random() * pool.length)]).join(''))
|
||||||
|
}, 120, 340, 0.15)
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||||
|
<span style={{ color, opacity: .85 }}>tok</span>
|
||||||
|
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{tokens.toLocaleString()}</span>
|
||||||
|
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.35)' }}>↑</span>
|
||||||
|
<span style={{ color }}>{stream}</span>
|
||||||
|
<Caret color={color} />
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickEmbeddings({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const [vecs, setVecs] = useState(284112)
|
||||||
|
useTicker(() => setVecs(v => v + 1 + Math.floor(Math.random() * 8)), 180, 420, 0.1)
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||||
|
<span style={{ color, opacity: .85 }}>idx</span>
|
||||||
|
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{vecs.toLocaleString()}</span>
|
||||||
|
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.4)' }}>· 1024d</span>
|
||||||
|
<Caret color={color} />
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TickTools({ color, isLight }: { color: string; isLight: boolean }) {
|
||||||
|
const ops = [
|
||||||
|
'search("BSI C5 controls")', 'fetch eur-lex.europa.eu',
|
||||||
|
'grep -r "DSGVO"', 'read docs/policy.md',
|
||||||
|
'mcp.call(filesystem)', 'search("vLLM 0.6 release")',
|
||||||
|
]
|
||||||
|
const [i, setI] = useState(0)
|
||||||
|
useTicker(() => setI(x => (x + 1) % ops.length), 900, 1600, 0.05)
|
||||||
|
return (
|
||||||
|
<TickerShell color={color} isLight={isLight}>
|
||||||
|
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||||
|
<span style={{ color, opacity: .85 }}>call</span>
|
||||||
|
<span style={{ color: isLight ? '#334155' : '#f5f3fc', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{ops[i]}</span>
|
||||||
|
</TickerShell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_TICKER: Record<NodeId, React.ComponentType<{ color: string; isLight: boolean }>> = {
|
||||||
|
certifai: TickCertifAI,
|
||||||
|
complai: TickComplAI,
|
||||||
|
scanner: TickScanner,
|
||||||
|
litellm: TickLiteLLM,
|
||||||
|
llm: TickLLM,
|
||||||
|
embeddings: TickEmbeddings,
|
||||||
|
tools: TickTools,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animated connector ────────────────────────────────────────────────────────
|
||||||
|
function LayerConnector({ tint }: { tint: string }) {
|
||||||
|
const tracks = [
|
||||||
|
{ x: '32%', primary: false },
|
||||||
|
{ x: '50%', primary: true },
|
||||||
|
{ x: '68%', primary: false },
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', height: 34, width: '100%', maxWidth: 960, margin: '0 auto' }}>
|
||||||
|
{tracks.map(({ x, primary }, ti) => {
|
||||||
|
const color = primary ? '#fbbf24' : tint
|
||||||
|
const dots = primary ? 4 : 3
|
||||||
|
const dur = primary ? 1.6 : 2.4
|
||||||
|
return (
|
||||||
|
<div key={ti} style={{ position: 'absolute', left: x, top: 0, bottom: 0, transform: 'translateX(-50%)' }}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', left: -0.75, top: 0, bottom: 0, width: 1.5,
|
||||||
|
background: `linear-gradient(180deg, ${color}00, ${color}55 40%, ${color}55 60%, ${color}00)`,
|
||||||
|
}} />
|
||||||
|
{Array.from({ length: dots }, (_, j) => (
|
||||||
|
<div key={j} style={{
|
||||||
|
position: 'absolute', top: 0, left: -3, width: 6, height: 6, borderRadius: '50%',
|
||||||
|
background: color, boxShadow: `0 0 7px ${color}`,
|
||||||
|
animation: `v4DotFall ${dur}s ${-(j / dots) * dur}s linear infinite`,
|
||||||
|
}} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Node card ─────────────────────────────────────────────────────────────────
|
||||||
|
function NodeCard({ node, selected, onClick, isLight }: {
|
||||||
|
node: NodeDef; selected: boolean; onClick: () => void; isLight: boolean
|
||||||
|
}) {
|
||||||
|
const [hover, setHover] = useState(false)
|
||||||
|
const active = hover || selected
|
||||||
|
const c = node.color
|
||||||
|
const Ticker = NODE_TICKER[node.id]
|
||||||
|
const Icon = node.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => setHover(true)}
|
||||||
|
onMouseLeave={() => setHover(false)}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
background: active
|
||||||
|
? `linear-gradient(180deg, ${c}${isLight ? '20' : '33'}, ${c}${isLight ? '0a' : '12'})`
|
||||||
|
: isLight
|
||||||
|
? 'linear-gradient(180deg, #ffffff, #f8fafc)'
|
||||||
|
: 'linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.015))',
|
||||||
|
border: `1px solid ${active ? c : isLight ? 'rgba(0,0,0,.1)' : 'rgba(255,255,255,.14)'}`,
|
||||||
|
borderRadius: 12, padding: '12px 14px',
|
||||||
|
cursor: 'pointer', textAlign: 'left',
|
||||||
|
color: isLight ? '#1a1a2e' : '#ece9f7', fontFamily: 'inherit',
|
||||||
|
display: 'flex', flexDirection: 'column',
|
||||||
|
transition: 'all .2s ease',
|
||||||
|
transform: active ? 'translateY(-1px)' : 'none',
|
||||||
|
boxShadow: active
|
||||||
|
? `0 8px 26px ${c}44, 0 0 0 4px ${c}14`
|
||||||
|
: isLight ? '0 1px 4px rgba(0,0,0,.06)' : '0 1px 0 rgba(255,255,255,.04)',
|
||||||
|
minWidth: 0, position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 36, height: 36, borderRadius: 10, flexShrink: 0,
|
||||||
|
background: `linear-gradient(135deg, ${c}3a, ${c}10)`,
|
||||||
|
border: `1px solid ${c}66`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
color: c,
|
||||||
|
boxShadow: node.primary ? `inset 0 0 14px ${c}40` : 'none',
|
||||||
|
}}>
|
||||||
|
<Icon style={{ width: 18, height: 18 }} />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 13, fontWeight: 600,
|
||||||
|
color: isLight ? '#1a1a2e' : '#f7f5fc',
|
||||||
|
letterSpacing: -0.1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
}}>{node.title}</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: isLight ? '#64748b' : 'rgba(236,233,247,.65)',
|
||||||
|
marginTop: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||||
|
}}>{node.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Ticker color={c} isLight={isLight} />
|
||||||
|
{node.primary && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: -1, right: -1,
|
||||||
|
width: 6, height: 6, borderRadius: '50%',
|
||||||
|
background: '#fbbf24', boxShadow: '0 0 8px #fbbf24',
|
||||||
|
animation: 'v4Pulse 1.6s ease-in-out infinite',
|
||||||
|
}} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3D slab ───────────────────────────────────────────────────────────────────
|
||||||
|
function LayerSlab({ label, sublabel, nodes, tint, depth, selectedId, onSelect, isLight }: {
|
||||||
|
label: string; sublabel: string; nodes: NodeDef[]
|
||||||
|
tint: string; depth: number
|
||||||
|
selectedId: NodeId | null; onSelect: (id: NodeId) => void
|
||||||
|
isLight: boolean
|
||||||
|
}) {
|
||||||
|
const isProxy = nodes.length === 1 && !!nodes[0].primary
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'relative', margin: '0 auto',
|
||||||
|
padding: '14px 20px 18px', width: '100%', maxWidth: 960,
|
||||||
|
background: isLight
|
||||||
|
? `linear-gradient(180deg, ${tint}18 0%, ${tint}08 60%, rgba(248,250,252,.98) 100%)`
|
||||||
|
: `linear-gradient(180deg, ${tint}26 0%, ${tint}12 60%, rgba(14,8,28,.85) 100%)`,
|
||||||
|
border: `1px solid ${tint}${isLight ? '44' : '66'}`,
|
||||||
|
borderRadius: 16,
|
||||||
|
boxShadow: isLight
|
||||||
|
? `0 -2px 16px ${tint}18, 0 8px 30px rgba(0,0,0,.05), inset 0 1px 0 ${tint}44`
|
||||||
|
: `0 -6px 30px ${tint}22, 0 24px 60px rgba(0,0,0,.6), inset 0 1px 0 ${tint}55, inset 0 -1px 0 rgba(0,0,0,.4)`,
|
||||||
|
transform: `perspective(2000px) rotateX(12deg) translateZ(${depth}px)`,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: 0, left: 20, right: 20, height: 1,
|
||||||
|
background: `linear-gradient(90deg, transparent, ${tint}${isLight ? 'aa' : 'cc'}, transparent)`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}} />
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 9.5, letterSpacing: 2, textTransform: 'uppercase' as const, fontWeight: 600,
|
||||||
|
color: tint,
|
||||||
|
background: isLight ? `${tint}18` : `${tint}20`,
|
||||||
|
padding: '3px 9px', borderRadius: 99,
|
||||||
|
border: `1px solid ${tint}${isLight ? '44' : '50'}`, whiteSpace: 'nowrap',
|
||||||
|
}}>{label}</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: isLight ? '#64748b' : 'rgba(236,233,247,.55)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>{sublabel}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 10, justifyContent: isProxy ? 'center' : undefined }}>
|
||||||
|
{isProxy ? (
|
||||||
|
<div style={{ width: '42%', minWidth: 260, display: 'flex' }}>
|
||||||
|
<NodeCard node={nodes[0]} selected={selectedId === nodes[0].id} onClick={() => onSelect(nodes[0].id)} isLight={isLight} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
nodes.map(n => (
|
||||||
|
<NodeCard key={n.id} node={n} selected={selectedId === n.id} onClick={() => onSelect(n.id)} isLight={isLight} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main slide ────────────────────────────────────────────────────────────────
|
||||||
export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||||
const i = t(lang)
|
const i = t(lang)
|
||||||
const de = lang === 'de'
|
const de = lang === 'de'
|
||||||
|
const isLight = useIsLight()
|
||||||
|
const allNodes = getNodes(de)
|
||||||
|
const nodeMap = Object.fromEntries(allNodes.map(n => [n.id, n])) as Record<NodeId, NodeDef>
|
||||||
|
|
||||||
const layers = [
|
const [activeId, setActiveId] = useState<NodeId | null>(null)
|
||||||
{
|
function toggle(id: NodeId) { setActiveId(prev => prev === id ? null : id) }
|
||||||
icon: Server,
|
const active = activeId ? nodeMap[activeId] : null
|
||||||
color: 'text-indigo-400',
|
|
||||||
bg: 'bg-indigo-500/10 border-indigo-500/20',
|
|
||||||
title: de ? 'Hardware-Schicht' : 'Hardware Layer',
|
|
||||||
items: [
|
|
||||||
{ label: 'ComplAI Mini', desc: de ? 'Mac Mini M4 (geplant, optional)' : 'Mac Mini M4 (planned, optional)' },
|
|
||||||
{ label: 'ComplAI Studio', desc: de ? 'Mac Studio M4 Max (geplant, optional)' : 'Mac Studio M4 Max (planned, optional)' },
|
|
||||||
{ label: 'ComplAI Cloud', desc: de ? 'BSI-zertifizierte Cloud in Deutschland' : 'BSI-certified cloud in Germany' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Cpu,
|
|
||||||
color: 'text-purple-400',
|
|
||||||
bg: 'bg-purple-500/10 border-purple-500/20',
|
|
||||||
title: de ? 'KI-Engine' : 'AI Engine',
|
|
||||||
items: [
|
|
||||||
{ label: 'Ollama Runtime', desc: de ? 'Lokale LLM-Inferenz, GPU-optimiert' : 'Local LLM inference, GPU-optimized' },
|
|
||||||
{ label: 'RAG Pipeline', desc: de ? 'Vektorsuche mit Compliance-Wissensbasis' : 'Vector search with compliance knowledge base' },
|
|
||||||
{ label: 'Agent Framework', desc: de ? 'Autonome Compliance-Agenten (Audit, Monitoring, Reporting)' : 'Autonomous compliance agents (Audit, Monitoring, Reporting)' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Shield,
|
|
||||||
color: 'text-emerald-400',
|
|
||||||
bg: 'bg-emerald-500/10 border-emerald-500/20',
|
|
||||||
title: de ? 'Compliance-Module' : 'Compliance Modules',
|
|
||||||
items: [
|
|
||||||
{ label: 'DSGVO Engine', desc: de ? 'VVT, DSFA, Betroffenenrechte, Löschkonzept' : 'RoPA, DPIA, Data Subject Rights, Deletion Concept' },
|
|
||||||
{ label: 'AI Act Module', desc: de ? 'Risikoklassifizierung, Konformitätsbewertung, Dokumentation' : 'Risk Classification, Conformity Assessment, Documentation' },
|
|
||||||
{ label: 'NIS2 Module', desc: de ? 'Cybersecurity-Policies, Incident Response, Meldewege' : 'Cybersecurity Policies, Incident Response, Reporting Chains' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Layers,
|
|
||||||
color: 'text-blue-400',
|
|
||||||
bg: 'bg-blue-500/10 border-blue-500/20',
|
|
||||||
title: de ? 'Plattform-Services' : 'Platform Services',
|
|
||||||
items: [
|
|
||||||
{ label: de ? 'Admin-Dashboard' : 'Admin Dashboard', desc: 'Next.js · ' + (de ? 'Mandantenfähig · Rollenbasiert' : 'Multi-Tenant · Role-Based') },
|
|
||||||
{ label: 'SDK API', desc: 'Go/Gin · REST · ' + (de ? 'Tenant-isoliert' : 'Tenant-Isolated') },
|
|
||||||
{ label: 'DevSecOps Suite', desc: 'Semgrep · Trivy · Gitleaks · CycloneDX SBOM' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const securityFeatures = [
|
const tenants = de
|
||||||
{ icon: Lock, label: de ? 'Zero-Trust Architektur' : 'Zero-Trust Architecture' },
|
? ['Mandant A', 'Mandant B', 'Mandant C', 'Mandant N…']
|
||||||
{ icon: Database, label: de ? 'Daten verlassen nie BSI-zertifizierte Server in DE' : 'Data Never Leaves BSI-Certified Servers in DE' },
|
: ['Namespace A', 'Namespace B', 'Namespace C', 'Namespace N…']
|
||||||
{ icon: Globe, label: de ? '100% EU-Cloud · Keine US-Anbieter' : '100% EU Cloud · No US Providers' },
|
|
||||||
{ icon: Workflow, label: de ? 'Air-Gap fähig' : 'Air-Gap Capable' },
|
const layerLabels = de
|
||||||
]
|
? ['01 · Anwendung', '02 · Gateway', '03 · Infrastruktur']
|
||||||
|
: ['01 · Application', '02 · Gateway', '03 · Infrastructure']
|
||||||
|
const layerSublabels = de
|
||||||
|
? ['Benutzeroberflächen', 'Routing & Guardrails', 'Compute & Daten']
|
||||||
|
: ['User-facing services', 'Routing & guardrails', 'Compute & data']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-3">
|
||||||
<FadeInView className="text-center mb-8">
|
<style>{CSS_KF}</style>
|
||||||
<p className="text-xs font-mono text-indigo-400/60 uppercase tracking-widest mb-2">
|
|
||||||
|
<FadeInView className="text-center mb-3">
|
||||||
|
<p className="text-[10px] font-mono text-indigo-400/50 uppercase tracking-widest mb-1.5">
|
||||||
{de ? 'Anhang' : 'Appendix'}
|
{de ? 'Anhang' : 'Appendix'}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
<h2 className="text-3xl md:text-4xl font-bold mb-1.5">
|
||||||
<GradientText>{i.annex.architecture.title}</GradientText>
|
<GradientText>{i.annex.architecture.title}</GradientText>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.annex.architecture.subtitle}</p>
|
<p className="text-xs text-white/35">
|
||||||
|
{de ? 'Klicke auf eine Station für Details' : 'Click any node to explore'}
|
||||||
|
</p>
|
||||||
</FadeInView>
|
</FadeInView>
|
||||||
|
|
||||||
{/* Architecture Layers */}
|
<FadeInView delay={0.15}>
|
||||||
<div className="grid md:grid-cols-2 gap-4 mb-6">
|
<div className="flex items-center justify-center gap-2 flex-wrap mb-3 px-[4%]">
|
||||||
{layers.map((layer, idx) => {
|
<Users className="w-3 h-3 text-white/25 flex-shrink-0" />
|
||||||
const Icon = layer.icon
|
<span className="text-[9px] font-mono text-white/25 uppercase tracking-widest mr-1">
|
||||||
return (
|
{de ? 'Kundenmandanten' : 'Customer Namespaces'}
|
||||||
<FadeInView key={idx} delay={0.2 + idx * 0.1}>
|
</span>
|
||||||
<div className={`border rounded-xl p-4 ${layer.bg}`}>
|
{tenants.map(tn => (
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<span key={tn} className="text-[9px] px-2 py-0.5 rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35 font-mono">
|
||||||
<Icon className={`w-5 h-5 ${layer.color}`} />
|
{tn}
|
||||||
<h3 className="text-sm font-bold text-white">{layer.title}</h3>
|
</span>
|
||||||
</div>
|
))}
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
{layer.items.map((item, iidx) => (
|
|
||||||
<div key={iidx} className="flex items-start gap-2">
|
|
||||||
<div className={`w-1.5 h-1.5 rounded-full mt-1.5 ${layer.color} bg-current opacity-50`} />
|
|
||||||
<div>
|
|
||||||
<span className="text-xs font-semibold text-white/80">{item.label}</span>
|
|
||||||
<span className="text-xs text-white/40 ml-2">{item.desc}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FadeInView>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Security Bar */}
|
{/* ── Main canvas ── */}
|
||||||
<FadeInView delay={0.6}>
|
<div style={{
|
||||||
<GlassCard hover={false} className="p-4">
|
position: 'relative',
|
||||||
<div className="flex items-center justify-center gap-8 flex-wrap">
|
background: isLight
|
||||||
{securityFeatures.map((feat, idx) => {
|
? 'linear-gradient(180deg, #f0f4ff 0%, #eef2ff 50%, #f0f4ff 100%)'
|
||||||
const Icon = feat.icon
|
: 'linear-gradient(180deg, #0a0618 0%, #140a28 50%, #1a0f34 100%)',
|
||||||
|
borderRadius: 16, overflow: 'hidden',
|
||||||
|
padding: '22px 16px 20px',
|
||||||
|
fontFamily: '"Inter", system-ui, -apple-system, sans-serif',
|
||||||
|
WebkitFontSmoothing: 'antialiased',
|
||||||
|
MozOsxFontSmoothing: 'grayscale',
|
||||||
|
} as React.CSSProperties}>
|
||||||
|
|
||||||
|
{/* Ambient glows */}
|
||||||
|
{!isLight && (
|
||||||
|
<>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', top: -80, left: '25%',
|
||||||
|
width: 400, height: 400, borderRadius: '50%',
|
||||||
|
background: 'radial-gradient(circle, rgba(167,139,250,.2), transparent 65%)',
|
||||||
|
filter: 'blur(50px)', pointerEvents: 'none',
|
||||||
|
}} />
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute', bottom: -100, right: '15%',
|
||||||
|
width: 500, height: 500, borderRadius: '50%',
|
||||||
|
background: 'radial-gradient(circle, rgba(139,92,246,.15), transparent 65%)',
|
||||||
|
filter: 'blur(50px)', pointerEvents: 'none',
|
||||||
|
}} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Slabs + connectors */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||||
|
position: 'relative', zIndex: 1,
|
||||||
|
perspective: '2000px', perspectiveOrigin: '50% 0%',
|
||||||
|
}}>
|
||||||
|
{LAYERS.map((layer, li) => {
|
||||||
|
const nodes = layer.nodeIds.map(id => nodeMap[id])
|
||||||
return (
|
return (
|
||||||
<div key={idx} className="flex items-center gap-2">
|
<Fragment key={layer.id}>
|
||||||
<Icon className="w-4 h-4 text-emerald-400" />
|
<LayerSlab
|
||||||
<span className="text-xs text-white/60">{feat.label}</span>
|
label={layerLabels[li]}
|
||||||
</div>
|
sublabel={layerSublabels[li]}
|
||||||
|
nodes={nodes}
|
||||||
|
tint={layer.tint}
|
||||||
|
depth={layer.depth}
|
||||||
|
selectedId={activeId}
|
||||||
|
onSelect={toggle}
|
||||||
|
isLight={isLight}
|
||||||
|
/>
|
||||||
|
{li < LAYERS.length - 1 && <LayerConnector tint={layer.tint} />}
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
|
||||||
|
{/* Footer badges */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', justifyContent: 'center', gap: 8,
|
||||||
|
flexWrap: 'wrap', marginTop: 20, position: 'relative', zIndex: 1,
|
||||||
|
}}>
|
||||||
|
{([
|
||||||
|
{ Icon: Lock, label: de ? 'Kein US-Anbieter · 100% DSGVO' : 'No US providers · 100% GDPR' },
|
||||||
|
{ Icon: Server, label: de ? 'BSI-zertifiziertes Rechenzentrum' : 'BSI-certified data center' },
|
||||||
|
{ Icon: BadgeCheck, label: de ? 'EU-souveräne Inferenz' : 'EU-sovereign inference' },
|
||||||
|
] as { Icon: React.ElementType; label: string }[]).map(({ Icon, label }) => (
|
||||||
|
<div key={label} style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '5px 11px', borderRadius: 99,
|
||||||
|
background: isLight ? '#ffffff' : 'rgba(10,6,24,.82)',
|
||||||
|
border: `1px solid ${isLight ? 'rgba(0,0,0,.1)' : 'rgba(167,139,250,.28)'}`,
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: isLight ? '#64748b' : 'rgba(236,233,247,.7)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
boxShadow: isLight ? '0 1px 3px rgba(0,0,0,.06)' : 'none',
|
||||||
|
}}>
|
||||||
|
<Icon style={{ width: 12, height: 12, color: '#a78bfa' }} />
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Detail panel */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{active && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: '100%' }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: '100%' }}
|
||||||
|
transition={{ duration: 0.3, ease: [0.2, 0.7, 0.2, 1] }}
|
||||||
|
style={{
|
||||||
|
position: 'absolute', left: 0, right: 0, bottom: 0,
|
||||||
|
background: isLight ? 'rgba(255,255,255,.98)' : 'rgba(15,10,31,.97)',
|
||||||
|
borderTop: `1px solid ${active.color}${isLight ? '30' : '40'}`,
|
||||||
|
zIndex: 50,
|
||||||
|
padding: '18px 24px 20px',
|
||||||
|
boxShadow: isLight ? '0 -8px 30px rgba(0,0,0,.08)' : '0 -20px 60px rgba(0,0,0,.55)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ maxWidth: 900, margin: '0 auto' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 14 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 38, height: 38, borderRadius: 11, flexShrink: 0,
|
||||||
|
background: `linear-gradient(135deg, ${active.color}3a, ${active.color}10)`,
|
||||||
|
border: `1px solid ${active.color}66`,
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
color: active.color,
|
||||||
|
}}>
|
||||||
|
<active.icon style={{ width: 19, height: 19 }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||||
|
<span style={{ fontSize: 15, fontWeight: 600, color: isLight ? '#1a1a2e' : '#f5f3fc', letterSpacing: -0.2 }}>
|
||||||
|
{active.title}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 9, padding: '2px 7px', borderRadius: 4,
|
||||||
|
background: `${active.color}18`, color: active.color,
|
||||||
|
border: `1px solid ${active.color}40`,
|
||||||
|
letterSpacing: 0.8, textTransform: 'uppercase' as const, fontWeight: 600,
|
||||||
|
}}>
|
||||||
|
{active.tier === 'product' ? (de ? 'Anwendung' : 'Application') :
|
||||||
|
active.tier === 'proxy' ? 'Gateway' :
|
||||||
|
(de ? 'Inferenz' : 'Inference')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 11.5, color: isLight ? '#64748b' : 'rgba(236,233,247,.5)', marginTop: 2 }}>
|
||||||
|
{active.subtitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveId(null)}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: `1px solid ${isLight ? 'rgba(0,0,0,.15)' : 'rgba(167,139,250,.25)'}`,
|
||||||
|
color: isLight ? '#64748b' : 'rgba(236,233,247,.5)',
|
||||||
|
width: 28, height: 28, borderRadius: 14,
|
||||||
|
cursor: 'pointer', display: 'flex',
|
||||||
|
alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X style={{ width: 13, height: 13 }} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 8.5, letterSpacing: 1.5, textTransform: 'uppercase' as const, color: isLight ? '#94a3b8' : 'rgba(236,233,247,.32)', marginBottom: 7, fontWeight: 600 }}>
|
||||||
|
{de ? 'Stack' : 'Tech Stack'}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
|
||||||
|
{active.tech.map(tk => (
|
||||||
|
<span key={tk} style={{
|
||||||
|
...MONO,
|
||||||
|
fontSize: 10, padding: '3px 8px', borderRadius: 5,
|
||||||
|
background: isLight ? '#f1f5f9' : 'rgba(255,255,255,.05)',
|
||||||
|
border: `1px solid ${isLight ? 'rgba(0,0,0,.1)' : 'rgba(255,255,255,.1)'}`,
|
||||||
|
color: isLight ? '#334155' : 'rgba(236,233,247,.65)',
|
||||||
|
}}>{tk}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 8.5, letterSpacing: 1.5, textTransform: 'uppercase' as const, color: isLight ? '#94a3b8' : 'rgba(236,233,247,.32)', marginBottom: 7, fontWeight: 600 }}>
|
||||||
|
{de ? 'Funktionen' : 'Capabilities'}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||||
|
{active.services.map(s => (
|
||||||
|
<div key={s.name} style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
||||||
|
<div style={{ width: 3, height: 3, borderRadius: '50%', background: active.color, opacity: 0.7, flexShrink: 0, marginTop: 6 }} />
|
||||||
|
<span style={{ fontSize: 11.5, fontWeight: 600, color: isLight ? '#1a1a2e' : 'rgba(245,243,252,.82)' }}>{s.name}</span>
|
||||||
|
<span style={{ fontSize: 10, color: isLight ? '#64748b' : 'rgba(236,233,247,.38)' }}>{s.desc}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
</FadeInView>
|
</FadeInView>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
796
pitch-deck/components/slides/MilestonesSlide.tsx
Normal file
796
pitch-deck/components/slides/MilestonesSlide.tsx
Normal file
@@ -0,0 +1,796 @@
|
|||||||
|
'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: ['3–5 zahlende Pilot-Kunden', 'Public Beta verfügbar', 'Git-Integration live'],
|
||||||
|
en: ['3–5 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 — 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-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>
|
||||||
|
)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ const translations = {
|
|||||||
'Investition & Cap Table',
|
'Investition & Cap Table',
|
||||||
'Kundenersparnis',
|
'Kundenersparnis',
|
||||||
'KI Q&A',
|
'KI Q&A',
|
||||||
'Anhang: Architektur',
|
'Anhang: Systemarchitektur',
|
||||||
'Anhang: Regulatorik',
|
'Anhang: Regulatorik',
|
||||||
'Anhang: Engineering',
|
'Anhang: Engineering',
|
||||||
'Anhang: KI-Pipeline',
|
'Anhang: KI-Pipeline',
|
||||||
@@ -276,8 +276,8 @@ const translations = {
|
|||||||
subtitle: 'Drei Szenarien für robuste Planung',
|
subtitle: 'Drei Szenarien für robuste Planung',
|
||||||
},
|
},
|
||||||
architecture: {
|
architecture: {
|
||||||
title: 'Technische Architektur',
|
title: 'Systemarchitektur',
|
||||||
subtitle: 'Self-Hosted KI-Stack für maximale Datensouveränität',
|
subtitle: 'BreakPilot · CERTifAI · Compliance Scanner — verbunden über LiteLLM',
|
||||||
},
|
},
|
||||||
gtm: {
|
gtm: {
|
||||||
title: 'Go-to-Market Strategie',
|
title: 'Go-to-Market Strategie',
|
||||||
@@ -322,7 +322,7 @@ const translations = {
|
|||||||
'Investment & Cap Table',
|
'Investment & Cap Table',
|
||||||
'Customer Savings',
|
'Customer Savings',
|
||||||
'AI Q&A',
|
'AI Q&A',
|
||||||
'Appendix: Architecture',
|
'Appendix: System Architecture',
|
||||||
'Appendix: Regulatory',
|
'Appendix: Regulatory',
|
||||||
'Appendix: Engineering',
|
'Appendix: Engineering',
|
||||||
'Appendix: AI Pipeline',
|
'Appendix: AI Pipeline',
|
||||||
@@ -572,8 +572,8 @@ const translations = {
|
|||||||
subtitle: 'Three scenarios for robust planning',
|
subtitle: 'Three scenarios for robust planning',
|
||||||
},
|
},
|
||||||
architecture: {
|
architecture: {
|
||||||
title: 'Technical Architecture',
|
title: 'System Architecture',
|
||||||
subtitle: 'Self-hosted AI stack for maximum data sovereignty',
|
subtitle: 'BreakPilot · CERTifAI · Compliance Scanner — connected via LiteLLM',
|
||||||
},
|
},
|
||||||
gtm: {
|
gtm: {
|
||||||
title: 'Go-to-Market Strategy',
|
title: 'Go-to-Market Strategy',
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export async function middleware(request: NextRequest) {
|
|||||||
const { pathname } = request.nextUrl
|
const { pathname } = request.nextUrl
|
||||||
const secret = process.env.PITCH_JWT_SECRET
|
const secret = process.env.PITCH_JWT_SECRET
|
||||||
|
|
||||||
|
// Skip all auth in local dev when no secret is configured
|
||||||
|
if (!secret && process.env.NODE_ENV === 'development') {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
// Allow public paths
|
// Allow public paths
|
||||||
if (isPublicPath(pathname)) {
|
if (isPublicPath(pathname)) {
|
||||||
return NextResponse.next()
|
return NextResponse.next()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3012",
|
"dev": "next dev --turbopack -p 3012",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start -p 3012",
|
"start": "next start -p 3012",
|
||||||
"admin:create": "tsx scripts/create-admin.ts",
|
"admin:create": "tsx scripts/create-admin.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user