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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user