fix(pitch-print): page count, Finanzplan loading, visual energy
Build pitch-deck / build-push-deploy (push) Successful in 1m59s
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 52s
CI / test-python-voice (push) Successful in 37s
CI / test-bqas (push) Successful in 36s

Two bug fixes plus the requested visual rework — the deck now looks like a pitch deck, not a research paper.

Bugs:
- BASE_PAGES corrected from 28 to 29; disclaimer no longer shows "29/28"
- fmResults + fmAssumptions now load for the standard PDF, not only when financial=true; Finanzplan annex + KPI dashboard now render

Visual rework (per user: "graphic elements, not just text"):
- Cover: split layout — indigo block left (tagline + hero stats + version meta), white block right with oversized title and key terms
- Modules: 12 lucide icons in indigo-50 tiles (ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck, AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare)
- USP cards: icon-led card heads with FileSearch/ArrowLeftRight/Repeat/Layers/etc.; LoopDiagram SVG on the closing "Compliance ↔ Code" hub
- How It Works: StepStrip primitive with visible right-arrows between steps
- Market: nested-rectangle MarketFunnel (TAM > SAM > SOM) replaces three stacked boxes
- Customer Savings: 4 hero KPIs + ComparisonBars (today vs. with BP) per cost item
- The Ask: DonutChart for use-of-funds
- Cap Table: DonutChart for equity distribution
- Finanzplan p2: 2×2 chart grid — Revenue (bars), EBIT (bars, tone by sign), Cash balance (line+area), Headcount (bars)
- Architecture: ArchitectureDiagram primitive (3 tiers, vertical arrows between tiers)
- AI Pipeline: PipelineFlow primitive (4 stages, horizontal arrows)
- Team: founder photos (32×32mm) added; falls back to initials if photo_url missing

New primitives:
- PrintCharts.tsx — BarChart, LineChart, ComparisonBars, DonutChart, ProgressBar, MarketFunnel
- PrintDiagrams.tsx — FlowNode, VArrow, HArrow, StepStrip, ArchitectureDiagram, LoopDiagram, PipelineFlow

