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
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:
@@ -52,6 +52,7 @@ import StrategySlide from './slides/StrategySlide'
|
||||
import FinanzplanSlide from './slides/FinanzplanSlide'
|
||||
import GlossarySlide from './slides/GlossarySlide'
|
||||
import RiskSlide from './slides/RiskSlide'
|
||||
import MilestonesSlide from './slides/MilestonesSlide'
|
||||
|
||||
interface PitchDeckProps {
|
||||
lang: Language
|
||||
@@ -182,6 +183,8 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
||||
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
|
||||
case 'traction':
|
||||
return <TractionSlide lang={lang} milestones={data.milestones} metrics={data.metrics} />
|
||||
case 'milestones':
|
||||
return <MilestonesSlide lang={lang} />
|
||||
case 'competition':
|
||||
return <CompetitionSlide lang={lang} features={data.features} competitors={data.competitors} />
|
||||
case 'team':
|
||||
|
||||
@@ -139,6 +139,24 @@ const CSS_KF = `
|
||||
}
|
||||
`
|
||||
|
||||
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)
|
||||
@@ -154,19 +172,14 @@ function useTicker(fn: () => void, min = 140, max = 360, skipChance = 0.1) {
|
||||
}, [min, max, skipChance])
|
||||
}
|
||||
|
||||
const MONO: React.CSSProperties = {
|
||||
fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace',
|
||||
fontVariantNumeric: 'tabular-nums',
|
||||
}
|
||||
|
||||
function TickerShell({ color, children }: { color: string; children: React.ReactNode }) {
|
||||
function TickerShell({ color, children, isLight }: { color: string; children: React.ReactNode; isLight: boolean }) {
|
||||
return (
|
||||
<div style={{
|
||||
...MONO,
|
||||
marginTop: 7, padding: '5px 9px',
|
||||
background: 'rgba(0,0,0,.38)',
|
||||
border: `1px solid ${color}55`, borderRadius: 6,
|
||||
fontSize: 10, color: 'rgba(236,233,247,.88)',
|
||||
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>
|
||||
@@ -182,24 +195,24 @@ function Caret({ color }: { color: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Per-node live tickers ─────────────────────────────────────────────────────
|
||||
function TickCertifAI({ color }: { color: string }) {
|
||||
// ── 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}>
|
||||
<span style={{ color: '#4ade80' }}>✓</span>
|
||||
<TickerShell color={color} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>✓</span>
|
||||
<span style={{ color, opacity: .85 }}>sig</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{n.toLocaleString()}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.55)' }}>{hash}</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 }: { color: string }) {
|
||||
function TickComplAI({ color, isLight }: { color: string; isLight: boolean }) {
|
||||
const [evals, setEvals] = useState(1284)
|
||||
const [rate, setRate] = useState(99.2)
|
||||
useTicker(() => {
|
||||
@@ -207,37 +220,37 @@ function TickComplAI({ color }: { color: string }) {
|
||||
setRate(r => Math.max(97, Math.min(99.9, r + (Math.random() - 0.5) * 0.4)))
|
||||
}, 200, 500, 0.1)
|
||||
return (
|
||||
<TickerShell color={color}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell color={color} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color, opacity: .85 }}>eval</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{evals.toLocaleString()}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.45)' }}>pass</span>
|
||||
<span style={{ color: '#4ade80' }}>{rate.toFixed(1)}%</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 }: { color: string }) {
|
||||
function TickScanner({ color, isLight }: { color: string; isLight: boolean }) {
|
||||
const lines = [
|
||||
{ k: 'PASS', c: '#4ade80', t: 'CWE-79 xss check' },
|
||||
{ k: 'WARN', c: '#fbbf24', t: 'drift: model v2.1→2.2' },
|
||||
{ k: 'PASS', c: '#4ade80', t: 'bias: demographic parity' },
|
||||
{ k: 'FAIL', c: '#f87171', t: 'license: GPL-3 detected' },
|
||||
{ k: 'PASS', c: '#4ade80', t: 'prompt-inject: 214 vectors' },
|
||||
{ k: 'SCAN', c: '#a78bfa', t: 'artifact model-card.json' },
|
||||
{ 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}>
|
||||
<span style={{ color: l.c, fontWeight: 600, minWidth: 30 }}>{l.k}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.85)', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{l.t}</span>
|
||||
<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 }: { color: string }) {
|
||||
function TickLiteLLM({ color, isLight }: { color: string; isLight: boolean }) {
|
||||
const [rps, setRps] = useState(428)
|
||||
const [p50, setP50] = useState(84)
|
||||
useTicker(() => {
|
||||
@@ -245,19 +258,19 @@ function TickLiteLLM({ color }: { color: string }) {
|
||||
setP50(v => Math.max(40, Math.min(160, v + (Math.random() - 0.5) * 20)))
|
||||
}, 250, 500, 0.05)
|
||||
return (
|
||||
<TickerShell color={color}>
|
||||
<span style={{ color: '#fbbf24' }}>⚡</span>
|
||||
<TickerShell color={color} isLight={isLight}>
|
||||
<span style={{ color: '#d97706' }}>⚡</span>
|
||||
<span style={{ color, opacity: .9 }}>req/s</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{Math.round(rps)}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.4)' }}>·</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: '#f5f3fc', fontWeight: 600 }}>{Math.round(p50)}ms</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{Math.round(p50)}ms</span>
|
||||
<Caret color={color} />
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickLLM({ color }: { color: string }) {
|
||||
function TickLLM({ color, isLight }: { color: string; isLight: boolean }) {
|
||||
const [tokens, setTokens] = useState(14832)
|
||||
const [stream, setStream] = useState('t_a91f')
|
||||
const pool = 'abcdef0123456789'
|
||||
@@ -266,32 +279,32 @@ function TickLLM({ color }: { color: string }) {
|
||||
setStream('t_' + Array.from({ length: 4 }, () => pool[Math.floor(Math.random() * pool.length)]).join(''))
|
||||
}, 120, 340, 0.15)
|
||||
return (
|
||||
<TickerShell color={color}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell color={color} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color, opacity: .85 }}>tok</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{tokens.toLocaleString()}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.35)' }}>↑</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 }: { color: string }) {
|
||||
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}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell color={color} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color, opacity: .85 }}>idx</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{vecs.toLocaleString()}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.4)' }}>· 1024d</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 }: { color: string }) {
|
||||
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',
|
||||
@@ -300,15 +313,15 @@ function TickTools({ color }: { color: string }) {
|
||||
const [i, setI] = useState(0)
|
||||
useTicker(() => setI(x => (x + 1) % ops.length), 900, 1600, 0.05)
|
||||
return (
|
||||
<TickerShell color={color}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell color={color} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color, opacity: .85 }}>call</span>
|
||||
<span style={{ color: '#f5f3fc', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{ops[i]}</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 }>> = {
|
||||
const NODE_TICKER: Record<NodeId, React.ComponentType<{ color: string; isLight: boolean }>> = {
|
||||
certifai: TickCertifAI,
|
||||
complai: TickComplAI,
|
||||
scanner: TickScanner,
|
||||
@@ -318,7 +331,7 @@ const NODE_TICKER: Record<NodeId, React.ComponentType<{ color: string }>> = {
|
||||
tools: TickTools,
|
||||
}
|
||||
|
||||
// ── Animated connector between layers ────────────────────────────────────────
|
||||
// ── Animated connector ────────────────────────────────────────────────────────
|
||||
function LayerConnector({ tint }: { tint: string }) {
|
||||
const tracks = [
|
||||
{ x: '32%', primary: false },
|
||||
@@ -333,18 +346,14 @@ function LayerConnector({ tint }: { tint: string }) {
|
||||
const dur = primary ? 1.6 : 2.4
|
||||
return (
|
||||
<div key={ti} style={{ position: 'absolute', left: x, top: 0, bottom: 0, transform: 'translateX(-50%)' }}>
|
||||
{/* Rail */}
|
||||
<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)`,
|
||||
}} />
|
||||
{/* Staggered dots */}
|
||||
{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}`,
|
||||
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`,
|
||||
}} />
|
||||
))}
|
||||
@@ -355,11 +364,9 @@ function LayerConnector({ tint }: { tint: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Single node card ──────────────────────────────────────────────────────────
|
||||
function NodeCard({
|
||||
node, selected, onClick,
|
||||
}: {
|
||||
node: NodeDef; selected: boolean; onClick: () => void
|
||||
// ── 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
|
||||
@@ -375,19 +382,20 @@ function NodeCard({
|
||||
style={{
|
||||
flex: 1,
|
||||
background: active
|
||||
? `linear-gradient(180deg, ${c}33, ${c}12)`
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,.055), rgba(255,255,255,.015))',
|
||||
border: `1px solid ${active ? c : 'rgba(255,255,255,.14)'}`,
|
||||
borderRadius: 12,
|
||||
padding: '12px 14px',
|
||||
? `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: '#ece9f7', fontFamily: 'inherit',
|
||||
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`
|
||||
: '0 1px 0 rgba(255,255,255,.04)',
|
||||
: isLight ? '0 1px 4px rgba(0,0,0,.06)' : '0 1px 0 rgba(255,255,255,.04)',
|
||||
minWidth: 0, position: 'relative',
|
||||
}}
|
||||
>
|
||||
@@ -404,16 +412,18 @@ function NodeCard({
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 13, fontWeight: 600, color: '#f7f5fc', letterSpacing: -0.1,
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
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: 'rgba(236,233,247,.65)', marginTop: 1,
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
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} />
|
||||
<Ticker color={c} isLight={isLight} />
|
||||
{node.primary && (
|
||||
<div style={{
|
||||
position: 'absolute', top: -1, right: -1,
|
||||
@@ -426,51 +436,55 @@ function NodeCard({
|
||||
)
|
||||
}
|
||||
|
||||
// ── 3D perspective slab ───────────────────────────────────────────────────────
|
||||
function LayerSlab({
|
||||
label, sublabel, nodes, tint, depth, selectedId, onSelect,
|
||||
}: {
|
||||
// ── 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: `linear-gradient(180deg, ${tint}26 0%, ${tint}12 60%, rgba(14,8,28,.85) 100%)`,
|
||||
border: `1px solid ${tint}66`,
|
||||
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: `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)`,
|
||||
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)`,
|
||||
}}>
|
||||
{/* Top edge highlight */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, left: 20, right: 20, height: 1,
|
||||
background: `linear-gradient(90deg, transparent, ${tint}cc, transparent)`,
|
||||
background: `linear-gradient(90deg, transparent, ${tint}${isLight ? 'aa' : 'cc'}, transparent)`,
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
{/* Layer label row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<div style={{
|
||||
fontSize: 9.5, letterSpacing: 2, textTransform: 'uppercase', fontWeight: 600,
|
||||
color: tint, background: `${tint}20`, padding: '3px 9px', borderRadius: 99,
|
||||
border: `1px solid ${tint}50`, whiteSpace: 'nowrap',
|
||||
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: 'rgba(236,233,247,.55)', whiteSpace: 'nowrap' }}>{sublabel}</div>
|
||||
<div style={{
|
||||
fontSize: 11,
|
||||
color: isLight ? '#64748b' : 'rgba(236,233,247,.55)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>{sublabel}</div>
|
||||
</div>
|
||||
{/* Cards */}
|
||||
<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)} />
|
||||
<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)} />
|
||||
<NodeCard key={n.id} node={n} selected={selectedId === n.id} onClick={() => onSelect(n.id)} isLight={isLight} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
@@ -482,6 +496,7 @@ function LayerSlab({
|
||||
export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
const i = t(lang)
|
||||
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>
|
||||
|
||||
@@ -504,7 +519,6 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
<div className="space-y-3">
|
||||
<style>{CSS_KF}</style>
|
||||
|
||||
{/* Header */}
|
||||
<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'}
|
||||
@@ -518,24 +532,24 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.15}>
|
||||
{/* Customer namespace strip */}
|
||||
<div className="flex items-center justify-center gap-2 flex-wrap mb-3 px-[4%]">
|
||||
<Users className="w-3 h-3 text-white/25 flex-shrink-0" />
|
||||
<span className="text-[9px] font-mono text-white/25 uppercase tracking-widest mr-1">
|
||||
{de ? 'Kundenmandanten' : 'Customer Namespaces'}
|
||||
</span>
|
||||
{tenants.map(tn => (
|
||||
<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">
|
||||
<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">
|
||||
{tn}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── MAIN CANVAS ─────────────────────────────────────────────── */}
|
||||
{/* ── Main canvas ── */}
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
background: 'linear-gradient(180deg, #0a0618 0%, #140a28 50%, #1a0f34 100%)',
|
||||
background: isLight
|
||||
? 'linear-gradient(180deg, #f0f4ff 0%, #eef2ff 50%, #f0f4ff 100%)'
|
||||
: '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',
|
||||
@@ -544,18 +558,22 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
} as React.CSSProperties}>
|
||||
|
||||
{/* Ambient glows */}
|
||||
<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',
|
||||
}} />
|
||||
{!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={{
|
||||
@@ -575,6 +593,7 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
depth={layer.depth}
|
||||
selectedId={activeId}
|
||||
onSelect={toggle}
|
||||
isLight={isLight}
|
||||
/>
|
||||
{li < LAYERS.length - 1 && <LayerConnector tint={layer.tint} />}
|
||||
</Fragment>
|
||||
@@ -595,10 +614,12 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
<div key={label} style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 11px', borderRadius: 99,
|
||||
background: 'rgba(10,6,24,.82)',
|
||||
border: '1px solid rgba(167,139,250,.28)',
|
||||
fontSize: 10.5, color: 'rgba(236,233,247,.7)',
|
||||
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}
|
||||
@@ -606,7 +627,7 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── Detail panel: slides up from bottom ── */}
|
||||
{/* Detail panel */}
|
||||
<AnimatePresence>
|
||||
{active && (
|
||||
<motion.div
|
||||
@@ -616,15 +637,14 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
transition={{ duration: 0.3, ease: [0.2, 0.7, 0.2, 1] }}
|
||||
style={{
|
||||
position: 'absolute', left: 0, right: 0, bottom: 0,
|
||||
background: 'rgba(15,10,31,.97)',
|
||||
borderTop: `1px solid ${active.color}40`,
|
||||
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: '0 -20px 60px rgba(0,0,0,.55)',
|
||||
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' }}>
|
||||
{/* Panel header */}
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
@@ -638,7 +658,7 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: '#f5f3fc', letterSpacing: -0.2 }}>
|
||||
<span style={{ fontSize: 15, fontWeight: 600, color: isLight ? '#1a1a2e' : '#f5f3fc', letterSpacing: -0.2 }}>
|
||||
{active.title}
|
||||
</span>
|
||||
<span style={{
|
||||
@@ -652,7 +672,7 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
(de ? 'Inferenz' : 'Inference')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11.5, color: 'rgba(236,233,247,.5)', marginTop: 2 }}>
|
||||
<div style={{ fontSize: 11.5, color: isLight ? '#64748b' : 'rgba(236,233,247,.5)', marginTop: 2 }}>
|
||||
{active.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
@@ -661,8 +681,8 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
onClick={() => setActiveId(null)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(167,139,250,.25)',
|
||||
color: 'rgba(236,233,247,.5)',
|
||||
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,
|
||||
@@ -671,10 +691,9 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
<X style={{ width: 13, height: 13 }} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Tech + capabilities grid */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 8.5, letterSpacing: 1.5, textTransform: 'uppercase' as const, color: 'rgba(236,233,247,.32)', marginBottom: 7, fontWeight: 600 }}>
|
||||
<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 }}>
|
||||
@@ -682,23 +701,23 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
|
||||
<span key={tk} style={{
|
||||
...MONO,
|
||||
fontSize: 10, padding: '3px 8px', borderRadius: 5,
|
||||
background: 'rgba(255,255,255,.05)',
|
||||
border: '1px solid rgba(255,255,255,.1)',
|
||||
color: 'rgba(236,233,247,.65)',
|
||||
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: 'rgba(236,233,247,.32)', marginBottom: 7, fontWeight: 600 }}>
|
||||
<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: 'rgba(245,243,252,.82)' }}>{s.name}</span>
|
||||
<span style={{ fontSize: 10, color: 'rgba(236,233,247,.38)' }}>{s.desc}</span>
|
||||
<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>
|
||||
|
||||
814
pitch-deck/components/slides/MilestonesSlide.tsx
Normal file
814
pitch-deck/components/slides/MilestonesSlide.tsx
Normal 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: ['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 */}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -21,12 +21,29 @@ const CSS_KF = `
|
||||
0%,100% { box-shadow: 0 0 38px rgba(167,139,250,.55), 0 0 80px rgba(167,139,250,.2), inset 0 3px 0 rgba(255,255,255,.35), inset 0 -6px 12px rgba(0,0,0,.35); }
|
||||
50% { box-shadow: 0 0 58px rgba(167,139,250,.85), 0 0 110px rgba(167,139,250,.35), inset 0 3px 0 rgba(255,255,255,.4), inset 0 -6px 12px rgba(0,0,0,.35); }
|
||||
}
|
||||
@keyframes uspPulseLight {
|
||||
0%,100% { box-shadow: 0 0 28px rgba(167,139,250,.4), 0 0 56px rgba(167,139,250,.15), inset 0 3px 0 rgba(255,255,255,.5), inset 0 -6px 12px rgba(0,0,0,.2); }
|
||||
50% { box-shadow: 0 0 44px rgba(167,139,250,.65), 0 0 80px rgba(167,139,250,.25), inset 0 3px 0 rgba(255,255,255,.55), inset 0 -6px 12px rgba(0,0,0,.2); }
|
||||
}
|
||||
@keyframes uspHeading {
|
||||
0%,100% { text-shadow: 0 0 22px rgba(167,139,250,.3); }
|
||||
50% { text-shadow: 0 0 36px rgba(167,139,250,.55); }
|
||||
}
|
||||
`
|
||||
|
||||
// ── 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
|
||||
}
|
||||
|
||||
// ── Ticker ────────────────────────────────────────────────────────────────────
|
||||
function useTicker(fn: () => void, min = 180, max = 420, skip = 0.1) {
|
||||
const ref = useRef(fn)
|
||||
@@ -42,32 +59,34 @@ function useTicker(fn: () => void, min = 180, max = 420, skip = 0.1) {
|
||||
}, [min, max, skip])
|
||||
}
|
||||
|
||||
function TickerShell({ tint, children }: { tint: string; children: React.ReactNode }) {
|
||||
function TickerShell({ tint, isLight, children }: { tint: string; isLight: boolean; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{
|
||||
...MONO, marginTop: 10, padding: '5px 9px',
|
||||
background: 'rgba(0,0,0,.38)', border: `1px solid ${tint}55`,
|
||||
borderRadius: 6, fontSize: 10.5, color: 'rgba(236,233,247,.88)',
|
||||
background: isLight ? '#f1f5f9' : 'rgba(0,0,0,.38)',
|
||||
border: `1px solid ${tint}55`,
|
||||
borderRadius: 6, fontSize: 10.5,
|
||||
color: isLight ? '#475569' : 'rgba(236,233,247,.88)',
|
||||
display: 'flex', alignItems: 'center', gap: 7,
|
||||
whiteSpace: 'nowrap', overflow: 'hidden', height: 22,
|
||||
}}>{children}</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TickTrace({ tint }: { tint: string }) {
|
||||
function TickTrace({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const [n, setN] = useState(12748)
|
||||
useTicker(() => setN(v => v + 1 + Math.floor(Math.random() * 3)), 250, 500)
|
||||
return (
|
||||
<TickerShell tint={tint}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>trace</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{n.toLocaleString()}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.45)' }}>evidence-chain</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{n.toLocaleString()}</span>
|
||||
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.45)' }}>evidence-chain</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickEngine({ tint }: { tint: string }) {
|
||||
function TickEngine({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const [v, setV] = useState(428)
|
||||
const [rate, setRate] = useState(99.4)
|
||||
useTicker(() => {
|
||||
@@ -75,40 +94,40 @@ function TickEngine({ tint }: { tint: string }) {
|
||||
setRate(r => Math.max(97, Math.min(99.9, r + (Math.random() - 0.5) * 0.3)))
|
||||
}, 220, 420)
|
||||
return (
|
||||
<TickerShell tint={tint}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>validate</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{v.toLocaleString()}</span>
|
||||
<span style={{ color: '#4ade80' }}>{rate.toFixed(1)}%</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{v.toLocaleString()}</span>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>{rate.toFixed(1)}%</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickOptimizer({ tint }: { tint: string }) {
|
||||
function TickOptimizer({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const ops = ['ROI: 2.418 € / dev', 'gap → policy §4.2', 'dedup 128 tickets', 'sweet-spot: 22 KLOC', 'tradeoff: speed↔risk']
|
||||
const [i, setI] = useState(0)
|
||||
useTicker(() => setI(x => (x + 1) % ops.length), 900, 1600, 0.05)
|
||||
return (
|
||||
<TickerShell tint={tint}>
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: '#fbbf24' }}>✦</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>optimize</span>
|
||||
<span style={{ color: '#f5f3fc', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{ops[i]}</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>{ops[i]}</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
|
||||
function TickStack({ tint }: { tint: string }) {
|
||||
function TickStack({ tint, isLight }: { tint: string; isLight: boolean }) {
|
||||
const regs = ['DSGVO', 'NIS-2', 'DORA', 'EU AI Act', 'ISO 27001', 'BSI C5']
|
||||
const [i, setI] = useState(0)
|
||||
const [c, setC] = useState(1208)
|
||||
useTicker(() => { setI(x => (x + 1) % regs.length); setC(v => v + Math.floor(Math.random() * 3)) }, 800, 1400, 0.05)
|
||||
return (
|
||||
<TickerShell tint={tint}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<TickerShell tint={tint} isLight={isLight}>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: tint, opacity: .85 }}>check</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{regs[i]}</span>
|
||||
<span style={{ color: 'rgba(236,233,247,.4)' }}>·</span>
|
||||
<span style={{ color: '#f5f3fc' }}>{c.toLocaleString()}</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{regs[i]}</span>
|
||||
<span style={{ color: isLight ? '#94a3b8' : 'rgba(236,233,247,.4)' }}>·</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc' }}>{c.toLocaleString()}</span>
|
||||
</TickerShell>
|
||||
)
|
||||
}
|
||||
@@ -233,10 +252,10 @@ function getDetails(de: boolean): Record<string, DetailItem> {
|
||||
}
|
||||
|
||||
// ── Pillar row ────────────────────────────────────────────────────────────────
|
||||
function PillarRow({ side, title, body, tint, onClick, active }: {
|
||||
function PillarRow({ side, title, body, tint, onClick, active, isLight }: {
|
||||
side: 'left' | 'right'
|
||||
title: string; body: string; tint: string
|
||||
onClick: () => void; active: boolean
|
||||
onClick: () => void; active: boolean; isLight: boolean
|
||||
}) {
|
||||
const [hover, setHover] = useState(false)
|
||||
const lit = hover || active
|
||||
@@ -266,13 +285,15 @@ function PillarRow({ side, title, body, tint, onClick, active }: {
|
||||
background: lit ? `${tint}3a` : `${tint}22`,
|
||||
border: `1px solid ${lit ? tint : tint + '66'}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: lit ? '#fff' : tint, fontSize: 13, fontWeight: 700, marginTop: 2,
|
||||
color: lit ? (isLight ? tint : '#fff') : tint, fontSize: 13, fontWeight: 700, marginTop: 2,
|
||||
boxShadow: lit ? `0 0 14px ${tint}88, inset 0 1px 0 ${tint}80` : `inset 0 1px 0 ${tint}50`,
|
||||
transition: 'all .25s',
|
||||
}}>◆</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: 13, fontWeight: 700, color: '#f7f5fc', letterSpacing: -0.15, marginBottom: 3,
|
||||
fontSize: 13, fontWeight: 700,
|
||||
color: isLight ? '#1a1a2e' : '#f7f5fc',
|
||||
letterSpacing: -0.15, marginBottom: 3,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
justifyContent: isLeft ? 'flex-end' : 'flex-start',
|
||||
}}>
|
||||
@@ -283,15 +304,21 @@ function PillarRow({ side, title, body, tint, onClick, active }: {
|
||||
transition: 'all .25s',
|
||||
}}>{isLeft ? '‹' : '›'}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, lineHeight: 1.55, color: `rgba(236,233,247,${lit ? .82 : .62})`, transition: 'color .25s' }}>{body}</div>
|
||||
<div style={{
|
||||
fontSize: 11, lineHeight: 1.55,
|
||||
color: isLight
|
||||
? `rgba(71,85,105,${lit ? 1 : .78})`
|
||||
: `rgba(236,233,247,${lit ? .82 : .62})`,
|
||||
transition: 'color .25s',
|
||||
}}>{body}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Column header ─────────────────────────────────────────────────────────────
|
||||
function ColHeader({ side, label, color, icon, sub }: {
|
||||
side: 'left' | 'right'; label: string; color: string; icon: string; sub: string
|
||||
function ColHeader({ side, label, color, icon, sub, isLight }: {
|
||||
side: 'left' | 'right'; label: string; color: string; icon: string; sub: string; isLight: boolean
|
||||
}) {
|
||||
const isLeft = side === 'left'
|
||||
return (
|
||||
@@ -305,11 +332,11 @@ function ColHeader({ side, label, color, icon, sub }: {
|
||||
background: `linear-gradient(135deg, ${color}55, ${color}20)`,
|
||||
border: `1px solid ${color}88`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', fontSize: 15, fontWeight: 700,
|
||||
color: isLight ? color : '#fff', fontSize: 15, fontWeight: 700,
|
||||
boxShadow: `0 0 18px ${color}55, inset 0 1px 0 ${color}aa`,
|
||||
}}>{icon}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: '#f7f5fc', letterSpacing: -0.3, lineHeight: 1 }}>{label}</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc', letterSpacing: -0.3, lineHeight: 1 }}>{label}</div>
|
||||
<div style={{ ...MONO, fontSize: 9.5, letterSpacing: 2, color, opacity: .75, marginTop: 3, textTransform: 'uppercase' as const }}>{sub}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,7 +344,7 @@ function ColHeader({ side, label, color, icon, sub }: {
|
||||
}
|
||||
|
||||
// ── Central hub ───────────────────────────────────────────────────────────────
|
||||
function CentralHub({ caption }: { caption: string }) {
|
||||
function CentralHub({ caption, isLight }: { caption: string; isLight: boolean }) {
|
||||
return (
|
||||
<div style={{ position: 'relative', width: 260, height: 320, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{
|
||||
@@ -325,11 +352,14 @@ function CentralHub({ caption }: { caption: string }) {
|
||||
background: 'radial-gradient(circle at 32% 28%, #f0e9ff 0%, #c4aaff 26%, #7b5cd6 58%, #2a1560 100%)',
|
||||
border: '1.5px solid rgba(216,202,255,.7)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 0 50px rgba(167,139,250,.65), 0 0 100px rgba(167,139,250,.25), inset 0 3px 0 rgba(255,255,255,.35), inset 0 -8px 14px rgba(0,0,0,.35)',
|
||||
animation: 'uspPulse 2.6s ease-in-out infinite', zIndex: 3,
|
||||
boxShadow: isLight
|
||||
? '0 0 30px rgba(167,139,250,.4), 0 0 60px rgba(167,139,250,.15), inset 0 3px 0 rgba(255,255,255,.5), inset 0 -8px 14px rgba(0,0,0,.2)'
|
||||
: '0 0 50px rgba(167,139,250,.65), 0 0 100px rgba(167,139,250,.25), inset 0 3px 0 rgba(255,255,255,.35), inset 0 -8px 14px rgba(0,0,0,.35)',
|
||||
animation: isLight ? 'uspPulseLight 2.6s ease-in-out infinite' : 'uspPulse 2.6s ease-in-out infinite',
|
||||
zIndex: 3,
|
||||
}}>
|
||||
<div style={{ position: 'absolute', inset: -14, borderRadius: '50%', border: '1px dashed rgba(216,202,255,.42)', animation: 'uspSpin 14s linear infinite' }} />
|
||||
<div style={{ position: 'absolute', inset: -30, borderRadius: '50%', border: '1px dashed rgba(216,202,255,.2)', animation: 'uspSpin 22s linear infinite reverse' }} />
|
||||
<div style={{ position: 'absolute', inset: -14, borderRadius: '50%', border: `1px dashed ${isLight ? 'rgba(167,139,250,.5)' : 'rgba(216,202,255,.42)'}`, animation: 'uspSpin 14s linear infinite' }} />
|
||||
<div style={{ position: 'absolute', inset: -30, borderRadius: '50%', border: `1px dashed ${isLight ? 'rgba(167,139,250,.3)' : 'rgba(216,202,255,.2)'}`, animation: 'uspSpin 22s linear infinite reverse' }} />
|
||||
<svg width="54" height="26" viewBox="0 0 54 26" fill="none" stroke="#fff" strokeWidth="2.8" strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ filter: 'drop-shadow(0 1px 3px rgba(0,0,0,.5))' }}>
|
||||
<path d="M 10 13 C 10 5, 22 5, 27 13 C 32 21, 44 21, 44 13 C 44 5, 32 5, 27 13 C 22 21, 10 21, 10 13 Z" />
|
||||
@@ -337,7 +367,8 @@ function CentralHub({ caption }: { caption: string }) {
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, right: 0, bottom: 24, textAlign: 'center',
|
||||
...MONO, fontSize: 9.5, letterSpacing: 2.5, color: 'rgba(216,202,255,.75)',
|
||||
...MONO, fontSize: 9.5, letterSpacing: 2.5,
|
||||
color: isLight ? 'rgba(109,77,194,.75)' : 'rgba(216,202,255,.75)',
|
||||
textTransform: 'uppercase' as const, fontWeight: 600,
|
||||
}}>{caption}</div>
|
||||
</div>
|
||||
@@ -345,7 +376,7 @@ function CentralHub({ caption }: { caption: string }) {
|
||||
}
|
||||
|
||||
// ── Bridge SVG connectors ─────────────────────────────────────────────────────
|
||||
function BridgeConnectors() {
|
||||
function BridgeConnectors({ isLight }: { isLight: boolean }) {
|
||||
const rfpY = 130
|
||||
const sub2Y = 250
|
||||
const hubCx = 500
|
||||
@@ -356,12 +387,12 @@ function BridgeConnectors() {
|
||||
<defs>
|
||||
<linearGradient id="uspFromL" x1="0" x2="1">
|
||||
<stop offset="0" stopColor="#a78bfa" stopOpacity="0" />
|
||||
<stop offset=".3" stopColor="#a78bfa" stopOpacity=".85" />
|
||||
<stop offset="1" stopColor="#c084fc" stopOpacity=".3" />
|
||||
<stop offset=".3" stopColor="#a78bfa" stopOpacity={isLight ? '.6' : '.85'} />
|
||||
<stop offset="1" stopColor="#c084fc" stopOpacity={isLight ? '.2' : '.3'} />
|
||||
</linearGradient>
|
||||
<linearGradient id="uspToR" x1="0" x2="1">
|
||||
<stop offset="0" stopColor="#c084fc" stopOpacity=".3" />
|
||||
<stop offset=".7" stopColor="#fbbf24" stopOpacity=".85" />
|
||||
<stop offset="0" stopColor="#c084fc" stopOpacity={isLight ? '.2' : '.3'} />
|
||||
<stop offset=".7" stopColor="#fbbf24" stopOpacity={isLight ? '.6' : '.85'} />
|
||||
<stop offset="1" stopColor="#fbbf24" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
@@ -381,9 +412,9 @@ function BridgeConnectors() {
|
||||
|
||||
{([rfpY, sub2Y] as number[]).map(y => (
|
||||
<g key={y}>
|
||||
<circle cx={hubCx - hubR} cy={y} r="4" fill="#1a0f34" stroke="#a78bfa" strokeWidth="1.2" />
|
||||
<circle cx={hubCx - hubR} cy={y} r="4" fill={isLight ? '#eef2ff' : '#1a0f34'} stroke="#a78bfa" strokeWidth="1.2" />
|
||||
<circle cx={hubCx - hubR} cy={y} r="1.5" fill="#a78bfa" />
|
||||
<circle cx={hubCx + hubR} cy={y} r="4" fill="#1a0f34" stroke="#fbbf24" strokeWidth="1.2" />
|
||||
<circle cx={hubCx + hubR} cy={y} r="4" fill={isLight ? '#eef2ff' : '#1a0f34'} stroke="#fbbf24" strokeWidth="1.2" />
|
||||
<circle cx={hubCx + hubR} cy={y} r="1.5" fill="#fbbf24" />
|
||||
</g>
|
||||
))}
|
||||
@@ -401,10 +432,10 @@ function BridgeConnectors() {
|
||||
}
|
||||
|
||||
// ── Under-the-hood feature card ───────────────────────────────────────────────
|
||||
function FeatureCard({ icon, title, body, tint, Ticker, onClick, active }: {
|
||||
function FeatureCard({ icon, title, body, tint, Ticker, onClick, active, isLight }: {
|
||||
icon: string; title: string; body: string; tint: string
|
||||
Ticker: React.ComponentType<{ tint: string }>
|
||||
onClick: () => void; active: boolean
|
||||
Ticker: React.ComponentType<{ tint: string; isLight: boolean }>
|
||||
onClick: () => void; active: boolean; isLight: boolean
|
||||
}) {
|
||||
const [hover, setHover] = useState(false)
|
||||
const lit = hover || active
|
||||
@@ -415,12 +446,18 @@ function FeatureCard({ icon, title, body, tint, Ticker, onClick, active }: {
|
||||
onMouseLeave={() => setHover(false)}
|
||||
style={{
|
||||
position: 'relative', padding: '13px 15px',
|
||||
background: `linear-gradient(180deg, ${tint}${lit ? '2a' : '1a'} 0%, ${tint}07 55%, rgba(14,8,28,.85) 100%)`,
|
||||
border: `1px solid ${lit ? tint : tint + '4a'}`,
|
||||
background: isLight
|
||||
? lit
|
||||
? `linear-gradient(180deg, ${tint}18 0%, ${tint}08 55%, rgba(248,250,252,.95) 100%)`
|
||||
: 'linear-gradient(180deg, #ffffff, #f8fafc)'
|
||||
: `linear-gradient(180deg, ${tint}${lit ? '2a' : '1a'} 0%, ${tint}07 55%, rgba(14,8,28,.85) 100%)`,
|
||||
border: `1px solid ${lit ? tint : isLight ? 'rgba(0,0,0,.1)' : tint + '4a'}`,
|
||||
borderRadius: 12,
|
||||
boxShadow: lit
|
||||
? `0 18px 40px ${tint}33, 0 0 0 1px ${tint}66, inset 0 1px 0 ${tint}60`
|
||||
: `0 10px 24px rgba(0,0,0,.4), inset 0 1px 0 ${tint}35`,
|
||||
: isLight
|
||||
? '0 2px 8px rgba(0,0,0,.08), inset 0 1px 0 rgba(255,255,255,.8)'
|
||||
: `0 10px 24px rgba(0,0,0,.4), inset 0 1px 0 ${tint}35`,
|
||||
minWidth: 0, cursor: 'pointer',
|
||||
transform: lit ? 'translateY(-3px)' : 'translateY(0)',
|
||||
transition: 'transform .25s, box-shadow .25s, background .25s, border-color .25s',
|
||||
@@ -432,21 +469,27 @@ function FeatureCard({ icon, title, body, tint, Ticker, onClick, active }: {
|
||||
background: lit ? `${tint}44` : `${tint}22`,
|
||||
border: `1px solid ${lit ? tint : tint + '66'}`,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: lit ? '#fff' : tint, fontSize: 12,
|
||||
color: lit ? (isLight ? tint : '#fff') : tint, fontSize: 12,
|
||||
boxShadow: lit ? `0 0 12px ${tint}88` : 'none',
|
||||
transition: 'all .25s',
|
||||
}}>{icon}</span>
|
||||
<span style={{ fontSize: 12.5, fontWeight: 700, color: '#f7f5fc', letterSpacing: -0.15, flex: 1 }}>{title}</span>
|
||||
<span style={{ fontSize: 12.5, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc', letterSpacing: -0.15, flex: 1 }}>{title}</span>
|
||||
<span style={{ fontSize: 10, color: tint, opacity: lit ? 1 : 0.5, transform: `translateX(${lit ? 0 : -3}px)`, transition: 'all .25s' }}>↗</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, lineHeight: 1.45, color: `rgba(236,233,247,${lit ? .82 : .65})`, transition: 'color .25s' }}>{body}</div>
|
||||
<Ticker tint={tint} />
|
||||
<div style={{
|
||||
fontSize: 11, lineHeight: 1.45,
|
||||
color: isLight
|
||||
? `rgba(71,85,105,${lit ? 1 : .78})`
|
||||
: `rgba(236,233,247,${lit ? .82 : .65})`,
|
||||
transition: 'color .25s',
|
||||
}}>{body}</div>
|
||||
<Ticker tint={tint} isLight={isLight} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Detail modal ──────────────────────────────────────────────────────────────
|
||||
function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () => void }) {
|
||||
function DetailModal({ item, onClose, isLight }: { item: DetailItem | null; onClose: () => void; isLight: boolean }) {
|
||||
useEffect(() => {
|
||||
if (!item) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
||||
@@ -465,7 +508,8 @@ function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () =
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', inset: 0, zIndex: 50,
|
||||
background: 'rgba(5,2,16,.72)', backdropFilter: 'blur(6px)',
|
||||
background: isLight ? 'rgba(240,244,255,.72)' : 'rgba(5,2,16,.72)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -477,11 +521,16 @@ function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () =
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
width: 560, maxWidth: '88%',
|
||||
background: `linear-gradient(180deg, ${item.tint}18 0%, rgba(20,10,40,.96) 50%, rgba(14,8,28,.98) 100%)`,
|
||||
border: `1px solid ${item.tint}66`,
|
||||
background: isLight
|
||||
? `linear-gradient(180deg, ${item.tint}10 0%, rgba(255,255,255,.98) 50%, rgba(248,250,252,.99) 100%)`
|
||||
: `linear-gradient(180deg, ${item.tint}18 0%, rgba(20,10,40,.96) 50%, rgba(14,8,28,.98) 100%)`,
|
||||
border: `1px solid ${item.tint}${isLight ? '44' : '66'}`,
|
||||
borderRadius: 16,
|
||||
boxShadow: `0 30px 80px rgba(0,0,0,.6), 0 0 60px ${item.tint}33, inset 0 1px 0 ${item.tint}55`,
|
||||
padding: '22px 26px', color: '#ece9f7',
|
||||
boxShadow: isLight
|
||||
? `0 20px 60px rgba(0,0,0,.12), 0 0 40px ${item.tint}18, inset 0 1px 0 rgba(255,255,255,.9)`
|
||||
: `0 30px 80px rgba(0,0,0,.6), 0 0 60px ${item.tint}33, inset 0 1px 0 ${item.tint}55`,
|
||||
padding: '22px 26px',
|
||||
color: isLight ? '#1a1a2e' : '#ece9f7',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
|
||||
@@ -490,25 +539,25 @@ function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () =
|
||||
background: `linear-gradient(135deg, ${item.tint}66, ${item.tint}22)`,
|
||||
border: `1px solid ${item.tint}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', fontSize: 16, fontWeight: 700,
|
||||
color: isLight ? item.tint : '#fff', fontSize: 16, fontWeight: 700,
|
||||
boxShadow: `0 0 18px ${item.tint}66`,
|
||||
}}>{item.icon}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ ...MONO, fontSize: 9.5, letterSpacing: 2.5, color: item.tint, textTransform: 'uppercase' as const, fontWeight: 600, marginBottom: 2 }}>
|
||||
{item.kicker}
|
||||
</div>
|
||||
<div style={{ fontSize: 19, fontWeight: 700, color: '#f7f5fc', letterSpacing: -0.3 }}>{item.title}</div>
|
||||
<div style={{ fontSize: 19, fontWeight: 700, color: isLight ? '#1a1a2e' : '#f7f5fc', letterSpacing: -0.3 }}>{item.title}</div>
|
||||
</div>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'transparent', border: `1px solid ${item.tint}55`,
|
||||
borderRadius: 8, cursor: 'pointer', width: 30, height: 30,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: 'rgba(236,233,247,.6)',
|
||||
color: isLight ? '#64748b' : 'rgba(236,233,247,.6)',
|
||||
}}>
|
||||
<X style={{ width: 14, height: 14 }} />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.6, color: 'rgba(236,233,247,.82)', marginBottom: 16 }}>
|
||||
<div style={{ fontSize: 13, lineHeight: 1.6, color: isLight ? '#475569' : 'rgba(236,233,247,.82)', marginBottom: 16 }}>
|
||||
{item.body}
|
||||
</div>
|
||||
{item.bullets && (
|
||||
@@ -517,10 +566,11 @@ function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () =
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 10,
|
||||
padding: '8px 12px', borderRadius: 8,
|
||||
background: 'rgba(0,0,0,.3)', border: `1px solid ${item.tint}33`,
|
||||
background: isLight ? 'rgba(0,0,0,.04)' : 'rgba(0,0,0,.3)',
|
||||
border: `1px solid ${item.tint}${isLight ? '22' : '33'}`,
|
||||
}}>
|
||||
<span style={{ color: item.tint, fontSize: 12, marginTop: 1 }}>▸</span>
|
||||
<span style={{ fontSize: 12, lineHeight: 1.5, color: 'rgba(236,233,247,.78)' }}>{b}</span>
|
||||
<span style={{ fontSize: 12, lineHeight: 1.5, color: isLight ? '#475569' : 'rgba(236,233,247,.78)' }}>{b}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -528,13 +578,14 @@ function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () =
|
||||
{item.stat && (
|
||||
<div style={{
|
||||
...MONO, padding: '10px 14px', borderRadius: 8,
|
||||
background: 'rgba(0,0,0,.45)', border: `1px solid ${item.tint}55`,
|
||||
fontSize: 12, color: 'rgba(236,233,247,.9)',
|
||||
background: isLight ? 'rgba(0,0,0,.04)' : 'rgba(0,0,0,.45)',
|
||||
border: `1px solid ${item.tint}${isLight ? '33' : '55'}`,
|
||||
fontSize: 12, color: isLight ? '#475569' : 'rgba(236,233,247,.9)',
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<span style={{ color: '#4ade80' }}>●</span>
|
||||
<span style={{ color: isLight ? '#16a34a' : '#4ade80' }}>●</span>
|
||||
<span style={{ color: item.tint }}>{item.stat.k}</span>
|
||||
<span style={{ color: '#f5f3fc', fontWeight: 600 }}>{item.stat.v}</span>
|
||||
<span style={{ color: isLight ? '#1a1a2e' : '#f5f3fc', fontWeight: 600 }}>{item.stat.v}</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
@@ -545,12 +596,13 @@ function DetailModal({ item, onClose }: { item: DetailItem | null; onClose: () =
|
||||
}
|
||||
|
||||
// ── Star field ────────────────────────────────────────────────────────────────
|
||||
function StarField() {
|
||||
function StarField({ isLight }: { isLight: boolean }) {
|
||||
const stars = useMemo(() => {
|
||||
let s = 41
|
||||
const r = () => { s = (s * 9301 + 49297) % 233280; return s / 233280 }
|
||||
return Array.from({ length: 90 }, () => ({ x: r() * 100, y: r() * 100, size: r() * 1.4 + 0.3, op: r() * 0.5 + 0.15 }))
|
||||
}, [])
|
||||
if (isLight) return null
|
||||
return (
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
|
||||
{stars.map((st, i) => (
|
||||
@@ -568,6 +620,7 @@ function StarField() {
|
||||
// ── Main slide ────────────────────────────────────────────────────────────────
|
||||
export default function USPSlide({ lang }: USPSlideProps) {
|
||||
const de = lang === 'de'
|
||||
const isLight = useIsLight()
|
||||
const details = getDetails(de)
|
||||
const [detail, setDetail] = useState<DetailItem | null>(null)
|
||||
const open = (k: string) => setDetail(details[k])
|
||||
@@ -587,38 +640,45 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
{/* ── MAIN CANVAS ───────────────────────────────────────────────── */}
|
||||
<div style={{
|
||||
position: 'relative', overflow: 'hidden', borderRadius: 16,
|
||||
background: 'radial-gradient(ellipse at 50% 30%, #1a0f34 0%, #0e0720 55%, #050210 100%)',
|
||||
color: '#ece9f7', fontFamily: '"Inter", system-ui, sans-serif',
|
||||
background: isLight
|
||||
? 'linear-gradient(160deg, #f0f4ff 0%, #eff6ff 50%, #f5f0ff 100%)'
|
||||
: 'radial-gradient(ellipse at 50% 30%, #1a0f34 0%, #0e0720 55%, #050210 100%)',
|
||||
color: isLight ? '#1a1a2e' : '#ece9f7',
|
||||
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: 'radial-gradient(ellipse, rgba(167,139,250,.2), transparent 65%)',
|
||||
filter: 'blur(50px)', pointerEvents: 'none',
|
||||
}} />
|
||||
<StarField />
|
||||
{/* Ambient glow — dark only */}
|
||||
{!isLight && (
|
||||
<div style={{
|
||||
position: 'absolute', top: -120, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 800, height: 500, borderRadius: '50%',
|
||||
background: 'radial-gradient(ellipse, rgba(167,139,250,.2), transparent 65%)',
|
||||
filter: 'blur(50px)', pointerEvents: 'none',
|
||||
}} />
|
||||
)}
|
||||
<StarField isLight={isLight} />
|
||||
|
||||
{/* Interaction hint */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 20, right: 28, zIndex: 5,
|
||||
...MONO, fontSize: 9.5, letterSpacing: 2, color: 'rgba(167,139,250,.55)',
|
||||
...MONO, fontSize: 9.5, letterSpacing: 2,
|
||||
color: isLight ? 'rgba(109,77,194,.6)' : 'rgba(167,139,250,.55)',
|
||||
textTransform: 'uppercase', fontWeight: 600,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
}}>
|
||||
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#4ade80', boxShadow: '0 0 8px #4ade80' }} />
|
||||
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: isLight ? '#16a34a' : '#4ade80', boxShadow: `0 0 8px ${isLight ? '#16a34a' : '#4ade80'}` }} />
|
||||
{de ? 'Element anklicken' : 'Click any element'}
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<div style={{ position: 'relative', padding: '30px 48px 0', textAlign: 'center', zIndex: 2 }}>
|
||||
<div style={{ ...MONO, fontSize: 10, letterSpacing: 4, color: 'rgba(167,139,250,.7)', textTransform: 'uppercase', fontWeight: 600, marginBottom: 6 }}>
|
||||
<div style={{ ...MONO, fontSize: 10, letterSpacing: 4, color: isLight ? 'rgba(109,77,194,.7)' : 'rgba(167,139,250,.7)', textTransform: 'uppercase', fontWeight: 600, marginBottom: 6 }}>
|
||||
{de ? 'Alleinstellungsmerkmal' : 'Unique Selling Proposition'}
|
||||
</div>
|
||||
<h1 style={{
|
||||
fontSize: 34, fontWeight: 700, letterSpacing: -0.8, margin: 0, lineHeight: 1.2,
|
||||
color: '#f7f5fc', animation: 'uspHeading 4s ease-in-out infinite',
|
||||
color: isLight ? '#1a1a2e' : '#f7f5fc',
|
||||
animation: isLight ? undefined : 'uspHeading 4s ease-in-out infinite',
|
||||
}}>
|
||||
{de ? 'Die erste Plattform, die ' : 'The first platform bridging '}
|
||||
<span style={{ background: 'linear-gradient(90deg, #c4aaff, #a78bfa)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
|
||||
@@ -634,7 +694,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
|
||||
{/* Bridge */}
|
||||
<div style={{ position: 'relative', margin: '14px 48px 0', height: 340 }}>
|
||||
<BridgeConnectors />
|
||||
<BridgeConnectors isLight={isLight} />
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 2,
|
||||
display: 'grid', gridTemplateColumns: '1fr 260px 1fr', gap: 0,
|
||||
@@ -643,11 +703,11 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
{/* LEFT — Compliance */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', paddingRight: 20 }}>
|
||||
<div style={{ height: 40, marginBottom: 36 }}>
|
||||
<ColHeader side="left" label="Compliance" color="#a78bfa" icon="⎈" sub="policy · audit · proof" />
|
||||
<ColHeader side="left" label="Compliance" color="#a78bfa" icon="⎈" sub="policy · audit · proof" isLight={isLight} />
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="left" tint="#a78bfa"
|
||||
<PillarRow side="left" tint="#a78bfa" isLight={isLight}
|
||||
title={de ? 'RFQ-Prüfung' : 'RFQ Verification'}
|
||||
body={de
|
||||
? 'Kunden-Anforderungsdokumente automatisch gegen aktuellen Code validiert. Abweichungen erkannt, Änderungen vorgeschlagen.'
|
||||
@@ -659,7 +719,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="left" tint="#c084fc"
|
||||
<PillarRow side="left" tint="#c084fc" isLight={isLight}
|
||||
title={de ? 'Prozess-Compliance' : 'Process Compliance'}
|
||||
body={de
|
||||
? 'Vom Audit-Finding bis zur Code-Änderung vollständig automatisiert. Rollen, Fristen, Eskalation — End-to-End verwaltet.'
|
||||
@@ -679,18 +739,18 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLDivElement).style.transform = 'scale(1.05)'; (e.currentTarget as HTMLDivElement).style.filter = 'brightness(1.15)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.transform = 'scale(1)'; (e.currentTarget as HTMLDivElement).style.filter = 'brightness(1)' }}
|
||||
>
|
||||
<CentralHub caption={de ? 'Immer in Sync' : 'Always in sync'} />
|
||||
<CentralHub caption={de ? 'Immer in Sync' : 'Always in sync'} isLight={isLight} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RIGHT — Code */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', paddingLeft: 20 }}>
|
||||
<div style={{ height: 40, marginBottom: 36 }}>
|
||||
<ColHeader side="right" label="Code" color="#fbbf24" icon="⟨/⟩" sub="sast · dast · sbom" />
|
||||
<ColHeader side="right" label="Code" color="#fbbf24" icon="⟨/⟩" sub="sast · dast · sbom" isLight={isLight} />
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="right" tint="#fbbf24"
|
||||
<PillarRow side="right" tint="#fbbf24" isLight={isLight}
|
||||
title={de ? 'Bidirektional' : 'Bidirectional Sync'}
|
||||
body={de
|
||||
? 'Compliance-Änderungen fliessen in Code; Code-Änderungen aktualisieren Docs. Beide Seiten immer synchron — kein Informationsverlust.'
|
||||
@@ -702,7 +762,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
</div>
|
||||
<div style={{ height: 110, display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<PillarRow side="right" tint="#f59e0b"
|
||||
<PillarRow side="right" tint="#f59e0b" isLight={isLight}
|
||||
title={de ? 'Kontinuierlich' : 'Continuous, Not Yearly'}
|
||||
body={de
|
||||
? 'Klassische Audits einmal jährlich — wir prüfen bei jedem Commit. Findings werden sofort zu Tickets mit konkreten Fixes.'
|
||||
@@ -719,16 +779,17 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
{/* Under the Hood */}
|
||||
<div style={{ position: 'relative', zIndex: 2, padding: '0 48px 16px' }}>
|
||||
<div style={{
|
||||
...MONO, fontSize: 9.5, letterSpacing: 3.5, color: 'rgba(167,139,250,.7)',
|
||||
...MONO, fontSize: 9.5, letterSpacing: 3.5,
|
||||
color: isLight ? 'rgba(109,77,194,.7)' : 'rgba(167,139,250,.7)',
|
||||
textTransform: 'uppercase', fontWeight: 600, textAlign: 'center', marginBottom: 12,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 14,
|
||||
}}>
|
||||
<span style={{ width: 80, height: 1, background: 'linear-gradient(90deg, transparent, rgba(167,139,250,.5))' }} />
|
||||
<span style={{ width: 80, height: 1, background: isLight ? 'linear-gradient(90deg, transparent, rgba(109,77,194,.4))' : 'linear-gradient(90deg, transparent, rgba(167,139,250,.5))' }} />
|
||||
{de ? 'Unter der Haube' : 'Under the Hood'}
|
||||
<span style={{ width: 80, height: 1, background: 'linear-gradient(270deg, transparent, rgba(167,139,250,.5))' }} />
|
||||
<span style={{ width: 80, height: 1, background: isLight ? 'linear-gradient(270deg, transparent, rgba(109,77,194,.4))' : 'linear-gradient(270deg, transparent, rgba(167,139,250,.5))' }} />
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
|
||||
<FeatureCard tint="#a78bfa" icon="⇄"
|
||||
<FeatureCard tint="#a78bfa" icon="⇄" isLight={isLight}
|
||||
title={de ? 'End-to-End Rückverfolgbarkeit' : 'End-to-End Traceability'}
|
||||
body={de
|
||||
? 'Regulatorische Anforderungen deterministisch mit Code und System verknüpft — inklusive revisionssicherem Evidence-Layer.'
|
||||
@@ -737,7 +798,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
active={detail?.title === details.trace.title}
|
||||
Ticker={TickTrace}
|
||||
/>
|
||||
<FeatureCard tint="#c084fc" icon="◉"
|
||||
<FeatureCard tint="#c084fc" icon="◉" isLight={isLight}
|
||||
title={de ? 'Continuous Compliance Engine' : 'Continuous Compliance Engine'}
|
||||
body={de
|
||||
? 'Automatische Audit-Validierung bei jeder Änderung — Code, Infrastruktur, Prozesse.'
|
||||
@@ -746,7 +807,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
active={detail?.title === details.engine.title}
|
||||
Ticker={TickEngine}
|
||||
/>
|
||||
<FeatureCard tint="#fbbf24" icon="✦"
|
||||
<FeatureCard tint="#fbbf24" icon="✦" isLight={isLight}
|
||||
title={de ? 'Compliance Optimizer' : 'Compliance Optimizer'}
|
||||
body={de
|
||||
? 'Maximale Compliance pro €. Findet den Sweet Spot zwischen Geschwindigkeit und Risiko.'
|
||||
@@ -755,7 +816,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
active={detail?.title === details.opt.title}
|
||||
Ticker={TickOptimizer}
|
||||
/>
|
||||
<FeatureCard tint="#f59e0b" icon="◎"
|
||||
<FeatureCard tint="#f59e0b" icon="◎" isLight={isLight}
|
||||
title={de ? 'EU-Trust & Governance' : 'EU Trust & Governance'}
|
||||
body={de
|
||||
? 'DSGVO, NIS-2, DORA, EU AI Act — eine Plattform, EU-souverän.'
|
||||
@@ -771,10 +832,12 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 2, margin: '0 48px 24px',
|
||||
padding: '11px 20px', borderRadius: 10,
|
||||
background: 'linear-gradient(90deg, rgba(251,191,36,.08), rgba(167,139,250,.1), rgba(139,92,246,.08))',
|
||||
border: '1px solid rgba(167,139,250,.22)',
|
||||
background: isLight
|
||||
? 'linear-gradient(90deg, rgba(251,191,36,.08), rgba(167,139,250,.08), rgba(139,92,246,.06))'
|
||||
: 'linear-gradient(90deg, rgba(251,191,36,.08), rgba(167,139,250,.1), rgba(139,92,246,.08))',
|
||||
border: `1px solid ${isLight ? 'rgba(167,139,250,.2)' : 'rgba(167,139,250,.22)'}`,
|
||||
textAlign: 'center', fontStyle: 'italic',
|
||||
fontSize: 12.5, color: 'rgba(236,233,247,.82)',
|
||||
fontSize: 12.5, color: isLight ? '#475569' : 'rgba(236,233,247,.82)',
|
||||
}}>
|
||||
<span style={{ color: '#fbbf24', marginRight: 8, fontSize: 16, verticalAlign: -2 }}>"</span>
|
||||
{de
|
||||
@@ -783,7 +846,7 @@ export default function USPSlide({ lang }: USPSlideProps) {
|
||||
<span style={{ color: '#fbbf24', marginLeft: 8, fontSize: 16, verticalAlign: -2 }}>"</span>
|
||||
</div>
|
||||
|
||||
<DetailModal item={detail} onClose={close} />
|
||||
<DetailModal item={detail} onClose={close} isLight={isLight} />
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user