All files under 500 LOC cap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-05-20 09:31:28 +02:00
parent 0d5ebcd27a
commit bb85ee2e27
9 changed files with 937 additions and 433 deletions
@@ -1,5 +1,6 @@
import { Language } from '@/lib/types' import { Language } from '@/lib/types'
import { Page, COLORS, Callout, DataTable, ThreeCol, Bullets } from './PrintLayout' import { Page, COLORS, Callout, DataTable, ThreeCol, Bullets } from './PrintLayout'
import { ArchitectureDiagram, PipelineFlow } from './PrintDiagrams'
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string } interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
@@ -158,68 +159,31 @@ export function PrintRegulatoryPage({ lang, pageNum, totalPages, versionName }:
export function PrintArchitecturePage({ lang, pageNum, totalPages, versionName }: SlideBase) { export function PrintArchitecturePage({ lang, pageNum, totalPages, versionName }: SlideBase) {
const de = lang === 'de' const de = lang === 'de'
return ( return (
<Page kicker="20" section={de ? 'ANHANG · SYSTEMARCHITEKTUR' : 'APPENDIX · SYSTEM ARCHITECTURE'} title={de ? 'Drei Produkt-Domänen. Eine LiteLLM-Gateway. Lokale Inferenz.' : 'Three product domains. One LiteLLM gateway. Local inference.'} subtitle={de ? 'BreakPilot · CERTifAI · Compliance Scanner, über LiteLLM-Proxy mit lokalen LLM-Inferenz-Knoten verbunden. 100% EU, kein US-Anbieter.' : 'BreakPilot · CERTifAI · Compliance Scanner, connected via LiteLLM proxy to local LLM inference nodes. 100% EU, no US providers.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="20" section={de ? 'ANHANG · SYSTEMARCHITEKTUR' : 'APPENDIX · SYSTEM ARCHITECTURE'} title={de ? 'Drei Produkt-Domänen. Ein LiteLLM-Gateway. Lokale Inferenz.' : 'Three product domains. One LiteLLM gateway. Local inference.'} subtitle={de ? 'BreakPilot · CERTifAI · Compliance Scanner, über LiteLLM-Proxy mit lokalen Inferenz-Knoten verbunden. 100% EU, kein US-Anbieter.' : 'BreakPilot · CERTifAI · Compliance Scanner, connected via LiteLLM proxy to local inference nodes. 100% EU, no US providers.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<ArchitectureDiagram
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}> lang={lang}
{/* PRODUCT TIER */} product={[
<div style={{ marginBottom: '4mm' }}> { kicker: 'GENAI', title: 'CERTifAI', subtitle: de ? 'GenAI Mandantenportal' : 'GenAI Tenant Portal', tech: 'Rust · Dioxus · MongoDB · Keycloak · SearXNG · LangGraph', services: ['LiteLLM Dashboard', 'LibreChat + SSO', 'LangGraph Agents', 'MCP Hub'] },
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Produkt-Schicht' : 'Product Tier'}</div> { kicker: 'COMPLIANCE', title: 'COMPLAI', subtitle: de ? 'Compliance & Audit' : 'Compliance & Audit', tech: 'Next.js 15 · FastAPI · Go/Gin · PostgreSQL · Qdrant · Valkey', services: [de ? 'DSGVO/AI Act/NIS2 (25k+ Controls)' : 'GDPR/AI Act/NIS2 (25k+ controls)', de ? 'RAG (380+ Quellen)' : 'RAG (380+ sources)', de ? 'Control Pipeline (LLM)' : 'Control pipeline (LLM)', 'MCP Client'] },
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}> { kicker: 'SECURITY', title: 'Compliance Scanner', subtitle: de ? 'Code-Sicherheit' : 'Code Security', tech: 'Rust · Axum · MongoDB · Semgrep · Gitleaks · Syft', services: ['SAST · SBOM · CVE Pipeline', de ? 'KI-Triage (False Positives)' : 'AI Triage (false positives)', de ? 'KI-Pentest (autonom)' : 'AI Pentest (autonomous)', 'MCP Server'] },
{[ ]}
{ t: 'CERTifAI', s: de ? 'GenAI Mandantenportal' : 'GenAI Tenant Portal', tech: 'Rust · Dioxus · MongoDB · Keycloak · SearXNG · LangGraph', services: ['LiteLLM Dashboard', 'LibreChat + SSO', 'LangGraph Agents', 'MCP Hub'] }, proxy={{
{ t: 'COMPLAI', s: de ? 'Compliance & Audit' : 'Compliance & Audit', tech: 'Next.js 15 · FastAPI · Go/Gin · PostgreSQL · Qdrant · Valkey', services: [de ? 'DSGVO/AI Act/NIS2 (25k+ Controls)' : 'GDPR/AI Act/NIS2 (25k+ controls)', de ? 'RAG (380+ Quellen)' : 'RAG (380+ sources)', de ? 'Control Pipeline (LLM)' : 'Control pipeline (LLM)', 'MCP Client'] }, title: 'LiteLLM Proxy',
{ t: 'Compliance Scanner', s: de ? 'Code-Sicherheit' : 'Code Security', tech: 'Rust · Axum · MongoDB · Semgrep · Gitleaks · Syft', services: ['SAST · SBOM · CVE Pipeline', de ? 'KI-Triage (False Positives)' : 'AI Triage (false positives)', de ? 'KI-Pentest (autonom)' : 'AI Pentest (autonomous)', 'MCP Server'] }, subtitle: de ? 'KI-Gateway · Bearer-Auth · Rate-Limiting · PII-Filter · Spend-Tracking' : 'AI gateway · bearer auth · rate limiting · PII filter · spend tracking',
].map((p, i) => ( features: [
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '3mm 4mm' }}> de ? 'Token-Budget pro Mandant' : 'Per-tenant token budget',
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '0.5mm' }}>{p.t}</div> de ? 'PII-Guardrails alle Anfragen' : 'PII guardrails on all requests',
<div style={{ fontSize: '8pt', color: COLORS.indigo600, marginBottom: '2mm', fontWeight: 600 }}>{p.s}</div> de ? 'Anonyme EU-Web-Suche (SearXNG)' : 'Anonymous EU web search (SearXNG)',
<div style={{ fontSize: '7pt', color: COLORS.slate600, fontFamily: 'monospace', marginBottom: '2mm', lineHeight: 1.4 }}>{p.tech}</div> de ? 'Namespace-Isolierung pro API-Key' : 'Namespace isolation per API key',
<div> de ? 'Failover-Routing zwischen Modellen' : 'Failover routing between models',
{p.services.map((s, j) => ( ],
<div key={j} style={{ fontSize: '7.5pt', color: COLORS.slate700, padding: '0.8mm 0', borderTop: j > 0 ? `1px solid ${COLORS.slate100}` : 'none' }}>&middot; {s}</div> }}
))} inference={[
</div> { title: de ? 'LLM Inferenz' : 'LLM Inference', subtitle: de ? 'Lokale Sprachmodelle' : 'Local language models', tech: 'Qwen3-32B · Qwen3-Coder-30B · DeepSeek-R1-8B · Ollama', desc: de ? 'Vollständig lokal, air-gap fähig, GPU-optimiert. Daten verlassen nie den Server.' : 'Fully local, air-gap capable, GPU-optimized. Data never leaves the server.' },
</div> { title: 'Embeddings', subtitle: de ? 'Semantische Suche' : 'Semantic search', tech: 'bge-m3 · Qdrant Vector DB · Sentence-Transformers', desc: de ? 'RAG mit 380+ Rechtsquellen indexiert, multilinguale Einbettungen, lokal.' : 'RAG with 380+ legal sources indexed, multilingual embeddings, local.' },
))} { title: de ? 'KI-Tools' : 'AI Tools', subtitle: de ? 'Web-Suche & MCP' : 'Web search & MCP', tech: 'SearXNG · MCP Protocol · Semgrep API · Gitleaks API', desc: de ? 'Anonymisierte EU-Web-Suche, MCP-Integration für Audit-Dokumente und Code-Findings.' : 'Anonymized EU web search, MCP integration for audit docs and code findings.' },
</div> ]}
</div> />
{/* PROXY TIER */}
<div style={{ marginBottom: '4mm' }}>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Gateway-Schicht (KI-Proxy mit Guardrails)' : 'Gateway Tier (AI proxy with guardrails)'}</div>
<div style={{ border: `1px solid ${COLORS.amber600}`, borderTop: `2px solid ${COLORS.amber600}`, padding: '3mm 4mm', background: COLORS.amber50, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4mm', marginBottom: '2mm' }}>
<span style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900 }}>LiteLLM Proxy</span>
<span style={{ fontSize: '8pt', color: COLORS.amber700, fontWeight: 600 }}>{de ? 'KI-Gateway · Bearer-Auth · Rate-Limiting · PII-Filter · Spend-Tracking' : 'AI gateway · bearer auth · rate limiting · PII filter · spend tracking'}</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '3mm', fontSize: '7.5pt', color: COLORS.slate700 }}>
<div>&middot; {de ? 'Token-Budget pro Mandant' : 'Per-tenant token budget'}</div>
<div>&middot; {de ? 'PII-Guardrails alle Anfragen' : 'PII guardrails on all requests'}</div>
<div>&middot; {de ? 'Anonyme EU-Web-Suche (SearXNG)' : 'Anonymous EU web search (SearXNG)'}</div>
<div>&middot; {de ? 'Namespace-Isolierung pro API-Key' : 'Namespace isolation per API key'}</div>
<div>&middot; {de ? 'Failover-Routing zwischen Modellen' : 'Failover routing between models'}</div>
</div>
</div>
</div>
{/* INFERENCE TIER */}
<div>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Inferenz-Schicht (lokal, air-gap-fähig)' : 'Inference Tier (local, air-gap capable)'}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
{[
{ t: de ? 'LLM Inferenz' : 'LLM Inference', tech: 'Qwen3-32B · Qwen3-Coder-30B · DeepSeek-R1-8B · Ollama', desc: de ? 'Vollständig lokal, air-gap fähig, GPU-optimiert. Daten verlassen nie den Server.' : 'Fully local, air-gap capable, GPU-optimized. Data never leaves the server.' },
{ t: 'Embeddings', tech: 'bge-m3 · Qdrant Vector DB · Sentence-Transformers', desc: de ? 'RAG mit 380+ Rechtsquellen indexiert, multilinguale Einbettungen, lokal.' : 'RAG with 380+ legal sources indexed, multilingual embeddings, local.' },
{ t: de ? 'KI-Tools' : 'AI Tools', tech: 'SearXNG · MCP Protocol · Semgrep API · Gitleaks API', desc: de ? 'Anonymisierte EU-Web-Suche, MCP-Integration für Audit-Dokumente und Code-Findings.' : 'Anonymized EU web search, MCP integration for audit docs and code findings.' },
].map((p, i) => (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, padding: '3mm 4mm' }}>
<div style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '1mm' }}>{p.t}</div>
<div style={{ fontSize: '7pt', color: COLORS.slate500, fontFamily: 'monospace', marginBottom: '2mm', lineHeight: 1.4 }}>{p.tech}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.45 }}>{p.desc}</div>
</div>
))}
</div>
</div>
</div>
</Page> </Page>
) )
} }
@@ -318,23 +282,14 @@ export function PrintAIPipelinePage({ lang, pageNum, totalPages, versionName }:
{/* Pipeline flow */} {/* Pipeline flow */}
<div> <div>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Pipeline-Fluss' : 'Pipeline flow'}</div> <div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{de ? 'Pipeline-Fluss' : 'Pipeline flow'}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4mm' }}> <PipelineFlow
{[ stages={[
{ n: '01', t: 'Ingestion', d: de ? 'Rohdokumente (PDF, HTML, XML, Word) → Pass 0a OCR → Pass 0b Strukturierung. Markdown + Metadaten.' : 'Raw docs (PDF, HTML, XML, Word) → Pass 0a OCR → Pass 0b structuring. Markdown + metadata.', kpi: '385 docs' }, { n: '01', t: 'Ingestion', d: de ? 'Rohdokumente (PDF, HTML, XML, Word) → Pass 0a OCR → Pass 0b Strukturierung. Markdown + Metadaten.' : 'Raw docs (PDF, HTML, XML, Word) → Pass 0a OCR → Pass 0b structuring. Markdown + metadata.', kpi: '385 docs' },
{ n: '02', t: 'Chunking + Embed', d: de ? 'Semantisches Chunking, Sentence-Transformers (bge-m3), Qdrant Vector DB, Multi-Index.' : 'Semantic chunking, sentence-transformers (bge-m3), Qdrant vector DB, multi-index.', kpi: '~280k chunks' }, { n: '02', t: 'Chunking + Embed', d: de ? 'Semantisches Chunking, Sentence-Transformers (bge-m3), Qdrant Vector DB, Multi-Index.' : 'Semantic chunking, sentence-transformers (bge-m3), Qdrant vector DB, multi-index.', kpi: '~280k chunks' },
{ n: '03', t: 'Control Extraction', d: de ? 'BatchDedup → LLM-Extraktor (Qwen3-32B) → Strukturierte Controls mit Quellenangabe + Confidence.' : 'BatchDedup → LLM extractor (Qwen3-32B) → structured controls with source + confidence.', kpi: '25k+ controls' }, { n: '03', t: 'Control Extraction', d: de ? 'BatchDedup → LLM-Extraktor (Qwen3-32B) → Strukturierte Controls mit Quellenangabe + Confidence.' : 'BatchDedup → LLM extractor (Qwen3-32B) → structured controls with source + confidence.', kpi: '25k+ controls' },
{ n: '04', t: 'Quality Assurance', d: de ? 'BQAS (Batch Quality Assessment System): Cross-Validation, Inkonsistenz-Detection, Audit-Sampling.' : 'BQAS (Batch Quality Assessment System): cross-validation, inconsistency detection, audit sampling.', kpi: '>99% precision' }, { n: '04', t: 'Quality Assurance', d: de ? 'BQAS (Batch Quality Assessment System): Cross-Validation, Inkonsistenz-Detection, Audit-Sampling.' : 'BQAS (Batch Quality Assessment System): cross-validation, inconsistency detection, audit sampling.', kpi: '>99% precision' },
].map((s, i) => ( ]}
<div key={i} style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '4mm', display: 'flex', flexDirection: 'column' }}> />
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
<span style={{ fontSize: '14pt', fontWeight: 800, color: COLORS.indigo600, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>{s.n}</span>
<span style={{ fontSize: '7.5pt', color: COLORS.emerald700, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{s.kpi}</span>
</div>
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2mm', lineHeight: 1.2 }}>{s.t}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.5 }}>{s.d}</div>
</div>
))}
</div>
</div> </div>
{/* Agent system */} {/* Agent system */}
@@ -0,0 +1,267 @@
import React from 'react'
import { COLORS } from './PrintLayout'
/* ====================================================================== */
/* CHARTS */
/* ====================================================================== */
interface BarSeries {
label: string
value: number
/** Optional secondary label rendered above the bar value (e.g. "Mio."). */
unit?: string
tone?: 'default' | 'positive' | 'negative' | 'accent'
}
export function BarChart({
data, height = 36, maxOverride, formatValue, title, yAxisHint,
}: {
data: BarSeries[]
height?: number // mm
maxOverride?: number
formatValue?: (n: number) => string
title?: string
yAxisHint?: string
}) {
const max = maxOverride ?? Math.max(...data.map(d => d.value), 1)
const fmt = formatValue ?? ((n: number) => n.toLocaleString('de-DE'))
const ticks = [0, 0.25, 0.5, 0.75, 1].map(t => Math.round(max * t))
return (
<div>
{(title || yAxisHint) && (
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
{title && <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>{title}</div>}
{yAxisHint && <div style={{ fontSize: '7pt', color: COLORS.slate400 }}>{yAxisHint}</div>}
</div>
)}
<div style={{ display: 'flex', alignItems: 'stretch', gap: '4mm', position: 'relative' }}>
{/* Y-axis ticks */}
<div style={{ width: '14mm', display: 'flex', flexDirection: 'column', justifyContent: 'space-between', height: `${height}mm`, fontSize: '6.5pt', color: COLORS.slate400, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
{ticks.slice().reverse().map((t, i) => (
<div key={i}>{fmt(t)}</div>
))}
</div>
{/* Bars + grid */}
<div style={{ flex: 1, position: 'relative', height: `${height}mm`, borderLeft: `1px solid ${COLORS.slate300}`, borderBottom: `1px solid ${COLORS.slate300}` }}>
{/* Grid lines */}
{ticks.slice(1).map((_, i) => (
<div key={i} style={{ position: 'absolute', left: 0, right: 0, top: `${(1 - (i + 1) / 4) * 100}%`, height: '0.5pt', background: COLORS.slate100, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
))}
{/* Bars */}
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'flex-end', gap: '3mm', padding: '0 2mm' }}>
{data.map((d, i) => {
const h = (d.value / max) * 100
const color = d.tone === 'positive' ? COLORS.emerald600
: d.tone === 'negative' ? COLORS.red600
: d.tone === 'accent' ? COLORS.amber600
: COLORS.indigo600
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', height: '100%', position: 'relative' }}>
<div style={{ fontSize: '7pt', fontWeight: 700, color: color, marginBottom: '0.8mm', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{fmt(d.value)}</div>
<div style={{ width: '100%', height: `${h}%`, minHeight: '1pt', background: color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
</div>
)
})}
</div>
</div>
</div>
{/* X-axis labels */}
<div style={{ display: 'flex', gap: '3mm', paddingLeft: '18mm', paddingRight: '2mm', marginTop: '1mm' }}>
{data.map((d, i) => (
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{d.label}</div>
))}
</div>
</div>
)
}
interface LinePoint { label: string; value: number }
export function LineChart({
data, height = 36, formatValue, color = COLORS.indigo600, title, fill = true,
}: {
data: LinePoint[]
height?: number
formatValue?: (n: number) => string
color?: string
title?: string
fill?: boolean
}) {
if (data.length < 2) return null
const max = Math.max(...data.map(d => d.value), 1)
const fmt = formatValue ?? ((n: number) => n.toLocaleString('de-DE'))
const w = 100
const points = data.map((d, i) => ({
x: (i / (data.length - 1)) * w,
y: 100 - (d.value / max) * 100,
v: d.value,
label: d.label,
}))
const pathD = points.map((p, i) => (i === 0 ? `M${p.x},${p.y}` : `L${p.x},${p.y}`)).join(' ')
const areaD = `${pathD} L100,100 L0,100 Z`
return (
<div>
{title && <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate600, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{title}</div>}
<div style={{ position: 'relative', height: `${height}mm`, borderLeft: `1px solid ${COLORS.slate300}`, borderBottom: `1px solid ${COLORS.slate300}` }}>
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
{/* Grid */}
{[0.25, 0.5, 0.75].map(t => (
<line key={t} x1="0" x2="100" y1={t * 100} y2={t * 100} stroke={COLORS.slate100} strokeWidth="0.3" />
))}
{fill && <path d={areaD} fill={color} fillOpacity="0.12" />}
<path d={pathD} fill="none" stroke={color} strokeWidth="0.6" strokeLinejoin="round" strokeLinecap="round" />
{points.map((p, i) => (
<circle key={i} cx={p.x} cy={p.y} r="1.2" fill={color} />
))}
</svg>
{/* Value labels above each point */}
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
{points.map((p, i) => (
<div key={i} style={{ position: 'absolute', left: `${p.x}%`, top: `${p.y}%`, transform: 'translate(-50%, -120%)', fontSize: '6.5pt', fontWeight: 700, color, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' }}>{fmt(p.v)}</div>
))}
</div>
</div>
{/* X labels */}
<div style={{ display: 'flex', marginTop: '1mm' }}>
{points.map((p, i) => (
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>{p.label}</div>
))}
</div>
</div>
)
}
/** Horizontal stacked-bar comparison (e.g. "you pay" vs "you save") */
export function ComparisonBars({
rows, formatValue,
}: {
rows: { label: string; bars: { tone: 'positive' | 'negative' | 'accent' | 'default'; value: number; cap?: string }[] }[]
formatValue?: (n: number) => string
}) {
const max = Math.max(...rows.flatMap(r => r.bars.map(b => b.value)), 1)
const fmt = formatValue ?? ((n: number) => n.toLocaleString('de-DE'))
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '3mm' }}>
{rows.map((row, i) => (
<div key={i}>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate700, marginBottom: '1.5mm' }}>{row.label}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1mm' }}>
{row.bars.map((b, j) => {
const w = (b.value / max) * 100
const color = b.tone === 'positive' ? COLORS.emerald600
: b.tone === 'negative' ? COLORS.red600
: b.tone === 'accent' ? COLORS.amber600
: COLORS.indigo600
return (
<div key={j} style={{ display: 'flex', alignItems: 'center', gap: '3mm' }}>
<div style={{ flex: 1, position: 'relative', height: '4mm', background: COLORS.slate50, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${w}%`, background: color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
{b.cap && <div style={{ position: 'absolute', left: '2mm', top: 0, bottom: 0, display: 'flex', alignItems: 'center', fontSize: '7pt', color: '#ffffff', fontWeight: 700, letterSpacing: '0.04em' }}>{b.cap}</div>}
</div>
<div style={{ width: '20mm', textAlign: 'right', fontSize: '9pt', fontWeight: 800, color, fontVariantNumeric: 'tabular-nums' }}>{fmt(b.value)}</div>
</div>
)
})}
</div>
</div>
))}
</div>
)
}
/** Donut chart for percentages (use-of-funds, equity, etc.) */
export function DonutChart({
segments, size = 32, thickness = 6,
}: {
segments: { label: string; pct: number; color: string }[]
size?: number // mm
thickness?: number // mm
}) {
const R = 50
const r = R - (thickness / size) * 50
let acc = 0
const arcs = segments.map(s => {
const start = acc / 100 * Math.PI * 2 - Math.PI / 2
acc += s.pct
const end = acc / 100 * Math.PI * 2 - Math.PI / 2
const x1 = 50 + R * Math.cos(start), y1 = 50 + R * Math.sin(start)
const x2 = 50 + R * Math.cos(end), y2 = 50 + R * Math.sin(end)
const x3 = 50 + r * Math.cos(end), y3 = 50 + r * Math.sin(end)
const x4 = 50 + r * Math.cos(start), y4 = 50 + r * Math.sin(start)
const large = s.pct > 50 ? 1 : 0
const d = `M${x1},${y1} A${R},${R} 0 ${large} 1 ${x2},${y2} L${x3},${y3} A${r},${r} 0 ${large} 0 ${x4},${y4} Z`
return { d, color: s.color, pct: s.pct, label: s.label }
})
return (
<svg viewBox="0 0 100 100" style={{ width: `${size}mm`, height: `${size}mm`, display: 'block' }}>
{arcs.map((a, i) => (
<path key={i} d={a.d} fill={a.color} />
))}
</svg>
)
}
/** Progress meter (0-100%) — horizontal */
export function ProgressBar({ pct, color = COLORS.indigo600, label, value }: { pct: number; color?: string; label?: string; value?: string }) {
return (
<div>
{(label || value) && (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1mm', fontSize: '8pt' }}>
{label && <span style={{ color: COLORS.slate700, fontWeight: 500 }}>{label}</span>}
{value && <span style={{ color: COLORS.slate900, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{value}</span>}
</div>
)}
<div style={{ height: '3mm', background: COLORS.slate100, position: 'relative', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: `${Math.min(100, Math.max(0, pct))}%`, background: color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
</div>
</div>
)
}
/** Nested market-size visual (TAM/SAM/SOM) */
export function MarketFunnel({
tam, sam, som, fmt,
}: {
tam: { value: number; label: string; growth?: number; note?: string }
sam: { value: number; label: string; growth?: number; note?: string }
som: { value: number; label: string; growth?: number; note?: string }
fmt: (v: number) => string
}) {
const samPct = sam.value / tam.value
const somPct = som.value / tam.value
return (
<div>
{/* TAM outer */}
<div style={{ border: `1px solid ${COLORS.slate300}`, padding: '5mm', position: 'relative' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1.5mm' }}>
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.14em' }}>TAM &middot; {tam.label}</span>
{tam.growth != null && <span style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600 }}>+{tam.growth}% p.a.</span>}
</div>
<div style={{ fontSize: '32pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1, letterSpacing: '-0.025em', fontVariantNumeric: 'tabular-nums' }}>{fmt(tam.value)}</div>
{tam.note && <div style={{ fontSize: '8pt', color: COLORS.slate600, marginTop: '2mm', maxWidth: '120mm' }}>{tam.note}</div>}
{/* SAM inner */}
<div style={{ marginTop: '4mm', marginLeft: '10mm', border: `1px solid ${COLORS.indigo600}`, background: COLORS.indigo50, padding: '4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1.5mm' }}>
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.indigo700, textTransform: 'uppercase', letterSpacing: '0.14em' }}>SAM &middot; {sam.label}</span>
{sam.growth != null && <span style={{ fontSize: '7.5pt', color: COLORS.indigo700, fontWeight: 600 }}>+{sam.growth}% p.a. &middot; {Math.round(samPct * 100)}% TAM</span>}
</div>
<div style={{ fontSize: '26pt', fontWeight: 800, color: COLORS.indigo700, lineHeight: 1, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{fmt(sam.value)}</div>
{sam.note && <div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm' }}>{sam.note}</div>}
{/* SOM inner-inner */}
<div style={{ marginTop: '3mm', marginLeft: '8mm', border: `2px solid ${COLORS.emerald600}`, background: COLORS.emerald50, padding: '4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1.5mm' }}>
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.emerald700, textTransform: 'uppercase', letterSpacing: '0.14em' }}>SOM &middot; {som.label}</span>
{som.growth != null && <span style={{ fontSize: '7.5pt', color: COLORS.emerald700, fontWeight: 600 }}>+{som.growth}% p.a. &middot; {(somPct * 100).toFixed(1)}% TAM</span>}
</div>
<div style={{ fontSize: '24pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{fmt(som.value)}</div>
{som.note && <div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm' }}>{som.note}</div>}
</div>
</div>
</div>
</div>
)
}
@@ -46,12 +46,13 @@ export default function PrintDeck({ pitchData, versionName, fmResults, fmAssumpt
const hasFinancialDetail = financial && annualRows.length > 0 const hasFinancialDetail = financial && annualRows.length > 0
const de = lang === 'de' const de = lang === 'de'
// Base standard PDF: 28 pages (25 slides, 3 of them 2-page: exec-summary, usp, competition; finanzplan annex is 2 pages) // Base standard PDF: 29 physical pages.
// 1 (exec1) + 1 (exec2) + 1 (cover) + 1 (problem) + 1 (solution) + 2 (usp) + 1 (regL) + 1 (product) + // 2 (exec) + 1 (cover) + 1 (problem) + 1 (solution) + 2 (usp) + 1 (regL) +
// 1 (how) + 1 (market) + 1 (bm) + 1 (traction) + 2 (competition) + 1 (team) + 1 (ask) + 1 (savings) + // 1 (product) + 1 (how) + 1 (market) + 1 (bm) + 1 (milestones) + 2 (competition) +
// 1 (strategy) + 2 (finanzplan) + 1 (assumptions) + 1 (regulatory) + 1 (architecture) + 1 (engineering) + // 1 (team) + 1 (ask) + 1 (savings) + 1 (strategy) + 2 (finanzplan) +
// 1 (aipipeline) + 1 (risks) + 1 (glossary) + 1 (disclaimer) = 28 // 1 (assumptions) + 1 (regulatory) + 1 (architecture) + 1 (engineering) +
const BASE_PAGES = 28 // 1 (aipipeline) + 1 (risks) + 1 (glossary) + 1 (disclaimer) = 29
const BASE_PAGES = 29
const totalPages = BASE_PAGES + (hasFinancialDetail ? 1 : 0) + (hasCapTable ? 1 : 0) const totalPages = BASE_PAGES + (hasFinancialDetail ? 1 : 0) + (hasCapTable ? 1 : 0)
useEffect(() => { useEffect(() => {
@@ -0,0 +1,263 @@
import React from 'react'
import { COLORS } from './PrintLayout'
/* ====================================================================== */
/* DIAGRAMS */
/* ====================================================================== */
/** A single "node" in a flow / architecture diagram. */
export function FlowNode({
title, subtitle, items, accent = COLORS.indigo600, icon, kicker, footer,
}: {
title: string
subtitle?: string
items?: string[]
accent?: string
icon?: React.ReactNode
kicker?: string
footer?: string
}) {
return (
<div style={{
border: `1px solid ${COLORS.slate200}`,
borderTop: `3px solid ${accent}`,
background: '#ffffff',
padding: '3mm 4mm',
display: 'flex',
flexDirection: 'column',
WebkitPrintColorAdjust: 'exact',
printColorAdjust: 'exact',
}}>
{kicker && (
<div style={{ fontSize: '6.5pt', fontWeight: 700, color: accent, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: '1.5mm' }}>{kicker}</div>
)}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '3mm', marginBottom: '1mm' }}>
{icon && <div style={{ flexShrink: 0, color: accent, marginTop: '0.5mm' }}>{icon}</div>}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15 }}>{title}</div>
{subtitle && <div style={{ fontSize: '8pt', color: accent, fontWeight: 600, marginTop: '0.5mm' }}>{subtitle}</div>}
</div>
</div>
{items && items.length > 0 && (
<div style={{ marginTop: '2mm', display: 'flex', flexDirection: 'column' }}>
{items.map((it, i) => (
<div key={i} style={{
fontSize: '7.5pt',
color: COLORS.slate700,
padding: '1mm 0',
borderTop: i > 0 ? `1px solid ${COLORS.slate100}` : 'none',
lineHeight: 1.35,
}}>{it}</div>
))}
</div>
)}
{footer && (
<div style={{ marginTop: '2mm', paddingTop: '1.5mm', borderTop: `1px solid ${COLORS.slate100}`, fontSize: '6.5pt', color: COLORS.slate500, fontFamily: 'monospace', lineHeight: 1.4 }}>{footer}</div>
)}
</div>
)
}
/** Vertical down arrow (between rows of a flow diagram). */
export function VArrow({ color = COLORS.slate400, label }: { color?: string; label?: string }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '1.5mm 0' }}>
<svg viewBox="0 0 24 32" style={{ width: '6mm', height: '6mm' }}>
<line x1="12" y1="0" x2="12" y2="24" stroke={color} strokeWidth="1.5" />
<polyline points="6,22 12,30 18,22" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{label && <div style={{ fontSize: '6.5pt', color: COLORS.slate500, marginTop: '0.5mm', fontWeight: 600 }}>{label}</div>}
</div>
)
}
/** Horizontal right arrow (between steps of a horizontal flow). */
export function HArrow({ color = COLORS.slate400, label }: { color?: string; label?: string }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minWidth: '6mm', position: 'relative' }}>
<svg viewBox="0 0 32 24" style={{ width: '8mm', height: '5mm' }}>
<line x1="0" y1="12" x2="24" y2="12" stroke={color} strokeWidth="1.5" />
<polyline points="20,6 30,12 20,18" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{label && <div style={{ fontSize: '6.5pt', color: COLORS.slate500, marginTop: '0.5mm', fontWeight: 600, textAlign: 'center', whiteSpace: 'nowrap' }}>{label}</div>}
</div>
)
}
/** Horizontal step strip with built-in arrows between items. */
export function StepStrip({
steps, accent = COLORS.indigo600,
}: {
steps: { n: string; t: string; d: string }[]
accent?: string
}) {
return (
<div style={{ display: 'flex', alignItems: 'stretch' }}>
{steps.map((s, i) => (
<React.Fragment key={i}>
<div style={{ flex: 1, padding: '0 2mm', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '3mm', marginBottom: '2mm' }}>
<span style={{ fontSize: '24pt', fontWeight: 800, color: accent, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{s.n}</span>
<div style={{ flex: 1, height: '1px', background: COLORS.slate200, alignSelf: 'center' }} />
</div>
<div style={{ fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '2mm', letterSpacing: '-0.005em' }}>{s.t}</div>
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, flex: 1 }}>{s.d}</div>
</div>
{i < steps.length - 1 && (
<div style={{ width: '6mm', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg viewBox="0 0 24 24" style={{ width: '6mm', height: '6mm', color: COLORS.slate300 }}>
<line x1="2" y1="12" x2="18" y2="12" stroke={COLORS.slate300} strokeWidth="1.5" />
<polyline points="14,6 20,12 14,18" fill="none" stroke={COLORS.slate300} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)}
</React.Fragment>
))}
</div>
)
}
/** 3-tier architecture diagram (product → proxy → inference) */
export function ArchitectureDiagram({
product, proxy, inference, lang,
}: {
product: { kicker: string; title: string; subtitle: string; tech: string; services: string[] }[]
proxy: { title: string; subtitle: string; features: string[] }
inference: { title: string; subtitle: string; tech: string; desc: string }[]
lang: 'de' | 'en'
}) {
const de = lang === 'de'
return (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{/* PRODUCT TIER */}
<div style={{ marginBottom: '2mm' }}>
<div style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: '2mm' }}>{de ? 'Produkt-Schicht' : 'Product Tier'}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
{product.map((p, i) => (
<FlowNode key={i} kicker={p.kicker} title={p.title} subtitle={p.subtitle} items={p.services} accent={COLORS.indigo600} footer={p.tech} />
))}
</div>
</div>
{/* Down arrows between product and proxy */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', marginBottom: '1mm' }}>
{[0, 1, 2].map(i => <VArrow key={i} color={COLORS.indigo500} />)}
</div>
{/* PROXY */}
<div style={{ marginBottom: '1mm' }}>
<div style={{
border: `1.5px solid ${COLORS.amber600}`,
borderTop: `3px solid ${COLORS.amber600}`,
background: COLORS.amber50,
padding: '3mm 5mm',
WebkitPrintColorAdjust: 'exact',
printColorAdjust: 'exact',
}}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
<div>
<span style={{ fontSize: '6.5pt', fontWeight: 700, color: COLORS.amber700, textTransform: 'uppercase', letterSpacing: '0.14em', marginRight: '3mm' }}>{de ? 'Gateway' : 'Gateway'}</span>
<span style={{ fontSize: '12pt', fontWeight: 800, color: COLORS.slate900 }}>{proxy.title}</span>
<span style={{ fontSize: '8.5pt', color: COLORS.amber700, marginLeft: '3mm', fontWeight: 600 }}>{proxy.subtitle}</span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: '3mm', fontSize: '7.5pt', color: COLORS.slate700 }}>
{proxy.features.map((f, i) => (
<div key={i} style={{ paddingLeft: '2mm', borderLeft: `1px solid ${COLORS.amber600}`, lineHeight: 1.3 }}>{f}</div>
))}
</div>
</div>
</div>
{/* Down arrows between proxy and inference */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', marginBottom: '1mm' }}>
{[0, 1, 2].map(i => <VArrow key={i} color={COLORS.amber600} />)}
</div>
{/* INFERENCE TIER */}
<div>
<div style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.14em', marginBottom: '2mm' }}>{de ? 'Inferenz-Schicht (lokal, air-gap-fähig)' : 'Inference Tier (local, air-gap capable)'}</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm' }}>
{inference.map((p, i) => (
<FlowNode key={i} title={p.title} subtitle={p.subtitle} items={[p.desc]} accent={COLORS.slate700} footer={p.tech} />
))}
</div>
</div>
</div>
)
}
/** Connected loop diagram for the USP "Compliance ↔ Code always in sync" closing card */
export function LoopDiagram({ lang }: { lang: 'de' | 'en' }) {
const de = lang === 'de'
return (
<svg viewBox="0 0 320 80" style={{ width: '100%', height: '24mm', display: 'block' }}>
<defs>
<marker id="arrR" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill={COLORS.indigo600} />
</marker>
</defs>
{/* Compliance box */}
<rect x="6" y="20" width="80" height="40" rx="2" fill={COLORS.indigo50} stroke={COLORS.indigo600} strokeWidth="1" />
<text x="46" y="38" fontSize="9" fontWeight="700" textAnchor="middle" fill={COLORS.indigo700}>Compliance</text>
<text x="46" y="52" fontSize="7" textAnchor="middle" fill={COLORS.slate600}>{de ? 'Policies · Audits · VVT' : 'Policies · Audits · RoPA'}</text>
{/* Code box */}
<rect x="234" y="20" width="80" height="40" rx="2" fill={COLORS.indigo50} stroke={COLORS.indigo600} strokeWidth="1" />
<text x="274" y="38" fontSize="9" fontWeight="700" textAnchor="middle" fill={COLORS.indigo700}>Code</text>
<text x="274" y="52" fontSize="7" textAnchor="middle" fill={COLORS.slate600}>{de ? 'Repos · CI/CD · Findings' : 'Repos · CI/CD · Findings'}</text>
{/* Top arrow: compliance → code */}
<path d="M86,30 Q160,5 234,30" fill="none" stroke={COLORS.indigo600} strokeWidth="1.4" markerEnd="url(#arrR)" />
<text x="160" y="12" fontSize="7" fontWeight="600" textAnchor="middle" fill={COLORS.indigo700}>{de ? 'Policies → Code (Real-time)' : 'Policies → Code (Real-time)'}</text>
{/* Bottom arrow: code → compliance */}
<path d="M234,50 Q160,75 86,50" fill="none" stroke={COLORS.indigo600} strokeWidth="1.4" markerEnd="url(#arrR)" />
<text x="160" y="72" fontSize="7" fontWeight="600" textAnchor="middle" fill={COLORS.indigo700}>{de ? 'Code-Δ → Evidence (Auto)' : 'Code-Δ → Evidence (Auto)'}</text>
</svg>
)
}
/** 4-stage horizontal pipeline (e.g. RAG pipeline ingestion → ... → QA) */
export function PipelineFlow({
stages, accent = COLORS.indigo600,
}: {
stages: { n: string; t: string; d: string; kpi?: string }[]
accent?: string
}) {
return (
<div style={{ display: 'flex', alignItems: 'stretch' }}>
{stages.map((s, i) => (
<React.Fragment key={i}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{
border: `1px solid ${COLORS.slate200}`,
borderTop: `3px solid ${accent}`,
padding: '3mm 4mm',
display: 'flex',
flexDirection: 'column',
height: '100%',
WebkitPrintColorAdjust: 'exact',
printColorAdjust: 'exact',
}}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
<span style={{ fontSize: '18pt', fontWeight: 800, color: accent, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>{s.n}</span>
{s.kpi && <span style={{ fontSize: '7pt', color: COLORS.emerald700, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{s.kpi}</span>}
</div>
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '2mm' }}>{s.t}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.5, flex: 1 }}>{s.d}</div>
</div>
</div>
{i < stages.length - 1 && (
<div style={{ width: '7mm', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg viewBox="0 0 24 24" style={{ width: '6mm', height: '6mm' }}>
<line x1="2" y1="12" x2="18" y2="12" stroke={accent} strokeWidth="1.5" />
<polyline points="14,6 20,12 14,18" fill="none" stroke={accent} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)}
</React.Fragment>
))}
</div>
)
}
@@ -1,5 +1,6 @@
import { Language, FMResult, FMAssumption } from '@/lib/types' import { Language, FMResult, FMAssumption } from '@/lib/types'
import { Page, COLORS, Callout, DataTable } from './PrintLayout' import { Page, COLORS, Callout, DataTable } from './PrintLayout'
import { BarChart, LineChart, DonutChart } from './PrintCharts'
import { computeAnnualKPIs } from '@/lib/finanzplan/annual-kpis' import { computeAnnualKPIs } from '@/lib/finanzplan/annual-kpis'
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string } interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
@@ -140,91 +141,64 @@ export function PrintFinanzplanPage2({ fmResults, lang, pageNum, totalPages, ver
</Page> </Page>
} }
const maxRev = Math.max(...kpis.map(k => k.totalRevenue), 1) const fmtM = (n: number) => '€' + (n / 1e6).toFixed(1) + 'M'
const maxEmp = Math.max(...kpis.map(k => k.employees), 1) const fmtK = (n: number) => '€' + (n / 1e3).toFixed(0) + 'k'
const pickFmt = (max: number) => max >= 1e6 ? fmtM : fmtK
return ( return (
<Page kicker="17" section={de ? 'ANHANG · FINANZPLAN · 2/2' : 'APPENDIX · FINANCIAL PLAN · 2/2'} title={de ? 'KPI-Dashboard und Wachstumskurve.' : 'KPI dashboard and growth trajectory.'} subtitle={de ? '19 KPIs pro Jahr aus den fp_*-Tabellen abgeleitet. Keine hardcodierten Werte. Quelle: aktuelles Base-Case-Szenario.' : '19 KPIs per year derived from fp_* tables. No hardcoded values. Source: current base-case scenario.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="17" section={de ? 'ANHANG · FINANZPLAN · 2/2' : 'APPENDIX · FINANCIAL PLAN · 2/2'} title={de ? 'KPI-Dashboard und Wachstumskurve.' : 'KPI dashboard and growth trajectory.'} subtitle={de ? '19 KPIs pro Jahr aus den fp_*-Tabellen abgeleitet. Keine hardcodierten Werte. Base-Case-Szenario.' : '19 KPIs per year derived from fp_* tables. No hardcoded values. Base-case scenario.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
{/* KPI table */} {/* Compact KPI table */}
<div style={{ marginBottom: '4mm' }}> <div style={{ marginBottom: '5mm' }}>
<DataTable <DataTable
dense dense
cols={[ cols={[
{ header: de ? 'KPI' : 'KPI', width: '24%' }, { header: de ? 'KPI' : 'KPI', width: '22%' },
...kpis.map(k => ({ header: String(k.year), numeric: true, width: '15.2%' as string })), ...kpis.map(k => ({ header: String(k.year), numeric: true })),
]} ]}
rows={[ rows={[
['ARR (Dez)', ...kpis.map(k => fmtEur(k.arr))], ['ARR (Dez)', ...kpis.map(k => fmtEur(k.arr))],
['MRR (Dez)', ...kpis.map(k => fmtEur(k.mrr))], [de ? 'MRR · ARPU' : 'MRR · ARPU', ...kpis.map(k => fmtEur(k.mrr) + ' · ' + fmtEur(k.arpu))],
[de ? 'ARPU (€/Monat)' : 'ARPU (€/mo)', ...kpis.map(k => fmtEur(k.arpu))], [de ? 'Kunden · MA' : 'Customers · FTE', ...kpis.map(k => k.customers.toLocaleString('de-DE') + ' · ' + k.employees)],
[de ? 'Kunden' : 'Customers', ...kpis.map(k => k.customers.toLocaleString('de-DE'))],
[de ? 'Mitarbeiter' : 'Employees', ...kpis.map(k => k.employees.toLocaleString('de-DE'))],
[de ? 'Umsatz / MA' : 'Revenue / FTE', ...kpis.map(k => fmtEur(k.revenuePerEmployee))], [de ? 'Umsatz / MA' : 'Revenue / FTE', ...kpis.map(k => fmtEur(k.revenuePerEmployee))],
[de ? 'Bruttomarge %' : 'Gross margin %', ...kpis.map(k => k.grossMargin + '%')], [de ? 'Bruttomarge' : 'Gross margin', ...kpis.map(k => k.grossMargin + '%')],
[de ? 'EBIT' : 'EBIT', ...kpis.map(k => <span style={{ color: k.ebit >= 0 ? COLORS.emerald700 : COLORS.red700, fontWeight: 700 }}>{fmtEur(k.ebit)}</span>)], [de ? 'EBIT · Marge' : 'EBIT · margin', ...kpis.map(k => <span key="e" style={{ color: k.ebit >= 0 ? COLORS.emerald700 : COLORS.red700, fontWeight: 700 }}>{fmtEur(k.ebit)} · {k.ebitMargin}%</span>)],
[de ? 'EBIT-Marge' : 'EBIT margin', ...kpis.map(k => <span style={{ color: k.ebitMargin >= 0 ? COLORS.emerald700 : COLORS.red700 }}>{k.ebitMargin}%</span>)], [de ? 'Netto-Ergebnis' : 'Net income', ...kpis.map(k => <strong key="ni" style={{ color: k.netIncome >= 0 ? COLORS.emerald700 : COLORS.red700 }}>{fmtEur(k.netIncome)}</strong>)],
[de ? 'Steuern (~30%)' : 'Taxes (~30%)', ...kpis.map(k => fmtEur(k.taxes))], [de ? 'Burn · Runway' : 'Burn · runway', ...kpis.map(k => fmtEur(k.burnRate) + ' · ' + (k.runway == null ? '∞' : String(k.runway) + 'm'))],
[de ? 'Netto-Ergebnis' : 'Net income', ...kpis.map(k => <strong style={{ color: k.netIncome >= 0 ? COLORS.emerald700 : COLORS.red700 }}>{fmtEur(k.netIncome)}</strong>)], [de ? 'Cash-Bestand' : 'Cash balance', ...kpis.map(k => fmtEur(k.cashBalance))],
[de ? 'Burn-Rate (Dez)' : 'Burn rate (Dec)', ...kpis.map(k => fmtEur(k.burnRate))],
[de ? 'Runway (Monate)' : 'Runway (months)', ...kpis.map(k => k.runway == null ? '∞' : String(k.runway))],
[de ? 'Cash-Bestand (Dez)' : 'Cash balance (Dec)', ...kpis.map(k => fmtEur(k.cashBalance))],
]} ]}
highlightFirstCol highlightFirstCol
/> />
</div> </div>
{/* Charts */} {/* Charts grid 2x2 */}
<div style={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm' }}> <div style={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr', gap: '5mm 8mm' }}>
{/* Revenue chart */} <BarChart
<div> title={de ? 'Umsatz (€ Mio.)' : 'Revenue (€M)'}
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Umsatz-Wachstum (Mio. €)' : 'Revenue growth (€M)'}</div> data={kpis.map(k => ({ label: String(k.year), value: k.totalRevenue, tone: 'default' }))}
<div style={{ display: 'flex', alignItems: 'flex-end', height: '32mm', gap: '4mm', borderBottom: `1px solid ${COLORS.slate300}`, paddingBottom: '1mm' }}> height={26}
{kpis.map((k, i) => { formatValue={pickFmt(Math.max(...kpis.map(k => k.totalRevenue)))}
const h = (k.totalRevenue / maxRev) * 100 />
return ( <BarChart
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}> title={de ? 'EBIT (€)' : 'EBIT (€)'}
<div style={{ fontSize: '7pt', color: COLORS.indigo700, fontWeight: 700, marginBottom: '1mm', fontVariantNumeric: 'tabular-nums' }}>{(k.totalRevenue / 1e6).toFixed(1)}</div> data={kpis.map(k => ({ label: String(k.year), value: k.ebit, tone: k.ebit >= 0 ? 'positive' : 'negative' }))}
<div style={{ width: '100%', height: `${h}%`, minHeight: '2pt', background: COLORS.indigo600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} /> height={26}
</div> formatValue={pickFmt(Math.max(...kpis.map(k => Math.abs(k.ebit))))}
) />
})} <LineChart
</div> title={de ? 'Cash-Bestand (€)' : 'Cash balance (€)'}
<div style={{ display: 'flex', gap: '4mm', marginTop: '1mm' }}> data={kpis.map(k => ({ label: String(k.year), value: Math.max(k.cashBalance, 0) }))}
{kpis.map((k, i) => ( height={26}
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600 }}>{k.year}</div> color={COLORS.indigo600}
))} formatValue={pickFmt(Math.max(...kpis.map(k => k.cashBalance)))}
</div> fill
</div> />
<BarChart
{/* Headcount + customers chart */} title={de ? 'Mitarbeiter · FTE' : 'Employees · FTE'}
<div> data={kpis.map(k => ({ label: String(k.year), value: k.employees, tone: 'accent' }))}
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Mitarbeiter & Kunden' : 'Employees & customers'}</div> height={26}
<div style={{ display: 'flex', alignItems: 'flex-end', height: '32mm', gap: '4mm', borderBottom: `1px solid ${COLORS.slate300}`, paddingBottom: '1mm' }}> formatValue={(n) => String(Math.round(n))}
{kpis.map((k, i) => { />
const h = (k.employees / maxEmp) * 100
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
<div style={{ fontSize: '7pt', color: COLORS.slate700, marginBottom: '1mm', fontVariantNumeric: 'tabular-nums', textAlign: 'center', lineHeight: 1.2 }}>
<strong style={{ color: COLORS.amber700 }}>{k.employees}</strong>
<br />
<span style={{ fontSize: '6.5pt', color: COLORS.slate500 }}>{k.customers}c</span>
</div>
<div style={{ width: '100%', height: `${h}%`, minHeight: '2pt', background: COLORS.amber600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
</div>
)
})}
</div>
<div style={{ display: 'flex', gap: '4mm', marginTop: '1mm' }}>
{kpis.map((k, i) => (
<div key={i} style={{ flex: 1, fontSize: '7pt', color: COLORS.slate500, textAlign: 'center', fontWeight: 600 }}>{k.year}</div>
))}
</div>
<div style={{ marginTop: '2mm', fontSize: '7pt', color: COLORS.slate500, display: 'flex', gap: '6mm' }}>
<span><span style={{ display: 'inline-block', width: '8pt', height: '8pt', background: COLORS.amber600 }} /> {de ? 'Mitarbeiter (FTE)' : 'Employees (FTE)'}</span>
<span style={{ color: COLORS.slate500 }}>{de ? 'Zahl unten: Kunden' : 'Number below: customers'}</span>
</div>
</div>
</div> </div>
</Page> </Page>
) )
@@ -341,18 +315,19 @@ export function PrintCapTablePage({ lang, pageNum, totalPages, versionName }: Sl
<div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}> <div style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
<div> <div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Anteilsverteilung Post Pre-Seed' : 'Share distribution post pre-seed'}</div> <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Anteilsverteilung Post Pre-Seed' : 'Share distribution post pre-seed'}</div>
<div style={{ display: 'flex', height: '10mm', border: `1px solid ${COLORS.slate300}`, marginBottom: '6mm' }}>
{CAP_TABLE_DATA.map(d => ( <div style={{ display: 'flex', alignItems: 'center', gap: '8mm', marginBottom: '5mm' }}>
<div key={d.name} style={{ width: `${d.pct}%`, background: d.color, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} /> <DonutChart size={50} thickness={11} segments={CAP_TABLE_DATA.map(d => ({ label: d.name, pct: d.pct, color: d.color }))} />
))} <div style={{ flex: 1 }}>
</div> {CAP_TABLE_DATA.map(d => (
{CAP_TABLE_DATA.map(d => ( <div key={d.name} style={{ display: 'flex', alignItems: 'center', gap: '3mm', padding: '2mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}>
<div key={d.name} style={{ display: 'flex', alignItems: 'center', gap: '4mm', padding: '2.5mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}> <div style={{ width: '4mm', height: '4mm', background: d.color, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
<div style={{ width: '4mm', height: '4mm', background: d.color, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} /> <span style={{ fontSize: '9.5pt', color: COLORS.slate800, flex: 1, fontWeight: 500 }}>{d.name}</span>
<span style={{ fontSize: '10pt', color: COLORS.slate800, flex: 1, fontWeight: 500 }}>{d.name}</span> <span style={{ fontSize: '14pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums' }}>{d.pct}%</span>
<span style={{ fontSize: '14pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums' }}>{d.pct}%</span> </div>
))}
</div> </div>
))} </div>
</div> </div>
<div> <div>
@@ -8,54 +8,95 @@ interface SlideBase { lang: Language; pageNum: number; totalPages: number; versi
export function PrintCoverPage({ company, funding, lang, versionName }: { company: PitchCompany; funding: PitchFunding; lang: Language; versionName: string }) { export function PrintCoverPage({ company, funding, lang, versionName }: { company: PitchCompany; funding: PitchFunding; lang: Language; versionName: string }) {
const de = lang === 'de' const de = lang === 'de'
const instrument = funding?.instrument || 'Pre-Seed' const instrument = funding?.instrument || 'Pre-Seed'
const amount = funding?.amount_eur || 1_000_000
const tagline = de ? (company?.tagline_de || 'Kontinuierliche Compliance für europäische Unternehmen.') : (company?.tagline_en || 'Continuous compliance for European companies.') const tagline = de ? (company?.tagline_de || 'Kontinuierliche Compliance für europäische Unternehmen.') : (company?.tagline_en || 'Continuous compliance for European companies.')
return ( return (
<div className="print-page-break"> <div className="print-page-break">
<div className="print-page" style={{ width: '297mm', height: '210mm', background: '#ffffff', color: COLORS.slate900, fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", display: 'flex', flexDirection: 'column', boxSizing: 'border-box', padding: '14mm', margin: '0 auto 24px', boxShadow: '0 4px 24px rgba(15,23,42,0.10)', overflow: 'hidden' }}> <div className="print-page" style={{ width: '297mm', height: '210mm', background: '#ffffff', color: COLORS.slate900, fontFamily: "'Plus Jakarta Sans', system-ui, sans-serif", display: 'flex', boxSizing: 'border-box', margin: '0 auto 24px', boxShadow: '0 4px 24px rgba(15,23,42,0.10)', overflow: 'hidden', padding: 0 }}>
{/* TOP META */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderBottom: `1px solid ${COLORS.slate200}`, paddingBottom: '3mm' }}>
<span style={{ fontSize: '8pt', fontWeight: 700, letterSpacing: '0.16em', color: COLORS.indigo600, textTransform: 'uppercase' }}>BreakPilot &middot; Investor Brief</span>
<span style={{ fontSize: '8pt', color: COLORS.slate500, fontWeight: 500 }}>{versionName}</span>
</div>
{/* HERO */} {/* LEFT INDIGO BLOCK */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', paddingTop: '8mm' }}> <div style={{
<p style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.16em', margin: 0 }}> width: '95mm',
{instrument} &middot; Q4 2026 background: COLORS.indigo600,
</p> color: '#ffffff',
<h1 style={{ fontSize: '72pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 0.95, letterSpacing: '-0.025em', margin: '6mm 0 4mm' }}> padding: '16mm 12mm',
{company?.name || 'BreakPilot'}<span style={{ color: COLORS.indigo600 }}>.</span> display: 'flex',
</h1> flexDirection: 'column',
<p style={{ fontSize: '14pt', fontWeight: 500, color: COLORS.slate700, lineHeight: 1.35, maxWidth: '180mm', margin: 0, letterSpacing: '-0.005em' }}> justifyContent: 'space-between',
{tagline} WebkitPrintColorAdjust: 'exact',
</p> printColorAdjust: 'exact',
<div style={{ marginTop: '14mm', height: '1px', background: COLORS.slate200, maxWidth: '120mm' }} /> }}>
<p style={{ fontSize: '11pt', color: COLORS.slate600, marginTop: '6mm', maxWidth: '170mm', lineHeight: 1.5, fontWeight: 400 }}> <div>
{de <div style={{ fontSize: '8pt', fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)' }}>
? 'DSGVO-konforme KI-Plattform für kontinuierliche Code-Security und automatisierte Compliance. Souverän gehostet, integriert in europäische Workflows.' {de ? 'Investor Brief' : 'Investor Brief'}
: 'GDPR-compliant AI platform for continuous code security and automated compliance. Sovereign-hosted, integrated into European workflows.'}
</p>
</div>
{/* META GRID */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8mm', borderTop: `1px solid ${COLORS.slate200}`, borderBottom: `1px solid ${COLORS.slate200}`, padding: '5mm 0', marginBottom: '5mm' }}>
{([
[de ? 'Gegründet' : 'Founded', company?.founding_date ? new Date(company.founding_date).getFullYear().toString() : 'Aug 2026'],
[de ? 'Standort' : 'HQ', company?.hq_city || 'Bodman-Ludwigshafen'],
[de ? 'Instrument' : 'Instrument', instrument],
[de ? 'Runde' : 'Round', funding?.round_name || 'Pre-Seed'],
] as [string, string][]).map(([label, val]) => (
<div key={label}>
<p style={{ fontSize: '7.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', margin: 0, fontWeight: 600 }}>{label}</p>
<p style={{ fontSize: '14pt', fontWeight: 700, color: COLORS.slate900, margin: '2mm 0 0', fontVariantNumeric: 'tabular-nums' }}>{val}</p>
</div> </div>
))} <div style={{ marginTop: '6mm', height: '1px', background: 'rgba(255,255,255,0.3)', width: '32mm' }} />
<div style={{ marginTop: '6mm', fontSize: '11pt', color: 'rgba(255,255,255,0.85)', lineHeight: 1.55, fontWeight: 500 }}>
{de
? 'DSGVO-konforme KI-Plattform für kontinuierliche Code-Security und automatisierte Compliance. Souverän gehostet, integriert in europäische Workflows.'
: 'GDPR-compliant AI platform for continuous code security and automated compliance. Sovereign-hosted, integrated into European workflows.'}
</div>
</div>
{/* Hero stat */}
<div>
<div style={{ fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)' }}>
{de ? '25 000 + atomare Prüfaspekte' : '25 000 + atomic audit aspects'}
</div>
<div style={{ fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)', marginTop: '1.5mm' }}>
{de ? '380 + Regularien · 10 Branchen' : '380 + regulations · 10 industries'}
</div>
<div style={{ fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.7)', marginTop: '1.5mm' }}>
{de ? '500 K + Lines of Code · 45 Container' : '500 K + lines of code · 45 containers'}
</div>
</div>
{/* Bottom: version + confidential */}
<div>
<div style={{ fontSize: '7pt', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)', fontWeight: 600 }}>{versionName}</div>
<div style={{ marginTop: '2mm', fontSize: '7pt', letterSpacing: '0.16em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.55)', fontWeight: 600 }}>{de ? 'Vertraulich · Nur Investoren' : 'Confidential · Investors only'}</div>
</div>
</div> </div>
{/* FOOTER */} {/* RIGHT WHITE PANE */}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '7.5pt', color: COLORS.slate500 }}> <div style={{ flex: 1, padding: '16mm 16mm', display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<span>{de ? 'Vertraulich, Nur für Investoren' : 'Confidential, For Investor Use Only'}</span> <div>
<span style={{ fontWeight: 700, letterSpacing: '0.14em', color: COLORS.indigo600 }}>CONFIDENTIAL</span> <div style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.16em' }}>
{instrument} &middot; Q4 2026
</div>
<h1 style={{ fontSize: '88pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 0.92, letterSpacing: '-0.035em', margin: '8mm 0 6mm' }}>
{company?.name || 'BreakPilot'}<span style={{ color: COLORS.indigo600 }}>.</span>
</h1>
<div style={{ height: '2px', width: '40mm', background: COLORS.indigo600, marginBottom: '6mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
<p style={{ fontSize: '16pt', fontWeight: 500, color: COLORS.slate700, lineHeight: 1.3, maxWidth: '160mm', margin: 0, letterSpacing: '-0.008em' }}>
{tagline}
</p>
</div>
{/* Key terms */}
<div>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '3mm', paddingBottom: '2mm', borderBottom: `1px solid ${COLORS.slate200}` }}>
{de ? 'Key Terms' : 'Key terms'}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6mm' }}>
{([
[de ? 'Funding' : 'Funding', '€' + (amount / 1_000_000).toFixed(1) + 'M'],
[de ? 'Pre-Money' : 'Pre-money', '€4.0M'],
[de ? 'Instrument' : 'Instrument', instrument],
[de ? 'Standort' : 'HQ', company?.hq_city || 'Bodman'],
] as [string, string][]).map(([label, val]) => (
<div key={label}>
<div style={{ fontSize: '7pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', fontWeight: 700 }}>{label}</div>
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, marginTop: '1.5mm', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.01em' }}>{val}</div>
</div>
))}
</div>
<div style={{ marginTop: '5mm', fontSize: '8pt', color: COLORS.slate500, lineHeight: 1.5 }}>
{de
? 'Gründerteam Benjamin Bönisch (CEO) und Sharang Parnerkar (CTO). Markeneintragung DPMA · EUIPO-Anmeldung in Bearbeitung · GmbH-Gründung August 2026.'
: 'Founding team Benjamin Bönisch (CEO) and Sharang Parnerkar (CTO). Trademark DPMA registered · EUIPO filing in progress · GmbH incorporation August 2026.'}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,5 +1,6 @@
import { Language, PitchMarket, PitchTeamMember, PitchMilestone, PitchFunding } from '@/lib/types' import { Language, PitchMarket, PitchTeamMember, PitchMilestone, PitchFunding } from '@/lib/types'
import { Page, TwoCol, Bullets, Callout, COLORS, DataTable, StatLine } from './PrintLayout' import { Page, Callout, COLORS, DataTable, StatLine } from './PrintLayout'
import { MarketFunnel, ComparisonBars, DonutChart } from './PrintCharts'
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string } interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
@@ -21,49 +22,19 @@ export function PrintMarketPage({ market, lang, pageNum, totalPages, versionName
return ( return (
<Page kicker="09" section={de ? 'MARKT' : 'MARKET'} title={de ? 'Compliance & Code-Security für produzierende Unternehmen.' : 'Compliance & code security for manufacturing companies.'} subtitle={de ? 'Validierter Markt: Top-10 Compliance-Anbieter erwirtschaften >$1,1 Mrd. ARR. Kein Anbieter bedient den Maschinenbau spezifisch.' : 'Validated market: top-10 compliance vendors generate >$1.1B ARR. No vendor specifically serves manufacturing.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName} footnote={de ? 'Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista' : 'Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista'}> <Page kicker="09" section={de ? 'MARKT' : 'MARKET'} title={de ? 'Compliance & Code-Security für produzierende Unternehmen.' : 'Compliance & code security for manufacturing companies.'} subtitle={de ? 'Validierter Markt: Top-10 Compliance-Anbieter erwirtschaften >$1,1 Mrd. ARR. Kein Anbieter bedient den Maschinenbau spezifisch.' : 'Validated market: top-10 compliance vendors generate >$1.1B ARR. No vendor specifically serves manufacturing.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName} footnote={de ? 'Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista' : 'Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista'}>
{/* TAM/SAM/SOM as nested rectangles */} <div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
<div> <div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Marktdimensionierung' : 'Market sizing'}</div> <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Marktdimensionierung' : 'Market sizing'}</div>
{tam && sam && som && (
{/* TAM */} <MarketFunnel
{tam && ( tam={{ value: tam.value_eur, label: de ? 'Total Addressable' : 'Total Addressable', growth: tam.growth_rate_pct ?? 14, note: de ? 'Globaler Compliance- und GRC-Markt (alle Branchen, alle Größen).' : 'Global compliance and GRC market (all industries, all sizes).' }}
<div style={{ border: `1px solid ${COLORS.slate300}`, padding: '4mm', marginBottom: '3mm' }}> sam={{ value: sam.value_eur, label: de ? 'Serviceable Addressable' : 'Serviceable Addressable', growth: sam.growth_rate_pct ?? 18, note: de ? 'DACH + EU: regulierte Branchen, KMU und Enterprise.' : 'DACH + EU: regulated industries, SMB and enterprise.' }}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1.5mm' }}> som={{ value: som.value_eur, label: de ? 'Kernmarkt 5 Jahre' : 'Core market 5 yrs', growth: som.growth_rate_pct ?? 25, note: de ? 'Anlagen- und Maschinenbau DACH, unser Kernsegment.' : 'Machine and plant manufacturing DACH, our core segment.' }}
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em' }}>TAM &middot; {de ? 'Total Addressable Market' : 'Total Addressable Market'}</span> fmt={(v) => fmtEur(v, de)}
<span style={{ fontSize: '7.5pt', color: COLORS.slate500 }}>+{tam.growth_rate_pct ?? 14}% p.a.</span> />
</div>
<div style={{ fontSize: '28pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{fmtEur(tam.value_eur, de)}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate600, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'Globaler Compliance- und GRC-Markt (alle Branchen, alle Größen).' : 'Global compliance and GRC market (all industries, all sizes).'}</div>
</div>
)}
{/* SAM */}
{sam && (
<div style={{ border: `1px solid ${COLORS.indigo600}`, background: COLORS.indigo50, padding: '4mm', marginBottom: '3mm', marginLeft: '6mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1.5mm' }}>
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.indigo700, textTransform: 'uppercase', letterSpacing: '0.12em' }}>SAM &middot; {de ? 'Serviceable Addressable' : 'Serviceable Addressable'}</span>
<span style={{ fontSize: '7.5pt', color: COLORS.indigo700 }}>+{sam.growth_rate_pct ?? 18}% p.a.</span>
</div>
<div style={{ fontSize: '26pt', fontWeight: 800, color: COLORS.indigo700, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{fmtEur(sam.value_eur, de)}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'DACH + EU: regulierte Branchen, KMU + Enterprise.' : 'DACH + EU: regulated industries, SMB + enterprise.'}</div>
</div>
)}
{/* SOM */}
{som && (
<div style={{ border: `2px solid ${COLORS.emerald600}`, background: COLORS.emerald50, padding: '4mm', marginLeft: '12mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '1.5mm' }}>
<span style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.emerald700, textTransform: 'uppercase', letterSpacing: '0.12em' }}>SOM &middot; {de ? 'Kernmarkt 5 Jahre' : 'Core market 5 yrs'}</span>
<span style={{ fontSize: '7.5pt', color: COLORS.emerald700 }}>+{som.growth_rate_pct ?? 25}% p.a.</span>
</div>
<div style={{ fontSize: '24pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{fmtEur(som.value_eur, de)}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'Anlagen- und Maschinenbau DACH, unser Kernsegment.' : 'Machine & plant manufacturing DACH, our core segment.'}</div>
</div>
)} )}
</div> </div>
{/* Segment context */}
<div> <div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Kernsegment: Maschinen- und Anlagenbau DACH' : 'Core segment: Machine & plant manufacturing DACH'}</div> <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Kernsegment: Maschinen- und Anlagenbau DACH' : 'Core segment: Machine & plant manufacturing DACH'}</div>
<DataTable <DataTable
@@ -191,43 +162,47 @@ export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }:
return ( return (
<Page kicker="13" section={de ? 'TEAM' : 'TEAM'} title={de ? 'Gründer mit Domain-Expertise.' : 'Founders with domain expertise.'} subtitle={de ? 'Komplementäres Gründerduo: Vertrieb + Engineering. Beide haben bereits skaliert. Beide kennen den Markt.' : 'Complementary founding duo: sales + engineering. Both have scaled. Both know the market.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="13" section={de ? 'TEAM' : 'TEAM'} title={de ? 'Gründer mit Domain-Expertise.' : 'Founders with domain expertise.'} subtitle={de ? 'Komplementäres Gründerduo: Vertrieb + Engineering. Beide haben bereits skaliert. Beide kennen den Markt.' : 'Complementary founding duo: sales + engineering. Both have scaled. Both know the market.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<TwoCol ratio="1:1" gap="8mm" left={ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
<div style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '5mm', height: '100%', display: 'flex', flexDirection: 'column' }}> {[0, 1].map(idx => {
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}> const m = members[idx]
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>CO-FOUNDER &middot; 01 / 02</span> if (!m) return null
<span style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600 }}>Equity {members[0]?.equity_pct ?? 37.3}%</span> return (
</div> <div key={idx} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `3px solid ${COLORS.indigo600}`, padding: '5mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.1, marginBottom: '1mm', letterSpacing: '-0.01em' }}>{members[0]?.name || 'Benjamin Bönisch'}</div> <div style={{ display: 'flex', gap: '5mm', alignItems: 'flex-start', marginBottom: '4mm' }}>
<div style={{ fontSize: '10pt', fontWeight: 600, color: COLORS.indigo600, marginBottom: '3mm' }}>{de ? (members[0]?.role_de || 'CEO & Co-Founder') : (members[0]?.role_en || 'CEO & Co-Founder')}</div> {/* Photo */}
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{de ? (members[0]?.bio_de) : (members[0]?.bio_en)}</div> <div style={{ width: '32mm', height: '32mm', flexShrink: 0, border: `1px solid ${COLORS.slate200}`, background: COLORS.slate100, overflow: 'hidden', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ marginTop: '4mm', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}` }}> {m.photo_url ? (
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '2mm' }}>{de ? 'Expertise' : 'Expertise'}</div> /* eslint-disable-next-line @next/next/no-img-element */
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5mm' }}> <img src={m.photo_url} alt={m.name} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
{(members[0]?.expertise || []).map((e, i) => ( ) : (
<span key={i} style={{ fontSize: '8pt', padding: '0.5mm 2mm', border: `1px solid ${COLORS.slate300}`, color: COLORS.slate700 }}>{e}</span> <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '24pt', fontWeight: 800, color: COLORS.slate400, letterSpacing: '-0.02em' }}>
))} {m.name?.split(' ').map(s => s[0]).slice(0, 2).join('') || '?'}
</div>
)}
</div>
{/* Header text */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
<span style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.14em' }}>CO-FOUNDER &middot; {String(idx + 1).padStart(2, '0')} / 02</span>
<span style={{ fontSize: '7pt', color: COLORS.slate500, fontWeight: 700, fontVariantNumeric: 'tabular-nums' }}>{(m.equity_pct ?? 37.3).toLocaleString('de-DE')}% Equity</span>
</div>
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.05, marginBottom: '1mm', letterSpacing: '-0.015em' }}>{m.name}</div>
<div style={{ fontSize: '10pt', fontWeight: 600, color: COLORS.indigo600 }}>{de ? m.role_de : m.role_en}</div>
</div>
</div>
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{de ? m.bio_de : m.bio_en}</div>
<div style={{ marginTop: '4mm', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}` }}>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Expertise' : 'Expertise'}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5mm' }}>
{(m.expertise || []).map((e, i) => (
<span key={i} style={{ fontSize: '8pt', padding: '0.5mm 2mm', background: COLORS.indigo50, color: COLORS.indigo700, fontWeight: 600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>{e}</span>
))}
</div>
</div>
</div> </div>
</div> )
</div> })}
} right={ </div>
<div style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '5mm', height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}>
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>CO-FOUNDER &middot; 02 / 02</span>
<span style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600 }}>Equity {members[1]?.equity_pct ?? 37.3}%</span>
</div>
<div style={{ fontSize: '18pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.1, marginBottom: '1mm', letterSpacing: '-0.01em' }}>{members[1]?.name || 'Sharang Parnerkar'}</div>
<div style={{ fontSize: '10pt', fontWeight: 600, color: COLORS.indigo600, marginBottom: '3mm' }}>{de ? (members[1]?.role_de || 'CTO & Co-Founder') : (members[1]?.role_en || 'CTO & Co-Founder')}</div>
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{de ? (members[1]?.bio_de) : (members[1]?.bio_en)}</div>
<div style={{ marginTop: '4mm', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}` }}>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', marginBottom: '2mm' }}>{de ? 'Expertise' : 'Expertise'}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.5mm' }}>
{(members[1]?.expertise || []).map((e, i) => (
<span key={i} style={{ fontSize: '8pt', padding: '0.5mm 2mm', border: `1px solid ${COLORS.slate300}`, color: COLORS.slate700 }}>{e}</span>
))}
</div>
</div>
</div>
} />
<div style={{ marginTop: '5mm', flexShrink: 0 }}> <div style={{ marginTop: '5mm', flexShrink: 0 }}>
<Callout tone="accent" label={de ? 'Equity-Struktur' : 'Equity structure'}> <Callout tone="accent" label={de ? 'Equity-Struktur' : 'Equity structure'}>
@@ -292,27 +267,31 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam
{/* Use of funds */} {/* Use of funds */}
<div> <div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Use of Funds' : 'Use of Funds'}</div> <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Use of Funds' : 'Use of funds'}</div>
{/* Horizontal bar */} {(() => {
<div style={{ display: 'flex', height: '8mm', border: `1px solid ${COLORS.slate300}`, marginBottom: '5mm' }}> const palette = [COLORS.indigo600, COLORS.indigo500, COLORS.amber600, COLORS.slate600, COLORS.slate400]
{useOfFunds.map((u, i) => {
const colors = [COLORS.indigo600, COLORS.indigo500, COLORS.amber600, COLORS.slate600, COLORS.slate400]
return (
<div key={i} style={{ width: `${u.percentage}%`, background: colors[i % colors.length], WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
)
})}
</div>
{useOfFunds.map((u, i) => {
const colors = [COLORS.indigo600, COLORS.indigo500, COLORS.amber600, COLORS.slate600, COLORS.slate400]
return ( return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '3mm', padding: '2.5mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}> <div style={{ display: 'flex', alignItems: 'flex-start', gap: '6mm' }}>
<div style={{ width: '3mm', height: '3mm', background: colors[i % colors.length], flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} /> <div style={{ flexShrink: 0 }}>
<div style={{ flex: 1, fontSize: '9pt', color: COLORS.slate800, fontWeight: 500 }}>{de ? u.label_de : u.label_en}</div> <DonutChart
<div style={{ fontSize: '11pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums' }}>{u.percentage}%</div> size={48}
<div style={{ fontSize: '8pt', color: COLORS.slate500, fontVariantNumeric: 'tabular-nums', minWidth: '14mm', textAlign: 'right' }}>{(amount * u.percentage / 100 / 1000).toFixed(0)}k</div> thickness={9}
segments={useOfFunds.map((u, i) => ({ label: de ? u.label_de : u.label_en, pct: u.percentage, color: palette[i % palette.length] }))}
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
{useOfFunds.map((u, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: '3mm', padding: '2mm 0', borderBottom: `1px solid ${COLORS.slate100}` }}>
<div style={{ width: '3mm', height: '3mm', background: palette[i % palette.length], flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
<div style={{ flex: 1, fontSize: '8.5pt', color: COLORS.slate800, fontWeight: 500 }}>{de ? u.label_de : u.label_en}</div>
<div style={{ fontSize: '11pt', fontWeight: 800, color: COLORS.slate900, fontVariantNumeric: 'tabular-nums', minWidth: '12mm', textAlign: 'right' }}>{u.percentage}%</div>
<div style={{ fontSize: '7.5pt', color: COLORS.slate500, fontVariantNumeric: 'tabular-nums', minWidth: '14mm', textAlign: 'right' }}>{(amount * u.percentage / 100 / 1000).toFixed(0)}k</div>
</div>
))}
</div>
</div> </div>
) )
})} })()}
</div> </div>
</div> </div>
</Page> </Page>
@@ -323,68 +302,70 @@ export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionNam
export function PrintCustomerSavingsPage({ lang, pageNum, totalPages, versionName }: SlideBase) { export function PrintCustomerSavingsPage({ lang, pageNum, totalPages, versionName }: SlideBase) {
const de = lang === 'de' const de = lang === 'de'
const items = [
{ l: de ? 'Pentests' : 'Pentests', today: 15000, bp: 13000 },
{ l: de ? 'CE-SW-Risiko' : 'CE software risk', today: 12000, bp: 9000 },
{ l: de ? 'Compliance-Zeit' : 'Compliance time', today: 18000, bp: 15000 },
{ l: de ? 'Audit-Vorber.' : 'Audit prep', today: 9000, bp: 9000 },
{ l: de ? 'Legal (DSGVO/AI Act)' : 'Legal (GDPR/AI Act)', today: 8000, bp: 5000 },
{ l: de ? 'Auditmanager-Software' : 'Audit manager SW', today: 5000, bp: 0 },
{ l: de ? 'Schulungen extern' : 'External training', today: 4000, bp: 4000 },
]
const totalToday = items.reduce((s, i) => s + i.today, 0)
const totalSaved = items.reduce((s, i) => s + i.bp, 0)
const totalBpCost = 25000
return ( return (
<Page kicker="15" section={de ? 'KUNDENERSPARNIS' : 'CUSTOMER SAVINGS'} title={de ? 'Kunden sparen mehr als sie zahlen, vom ersten Tag an.' : 'Customers save more than they pay, from day one.'} subtitle={de ? 'Detaillierte Aufschlüsselung der Einsparungen für ein typisches KMU (50 Mitarbeiter) im ersten Jahr.' : 'Detailed savings breakdown for a typical SME (50 employees) in year one.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="15" section={de ? 'KUNDENERSPARNIS' : 'CUSTOMER SAVINGS'} title={de ? 'Kunden sparen mehr als sie zahlen, vom ersten Tag an.' : 'Customers save more than they pay, from day one.'} subtitle={de ? 'Detaillierte Aufschlüsselung für ein typisches KMU (50 MA) im ersten Jahr. €25k Kosten, €55k Ersparnis.' : 'Detailed breakdown for a typical SME (50 emp.) in year one. €25k cost, €55k savings.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}> {/* Big stat header */}
<div> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4mm', padding: '4mm 0', borderTop: `1px solid ${COLORS.slate200}`, borderBottom: `1px solid ${COLORS.slate200}`, marginBottom: '5mm' }}>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Was der Kunde heute bezahlt (ohne BreakPilot)' : 'What customer pays today (without BreakPilot)'}</div> {[
<DataTable { l: de ? 'Heute (ohne BP)' : 'Today (without BP)', v: '€' + (totalToday / 1000).toFixed(0) + 'k', tone: COLORS.red700 },
cols={[ { l: de ? 'BreakPilot Pro / Jahr' : 'BreakPilot Pro / year', v: '€' + (totalBpCost / 1000).toFixed(0) + 'k', tone: COLORS.indigo600 },
{ header: de ? 'Position' : 'Item' }, { l: de ? 'Ersparnis / KMU' : 'Savings / SME', v: '€' + (totalSaved / 1000).toFixed(0) + 'k', tone: COLORS.emerald700 },
{ header: de ? 'Heute' : 'Today', numeric: true, width: '22%' }, { l: de ? 'Netto-Effekt' : 'Net effect', v: '+€' + ((totalSaved - totalBpCost) / 1000).toFixed(0) + 'k', tone: COLORS.emerald700 },
{ header: de ? 'Frequenz' : 'Frequency', width: '24%' }, ].map((k, i) => (
]} <div key={i}>
rows={[ <div style={{ fontSize: '26pt', fontWeight: 800, color: k.tone, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>{k.v}</div>
[de ? 'Externe Pentests' : 'External pentests', '€15.000', de ? 'jährlich' : 'annually'], <div style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em', marginTop: '1.5mm' }}>{k.l}</div>
[de ? 'CE-Software-Risikobeurteilung' : 'CE software risk assessment', '€12.000', de ? 'pro Produkt' : 'per product'],
[de ? 'Compliance-Beauftragte/r (anteilig)' : 'Compliance officer (pro-rated)', '€18.000', de ? 'jährlich' : 'annually'],
[de ? 'Audit-Vorbereitung extern' : 'External audit prep', '€9.000', de ? 'jährlich' : 'annually'],
[de ? 'Rechtsanwälte (DSGVO, AI Act)' : 'Lawyers (GDPR, AI Act)', '€8.000', de ? 'jährlich' : 'annually'],
[de ? 'Auditmanager-Software' : 'Audit manager software', '€5.000', de ? 'jährlich' : 'annually'],
[de ? 'Schulungen extern' : 'External training', '€4.000', de ? 'jährlich' : 'annually'],
[<strong key="sum">{de ? 'Summe heute' : 'Today total'}</strong>, <strong key="v">71.000</strong>, ''],
]}
dense
/>
<div style={{ marginTop: '4mm' }}>
<Callout tone="negative" label={de ? 'Versteckte Kosten' : 'Hidden costs'}>
{de
? 'Zeit der GF + Compliance-Beauftragten (~30 Tage/Jahr), DSGVO-Bußgelder bei Fehlern (bis zu 4% Jahresumsatz), verlorene RFQs durch fehlende Compliance-Nachweise.'
: 'Time of management + compliance officer (~30 days/year), GDPR fines on errors (up to 4% annual revenue), lost RFQs due to missing compliance evidence.'}
</Callout>
</div> </div>
))}
</div>
{/* Bar comparison */}
<div style={{ flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm' }}>
<div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Heute vs. mit BreakPilot (€/Jahr/KMU)' : 'Today vs. with BreakPilot (€/yr/SME)'}</div>
<ComparisonBars
rows={items.map(it => ({
label: it.l,
bars: [
{ tone: 'negative', value: it.today, cap: de ? 'Heute' : 'Today' },
{ tone: 'positive', value: it.bp, cap: de ? 'gespart mit BP' : 'saved with BP' },
],
}))}
formatValue={(n) => '€' + (n / 1000).toFixed(0) + 'k'}
/>
</div> </div>
<div> <div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Mit BreakPilot' : 'With BreakPilot'}</div> <div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Ersparnis-Aufschlüsselung' : 'Savings breakdown'}</div>
<DataTable <div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
cols={[ <StatLine label={de ? 'Pentests (kontinuierlich, inklusive)' : 'Pentests (continuous, included)'} value="€13.000" tone="positive" />
{ header: de ? 'Position' : 'Item' }, <StatLine label={de ? 'CE-Risiko (Code-basiert, inkl.)' : 'CE risk (code-based, incl.)'} value="€9.000" tone="positive" />
{ header: de ? 'Mit BP' : 'With BP', numeric: true, width: '22%' }, <StatLine label={de ? 'Compliance-Zeit (80%)' : 'Compliance time (80%)'} value="€15.000" tone="positive" />
{ header: de ? 'Ersparnis' : 'Savings', numeric: true, width: '22%' }, <StatLine label={de ? 'Audit-Vorber. (auf Knopfdruck)' : 'Audit prep (one-click)'} value="€9.000" tone="positive" />
]} <StatLine label={de ? 'Legal-Stunden (60%)' : 'Legal hours (60%)'} value="€5.000" tone="positive" />
rows={[ <StatLine label={de ? 'Schulungen (Academy inkl.)' : 'Training (Academy incl.)'} value="€4.000" tone="positive" />
[de ? 'Pentests (kontinuierlich)' : 'Pentests (continuous)', de ? 'inklusive' : 'included', '€13.000'], </div>
[de ? 'CE-Risiko (Code-Basis)' : 'CE risk (code-based)', de ? 'inklusive' : 'included', '€9.000'],
[de ? 'Compliance-Zeit' : 'Compliance time', '80%', '€15.000'],
[de ? 'Audit-Vorbereitung' : 'Audit prep', de ? 'auf Knopfdruck' : 'one-click', '€9.000'],
[de ? 'Legal-Stunden' : 'Legal hours', '60%', '€5.000'],
[de ? 'Schulungen (Academy)' : 'Training (Academy)', de ? 'inklusive' : 'included', '€4.000'],
[<strong key="cost">{de ? 'BreakPilot Pro' : 'BreakPilot Pro'}</strong>, <strong key="bp">25.000</strong>, ''],
[<strong key="ts">{de ? 'Gesamt-Ersparnis' : 'Total savings'}</strong>, '', <strong key="v" style={{ color: COLORS.emerald700 }}>55.000</strong>],
]}
dense
/>
<div style={{ marginTop: '4mm', padding: '4mm', background: COLORS.emerald50, border: `1px solid ${COLORS.emerald600}`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}> <div style={{ marginTop: '5mm' }}>
<div style={{ fontSize: '7.5pt', color: COLORS.emerald700, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.08em' }}>{de ? 'Netto-Effekt Jahr 1' : 'Net effect year 1'}</div> <Callout tone="negative" label={de ? 'Versteckte Kosten' : 'Hidden costs'}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4mm', marginTop: '2mm' }}> {de
<div style={{ fontSize: '32pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>+30k</div> ? 'Zeit der GF + Compliance-Beauftragten (~30 Tage/Jahr), DSGVO-Bußgelder (bis 4% Jahresumsatz), verlorene RFQs durch fehlende Compliance-Nachweise. Nicht in obigen Zahlen enthalten.'
<div style={{ fontSize: '11pt', color: COLORS.emerald700, fontWeight: 600 }}>{de ? 'pro KMU / Jahr' : 'per SME / year'}</div> : 'Time of management + compliance officer (~30 days/year), GDPR fines (up to 4% annual revenue), lost RFQs from missing compliance evidence. Not included in numbers above.'}
</div> </Callout>
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>{de ? 'Kunde spart €55k, zahlt €25k. ROI ab Tag 1. Zusätzlich: keine Bußgelder, RFQ-Win-Rate ↑, Schlaf-Frieden für die GF.' : 'Customer saves €55k, pays €25k. ROI from day 1. Plus: no fines, RFQ win-rate ↑, peace of mind for management.'}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,9 +1,21 @@
import { Language, PitchProduct } from '@/lib/types' import { Language, PitchProduct } from '@/lib/types'
import { Page, TwoCol, ThreeCol, FourCol, Bullets, Callout, COLORS, Divider, DataTable, StatLine } from './PrintLayout' import { Page, Bullets, Callout, COLORS, DataTable, StatLine } from './PrintLayout'
import { StepStrip, LoopDiagram } from './PrintDiagrams'
import { getDetails } from '@/components/slides/USPSlide.data' import { getDetails } from '@/components/slides/USPSlide.data'
import {
ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck,
AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare,
Shield, Layers, Globe, FileSearch, Sparkles, Repeat, ArrowLeftRight, Infinity,
type LucideIcon,
} from 'lucide-react'
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string } interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
const USP_ICON: Record<string, LucideIcon> = {
rfq: FileSearch, process: ClipboardCheck, bidir: ArrowLeftRight, cont: Repeat,
trace: Layers, engine: Sparkles, opt: TrendingUp, stack: Globe, hub: Infinity,
}
/* ===== USP, PAGE 1 (4 pillars) ===== */ /* ===== USP, PAGE 1 (4 pillars) ===== */
export function PrintUSPPage1({ lang, pageNum, totalPages, versionName }: SlideBase) { export function PrintUSPPage1({ lang, pageNum, totalPages, versionName }: SlideBase) {
@@ -17,19 +29,25 @@ export function PrintUSPPage1({ lang, pageNum, totalPages, versionName }: SlideB
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm', flex: 1, minHeight: 0 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm', flex: 1, minHeight: 0 }}>
{pillars.map((k, i) => { {pillars.map((k, i) => {
const p = d[k] const p = d[k]
const Icon = USP_ICON[k]
return ( return (
<div key={k} style={{ borderLeft: `2px solid ${COLORS.indigo600}`, paddingLeft: '5mm', display: 'flex', flexDirection: 'column', minHeight: 0 }}> <div key={k} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `3px solid ${COLORS.indigo600}`, padding: '4mm 5mm', display: 'flex', flexDirection: 'column', minHeight: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '2mm' }}> <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '2mm' }}>
<span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{p.kicker}</span> <div style={{ width: '12mm', height: '12mm', background: COLORS.indigo50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.indigo600, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<span style={{ fontSize: '7.5pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{String(i + 1).padStart(2, '0')} / 04</span> {Icon && <Icon size={26} strokeWidth={1.5} />}
</div>
<div style={{ textAlign: 'right' }}>
<div style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{p.kicker}</div>
<div style={{ fontSize: '7pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums', marginTop: '1mm' }}>{String(i + 1).padStart(2, '0')} / 04</div>
</div>
</div> </div>
<div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, letterSpacing: '-0.005em', marginBottom: '3mm' }}>{p.title}</div> <div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, letterSpacing: '-0.005em', marginBottom: '3mm', marginTop: '1mm' }}>{p.title}</div>
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{p.body}</div> <div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{p.body}</div>
{p.bullets && <Bullets dense items={p.bullets} />} {p.bullets && <Bullets dense items={p.bullets} />}
{p.stat && ( {p.stat && (
<div style={{ marginTop: 'auto', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}`, display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}> <div style={{ marginTop: 'auto', paddingTop: '3mm', borderTop: `1px solid ${COLORS.slate200}`, display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
<span style={{ fontSize: '7.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>{p.stat.k}</span> <span style={{ fontSize: '7.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.08em', fontWeight: 600 }}>{p.stat.k}</span>
<span style={{ fontSize: '11pt', fontWeight: 800, color: COLORS.emerald700, fontVariantNumeric: 'tabular-nums' }}>{p.stat.v}</span> <span style={{ fontSize: '12pt', fontWeight: 800, color: COLORS.emerald700, fontVariantNumeric: 'tabular-nums' }}>{p.stat.v}</span>
</div> </div>
)} )}
</div> </div>
@@ -50,28 +68,32 @@ export function PrintUSPPage2({ lang, pageNum, totalPages, versionName }: SlideB
return ( return (
<Page kicker="05" section={de ? 'USP · 2 / 2' : 'USP · 2 / 2'} title={de ? 'Under the Hood, was die Plattform technisch trägt.' : 'Under the Hood, what the platform is built on.'} subtitle={de ? 'Vier technische Differentiator: Traceability, Continuous Engine, Compliance Optimizer, EU-Trust Stack.' : 'Four technical differentiators: traceability, continuous engine, compliance optimizer, EU trust stack.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="05" section={de ? 'USP · 2 / 2' : 'USP · 2 / 2'} title={de ? 'Under the Hood, was die Plattform technisch trägt.' : 'Under the Hood, what the platform is built on.'} subtitle={de ? 'Vier technische Differentiator: Traceability, Continuous Engine, Compliance Optimizer, EU-Trust Stack.' : 'Four technical differentiators: traceability, continuous engine, compliance optimizer, EU trust stack.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6mm' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '5mm' }}>
{cards.map(k => { {cards.map(k => {
const p = d[k] const p = d[k]
const Icon = USP_ICON[k]
return ( return (
<div key={k} style={{ borderLeft: `2px solid ${COLORS.amber600}`, paddingLeft: '5mm', display: 'flex', flexDirection: 'column' }}> <div key={k} style={{ borderLeft: `2px solid ${COLORS.amber600}`, paddingLeft: '5mm', display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.amber700, textTransform: 'uppercase', letterSpacing: '0.12em', marginBottom: '2mm' }}>{p.kicker}</div> <div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '2mm' }}>
<div style={{ fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '3mm' }}>{p.title}</div> {Icon && <Icon size={18} strokeWidth={1.5} color={COLORS.amber700} />}
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{p.body}</div> <span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.amber700, textTransform: 'uppercase', letterSpacing: '0.12em' }}>{p.kicker}</span>
</div>
<div style={{ fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '2.5mm' }}>{p.title}</div>
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '2.5mm' }}>{p.body}</div>
{p.bullets && <Bullets dense items={p.bullets} />} {p.bullets && <Bullets dense items={p.bullets} />}
</div> </div>
) )
})} })}
</div> </div>
<div style={{ marginTop: '6mm' }}> <div style={{ marginTop: '5mm', border: `1px solid ${COLORS.indigo600}`, background: COLORS.indigo50, padding: '4mm 5mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<Callout tone="accent" label={de ? 'Die Schleife' : 'The Loop'}> <div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '2mm' }}>
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2mm' }}>{d.hub.title}</div> <Infinity size={20} strokeWidth={1.5} color={COLORS.indigo700} />
<div>{d.hub.body}</div> <span style={{ fontSize: '7.5pt', fontWeight: 700, color: COLORS.indigo700, textTransform: 'uppercase', letterSpacing: '0.14em' }}>{de ? 'Die Schleife' : 'The Loop'}</span>
{d.hub.bullets && ( </div>
<div style={{ marginTop: '3mm' }}><Bullets dense tone="accent" items={d.hub.bullets} /></div> <div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2mm' }}>{d.hub.title}</div>
)} <div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, marginBottom: '3mm' }}>{d.hub.body}</div>
</Callout> <LoopDiagram lang={lang} />
</div> </div>
</Page> </Page>
) )
@@ -168,33 +190,37 @@ export function PrintRegulatoryLandscapePage({ lang, pageNum, totalPages, versio
/* ===== PRODUCT / MODULAR TOOLKIT ===== */ /* ===== PRODUCT / MODULAR TOOLKIT ===== */
const MODULE_ICONS: LucideIcon[] = [
ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck,
AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare,
]
const MODULES_FULL_DE = [ const MODULES_FULL_DE = [
{ name: 'Code Security', icon: '◇', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Bei jedem Push', 'Auto-Fix LLM', 'CI/CD-integriert'] }, { name: 'Code Security', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Bei jedem Push', 'Auto-Fix LLM', 'CI/CD-integriert'] },
{ name: 'CE-SW-Risikobeurteilung', icon: '◇', desc: 'CE-Kennzeichnung für Maschinen mit Software-Anteil', features: ['Maschinen-VO', 'CRA-konform', 'Code-Basis-Analyse'] }, { name: 'CE-SW-Risikobeurteilung', desc: 'CE-Kennzeichnung für Maschinen mit Software-Anteil', features: ['Maschinen-VO', 'CRA-konform', 'Code-Basis-Analyse'] },
{ name: 'Compliance-Dokumente', icon: '◇', desc: 'VVT (Art. 30) · TOMs · DSFA (Art. 35) · Löschkonzept', features: ['Auto-Generiert', 'Versionsverlauf', 'Audit-tauglich'] }, { name: 'Compliance-Dokumente', desc: 'VVT (Art. 30) · TOMs · DSFA (Art. 35) · Löschkonzept', features: ['Auto-Generiert', 'Versionsverlauf', 'Audit-tauglich'] },
{ name: 'Audit Manager', icon: '◇', desc: 'Abweichungen End-to-End: Rollen · Stichtage · Eskalation', features: ['Tickets + Nachweise', 'GF-Eskalation', 'Compliance-SLA'] }, { name: 'Audit Manager', desc: 'Abweichungen End-to-End: Rollen · Stichtage · Eskalation', features: ['Tickets + Nachweise', 'GF-Eskalation', 'Compliance-SLA'] },
{ name: 'DSR / Betroffenenrechte', icon: '◇', desc: 'Auskunft, Berichtigung, Löschung, Datenübertragbarkeit', features: ['Self-Service', 'Identitätsprüfung', 'Frist-Tracking'] }, { name: 'DSR / Betroffenenrechte', desc: 'Auskunft, Berichtigung, Löschung, Datenübertragbarkeit', features: ['Self-Service', 'Identitätsprüfung', 'Frist-Tracking'] },
{ name: 'Consent', icon: '◇', desc: 'Einwilligungs-Management, Cookie-Banner, ePrivacy', features: ['CMP integriert', 'Audit-Log', 'Multi-Tenant'] }, { name: 'Consent', desc: 'Einwilligungs-Management, Cookie-Banner, ePrivacy', features: ['CMP integriert', 'Audit-Log', 'Multi-Tenant'] },
{ name: 'Incident Response', icon: '◇', desc: 'Vorfälle, Meldung (72h), Mitigation, Forensik', features: ['Art. 33/34 DSGVO', 'BSI-Meldepfade', 'Forensik-Hooks'] }, { name: 'Incident Response', desc: 'Vorfälle, Meldung (72h), Mitigation, Forensik', features: ['Art. 33/34 DSGVO', 'BSI-Meldepfade', 'Forensik-Hooks'] },
{ name: 'Compliance LLM', icon: '◇', desc: 'GPT für Text + Audio, EU-gehostet, mit Quellenangabe', features: ['Self-Hosted', 'EU-souverän', 'Audit-zitierbar'] }, { name: 'Compliance LLM', desc: 'GPT für Text + Audio, EU-gehostet, mit Quellenangabe', features: ['Self-Hosted', 'EU-souverän', 'Audit-zitierbar'] },
{ name: 'Tender Matching', icon: '◇', desc: 'RFQ-Antworten automatisch gegen Codebase + Policies', features: ['Stunden statt Wochen', 'Win-ready', 'Klausel-Mapping'] }, { name: 'Tender Matching', desc: 'RFQ-Antworten automatisch gegen Codebase + Policies', features: ['Stunden statt Wochen', 'Win-ready', 'Klausel-Mapping'] },
{ name: 'Academy', icon: '◇', desc: 'Online-Schulungen für Geschäftsführung und Mitarbeiter', features: ['Mandatory Training', 'Zertifikate', 'GF-Pflicht erfüllt'] }, { name: 'Academy', desc: 'Online-Schulungen für Geschäftsführung und Mitarbeiter', features: ['Mandatory Training', 'Zertifikate', 'GF-Pflicht erfüllt'] },
{ name: 'Compliance Optimizer', icon: '◇', desc: 'Maximale KI-Nutzung im legalen Rahmen, ersetzt 20-200k € Anwaltskosten', features: ['ROI-ranking', 'Sweet-Spot', 'Risikobalance'] }, { name: 'Compliance Optimizer', desc: 'Maximale KI-Nutzung im legalen Rahmen, ersetzt 20-200k € Anwaltskosten', features: ['ROI-ranking', 'Sweet-Spot', 'Risikobalance'] },
{ name: 'Kommunikation', icon: '◇', desc: 'Chat (Matrix) + Video (Jitsi) + KI-Support', features: ['Self-Hosted', 'EU-Hosting', 'Audit-Logs'] }, { name: 'Kommunikation', desc: 'Chat (Matrix) + Video (Jitsi) + KI-Support', features: ['Self-Hosted', 'EU-Hosting', 'Audit-Logs'] },
] ]
const MODULES_FULL_EN = [ const MODULES_FULL_EN = [
{ name: 'Code Security', icon: '◇', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Every push', 'Auto-fix LLM', 'CI/CD integrated'] }, { name: 'Code Security', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Every push', 'Auto-fix LLM', 'CI/CD integrated'] },
{ name: 'CE SW Risk Assessment', icon: '◇', desc: 'CE marking for machinery with software', features: ['Machinery Reg.', 'CRA-compliant', 'Code-level analysis'] }, { name: 'CE SW Risk Assessment', desc: 'CE marking for machinery with software', features: ['Machinery Reg.', 'CRA-compliant', 'Code-level analysis'] },
{ name: 'Compliance Documents', icon: '◇', desc: 'RoPA (Art. 30) · TOMs · DPIA (Art. 35) · Retention', features: ['Auto-generated', 'Version history', 'Audit-ready'] }, { name: 'Compliance Documents', desc: 'RoPA (Art. 30) · TOMs · DPIA (Art. 35) · Retention', features: ['Auto-generated', 'Version history', 'Audit-ready'] },
{ name: 'Audit Manager', icon: '◇', desc: 'Deviations end-to-end: roles · deadlines · escalation', features: ['Tickets + evidence', 'Mgmt escalation', 'Compliance SLA'] }, { name: 'Audit Manager', desc: 'Deviations end-to-end: roles · deadlines · escalation', features: ['Tickets + evidence', 'Mgmt escalation', 'Compliance SLA'] },
{ name: 'DSR / Data Subject Rights', icon: '◇', desc: 'Access, rectification, erasure, portability', features: ['Self-service', 'Identity check', 'Deadline tracking'] }, { name: 'DSR / Data Subject Rights', desc: 'Access, rectification, erasure, portability', features: ['Self-service', 'Identity check', 'Deadline tracking'] },
{ name: 'Consent', icon: '◇', desc: 'Consent mgmt, cookie banner, ePrivacy', features: ['CMP integrated', 'Audit log', 'Multi-tenant'] }, { name: 'Consent', desc: 'Consent mgmt, cookie banner, ePrivacy', features: ['CMP integrated', 'Audit log', 'Multi-tenant'] },
{ name: 'Incident Response', icon: '◇', desc: 'Breaches, reporting (72h), mitigation, forensics', features: ['GDPR Art. 33/34', 'BSI channels', 'Forensic hooks'] }, { name: 'Incident Response', desc: 'Breaches, reporting (72h), mitigation, forensics', features: ['GDPR Art. 33/34', 'BSI channels', 'Forensic hooks'] },
{ name: 'Compliance LLM', icon: '◇', desc: 'GPT for text + audio, EU-hosted, with citations', features: ['Self-hosted', 'EU-sovereign', 'Audit-citable'] }, { name: 'Compliance LLM', desc: 'GPT for text + audio, EU-hosted, with citations', features: ['Self-hosted', 'EU-sovereign', 'Audit-citable'] },
{ name: 'Tender Matching', icon: '◇', desc: 'RFQ answers automatically against codebase + policies', features: ['Hours not weeks', 'Win-ready', 'Clause mapping'] }, { name: 'Tender Matching', desc: 'RFQ answers automatically against codebase + policies', features: ['Hours not weeks', 'Win-ready', 'Clause mapping'] },
{ name: 'Academy', icon: '◇', desc: 'Online training for management and staff', features: ['Mandatory training', 'Certificates', 'Mgmt duties fulfilled'] }, { name: 'Academy', desc: 'Online training for management and staff', features: ['Mandatory training', 'Certificates', 'Mgmt duties fulfilled'] },
{ name: 'Compliance Optimizer', icon: '◇', desc: 'Max AI use within legal limits, replaces €20-200k legal fees', features: ['ROI ranking', 'Sweet spot', 'Risk balance'] }, { name: 'Compliance Optimizer', desc: 'Max AI use within legal limits, replaces €20-200k legal fees', features: ['ROI ranking', 'Sweet spot', 'Risk balance'] },
{ name: 'Communication', icon: '◇', desc: 'Chat (Matrix) + video (Jitsi) + AI support', features: ['Self-hosted', 'EU hosting', 'Audit logs'] }, { name: 'Communication', desc: 'Chat (Matrix) + video (Jitsi) + AI support', features: ['Self-hosted', 'EU hosting', 'Audit logs'] },
] ]
export function PrintProductPage({ products, lang, pageNum, totalPages, versionName }: SlideBase & { products: PitchProduct[] }) { export function PrintProductPage({ products, lang, pageNum, totalPages, versionName }: SlideBase & { products: PitchProduct[] }) {
@@ -205,16 +231,22 @@ export function PrintProductPage({ products, lang, pageNum, totalPages, versionN
<Page kicker="07" section={de ? 'MODULARER BAUKASTEN' : 'MODULAR TOOLKIT'} title={de ? '12 Module, einzeln oder als Bundle.' : '12 modules, individual or bundled.'} subtitle={de ? 'Kunden wählen die Module, die sie brauchen. Jedes Modul liefert eigene Compliance-Werte; die Plattform integriert sie zu einem Audit-Trail.' : 'Customers pick the modules they need. Each module delivers compliance value on its own; the platform unifies them into a single audit trail.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="07" section={de ? 'MODULARER BAUKASTEN' : 'MODULAR TOOLKIT'} title={de ? '12 Module, einzeln oder als Bundle.' : '12 modules, individual or bundled.'} subtitle={de ? 'Kunden wählen die Module, die sie brauchen. Jedes Modul liefert eigene Compliance-Werte; die Plattform integriert sie zu einem Audit-Trail.' : 'Customers pick the modules they need. Each module delivers compliance value on its own; the platform unifies them into a single audit trail.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', flex: 1, minHeight: 0 }}> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', flex: 1, minHeight: 0 }}>
{modules.map((m, i) => ( {modules.map((m, i) => {
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '3mm 4mm', display: 'flex', flexDirection: 'column' }}> const Icon = MODULE_ICONS[i]
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: '1mm' }}> return (
<div style={{ fontSize: '10pt', fontWeight: 700, color: COLORS.slate900 }}>{m.name}</div> <div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '3mm 4mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ fontSize: '7pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{String(i + 1).padStart(2, '0')}</div> <div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '1.5mm' }}>
<div style={{ width: '8mm', height: '8mm', background: COLORS.indigo50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.indigo600, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<Icon size={16} strokeWidth={1.5} />
</div>
<div style={{ flex: 1, minWidth: 0, fontSize: '10pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15 }}>{m.name}</div>
<div style={{ fontSize: '7pt', color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{String(i + 1).padStart(2, '0')}</div>
</div>
<div style={{ fontSize: '8pt', color: COLORS.slate600, lineHeight: 1.4, marginBottom: '2mm' }}>{m.desc}</div>
<div style={{ marginTop: 'auto', borderTop: `1px solid ${COLORS.slate100}`, paddingTop: '2mm', fontSize: '7pt', color: COLORS.slate500 }}>{m.features.join(' · ')}</div>
</div> </div>
<div style={{ fontSize: '8pt', color: COLORS.slate600, lineHeight: 1.45, marginBottom: '2mm' }}>{m.desc}</div> )
<div style={{ marginTop: 'auto', borderTop: `1px solid ${COLORS.slate100}`, paddingTop: '2mm', fontSize: '7pt', color: COLORS.slate500 }}>{m.features.join(' · ')}</div> })}
</div>
))}
</div> </div>
<div style={{ marginTop: '4mm', flexShrink: 0 }}> <div style={{ marginTop: '4mm', flexShrink: 0 }}>
@@ -249,18 +281,8 @@ export function PrintHowItWorksPage({ lang, pageNum, totalPages, versionName }:
return ( return (
<Page kicker="08" section={de ? 'SO FUNKTIONIERT\'S' : 'HOW IT WORKS'} title={de ? 'In 4 Schritten zur kontinuierlichen Compliance.' : 'Continuous compliance in 4 steps.'} subtitle={de ? 'Vom Vertrag bis zur Audit-Bereitschaft in der Regel <30 Tage. Kein Excel, kein Pentest-Vendor, keine manuelle Dokumentenpflege.' : 'From contract to audit-ready typically in <30 days. No Excel, no pentest vendor, no manual document maintenance.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}> <Page kicker="08" section={de ? 'SO FUNKTIONIERT\'S' : 'HOW IT WORKS'} title={de ? 'In 4 Schritten zur kontinuierlichen Compliance.' : 'Continuous compliance in 4 steps.'} subtitle={de ? 'Vom Vertrag bis zur Audit-Bereitschaft in der Regel <30 Tage. Kein Excel, kein Pentest-Vendor, keine manuelle Dokumentenpflege.' : 'From contract to audit-ready typically in <30 days. No Excel, no pentest vendor, no manual document maintenance.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
{/* horizontal step flow */} <div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'stretch' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '6mm', flex: 1, minHeight: 0 }}> <StepStrip steps={steps} />
{steps.map((s, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', position: 'relative' }}>
{i < steps.length - 1 && (
<div style={{ position: 'absolute', top: '14mm', right: '-4mm', width: '2mm', height: '1px', background: COLORS.slate300 }} />
)}
<div style={{ fontSize: '32pt', fontWeight: 800, color: COLORS.indigo600, lineHeight: 1, marginBottom: '4mm', fontVariantNumeric: 'tabular-nums' }}>{s.n}</div>
<div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2, marginBottom: '3mm', letterSpacing: '-0.005em' }}>{s.t}</div>
<div style={{ fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{s.d}</div>
</div>
))}
</div> </div>
<div style={{ marginTop: '6mm', flexShrink: 0 }}> <div style={{ marginTop: '6mm', flexShrink: 0 }}>
+17 -18
View File
@@ -65,29 +65,28 @@ export default async function PitchPrintPage({ params, searchParams }: Ctx) {
fp_scenarios: (map.fm_scenarios || []) as FpScenarioRef[], fp_scenarios: (map.fm_scenarios || []) as FpScenarioRef[],
} }
// Financial variant: fetch FM results + parse assumptions // Always fetch FM results + assumptions so the standard PDF can render the
// annex-finanzplan slide. The `financial` flag only adds the extra detail
// P&L page and the cap-table page.
let fmResults: FMResult[] = [] let fmResults: FMResult[] = []
let fmAssumptions: FMAssumption[] = [] let fmAssumptions: FMAssumption[] = []
if (financial) { const scenarios = (map.fm_scenarios || []) as FpScenarioRef[]
const scenarios = (map.fm_scenarios || []) as FpScenarioRef[] const defaultScenario = scenarios.find(s => s.is_default) ?? scenarios[0] ?? null
const defaultScenario = scenarios.find(s => s.is_default) ?? scenarios[0] ?? null if (defaultScenario?.id) {
const resultsRes = await pool.query(
if (defaultScenario?.id) { `SELECT * FROM pitch_fm_results WHERE scenario_id = $1 ORDER BY month`,
const resultsRes = await pool.query( [defaultScenario.id],
`SELECT * FROM pitch_fm_results WHERE scenario_id = $1 ORDER BY month`, )
[defaultScenario.id], fmResults = resultsRes.rows as FMResult[]
)
fmResults = resultsRes.rows as FMResult[]
}
const rawAssumptions = (map.fm_assumptions || []) as Array<Record<string, unknown>>
fmAssumptions = rawAssumptions.map(a => ({
...a,
value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value,
})) as FMAssumption[]
} }
const rawAssumptions = (map.fm_assumptions || []) as Array<Record<string, unknown>>
fmAssumptions = rawAssumptions.map(a => ({
...a,
value: typeof a.value === 'string' ? JSON.parse(a.value as string) : a.value,
})) as FMAssumption[]
return ( return (
<PrintDeck <PrintDeck
pitchData={pitchData} pitchData={pitchData}