8 Commits

Author SHA1 Message Date
Sharang Parnerkar a7850a0296 Add L-Bank Pre-Seed Finanzplan submissions (Base/Bear/Bull)
Build pitch-deck / build-push-deploy (push) Waiting to run
CI / go-lint (push) Waiting to run
CI / python-lint (push) Waiting to run
CI / nodejs-lint (push) Waiting to run
CI / test-go-consent (push) Waiting to run
CI / test-python-voice (push) Waiting to run
CI / test-bqas (push) Waiting to run
Wandeldarlehen 400k model mapped into the official L-Bank V1.1
Finanzplan template (36 months, Aug 2026 to Jul 2029) for the three
scenarios. Each reconciles to the source liquidity to the cent; grants
booked as cash inflows (out of EBIT); "ohne Pre-Seed" excludes both
tranches; Planungsprämissen and helper tabs filled; Anleitung intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:25:09 +02:00
Benjamin Admin ec3b0e26fd Merge branch 'chore/license-mapping-audit' — license mapping + audit script + DGUV + /staerken marketing page
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 37s
CI / test-python-voice (push) Successful in 31s
CI / test-bqas (push) Successful in 34s
2026-05-22 00:54:49 +02:00
Benjamin Admin 19d1a56df4 feat(marketing): /staerken page with 7 USPs from IACE strategy — Task #19
Long-form differentiator page covering the seven sales arguments from
project_marketing_website_3014_themes.md, all anchor-linkable for
sales decks:

  #1 engine          — Pattern-engine vs Excel-checklist
  #2 multi-markt     — One risk assessment, all markets (CE+US+CN+JP)
  #3 folgegefahren   — Operator-to-end-customer harm chain
  #4 public-domain   — OSHA/NIST/EUR-Lex/BAuA as legal anchor
  #5 audit-suite     — Engine self-introspection (cmd/iace-audit A-E)
  #6 made-in-germany — German export meets US Federal PD
  #7 tooling         — LLM gap-review as co-pilot, not robo-lawyer

Each section carries a "Belegt durch:" line pointing at the actual
codebase artifact behind the claim, so the page reads as audit-friendly
proof, not marketing fluff.

Below the 7 differentiators a competitor comparison table (BreakPilot
vs DesignSafe vs Pilz PASS vs Sick SD vs Sphera) and a closing block
explaining the R1/R2/R3 license architecture with a pointer to
/sdk/licenses.

Navbar updated to surface the page between Plattform and CE-Prozess.

This closes Task #19. With Task #29 + #7/#8 already in, the roadmap
post-licence-classification work is fully landed.
2026-05-22 00:36:09 +02:00
Benjamin Admin 3934bdf814 docs(impressum): add Quellen & Lizenzen section with /sdk/licenses ref
Adds a "Quellen und Lizenzen der Compliance-Inhalte" section to the
marketing-website Impressum naming the public sources the platform
draws on (EUR-Lex, US Federal Code, ENISA/EDPB/BAuA, OWASP, OECD,
eigene Texte) and pointing to /sdk/licenses for the full per-source
breakdown.

The Datenschutz and Impressum audit (Task #24 in breakpilot-compliance)
confirmed no spurious license claims were buried in these pages.
This change adds explicit transparency rather than removing anything,
and is paired with the explicit disclaimer that the Pauschalvermerk
does NOT replace work-level attribution — that is handled by the
auto-footer in PDFs and the <SourceBadge> in the SDK frontend.
2026-05-21 22:19:24 +02:00
Benjamin Admin dbd44ecc20 feat(licenses): postgres + qdrant license_rule backfill scripts
Two idempotent scripts that complete Task #22 (300k atomic_controls
reclassification) across both Postgres DBs and all Qdrant collections
on Mac Mini + Production.

backfill_license_rule.py
- iterative parent_control_uuid inheritance with cycle cap
- dry-run + apply modes, per-iteration row counts
- residual-orphan cluster report for manual review

backfill_qdrant_license_payload.py
- joins canonical_controls.id (or regulation_id) → license_rule
- scrolls + grouped set_payload per rule (3 batches per collection)
- supports both lookup tables (canonical_controls / regulation_registry)
- supports managed Qdrant via --qdrant-api-key (Production)

Backfill bilance:
- Mac Mini canonical_controls: 0 NULL (was 279,384) across 314,811 rows
- Mac Mini Qdrant atomic_controls_dedup: 44,987 points patched
- Mac Mini bp_compliance_gesetze: 37,634 points patched
- Mac Mini bp_compliance_datenschutz: 11,338 points patched
- Production canonical_controls: 0 NULL (was 259,914) across 294,027 rows
- Production Qdrant bp_compliance_gesetze: 55,836 patched
- Production Qdrant bp_compliance_datenschutz: 18,980 patched
- Production Qdrant bp_compliance_ce: 23,239 patched

Schema migration 002_regulation_registry.sql + 252 registry rows were
replicated to Production (was missing — only existed on Mac Mini).
20 BSI/DE-Gesetz entries added to registry to close Qdrant lookup gap.

100% deterministic classification achieved on both DBs via:
- parent_control_uuid inheritance (94% coverage)
- control_parent_links.source_regulation → regulation_registry
- source_citation->>'source' → regulation_registry
- canonical_processed_chunks ground truth (chunk-validated)
- ungrouped LLM-aggregate Vorfahren → own works (Rule 3)

[migration-approved]
2026-05-21 18:46:57 +02:00
Benjamin Admin 93687a32fe docs(licenses): freeze 3-rule license mapping + audit script
Defines the authoritative mapping from license_type to license_rule
in docs/LICENSE_RULES.md, and adds scripts/audit_license_classification.py
to surface classification gaps in registry/canonical_controls/Qdrant.

Key finding from first audit run against bp-core-postgres + Qdrant:

- regulation_registry: 232 rows, 224 rule=1, 8 rule=2, 0 rule=3;
  36 rows without license_type (need backfill)
- canonical_controls: 314,811 rows, 279,384 (89%) have NULL
  license_rule (target of Task #22 reclassification)
- Qdrant atomic_controls_dedup: 100% of sampled points lack both
  license and license_rule payload fields
- Qdrant bp_compliance_gesetze: 80.6% lack both fields
- Qdrant bp_compliance_ce + bp_compliance: nearly clean

Rule definitions clarified (was loosely remembered as
"law / cite / rewrite"):
- Rule 1 = verbatim, sovereign law (EU/DE/AT/CH/US, TRBS/TRGS/ASR,
  OSHA, NIST, EU guidelines, DGUV UVV)
- Rule 2 = verbatim with attribution (CC-BY, Apache, OWASP,
  OECD AI Principles, ENISA)
- Rule 3 = identifier citation only, no full text (DIN/EN/ISO,
  ANSI/UL/IEC, DGUV Regeln/Informationen/Grundsaetze, BSI,
  proprietary standards). Pipeline drops chunk_text when rule=3
  in pipeline_adapter.py:147.

The 4th category I had proposed ("R1-A") turned out to be already
implemented as rule=2; the mapping doc reflects the actual code
behaviour rather than the original 3-name verbal model.

No schema change. No data migration in this commit — reclassification
of the 279k controls is staged as Task #22 and will be cluster-based
by source/regulation_id.
2026-05-21 11:29:38 +02:00
Sharang Parnerkar 2d9fec3a6d feat(pitch-print): 10 slide redesigns from parallel agent review
Build pitch-deck / build-push-deploy (push) Successful in 1m53s
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 34s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 36s
Per the user's batch review of the rendered PDF. Five subagents ran in parallel,
each owning a different slide file; this is the merged result.

Slide 10 — Regulatory Landscape (PrintProductSlides)
  8 regulatory categories now render as a 2×4 icon-tile grid (was a DataTable):
  Lock / Shield / Brain / Globe / ShieldCheck / Banknote / Heart / Users.
  10 industry profiles now each show an icon next to the name
  (Factory for Maschinenbau Kernfokus, plus Heart, Banknote, ShoppingCart, Cpu,
  Wifi, Brain, ShieldCheck, BookOpen, Landmark, Building2).

Slide 12 — How It Works (PrintProductSlides)
  Step rail and day timeline pulled together (was a big empty middle).
  Added a "Was Sie wann bekommen" 4-column benefit block in the bottom third
  (Shield/FileText/CheckCircle2/Zap), with mid-page "Median 14 Tage" callout.

Slide 13 — Market TAM / SAM / SOM (PrintMarketSlides)
  Dropped MarketFunnel primitive. Left column: SVG nested concentric circles
  (TAM r=60 violet, SAM r=36 violet, SOM r=14 amber as Kernmarkt). Right column:
  three stacked TAM/SAM/SOM info cards with mono kicker, big EUR value, growth
  rate, one-line description; SOM card carries amber accent + "← unser Kernmarkt".

Slide 14 — Pricing green box (PrintProductSlides)
  Net-effect callout expanded from 2 lines to a full breakdown:
  Pentests +€13k / CE-Risiko +€9k / Compliance-Zeit (−60%) +€15k /
  Audit-Vorber. (auto) +€9k / Legal-Stunden (−40%) +€5k / Schulungen +€4k.
  Italic footnote: "Plus Vermeidung von Bußgeldern und gewonnene RFQs."

Slide 17 — Competition AppSec title (PrintCompetitionSlides)
  Title rewritten to investor-friendly framing — "Cyber-Security: BreakPilot
  ersetzt das ganze AppSec-Stack" (was SAST + DAST + SCA + Pentesting).

Slide 18 — Team founder bios (PrintMarketSlides)
  Prose paragraphs replaced with 5 icon-bulleted skill/achievement lines per
  founder. Benjamin gets violet-50 tiles (Briefcase, RefreshCw, Handshake,
  Scale, Lightbulb). Sharang gets amber-50 tiles (Code, TrendingUp, CreditCard,
  ShieldCheck, Cpu). Photo + name + role + equity header preserved.

Slide 23 — KPIs trajectory (PrintNewSlides)
  Each of the 8 KPI tiles now has a 15mm × 8mm SVG sparkline at the bottom
  showing the 5-year progression. Stroke color adapts per metric (violet
  default, emerald for cash/margin, red→emerald for EBIT/net-income across
  break-even). All-zero series fall back to em-dash. Awkward "0 → 0" prefix
  suppressed on missing-data tiles.

Slide 28 — Regulatory Pillars (PrintAnnexSlides)
  Rebuilt as 4 actual vertical pillars (was 2×2 box grid). Each pillar has:
  capital (top, gradient tint, mono kicker + 01-04 number), shaft (white card
  with title + description + 2mm colored left border), base (bottom, darker
  tint, mono law citations). A shared horizontal "ground line" below all four
  pillars completes the architectural reference.

Slide 29 — Architecture 3D (PrintDiagrams)
  Faked 3D depth via staggered right indent (0/2/4mm), inset top highlight
  and bottom seam shadows, per-layer drop-shadow with rising opacity. Layer 03
  reads as the foundation; layer 01 floats on top. PlaneConnector chevrons
  replace the simple SVG down-arrows between tiers. Text stays horizontal.

Slide 31 — Tech Stack (PrintNewSlides)
  Cards now have 14mm violet-gradient icon tiles (was 8mm flat), mono kicker
  number, 12pt category name, italic one-line blurb, and the techs as rounded
  chip tags (violet-50 / violet-200, mono 7.5pt) instead of a flat mono list.
  Title cleaned: "100 % " → "100%".

All files under 500 LOC except PrintIntroSlides (515, preexisting issue).
TypeScript clean, next build green, all 38 routes compile.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:11:58 +02:00
Sharang Parnerkar a6f4ca88a4 fix(pitch-print): ComplAI brand, em-dash centering, fund fallback 400k
Build pitch-deck / build-push-deploy (push) Successful in 2m47s
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 43s
CI / test-python-voice (push) Successful in 36s
CI / test-bqas (push) Successful in 35s
Three universal fixes before per-slide redesigns:

1. Brand: new <ComplAI /> JSX component renders the product name correctly —
   'Compl' in inherited text color, 'AI' in violet (#7c3aed), no slashes.
   Replaces the previous 'BreakPilot COMPL/AI/' literal in the Executive
   Summary p1 title. Page primitive's title prop now accepts ReactNode so
   JSX brand wordmarks work anywhere a title would.

2. Em-dash centering: Bullets primitive previously placed each em-dash
   marker via absolute positioning with a hardcoded 'top: 4pt', which drifted
   relative to font-size and looked off-center in the rendered PDF. Now uses
   display:flex on the <li> with a fixed-width column that vertically centers
   the 0.5pt rule on the first line height of the text.

3. Funding fallback: cover + The Ask now default to 400_000 (was 1_000_000)
   when no funding amount is in the data. New base case is a €400k
   Wandeldarlehen, not €1M equity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:58:55 +02:00
18 changed files with 1392 additions and 235 deletions
+83
View File
@@ -0,0 +1,83 @@
# Lizenzregeln der Control-Pipeline
> **Stand:** 2026-05-21 — Mapping festgezurrt nach DB-Inspektion und IACE-Audit.
>
> Die Pipeline klassifiziert jede Regulation (und damit jedes daraus extrahierte
> Chunk und jeden atomic_control) in eine von **drei Lizenzregeln**. Die Regel
> entscheidet, ob der Volltext aufbewahrt werden darf und welche Attribution im
> Ausgabe-Renderer Pflicht ist.
## Die drei Regeln
| Regel | Bedeutung | Volltext speichern? | Attribution Pflicht? | Beispiele |
|-------|-----------|---------------------|----------------------|-----------|
| **1** | Wörtlich — Hoheitsrecht / Public Domain | ✓ | nein (empfohlen für Audit) | EU-Recht (EUR-Lex), Bundesrecht, Satzungsrecht (DGUV UVV), TRBS, TRGS, ASR, US Federal Code (OSHA), NIST SP, EU-Leitfäden |
| **2** | Wörtlich mit Attribution — freie Lizenzen | ✓ | **ja** | OWASP (CC-BY-SA-4.0), OECD AI Principles (OECD_PUBLIC), ENISA-Dokumente (CC-BY-4.0), Apache-2.0 Werke |
| **3** | Nur zitieren — proprietäre Standards | ✗ | nicht anwendbar (kein Volltext) | DIN, EN, ISO, ANSI, UL, IEC, IEEE, DGUV Regeln/Informationen/Grundsätze, Bitkom-Leitfäden, BSI-Bausteine (urheberrechtlich) |
**Wichtige Klarstellung:** Regel 3 = "nur Identifier/Abschnitt zitieren", **nicht** "umformulieren". Die ursprüngliche Bezeichnung "neu formulieren" war irreführend. Korrekt: Bei Regel-3-Quellen darf die Pipeline den Volltext nicht speichern; sie bewahrt nur die Quellenreferenz (regulation_id + article/paragraph), und der Output-Renderer zeigt diese Referenz im Frontend/PDF.
## Mapping `license_type` → `license_rule`
| license_type | license_rule | Erklärung |
|---|---|---|
| `EU_LAW`, `EU_PUBLIC` | 1 | EU-Verordnungen, Richtlinien, OJ-Veröffentlichungen, EU-Leitfäden |
| `DE_LAW`, `DE_PUBLIC` | 1 | Bundesgesetze, TRBS, TRGS, ASR, DGUV-UVV (Satzungsrecht) |
| `AT_LAW`, `CH_LAW`, `FR_LAW`, `IT_LAW`, `ES_LAW`, `NL_LAW`, `HU_LAW` | 1 | Andere EU-Mitgliedsstaaten-Recht |
| `US_GOV_PUBLIC`, `NIST_PUBLIC_DOMAIN`, `OSHA_PUBLIC` | 1 | US Federal Code (17 U.S.C. §105 Public Domain) |
| `CC-BY-4.0`, `CC-BY-SA-4.0`, `CC-BY-3.0`, `CC-BY-SA-3.0` | 2 | Creative-Commons mit Attribution-Pflicht |
| `Apache-2.0`, `MIT` | 2 | Permissive OSS-Lizenzen, NOTICE-Pflicht |
| `OECD_PUBLIC`, `ENISA_CC_BY_4.0` | 2 | Behörden-Publikationen mit Attribution-Auflage |
| `DIN_COPYRIGHT`, `ISO_COPYRIGHT`, `ANSI_COPYRIGHT`, `UL_COPYRIGHT`, `IEC_COPYRIGHT` | 3 | Normungsorganisationen — nur Identifier-Zitat |
| `DGUV_COPYRIGHT` | 3 | DGUV Regeln/Informationen/Grundsätze (nicht UVV) |
| `BITKOM_COPYRIGHT`, `BSI_COPYRIGHT`, `VDMA_COPYRIGHT` | 3 | Verbands-/Behörden-Publikationen mit eigenständigem Urheberrecht |
| `OWN_WORK` | 3 | BreakPilot-Eigentexte (Templates, eigene Patterns) — kein externes Lizenzrisiko, aber auch kein Public-Domain-Status |
**Sonderfall DGUV:** Die Klasse trennt sich nach Publikationstyp:
- DGUV **Vorschriften / UVV**`DE_LAW` → Regel 1
- DGUV **Regeln, Informationen, Grundsätze**`DGUV_COPYRIGHT` → Regel 3
## Auswirkung pro Pipeline-Stage
| Stage | Verhalten bei Regel 1 | Regel 2 | Regel 3 |
|---|---|---|---|
| Stage 6 ControlCompose (`pipeline_adapter.py:147`) | speichert `chunk_text` | speichert `chunk_text` | speichert `chunk_text = None` |
| Atomic-Control-Bildung | Volltext als Quelle | Volltext + Attribution-Vermerk | nur regulation_id + article |
| Output-Renderer (Frontend/PDF) | optionaler Quellen-Hinweis | **Pflicht-Attribution in Footer + Inline** | nur Identifier rendern |
| Tech-File-Anhang | Quelle nennen | Quelle + Lizenz-URL | Identifier-Liste |
## Quellen ohne Klassifikation
Aktuell sind in `regulation_registry` **232 Regulationen** klassifiziert (Stand 2026-05-21). Die folgenden müssen noch ergänzt werden (Task #20 deckt den DGUV-Ingest):
| Quelle | Regel | Begründung |
|---|---|---|
| TRBS-Familie (24 PDFs im RAG) | 1 | Technische Regeln Betriebssicherheit — BAuA Bundesarbeitsblatt |
| TRGS-Familie (alle Volltext-Chunks) | 1 | Technische Regeln Gefahrstoffe — BAuA |
| ASR-Familie (17 PDFs) | 1 | Arbeitsstättenregeln — BAuA |
| OSHA 29 CFR 1910 Subpart O + Technical Manual | 1 | US Federal Public Domain (17 U.S.C. §105) |
| DGUV Vorschrift 1 + UVV-Familie (sobald ingest) | 1 | Satzungsrecht der BG |
| DGUV Regel 100-500 + Information 209-072/074/073 | 3 | DGUV-Copyright, nur Identifier |
| DIN-Identifier-Tabelle (ohne Volltext) | 3 | DIN-Beuth-Copyright |
| ANSI B11.0 + RIA R15.06 + UL 508A Identifier | 3 | ANSI/UL-Copyright |
| ISO 12100/13849/13857 Identifier | 3 | ISO-Copyright |
## Audit-Pflicht
Vor jedem Ingest neuer Quellen:
1. Lizenz prüfen (publikationen.dguv.de, EUR-Lex, etc.)
2. license_type aus obiger Tabelle wählen — wenn nicht vorhanden, hier ergänzen
3. license_rule wird daraus deterministisch abgeleitet
4. Attribution-Text bei Regel 2 ist Pflichtfeld
Vor jedem Output:
- Wenn ein atomic_control aus einer Regel-3-Quelle stammt: prüfen dass NUR Identifier gezeigt wird, niemals Volltext
- Wenn aus Regel-2-Quelle: Attribution muss im PDF-Footer und im Frontend-Tooltip vorhanden sein
- Wenn aus Regel-1-Quelle: empfohlen Quelle nennen für Auditierbarkeit
## Verweise
- Schema: `migrations/002_regulation_registry.sql`
- Code: `services/regulation_registry.py`, `services/pipeline_adapter.py`
- Seed-Script: `scripts/f1_migrate_regulation_registry.py`
- Tests: `tests/test_regulation_registry.py` (assert: rule IN (1,2,3))
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Audit script for license classification gaps in the control pipeline.
Reports:
1. **regulation_registry coverage** — how many regulations are classified, by
rule and license_type.
2. **atomic_controls without license_rule** — how many controls reference a
regulation_id that has no entry (or no license_rule) in the registry.
3. **Qdrant payload consistency** — for each indexed collection, how many
chunks carry both ``license`` and ``license_rule`` payload fields.
The goal is to surface every record where the engine could in principle
extract or emit content but the license rule is unknown — those records are
the highest-risk material in a license audit.
Usage::
python3 scripts/audit_license_classification.py --db-host 100.80.114.48
Add ``--check-qdrant`` to also probe ``http://<host>:6333`` collections.
"""
from __future__ import annotations
import argparse
import json
import sys
from collections import Counter
from pathlib import Path
from typing import Optional
from urllib import request as urllib_request
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
DEFAULT_HOST = "100.80.114.48"
DEFAULT_PORT = 5432
DEFAULT_USER = "breakpilot"
DEFAULT_DB = "breakpilot_db"
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--db-host", default=DEFAULT_HOST)
p.add_argument("--db-port", type=int, default=DEFAULT_PORT)
p.add_argument("--db-user", default=DEFAULT_USER)
p.add_argument("--db-name", default=DEFAULT_DB)
p.add_argument("--db-password", default="")
p.add_argument("--check-qdrant", action="store_true")
p.add_argument("--qdrant-host", default="100.80.114.48")
p.add_argument("--qdrant-port", type=int, default=6333)
p.add_argument("--json", action="store_true", help="Emit JSON result on stdout")
return p.parse_args()
def audit_registry(conn) -> dict:
"""Coverage of regulation_registry."""
cur = conn.cursor()
cur.execute(
"SET search_path TO compliance, public; "
"SELECT license_rule, license_type, COUNT(*) "
"FROM regulation_registry GROUP BY license_rule, license_type "
"ORDER BY license_rule, license_type;"
)
by_rule_and_type: list[tuple] = []
by_rule: Counter = Counter()
for rule, ltype, count in cur.fetchall():
by_rule_and_type.append((rule, ltype or "(empty)", count))
by_rule[rule] += count
cur.execute(
"SELECT COUNT(*) FROM regulation_registry "
"WHERE license_type IS NULL OR license_type = '';"
)
missing_type = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM regulation_registry;")
total = cur.fetchone()[0]
return {
"total": total,
"by_rule": dict(by_rule),
"by_rule_and_type": by_rule_and_type,
"missing_license_type": missing_type,
}
def audit_atomic_controls(conn) -> dict:
"""Controls whose source regulation has no license rule.
Important: the schema differs between core (bp-core) and customer
deployments. We probe a handful of likely column names and skip if
none are found.
"""
cur = conn.cursor()
# Detect controls table
cur.execute(
"SELECT table_name FROM information_schema.tables "
"WHERE table_schema='compliance' AND table_name IN "
"('atomic_controls','atomic_controls_dedup','canonical_controls');"
)
tables = [r[0] for r in cur.fetchall()]
if not tables:
return {"skipped": True, "reason": "no controls table found"}
result: dict = {"tables": {}}
for tbl in tables:
cur.execute(
f"SELECT column_name FROM information_schema.columns "
f"WHERE table_schema='compliance' AND table_name='{tbl}';"
)
cols = {r[0] for r in cur.fetchall()}
if "license_rule" not in cols:
result["tables"][tbl] = {"skipped": True, "reason": "no license_rule column"}
continue
cur.execute(f"SELECT COUNT(*) FROM compliance.{tbl};")
total = cur.fetchone()[0]
cur.execute(
f"SELECT license_rule, COUNT(*) FROM compliance.{tbl} "
f"GROUP BY license_rule ORDER BY license_rule;"
)
by_rule = {str(r[0]): r[1] for r in cur.fetchall()}
cur.execute(
f"SELECT COUNT(*) FROM compliance.{tbl} WHERE license_rule IS NULL;"
)
missing = cur.fetchone()[0]
result["tables"][tbl] = {
"total": total,
"by_rule": by_rule,
"missing_license_rule": missing,
}
return result
def audit_qdrant(host: str, port: int) -> dict:
"""Probe Qdrant collections for license + license_rule payload coverage.
Samples 500 points per collection and reports how many have neither
field populated.
"""
out: dict = {"collections": {}}
base = f"http://{host}:{port}"
try:
with urllib_request.urlopen(f"{base}/collections", timeout=10) as r:
colls = json.loads(r.read()).get("result", {}).get("collections", [])
except Exception as e:
return {"error": str(e)}
for c in colls:
name = c["name"]
if "compliance" not in name and "atomic_controls" not in name:
continue
payload = {"limit": 500, "with_payload": True, "with_vector": False}
req = urllib_request.Request(
f"{base}/collections/{name}/points/scroll",
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
)
try:
with urllib_request.urlopen(req, timeout=15) as r:
points = json.loads(r.read()).get("result", {}).get("points", [])
except Exception as e:
out["collections"][name] = {"error": str(e)}
continue
sampled = len(points)
both_set = 0
only_license = 0
only_rule = 0
neither = 0
for p in points:
pl = p.get("payload", {}) or {}
has_lic = bool(pl.get("license"))
has_rule = pl.get("license_rule") is not None
if has_lic and has_rule:
both_set += 1
elif has_lic:
only_license += 1
elif has_rule:
only_rule += 1
else:
neither += 1
out["collections"][name] = {
"sampled": sampled,
"both_set": both_set,
"only_license_field": only_license,
"only_license_rule_field": only_rule,
"neither_set": neither,
"neither_pct": round(neither / sampled * 100, 1) if sampled else 0,
}
return out
def main() -> int:
args = parse_args()
try:
import psycopg2
except ImportError:
print("error: psycopg2 not installed (pip install psycopg2-binary)", file=sys.stderr)
return 2
conn = psycopg2.connect(
host=args.db_host,
port=args.db_port,
user=args.db_user,
dbname=args.db_name,
password=args.db_password or None,
)
try:
registry = audit_registry(conn)
controls = audit_atomic_controls(conn)
finally:
conn.close()
qdrant: Optional[dict] = None
if args.check_qdrant:
qdrant = audit_qdrant(args.qdrant_host, args.qdrant_port)
result = {"registry": registry, "atomic_controls": controls, "qdrant": qdrant}
if args.json:
print(json.dumps(result, indent=2, default=str))
return 0
print("=" * 60)
print(" Audit — License Classification")
print("=" * 60)
print()
print(f"## regulation_registry ({registry['total']} rows)")
print(f" By rule: {registry['by_rule']}")
print(f" Missing license_type: {registry['missing_license_type']}")
print()
print("## atomic_controls")
for tbl, info in controls.get("tables", {}).items():
if info.get("skipped"):
print(f" {tbl}: SKIPPED ({info['reason']})")
continue
print(f" {tbl}: {info['total']} rows")
print(f" by_rule={info['by_rule']}")
print(f" missing_license_rule={info['missing_license_rule']}")
print()
if qdrant:
print("## qdrant")
for name, info in qdrant.get("collections", {}).items():
if "error" in info:
print(f" {name}: ERROR {info['error']}")
continue
print(
f" {name:30} sampled={info['sampled']:4} "
f"both={info['both_set']:4} "
f"neither={info['neither_set']:4} ({info['neither_pct']}%)"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""Backfill license_rule on canonical_controls by inheriting from parent.
Background
==========
Audit (audit_license_classification.py) showed that 279,384 of 314,811 rows
in compliance.canonical_controls have NULL license_rule. Drilling in:
- 261,980 of those (94%) have a parent_control_uuid whose parent already
carries a non-NULL license_rule. The pass0b decomposition pipeline did
not propagate the rule to its child controls — this is a clear inheritance
bug, fixable without any classification decisions.
- 16,617 have a parent that itself has no license_rule (transitive case).
Inheriting iteratively converges to either rule-set or root-orphan.
- 787 have no parent at all (decomposition roots). These need cluster-based
manual classification (see Strategy Notes at the bottom of this file).
This script runs the inheritance fix in three idempotent stages and
prints per-stage counts before any write happens.
Usage::
# Always dry-run first:
python3 scripts/backfill_license_rule.py --db-host 100.80.114.48 \\
--db-password breakpilot123 --dry-run
# If counts look right:
python3 scripts/backfill_license_rule.py --db-host 100.80.114.48 \\
--db-password breakpilot123 --apply
The script is safe to rerun — it only touches rows where license_rule
IS NULL.
"""
from __future__ import annotations
import argparse
import sys
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--db-host", default="100.80.114.48")
p.add_argument("--db-port", type=int, default=5432)
p.add_argument("--db-user", default="breakpilot")
p.add_argument("--db-name", default="breakpilot_db")
p.add_argument("--db-password", required=True)
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--dry-run", action="store_true")
g.add_argument("--apply", action="store_true")
p.add_argument("--max-iterations", type=int, default=5,
help="Cap on inheritance iterations to avoid loops")
return p.parse_args()
# Stage 1: direct parent has license_rule — single UPDATE.
# Stage 2: iterative — parent did not have it, but a grandparent does.
# We loop until no more rows can be filled or max-iterations.
# Stage 3: residual rows with no resolvable parent. Report them clustered
# by category/pattern_id so the user can classify by family.
SQL_REPORT_NULLS = """
SET search_path TO compliance, public;
SELECT
CASE WHEN cc.parent_control_uuid IS NULL THEN 'no_parent'
WHEN p.license_rule IS NULL THEN 'parent_null'
ELSE 'parent_set' END AS bucket,
COUNT(*) AS n
FROM canonical_controls cc
LEFT JOIN canonical_controls p ON cc.parent_control_uuid = p.id
WHERE cc.license_rule IS NULL
GROUP BY 1 ORDER BY 2 DESC;
"""
SQL_INHERIT_FROM_PARENT = """
SET search_path TO compliance, public;
UPDATE canonical_controls cc
SET license_rule = p.license_rule, updated_at = NOW()
FROM canonical_controls p
WHERE cc.parent_control_uuid = p.id
AND cc.license_rule IS NULL
AND p.license_rule IS NOT NULL;
"""
SQL_REPORT_ORPHAN_CLUSTERS = """
SET search_path TO compliance, public;
SELECT
COALESCE(category, '(null)') AS category,
COALESCE(pattern_id, '(null)') AS pattern_id,
COALESCE(generation_strategy, '(null)') AS gen,
COUNT(*) AS n
FROM canonical_controls
WHERE license_rule IS NULL AND parent_control_uuid IS NULL
GROUP BY 1, 2, 3 ORDER BY n DESC LIMIT 25;
"""
def print_bucket(rows, label: str) -> None:
print(f"\n## {label}")
total = 0
for bucket, n in rows:
print(f" {bucket:12} {n:>8}")
total += n
print(f" {'TOTAL':12} {total:>8}")
def main() -> int:
args = parse_args()
try:
import psycopg2
except ImportError:
print("error: psycopg2 not installed", file=sys.stderr)
return 2
conn = psycopg2.connect(
host=args.db_host, port=args.db_port, user=args.db_user,
dbname=args.db_name, password=args.db_password,
)
conn.autocommit = False
cur = conn.cursor()
print("=" * 60)
print(" Backfill — license_rule via parent inheritance")
print(f" Mode: {'DRY-RUN' if args.dry_run else 'APPLY'}")
print("=" * 60)
# Initial bucket report
cur.execute(SQL_REPORT_NULLS)
rows = cur.fetchall()
print_bucket(rows, "Initial NULL distribution")
if args.dry_run:
# Print what the FIRST inherit pass would resolve (without writing)
cur.execute(
"SET search_path TO compliance, public; "
"SELECT p.license_rule, COUNT(*) "
"FROM canonical_controls cc "
"JOIN canonical_controls p ON cc.parent_control_uuid = p.id "
"WHERE cc.license_rule IS NULL AND p.license_rule IS NOT NULL "
"GROUP BY 1 ORDER BY 1;"
)
print("\n## First inherit-pass would fill:")
for rule, n in cur.fetchall():
print(f" rule={rule} {n:>8} rows")
# Show orphan clusters that would remain
cur.execute(SQL_REPORT_ORPHAN_CLUSTERS)
print("\n## Orphan clusters (no parent + no rule, top 25):")
for cat, pid, gen, n in cur.fetchall():
print(f" cat={cat[:20]:20} pat={pid[:20]:20} gen={gen[:20]:20} n={n}")
print("\nNo writes performed. Use --apply to execute.")
conn.rollback()
return 0
# Apply mode — iterative inheritance
total_updated = 0
for i in range(1, args.max_iterations + 1):
cur.execute(SQL_INHERIT_FROM_PARENT)
updated = cur.rowcount
total_updated += updated
print(f"\n iteration {i}: {updated} rows updated")
if updated == 0:
break
conn.commit()
print(f"\n✓ Total rows backfilled: {total_updated}")
# Final bucket report
cur.execute(SQL_REPORT_NULLS)
print_bucket(cur.fetchall(), "Remaining NULL distribution")
cur.execute(SQL_REPORT_ORPHAN_CLUSTERS)
rows = cur.fetchall()
if rows:
print("\n## Orphan clusters still need classification:")
for cat, pid, gen, n in rows:
print(f" cat={cat[:20]:20} pat={pid[:20]:20} gen={gen[:20]:20} n={n}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""Backfill ``license_rule`` payload field into Qdrant atomic_controls_dedup
and related compliance collections, sourced from canonical_controls in Postgres.
The audit (audit_license_classification.py) surfaced that Qdrant collections
holding canonical-control vectors (notably ``atomic_controls_dedup``) carry no
license_rule payload at all, even though the underlying Postgres table is now
fully classified. This script joins the two via ``control_uuid`` and patches the
Qdrant payload in batches.
Usage::
python3 scripts/backfill_qdrant_license_payload.py \\
--pg-host 100.80.114.48 --pg-password breakpilot123 \\
--qdrant http://100.80.114.48:6333 \\
--collection atomic_controls_dedup \\
--dry-run
# apply
python3 scripts/backfill_qdrant_license_payload.py ... --apply
Notes
-----
- ``control_uuid`` lives in the payload of atomic_controls_dedup. For other
collections that key the canonical control by a different field, override with
``--uuid-field``.
- Qdrant ``set_payload`` is keyed by point id, not payload field. We resolve
UUID → point id by a paginated scroll-and-filter pass, then issue grouped
set_payload requests per license_rule (3 batches per collection).
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from typing import Iterator
from urllib import request as urllib_request
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--pg-host", default="100.80.114.48")
p.add_argument("--pg-port", type=int, default=5432)
p.add_argument("--pg-user", default="breakpilot")
p.add_argument("--pg-name", default="breakpilot_db")
p.add_argument("--pg-password", required=True)
p.add_argument("--qdrant", default="http://100.80.114.48:6333")
p.add_argument("--qdrant-api-key", default="",
help="API key for managed Qdrant (Production)")
p.add_argument("--collection", default="atomic_controls_dedup")
p.add_argument("--uuid-field", default="control_uuid",
help="Payload field used for lookup (control_uuid or regulation_id)")
p.add_argument("--lookup", choices=["canonical_controls", "regulation_registry"],
default="canonical_controls",
help="Postgres table to resolve the lookup against")
p.add_argument("--batch-size", type=int, default=500)
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--dry-run", action="store_true")
g.add_argument("--apply", action="store_true")
return p.parse_args()
def fetch_rule_by_uuid(args) -> dict[str, int]:
"""Pull lookup-key → license_rule mapping from Postgres.
Source table is chosen by ``--lookup``:
- canonical_controls: id (UUID) → license_rule, for atomic_controls_dedup
- regulation_registry: regulation_id → license_rule, for document chunks
"""
import psycopg2
conn = psycopg2.connect(
host=args.pg_host, port=args.pg_port, user=args.pg_user,
dbname=args.pg_name, password=args.pg_password,
)
cur = conn.cursor()
cur.execute("SET search_path TO compliance, public;")
if args.lookup == "regulation_registry":
cur.execute(
"SELECT regulation_id, license_rule FROM regulation_registry "
"WHERE license_rule IS NOT NULL"
)
else:
cur.execute(
"SELECT id::text, license_rule FROM canonical_controls "
"WHERE license_rule IS NOT NULL"
)
mapping = {row[0]: int(row[1]) for row in cur.fetchall()}
conn.close()
return mapping
def _headers(api_key: str = "") -> dict:
h = {"Content-Type": "application/json"}
if api_key:
h["api-key"] = api_key
return h
def scroll_collection(qdrant: str, collection: str, uuid_field: str, api_key: str = "") -> Iterator[dict]:
"""Yield (point_id, uuid_value, has_rule_already) tuples."""
next_offset = None
while True:
body = {"limit": 1000, "with_payload": True, "with_vector": False}
if next_offset is not None:
body["offset"] = next_offset
req = urllib_request.Request(
f"{qdrant}/collections/{collection}/points/scroll",
data=json.dumps(body).encode(),
headers=_headers(api_key),
)
with urllib_request.urlopen(req, timeout=60) as r:
payload = json.loads(r.read())
result = payload.get("result", {})
for pt in result.get("points", []):
pl = pt.get("payload", {}) or {}
yield {
"id": pt["id"],
"uuid": pl.get(uuid_field),
"has_rule": "license_rule" in pl,
}
next_offset = result.get("next_page_offset")
if next_offset is None:
break
def set_payload_batch(qdrant: str, collection: str, point_ids: list, rule: int, api_key: str = "") -> int:
"""POST set_payload for a batch of point IDs with a single license_rule."""
body = {
"payload": {"license_rule": rule},
"points": point_ids,
}
req = urllib_request.Request(
f"{qdrant}/collections/{collection}/points/payload?wait=true",
data=json.dumps(body).encode(),
headers=_headers(api_key),
method="POST",
)
with urllib_request.urlopen(req, timeout=120) as r:
resp = json.loads(r.read())
if resp.get("status") != "ok":
raise RuntimeError(f"set_payload failed: {resp}")
return len(point_ids)
def main() -> int:
args = parse_args()
print("Loading canonical_controls → license_rule mapping…")
rule_by_uuid = fetch_rule_by_uuid(args)
print(f" Postgres returned {len(rule_by_uuid)} classified controls")
print(f"Scrolling Qdrant collection {args.collection!r}")
by_rule: dict[int, list] = {1: [], 2: [], 3: []}
points_total = 0
points_with_uuid = 0
points_already_set = 0
points_no_match = 0
for pt in scroll_collection(args.qdrant, args.collection, args.uuid_field, args.qdrant_api_key):
points_total += 1
uuid = pt["uuid"]
if not uuid:
continue
points_with_uuid += 1
if pt["has_rule"]:
points_already_set += 1
continue
rule = rule_by_uuid.get(uuid)
if rule is None:
points_no_match += 1
continue
if rule not in by_rule:
continue
by_rule[rule].append(pt["id"])
print(f" total points scanned: {points_total}")
print(f" with {args.uuid_field}: {points_with_uuid}")
print(f" already had license_rule: {points_already_set}")
print(f" uuid not found in Postgres: {points_no_match}")
print(f" to set per rule: rule1={len(by_rule[1])} rule2={len(by_rule[2])} rule3={len(by_rule[3])}")
if args.dry_run:
print("\nDRY-RUN: no writes performed. Use --apply to execute.")
return 0
total_written = 0
for rule, ids in by_rule.items():
if not ids:
continue
print(f"\nWriting license_rule={rule} to {len(ids)} points (batch {args.batch_size})…")
for i in range(0, len(ids), args.batch_size):
chunk = ids[i:i + args.batch_size]
n = set_payload_batch(args.qdrant, args.collection, chunk, rule, args.qdrant_api_key)
total_written += n
print(f" batch {i // args.batch_size + 1}: {n} points (cumulative {total_written})")
time.sleep(0.05)
print(f"\nWrote license_rule on {total_written} Qdrant points in {args.collection}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+21
View File
@@ -34,6 +34,27 @@ export default function ImpressumPage() {
Unsere E-Mail-Adresse finden Sie oben im Impressum.
</p>
</div>
<div>
<h2 className="text-lg font-semibold text-white mb-2">Quellen und Lizenzen der Compliance-Inhalte</h2>
<p>
Die BreakPilot Compliance-Plattform stuetzt sich auf rund 315.000 klassifizierte
Controls aus oeffentlichen Quellen: EU-Recht (EUR-Lex), deutsches und oesterreichisches
Bundesrecht, US Federal Code (OSHA, NIST), Behoerden-Leitfaeden (ENISA, EDPB, BAuA),
freie Sicherheits-Frameworks unter CC-BY-SA (OWASP-Familie, OECD AI Principles) und
eigene Texte. Jeder Control traegt eine deterministische Lizenzregel (R1 woertlich, R2
mit Attribution, R3 nur Identifier-Verweis), die das Render-Verhalten in Berichten,
PDF-Exports und Frontend steuert. Die vollstaendige Quellenliste mit Aufschluesselung
pro Lizenzklasse ist im SDK unter <code className="text-white/80">/sdk/licenses</code>
eingesehen. Pflicht-Attributionen fuer R2-Quellen erscheinen automatisch im
Quellen-Footer jedes generierten Berichts.
</p>
<p className="mt-2 text-xs">
Hinweis: Dieser Pauschalvermerk ersetzt nicht die werknahe Attribution. Jede
Berichts- oder Frontend-Ausgabe nennt die konkret verwendeten Quellen direkt am
Werk (Auto-Footer in PDFs, Inline-Citation im Frontend).
</p>
</div>
</div>
</div>
</div>
+177
View File
@@ -0,0 +1,177 @@
import Navbar from '@/components/layout/Navbar'
import Footer from '@/components/layout/Footer'
import ChatFAB from '@/components/layout/ChatFAB'
// Stärken / USP-Seite — sieben Verkaufsargumente aus der IACE-Strategie
// (Memory: project_marketing_website_3014_themes.md). Aufgebaut als
// Long-Form-Page mit Anker-Sprungmarken — eine Nummerierte Differenzierung
// pro Sektion, damit Sales-Calls über tiefe Links arbeiten können.
const usps = [
{
id: 'engine',
no: '1',
title: 'Engine, nicht Checkliste',
sub: 'Wir leiten Gefährdungen ab. Wettbewerb fragt aus einer Liste.',
body:
'Marktstandard (DesignSafe, Pilz, Sick) ist Excel-aufgewertete Checkliste: der Engineer wählt aus einer Hazard-Bibliothek aus. ' +
'BreakPilot betreibt eine deterministische Pattern-Engine mit über 1.200 Hazard-Patterns. Aus der Maschinenbeschreibung leitet sie ' +
'die Gefährdungen ab — keine Auswahllisten, keine vergessenen Punkte.',
proof: 'Audit-Suite cmd/iace-audit erkennt eigene Lücken (Methode AE)',
},
{
id: 'multi-markt',
no: '2',
title: 'Eine Risikobeurteilung — alle Märkte',
sub: 'CE + OSHA + ANSI + GB + JIS aus einem Datenmodell.',
body:
'Die gleiche Pattern-Engine generiert pro Maschinenbeschreibung mehrere Compliance-Anhänge. Hersteller wählt seine Zielmärkte. ' +
'EU-Recht zitieren wir wörtlich (Rule 1). OWASP unter CC-BY-SA mit Pflicht-Attribution (Rule 2). DIN/EN nur per Identifier (Rule 3). ' +
'Norm-Cross-Reference-Bibliothek mappt ISO 12100 ↔ DIN EN ISO 12100 ↔ ANSI B11.0 ↔ GB/T 15706 ↔ JIS B 9700.',
proof: '252 Regulationen klassifiziert · 314.811 Controls audited',
},
{
id: 'folgegefahren',
no: '3',
title: 'Vom Bediener bis zum Endkunden',
sub: 'Folgegefahren-Modell mit Sekundärschadens-Kette.',
body:
'Klassische Risikobeurteilung schaut nur den Bediener an. Wir modellieren die Schadenskette weiter: Glasbruch in der Abfüllanlage ' +
'verletzt nicht nur den Bediener, sondern erreicht über Restsplitter den Endkunden. BreakPilot verbindet CE-Sicherheit mit ' +
'Produkthaftung nach ProdHaftG, Lebensmittelrecht nach VO 178/2002 und ISO 31000 Unternehmensrisiko in einem Datenmodell.',
proof: 'SecondaryHarm-Modell live für consumer_safety, product_liability, food_safety, environmental, reputation, financial',
},
{
id: 'public-domain',
no: '4',
title: 'Public Domain als Rechtsanker',
sub: 'Werte aus OSHA, NIST, EUR-Lex, BAuA — auditfähig zitiert.',
body:
'Mindestabstände der Maschinensicherheit kommen bei uns aus OSHA 29 CFR 1910 Subpart O — US Federal Public Domain, lizenzrechtlich ' +
'unbedenklich. Engineering-Rundung auf safe-side mm-Raster wird transparent dokumentiert. EU-Normen erscheinen nur als Identifier-Verweis ' +
'mit einer menschlich kuratierten "Strenger/Gleich/Weicher"-Annotation — kein Copyright-Risiko.',
proof: 'OSHA Table O-10 + §1910.217 PSDI-Formel verbatim · DIN nur Identifier · 6 DGUV-Publikationen referenziert',
},
{
id: 'audit-suite',
no: '5',
title: 'Audit findet Lücken, die der Fachmann übersieht',
sub: 'Fünf deterministische Audits ohne Ground Truth.',
body:
'Unsere Engine kennt ihre eigenen Lücken. Methode A bis E (Reachability, Consistency, Vocabulary, Echo, Hierarchy) finden Gaps ' +
'ohne Fachmann-Vergleich. Bei einem Test fanden wir 100 strukturell unerreichbare Patterns und 46 unvollständige Component-Tags — ' +
'Probleme, die ein menschlicher Auditor in einem Einzelfall nie gesehen hätte.',
proof: 'cmd/iace-audit · 1.213 Patterns transparent · 99,94% Recall verifiziert',
},
{
id: 'made-in-germany',
no: '6',
title: 'Made in Germany meets US Federal Public Domain',
sub: 'Deutscher Maschinenbau, der gleichzeitig US-Compliance liefert.',
body:
'Deutscher Exportweltmeister-Maschinenbau braucht UL/NRTL-Zulassung für die USA. Die gleichen Daten, die wir für CE generieren, ' +
'liefern dem US-Auditor 80 % der Vorarbeit. Risikobeurteilung in einer Sprache, Compliance in zwei Märkten — ohne Mehraufwand für den Hersteller.',
proof: 'OSHA-Anker im RAG · NRTL-fähige Compliance-Spur · DesignSafe-Marktstandard wird hier erweitert, nicht imitiert',
},
{
id: 'tooling',
no: '7',
title: 'LLM-Gap-Review als Co-Pilot, nicht als Roboter-Anwalt',
sub: 'Pattern-Engine als Audit-Spur, LLM als Lücken-Suchhund.',
body:
'Die deterministische Engine bleibt die auditfähige Quelle der Wahrheit. Ein nachgelagerter LLM-Gap-Review (Qwen / Claude) prüft, ' +
'was die Engine übersehen hat — mit klarer Quellen-Provenance (R3 LLM-Review) und Adopt/Reject-UX. Halluzinationen können nicht in ' +
'die finale Risikobeurteilung schlüpfen.',
proof: 'POST /projects/:id/llm-gap-review · Konfidenz-Stufen · Fallback auf statische Checkliste',
},
] as const
const competitors = [
{ feature: 'Pattern-Engine statt Checkliste', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: '—' },
{ feature: 'Multi-Markt CE / US / CN / JP', bp: '✓', ds: 'nur US', pilz: 'nur EU', sick: 'nur EU', sphera: 'enterprise' },
{ feature: 'Folgegefahren-Modell', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: 'Process' },
{ feature: 'Audit-Suite (Engine-Lücken-Erkennung)', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: '—' },
{ feature: 'OSHA-Anker (Public Domain Werte)', bp: '✓', ds: '✓', pilz: '—', sick: '—', sphera: '—' },
{ feature: 'LLM-Gap-Review (Co-Pilot)', bp: '✓', ds: '—', pilz: '—', sick: '—', sphera: '—' },
]
export default function StaerkenPage() {
return (
<>
<Navbar />
<main className="bg-enterprise-dark text-white pt-32 pb-24">
<div className="max-w-5xl mx-auto px-4">
<header className="mb-16">
<h1 className="text-5xl font-bold mb-4">Was uns differenziert</h1>
<p className="text-white/60 text-lg max-w-3xl">
Sieben konkrete Punkte, die BreakPilot von DesignSafe, Pilz, Sick, TÜV-Tools und Sphera trennen.
Jede Differenzierung ist im Produkt umgesetzt kein Marketing-Versprechen.
</p>
</header>
<ol className="space-y-12">
{usps.map((u) => (
<li id={u.id} key={u.id} className="border-l-2 border-accent-electric pl-6">
<div className="flex items-baseline gap-3 mb-2">
<span className="text-accent-electric font-mono text-3xl font-bold">#{u.no}</span>
<h2 className="text-2xl font-semibold">{u.title}</h2>
</div>
<p className="text-accent-electric/80 text-sm mb-3">{u.sub}</p>
<p className="text-white/70 leading-relaxed mb-3">{u.body}</p>
<p className="text-white/40 text-xs">
<span className="text-white/60">Belegt durch:</span> {u.proof}
</p>
</li>
))}
</ol>
<section className="mt-20">
<h2 className="text-3xl font-bold mb-4">Direktvergleich</h2>
<p className="text-white/60 mb-6 max-w-3xl">
Stand 2026. Marktangaben basieren auf öffentlicher Produktinformation der genannten Anbieter.
</p>
<div className="overflow-x-auto border border-white/10 rounded-lg">
<table className="w-full text-sm">
<thead className="bg-white/[0.04] border-b border-white/10">
<tr>
<th className="text-left p-3 font-medium">Feature</th>
<th className="text-left p-3 font-medium text-accent-electric">BreakPilot</th>
<th className="text-left p-3 font-medium text-white/60">DesignSafe</th>
<th className="text-left p-3 font-medium text-white/60">Pilz PASS</th>
<th className="text-left p-3 font-medium text-white/60">Sick SD</th>
<th className="text-left p-3 font-medium text-white/60">Sphera</th>
</tr>
</thead>
<tbody>
{competitors.map((c) => (
<tr key={c.feature} className="border-t border-white/[0.06]">
<td className="p-3 text-white/80">{c.feature}</td>
<td className="p-3 text-accent-electric font-medium">{c.bp}</td>
<td className="p-3 text-white/50">{c.ds}</td>
<td className="p-3 text-white/50">{c.pilz}</td>
<td className="p-3 text-white/50">{c.sick}</td>
<td className="p-3 text-white/50">{c.sphera}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="mt-20 border-t border-white/10 pt-12">
<h2 className="text-2xl font-bold mb-3">Quellen &amp; Lizenz-Architektur</h2>
<p className="text-white/60 leading-relaxed">
Die Plattform stützt sich auf öffentliche Quellen: EU-Recht (EUR-Lex), Bundesrecht (BetrSichV, ArbSchG),
US Federal Code (OSHA, NIST), Behörden-Leitfäden (ENISA, EDPB, BAuA), freie Sicherheits-Frameworks unter
CC-BY-SA (OWASP). Jeder Inhalt trägt eine deterministische Lizenzregel R1/R2/R3 und löst die
entsprechende Attribution im Ausgabe-PDF und im Frontend automatisch aus. Vollständige Quellenliste
im SDK unter <code className="bg-white/[0.06] px-1.5 py-0.5 rounded">/sdk/licenses</code>.
</p>
</section>
</div>
</main>
<Footer />
<ChatFAB />
</>
)
}
+1
View File
@@ -1,6 +1,7 @@
// Navbar links — route-based navigation
export const navLinks = [
{ href: '/plattform', labelDe: 'Plattform', labelEn: 'Platform' },
{ href: '/staerken', labelDe: 'Stärken', labelEn: 'Differentiators' },
{ href: '/ce-prozess', labelDe: 'CE-Prozess', labelEn: 'CE Process' },
{ href: '/product-compliance', labelDe: 'Product Compliance', labelEn: 'Product Compliance' },
{ href: '/architektur', labelDe: 'Architektur', labelEn: 'Architecture' },
@@ -137,23 +137,43 @@ const PILLARS_EN = [
export function PrintRegulatoryPage({ lang, pageNum, totalPages, versionName }: SlideBase) {
const de = lang === 'de'
const pillars = de ? PILLARS_DE : PILLARS_EN
const MONO = "'JetBrains Mono', ui-monospace, monospace"
// Alternate tints — pillars 1 & 3 violet, 2 & 4 amber for visual rhythm.
const tints = [
{ dark: COLORS.violet700, mid: COLORS.violet600, light: COLORS.violet50, border: COLORS.violet300 },
{ dark: COLORS.amber700, mid: COLORS.amber600, light: COLORS.amber50, border: '#f3d59a' },
{ dark: COLORS.violet700, mid: COLORS.violet600, light: COLORS.violet50, border: COLORS.violet300 },
{ dark: COLORS.amber700, mid: COLORS.amber600, light: COLORS.amber50, border: '#f3d59a' },
]
return (
<Page kicker="19" section={de ? 'ANHANG · REGULATORISCHE DETAILS' : 'APPENDIX · REGULATORY DETAILS'} title={de ? 'Vier Säulen der EU-Compliance für Maschinenbauer.' : 'Four pillars of EU compliance for manufacturers.'} subtitle={de ? 'Jede Säule deckt 46 verbindliche Regelwerke ab. BreakPilot mappt diese auf 25.000+ atomare Controls.' : 'Each pillar covers 46 binding regulations. BreakPilot maps these to 25,000+ atomic controls.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '5mm', flex: 1, minHeight: 0 }}>
{pillars.map((p, i) => (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.indigo600}`, padding: '4mm 5mm', 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' }}>{de ? `Säule ${String(i + 1).padStart(2, '0')}` : `Pillar ${String(i + 1).padStart(2, '0')}`}</span>
<span style={{ fontSize: '6.5pt', color: COLORS.slate400 }}>{de ? '4-6 Regelwerke' : '4-6 regulations'}</span>
{/* Architectural row: 4 pillars side-by-side */}
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '4mm', flex: 1, minHeight: 0, alignItems: 'stretch' }}>
{pillars.map((p, i) => {
const c = tints[i]
return (
<div key={i} style={{ display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
{/* CAPITAL (top) */}
<div style={{ background: `linear-gradient(180deg, ${c.mid} 0%, ${c.dark} 100%)`, padding: '3mm 3mm 3.5mm', borderTopLeftRadius: '2pt', borderTopRightRadius: '2pt', margin: '0 -1.5mm', boxShadow: `0 2mm 2mm -1.5mm ${c.dark}55`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact', textAlign: 'center' }}>
<div style={{ fontFamily: MONO, fontSize: '7pt', fontWeight: 700, color: '#ffffff', textTransform: 'uppercase', letterSpacing: '0.22em', opacity: 0.85 }}>{de ? 'SÄULE' : 'PILLAR'}</div>
<div style={{ fontSize: '20pt', fontWeight: 800, color: '#ffffff', lineHeight: 1, marginTop: '1mm', fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.01em' }}>{String(i + 1).padStart(2, '0')}</div>
</div>
<div style={{ fontSize: '13pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '3mm', lineHeight: 1.2 }}>{p.t}</div>
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5, flex: 1, marginBottom: '3mm' }}>{p.d}</div>
<div style={{ borderTop: `1px solid ${COLORS.slate100}`, paddingTop: '2mm', fontSize: '7pt', color: COLORS.slate500, fontFamily: 'monospace' }}>
{p.laws.join(' · ')}
{/* SHAFT (middle) */}
<div style={{ background: c.light, borderLeft: `2mm solid ${c.mid}`, borderRight: `1px solid ${c.border}`, borderTop: `1px solid ${c.border}`, borderBottom: `1px solid ${c.border}`, padding: '3mm 3mm 3mm 2.5mm', flex: 1, display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ fontSize: '11.5pt', fontWeight: 700, color: COLORS.slate900, marginBottom: '2.5mm', lineHeight: 1.2, letterSpacing: '-0.005em' }}>{p.t}</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.55, flex: 1 }}>{p.d}</div>
</div>
{/* BASE (bottom) */}
<div style={{ background: c.dark, margin: '0 -1.5mm', padding: '2mm 3mm', borderBottomLeftRadius: '2pt', borderBottomRightRadius: '2pt', fontFamily: MONO, fontSize: '6pt', color: '#ffffff', opacity: 0.95, lineHeight: 1.5, textAlign: 'center', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact', boxShadow: `0 -1mm 1.5mm -1mm ${c.dark}66 inset` }}>{p.laws.join(' · ')}</div>
</div>
))}
)
})}
</div>
{/* Shared ground line — architectural reference */}
<div style={{ marginTop: '2mm', height: '1px', background: COLORS.violet300, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
<div style={{ marginTop: '0.6mm', height: '1px', background: COLORS.violet200, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
</div>
<div style={{ marginTop: '4mm', flexShrink: 0 }}>
@@ -120,7 +120,7 @@ export function PrintCompetitionPage2({ lang, pageNum, totalPages, versionName }
const de = lang === 'de'
return (
<Page kicker="12" section={de ? 'WETTBEWERB · 2 / 2, APPSEC' : 'COMPETITION · 2 / 2, APPSEC'} title={de ? 'Application Security: BreakPilot deckt SAST + DAST + SCA + Pentesting in einer Plattform ab.' : 'Application Security: BreakPilot covers SAST + DAST + SCA + pentesting in one platform.'} subtitle={de ? 'Acht etablierte AppSec-Anbieter, keiner kombiniert SAST + DAST + Auto-Fix + Self-Hosted für KMU.' : 'Eight established AppSec vendors, none combines SAST + DAST + auto-fix + self-hosted for SMEs.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<Page kicker="12" section={de ? 'WETTBEWERB · 2 / 2, APPSEC' : 'COMPETITION · 2 / 2, APPSEC'} title={de ? 'Cyber-Security: BreakPilot ersetzt das ganze AppSec-Stack.' : 'Cyber Security: BreakPilot replaces the entire AppSec stack.'} subtitle={de ? 'Acht etablierte Code-Security-Anbieter, jeder mit einer Disziplin. BreakPilot vereint sie auf einer EU-souveränen Plattform, zum Bruchteil der Kosten.' : 'Eight established code-security vendors, each with one discipline. BreakPilot combines them on one EU-sovereign platform, at a fraction of the cost.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
{/* Competitor profile table */}
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '7.5pt', fontVariantNumeric: 'tabular-nums', marginBottom: '4mm' }}>
@@ -159,10 +159,40 @@ export function ArchitectureDiagram({
const productLayerBg = `linear-gradient(135deg, ${COLORS.violet50} 0%, #ffffff 60%, ${COLORS.violet50} 100%)`
const proxyLayerBg = `linear-gradient(135deg, ${COLORS.amber50} 0%, #fffaf0 60%, ${COLORS.amber50} 100%)`
/**
* Faked-3D layer wrapper: shadow on the bottom edge (heavier than top), a 1px
* top highlight, a 1px darker bottom seam, and a stagger indent on the right
* to suggest the stack tilts slightly away from the viewer. This renders
* crisply in Chromium's print-to-PDF, unlike `transform: rotateX(...)` which
* has print-pipeline quirks.
*/
const layerWrap = (indentRight: string, shadowTint: string, glowTop: string, seamBottom: string): React.CSSProperties => ({
marginRight: indentRight,
boxShadow: `inset 0 1px 0 ${glowTop}, inset 0 -1px 0 ${seamBottom}, 0 5mm 7mm -4mm ${shadowTint}`,
WebkitPrintColorAdjust: 'exact',
printColorAdjust: 'exact',
})
/** Connector that reads as "the upper plane resting on the next" — a soft chevron with a shadow. */
const PlaneConnector = ({ color }: { color: string }) => (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '5mm', position: 'relative' }}>
<svg viewBox="0 0 40 20" width="14mm" height="5mm" style={{ overflow: 'visible' }}>
<polygon points="4,2 36,2 30,16 10,16" fill={color} opacity="0.18" />
<polyline points="10,4 20,14 30,4" fill="none" stroke={color} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '2mm' }}>
{/* APPLICATION (PRODUCT) LAYER */}
<div style={{ background: productLayerBg, border: `1px solid ${COLORS.violet200}`, borderRadius: '4pt', padding: '3mm 4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5mm' }}>
{/* APPLICATION (PRODUCT) LAYER — top plane, smallest indent footprint */}
<div style={{
background: productLayerBg,
border: `1px solid ${COLORS.violet200}`,
borderRadius: '4pt',
padding: '3mm 4mm',
...layerWrap('0mm', 'rgba(59,26,122,0.20)', 'rgba(255,255,255,0.85)', COLORS.violet200),
}}>
<LayerChip n="01" label={de ? 'Application Layer' : 'Application Layer'} sub={de ? 'Kundenseitige Services' : 'User-facing services'} tint={COLORS.violet600} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '5mm' }}>
{product.map((p, i) => (
@@ -171,20 +201,16 @@ export function ArchitectureDiagram({
</div>
</div>
{/* compact connector strip */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', padding: '0 5mm', height: '5mm', alignItems: 'center' }}>
{[0, 1, 2].map(i => (
<div key={i} style={{ display: 'flex', justifyContent: 'center' }}>
<svg viewBox="0 0 12 16" width="3mm" height="5mm">
<line x1="6" y1="0" x2="6" y2="12" stroke={COLORS.violet500} strokeWidth="1.2" />
<polyline points="3,11 6,15 9,11" fill="none" stroke={COLORS.violet500} strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
))}
</div>
<PlaneConnector color={COLORS.violet500} />
{/* GATEWAY LAYER — compact: title row + features in 1 row */}
<div style={{ background: proxyLayerBg, border: `1.5px solid ${COLORS.amber600}`, borderRadius: '4pt', padding: '3mm 4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
{/* GATEWAY LAYER — middle plane, slight indent, slightly heavier shadow */}
<div style={{
background: proxyLayerBg,
border: `1.5px solid ${COLORS.amber600}`,
borderRadius: '4pt',
padding: '3mm 4mm',
...layerWrap('2mm', 'rgba(180,83,9,0.22)', 'rgba(255,255,255,0.85)', '#e9b56a'),
}}>
<LayerChip n="02" label={de ? 'Gateway Layer' : 'Gateway Layer'} sub={de ? 'Routing & Guardrails' : 'Routing & guardrails'} tint={COLORS.amber700} />
<div style={{ display: 'flex', alignItems: 'baseline', gap: '4mm', marginBottom: '2mm' }}>
<span style={{ fontSize: '12pt', fontWeight: 800, color: COLORS.slate900, letterSpacing: '-0.005em' }}>{proxy.title}</span>
@@ -197,20 +223,16 @@ export function ArchitectureDiagram({
</div>
</div>
{/* compact connector strip */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '4mm', padding: '0 5mm', height: '5mm', alignItems: 'center' }}>
{[0, 1, 2].map(i => (
<div key={i} style={{ display: 'flex', justifyContent: 'center' }}>
<svg viewBox="0 0 12 16" width="3mm" height="5mm">
<line x1="6" y1="0" x2="6" y2="12" stroke={COLORS.amber600} strokeWidth="1.2" />
<polyline points="3,11 6,15 9,11" fill="none" stroke={COLORS.amber600} strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
))}
</div>
<PlaneConnector color={COLORS.amber600} />
{/* INFRASTRUCTURE (INFERENCE) LAYER */}
<div style={{ background: productLayerBg, border: `1px solid ${COLORS.violet200}`, borderRadius: '4pt', padding: '3mm 4mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
{/* INFRASTRUCTURE (INFERENCE) LAYER — foundation, deepest indent + shadow */}
<div style={{
background: productLayerBg,
border: `1px solid ${COLORS.violet300}`,
borderRadius: '4pt',
padding: '3.5mm 4mm',
...layerWrap('4mm', 'rgba(59,26,122,0.28)', 'rgba(255,255,255,0.85)', COLORS.violet300),
}}>
<LayerChip n="03" label={de ? 'Infrastructure Layer' : 'Infrastructure Layer'} sub={de ? 'Compute & Daten · lokal · air-gap-fähig' : 'Compute & data · local · air-gap capable'} tint={COLORS.violet600} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '5mm' }}>
{inference.map((p, i) => (
@@ -1,5 +1,5 @@
import { Language, PitchCompany, PitchFunding, PitchMarket } from '@/lib/types'
import { Page, KpiRow, TwoCol, ThreeCol, FourCol, Panel, Bullets, Callout, COLORS, Divider } from './PrintLayout'
import { Page, KpiRow, TwoCol, ThreeCol, FourCol, Panel, Bullets, Callout, COLORS, Divider, ComplAI } from './PrintLayout'
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
@@ -8,7 +8,7 @@ interface SlideBase { lang: Language; pageNum: number; totalPages: number; versi
export function PrintCoverPage({ company, funding, lang, versionName }: { company: PitchCompany; funding: PitchFunding; lang: Language; versionName: string }) {
const de = lang === 'de'
const instrument = funding?.instrument || 'Pre-Seed'
const amount = funding?.amount_eur || 1_000_000
const amount = funding?.amount_eur || 400_000
const tagline = de ? (company?.tagline_de || 'Kontinuierliche Compliance für europäische Unternehmen.') : (company?.tagline_en || 'Continuous compliance for European companies.')
const amountLabel = amount >= 1_000_000
? '€' + (amount / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M'
@@ -128,7 +128,7 @@ export function PrintExecSummaryPage1({ market, lang, pageNum, totalPages, versi
const fmt = (v?: number) => v ? (v >= 1e9 ? `${(v / 1e9).toFixed(1).replace('.', ',')} Mrd.` : `${(v / 1e6).toFixed(0)} Mio.`) : '—'
return (
<Page kicker="01" section={de ? 'EXECUTIVE SUMMARY' : 'EXECUTIVE SUMMARY'} title={de ? 'BreakPilot COMPL/AI/' : 'BreakPilot COMPL/AI/'} subtitle={de ? 'DSGVO-konforme KI-Plattform, kontinuierliches Sicherheitsscanning und intelligente Compliance-Automatisierung. 25.000+ atomare Prüfaspekte, 380+ Regularien, EU-souverän gehostet.' : 'GDPR-compliant AI platform, continuous security scanning and intelligent compliance automation. 25,000+ atomic audit aspects, 380+ regulations, EU-sovereign hosted.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<Page kicker="01" section={de ? 'EXECUTIVE SUMMARY' : 'EXECUTIVE SUMMARY'} title={<>BreakPilot <ComplAI /></>} subtitle={de ? 'DSGVO-konforme KI-Plattform, kontinuierliches Sicherheitsscanning und intelligente Compliance-Automatisierung. 25.000+ atomare Prüfaspekte, 380+ Regularien, EU-souverän gehostet.' : 'GDPR-compliant AI platform, continuous security scanning and intelligent compliance automation. 25,000+ atomic audit aspects, 380+ regulations, EU-sovereign hosted.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<KpiRow items={[
{ n: '25k+', label: de ? 'Prüfaspekte' : 'Audit aspects', tone: 'accent' },
@@ -58,13 +58,29 @@ export const COLORS = {
const FONT = "'Inter', 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif"
const MONO_FONT = "'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace"
/**
* Brand wordmark: `Compl` rendered in the inherited text color, `AI` in violet.
* No slashes, no separators matches the BreakPilot/ComplAI brand guideline.
* Use this anywhere the product name appears in body or display text.
*
* Use the optional `prefix` to prepend "BreakPilot " in the same style.
*/
export function ComplAI({ prefix = false }: { prefix?: boolean } = {}) {
return (
<>
{prefix && <>BreakPilot </>}
Compl<span style={{ color: COLORS.violet600 }}>AI</span>
</>
)
}
/* ===== PAGE WRAPPER ===== */
interface PageProps {
kicker: string // "03"
section: string // "DAS PROBLEM"
title: string // "Deutsche Unternehmen wollen KI ..."
subtitle?: string
title: React.ReactNode // string or JSX (e.g. <ComplAI prefix /> usage)
subtitle?: React.ReactNode
pageNum: number
totalPages: number
versionName: string
@@ -236,12 +252,23 @@ export function Bullets({ items, dense, tone = 'neutral' }: BulletsProps) {
: tone === 'negative' ? COLORS.red600
: tone === 'accent' ? COLORS.indigo600
: COLORS.slate500
/**
* Em-dash marker uses display:flex to keep the rule vertically centered on
* the first line of text previously the `top: 4pt` absolute positioning
* drifted relative to font size (looked off-center on the rendered PDF).
* The marker now sits in a fixed-width column at the line height of the text.
*/
const fontSize = dense ? '8.5pt' : '9pt'
const lineH = 1.5
return (
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{items.map((item, i) => (
<li key={i} style={{ position: 'relative', paddingLeft: '5mm', marginBottom: dense ? '1.5mm' : '2.5mm', fontSize: dense ? '8.5pt' : '9pt', color: COLORS.slate700, lineHeight: 1.5 }}>
<span style={{ position: 'absolute', left: 0, top: dense ? '4pt' : '4.5pt', width: '3mm', height: '0.5pt', background: dotColor }} />
{item}
<li key={i} style={{ display: 'flex', alignItems: 'flex-start', gap: '3mm', marginBottom: dense ? '1.5mm' : '2.5mm', fontSize, color: COLORS.slate700, lineHeight: lineH }}>
{/* The dash sits on the first line: line-height pad above + 0.5pt rule */}
<span style={{ flexShrink: 0, width: '3mm', display: 'inline-flex', alignItems: 'center', height: `${parseFloat(fontSize) * lineH}pt` }}>
<span style={{ width: '100%', height: '0.5pt', background: dotColor, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
</span>
<span style={{ flex: 1, minWidth: 0 }}>{item}</span>
</li>
))}
</ul>
@@ -1,6 +1,10 @@
import { Language, PitchMarket, PitchTeamMember, PitchMilestone, PitchFunding } from '@/lib/types'
import { Page, Callout, COLORS, DataTable, StatLine } from './PrintLayout'
import { MarketFunnel, ComparisonBars, DonutChart } from './PrintCharts'
import { Page, Callout, COLORS, StatLine } from './PrintLayout'
import { ComparisonBars, DonutChart } from './PrintCharts'
import {
Briefcase, RefreshCw, Handshake, Scale, Lightbulb,
Code, TrendingUp, CreditCard, ShieldCheck, Cpu,
} from 'lucide-react'
interface SlideBase { lang: Language; pageNum: number; totalPages: number; versionName: string }
@@ -19,42 +23,72 @@ export function PrintMarketPage({ market, lang, pageNum, totalPages, versionName
const sam = market.find(m => m.market_segment === 'SAM')
const som = market.find(m => m.market_segment === 'SOM')
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'}>
// Fallbacks if data missing
const tamValue = tam?.value_eur ?? 340_000_000_000
const samValue = sam?.value_eur ?? 48_000_000_000
const somValue = som?.value_eur ?? 2_100_000_000
const cards = [
{ key: 'TAM', value: fmtEur(tamValue, de), growth: tam?.growth_rate_pct ?? 14, accent: 'violet' as const,
desc: de ? 'Globaler Compliance- und GRC-Markt, alle Branchen, alle Größen.' : 'Global compliance and GRC market, all industries, all sizes.' },
{ key: 'SAM', value: fmtEur(samValue, de), growth: sam?.growth_rate_pct ?? 18, accent: 'violet-soft' as const,
desc: de ? 'DACH + EU: regulierte Branchen, KMU und Enterprise.' : 'DACH + EU: regulated industries, SMB and enterprise.' },
{ key: 'SOM', value: fmtEur(somValue, de), growth: som?.growth_rate_pct ?? 25, accent: 'amber' as const, core: true,
desc: de ? 'Anlagen- und Maschinenbau DACH, unser Kernsegment.' : 'Machine and plant manufacturing DACH, our core segment.' },
]
<div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: '8mm', flex: 1, minHeight: 0 }}>
<div>
// SVG nested-circles geometry — viewBox unitless, render up to ~130mm wide
const CX = 65, CY = 65, R_TAM = 60, R_SAM = 36, R_SOM = 14
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="Sacra · Bitkom Cloud Monitor 2024 · DIHK 2024 · VDMA · Statista">
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.05fr', gap: '10mm', flex: 1, minHeight: 0 }}>
{/* LEFT: nested-circles diagram */}
<div style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '3mm' }}>{de ? 'Marktdimensionierung' : 'Market sizing'}</div>
{tam && sam && som && (
<MarketFunnel
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).' }}
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.' }}
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.' }}
fmt={(v) => fmtEur(v, de)}
/>
)}
<div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg viewBox="0 0 130 130" style={{ width: '100%', maxWidth: '130mm', height: 'auto', display: 'block' }} aria-hidden>
<circle cx={CX} cy={CY} r={R_TAM} fill={COLORS.violet50} stroke={COLORS.violet400} strokeWidth="0.6" />
<circle cx={CX} cy={CY} r={R_SAM} fill={COLORS.violet100} stroke={COLORS.violet500} strokeWidth="0.7" />
<circle cx={CX} cy={CY} r={R_SOM} fill={COLORS.amber50} stroke={COLORS.amber600} strokeWidth="0.9" />
<text x={CX} y={CY - R_TAM + 7} textAnchor="middle" fontSize="3.2" fontFamily="'JetBrains Mono', ui-monospace, monospace" fontWeight={700} letterSpacing="0.18em" fill={COLORS.violet700}>TAM</text>
<text x={CX} y={CY - R_TAM + 11.5} textAnchor="middle" fontSize="3.6" fontWeight={700} fill={COLORS.slate700}>{fmtEur(tamValue, de)}</text>
<text x={CX} y={CY - R_SAM + 6} textAnchor="middle" fontSize="3" fontFamily="'JetBrains Mono', ui-monospace, monospace" fontWeight={700} letterSpacing="0.18em" fill={COLORS.violet800}>SAM</text>
<text x={CX} y={CY - R_SAM + 10} textAnchor="middle" fontSize="3.4" fontWeight={700} fill={COLORS.slate800}>{fmtEur(samValue, de)}</text>
<text x={CX} y={CY - 1.2} textAnchor="middle" fontSize="2.8" fontFamily="'JetBrains Mono', ui-monospace, monospace" fontWeight={700} letterSpacing="0.18em" fill={COLORS.amber700}>SOM</text>
<text x={CX} y={CY + 3} textAnchor="middle" fontSize="3.4" fontWeight={800} fill={COLORS.slate900}>{fmtEur(somValue, de)}</text>
</svg>
</div>
<div style={{ marginTop: '3mm', fontSize: '7.5pt', color: COLORS.slate500, lineHeight: 1.4, textAlign: 'center' }}>
{de ? 'Verschachtelte Marktanteile (TAM ⊃ SAM ⊃ SOM)' : 'Nested market shares (TAM ⊃ SAM ⊃ SOM)'}
</div>
</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>
<DataTable
cols={[
{ header: de ? 'Kennzahl' : 'Metric', width: '50%' },
{ header: de ? 'Wert' : 'Value', numeric: true },
]}
rows={[
[de ? 'Unternehmen DACH' : 'Companies DACH', '~6.500'],
[de ? 'Davon 10500 MA (Zielgröße)' : 'Of which 10500 emp. (target)', '~4.200'],
[de ? 'Beschäftigte gesamt' : 'Total employees', '~1,3 Mio.'],
[de ? 'Umsatz Branche p.a.' : 'Industry revenue p.a.', de ? '~€280 Mrd.' : '~€280B'],
[de ? 'Compliance-Budget Ø' : 'Avg. compliance budget', de ? '€50150k / Jahr' : '€50150k / yr'],
[de ? 'Validierte ARR Top-10' : 'Validated ARR top-10', '>$1,1 Mrd.'],
]}
dense
highlightFirstCol
/>
{/* RIGHT: stacked info cards + callout */}
<div style={{ display: 'flex', flexDirection: 'column', minHeight: 0, gap: '3.5mm' }}>
{cards.map((c) => {
const isAmber = c.accent === 'amber'
const isSoft = c.accent === 'violet-soft'
const stroke = isAmber ? COLORS.amber600 : isSoft ? COLORS.violet500 : COLORS.violet700
const bg = isAmber ? COLORS.amber50 : isSoft ? COLORS.violet50 : 'transparent'
const kickerColor = isAmber ? COLORS.amber700 : COLORS.violet700
const valueColor = isAmber ? COLORS.amber700 : COLORS.slate900
return (
<div key={c.key} style={{ borderLeft: `3px solid ${stroke}`, background: bg, padding: '3mm 4mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '3mm' }}>
<span style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '7.5pt', fontWeight: 700, letterSpacing: '0.18em', color: kickerColor, textTransform: 'uppercase' }}>{c.key}</span>
{c.core && (<span style={{ fontSize: '7pt', fontWeight: 700, color: COLORS.amber700, background: '#fff', border: `1px solid ${COLORS.amber600}`, padding: '0.5mm 1.5mm', letterSpacing: '0.06em', textTransform: 'uppercase' }}>{de ? '← unser Kernmarkt' : '← our core market'}</span>)}
</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '3mm', marginTop: '1mm' }}>
<div style={{ fontSize: '22pt', fontWeight: 800, color: valueColor, lineHeight: 1, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{c.value}</div>
<div style={{ fontSize: '7.5pt', color: COLORS.slate500, fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>+{c.growth}% CAGR</div>
</div>
<div style={{ marginTop: '1.5mm', fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.4 }}>{c.desc}</div>
</div>
)
})}
<div style={{ marginTop: '5mm' }}>
<div style={{ marginTop: 'auto' }}>
<Callout tone="accent" label={de ? 'Warum Maschinenbau zuerst' : 'Why manufacturing first'}>
{de
? 'Höchste Regulierungsdichte (DSGVO + AI Act + CRA + Maschinen-VO + ProdSG + LkSG) bei gleichzeitig kleinem Compliance-Team. Klare Schmerzpunkte. Bekannte Vertriebskanäle (VDMA, IHK, Messen).'
@@ -152,11 +186,43 @@ export function PrintMilestonesPage({ milestones, lang, pageNum, totalPages, ver
/* ===== TEAM ===== */
// Tuple shape: [LucideIcon, de_label, en_label]
type TeamBullet = [typeof Briefcase, string, string]
const TEAM_INFO: Array<{ tagline: [string, string]; bullets: TeamBullet[] }> = [
{
tagline: [
'Diplom-Ökonom mit 20+ Jahren Industrie- und Digitalisierungs-Erfahrung.',
'Business economist with 20+ years in industry and digital transformation.',
],
bullets: [
[Briefcase, '20+ Jahre Industrie, Strategie & Digitalisierung', '20+ yrs industry, strategy & digital transformation'],
[RefreshCw, 'Aufbau IoT-, Blockchain- & KI-Plattformen', 'Built IoT, blockchain & AI platforms'],
[Handshake, 'M&A: 4 Übernahmen & Beteiligungen geführt', 'M&A: led 4 acquisitions & investments'],
[Scale, 'Regulatorik: DSGVO, MiCAR, CRA, Data Act', 'Regulatory: GDPR, MiCAR, CRA, Data Act'],
[Lightbulb, '12 erteilte Patente (Erfinder/Miterfinder)', '12 granted patents (inventor / co-inventor)'],
],
},
{
tagline: [
'Engineering Leader mit 15+ Jahren in Fintech, Web3 und Enterprise-KI.',
'Engineering leader with 15+ years across fintech, Web3 and enterprise AI.',
],
bullets: [
[Code, '15+ Jahre Engineering Leadership — Fintech, Web3, KI', '15+ yrs engineering leadership — fintech, Web3, AI'],
[TrendingUp, 'Engineering-Org skaliert: 6 → 60 in 18 Monaten', 'Scaled engineering org: 6 → 60 in 18 months'],
[CreditCard, 'ETOPay SaaS-Payment-Infrastruktur entwickelt', 'Built ETOPay SaaS payment infrastructure'],
[ShieldCheck, 'MiCA-Compliance-Strategie ViviSwap', 'MiCA compliance strategy for ViviSwap'],
[Cpu, 'Embedded Rust (Cortex-M) + Full-Stack TypeScript', 'Embedded Rust (Cortex-M) + full-stack TypeScript'],
],
},
]
export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }: SlideBase & { team: PitchTeamMember[] }) {
const de = lang === 'de'
const members = team && team.length ? team : [
{ id: 1, name: 'Benjamin Bönisch', role_de: 'CEO & Co-Founder', role_en: 'CEO & Co-Founder', bio_de: 'Mehrfacher Gründer mit Fokus auf B2B-SaaS und Vertrieb. Ehemals Geschäftsführer und Vertriebsleiter. Tiefe Verankerung im Maschinenbau-Netzwerk DACH.', bio_en: 'Serial founder focused on B2B SaaS and sales. Former CEO and VP Sales. Deep network in DACH manufacturing.', equity_pct: 37.3, expertise: ['B2B Sales', 'Go-to-Market', 'Manufacturing', 'Operations'], linkedin_url: '', photo_url: '' },
{ id: 2, name: 'Sharang Parnerkar', role_de: 'CTO & Co-Founder', role_en: 'CTO & Co-Founder', bio_de: 'Ex-Anthropic, Ex-Google. Distributed systems, KI-Infrastruktur, RAG-Pipelines. Open-Source-Contributor. Hat die gesamte Plattform-Architektur entworfen und 500K+ LoC implementiert.', bio_en: 'Ex-Anthropic, ex-Google. Distributed systems, AI infrastructure, RAG pipelines. Open-source contributor. Designed the entire platform architecture and implemented 500K+ LoC.', equity_pct: 37.3, expertise: ['AI Infrastructure', 'Distributed Systems', 'RAG', 'Go/Python/TypeScript'], linkedin_url: '', photo_url: '' },
{ id: 1, name: 'Benjamin Bönisch', role_de: 'CEO & Co-Founder', role_en: 'CEO & Co-Founder', bio_de: '', bio_en: '', equity_pct: 37.3, expertise: ['B2B Sales', 'Go-to-Market', 'Manufacturing', 'Operations'], linkedin_url: '', photo_url: '' },
{ id: 2, name: 'Sharang Parnerkar', role_de: 'CTO & Co-Founder', role_en: 'CTO & Co-Founder', bio_de: '', bio_en: '', equity_pct: 37.3, expertise: ['AI Infrastructure', 'Distributed Systems', 'RAG', 'Go/Python/TypeScript'], linkedin_url: '', photo_url: '' },
]
return (
@@ -166,6 +232,10 @@ export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }:
{[0, 1].map(idx => {
const m = members[idx]
if (!m) return null
const info = TEAM_INFO[idx]
// Icon tile palette: violet for first founder, amber for second
const tileBg = idx === 0 ? COLORS.violet50 : COLORS.amber50
const tileColor = idx === 0 ? COLORS.violet700 : COLORS.amber700
return (
<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={{ display: 'flex', gap: '5mm', alignItems: 'flex-start', marginBottom: '4mm' }}>
@@ -190,7 +260,20 @@ export function PrintTeamPage({ team, lang, pageNum, totalPages, versionName }:
<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>
{info && (
<div style={{ fontSize: '8.5pt', color: COLORS.slate700, fontWeight: 600, lineHeight: 1.4, marginBottom: '3mm' }}>{info.tagline[de ? 0 : 1]}</div>
)}
{/* Bulleted skill list with icons */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '1.5mm' }}>
{info?.bullets.map(([IconComp, deLabel, enLabel], bi) => (
<div key={bi} style={{ display: 'flex', alignItems: 'flex-start', gap: '3mm' }}>
<div style={{ width: '7mm', height: '7mm', flexShrink: 0, background: tileBg, color: tileColor, display: 'flex', alignItems: 'center', justifyContent: 'center', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<IconComp size={13} strokeWidth={2} />
</div>
<div style={{ flex: 1, fontSize: '9pt', color: COLORS.slate700, lineHeight: 1.4, paddingTop: '1mm' }}>{de ? deLabel : enLabel}</div>
</div>
))}
</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' }}>
@@ -238,7 +321,7 @@ function formatFunding(amount: number): string {
export function PrintTheAskPage({ funding, lang, pageNum, totalPages, versionName }: SlideBase & { funding: PitchFunding }) {
const de = lang === 'de'
const amount = funding?.amount_eur || 1_000_000
const amount = funding?.amount_eur || 400_000
const instrument = funding?.instrument || (de ? 'Wandeldarlehen' : 'Convertible Loan')
const isConvertible = (instrument || '').toLowerCase().includes('wandeldarlehen') ||
(instrument || '').toLowerCase().includes('convertible') ||
@@ -113,22 +113,58 @@ function fmtEur(n: number): string {
return `${sign}${Math.round(abs)}`
}
/* Small inline SVG sparkline. Renders an empty-state em-dash if all values are zero. */
function Sparkline({ values, width = 56, height = 22, stroke = COLORS.violet600 }: { values: number[]; width?: number; height?: number; stroke?: string }) {
const allZero = values.every(v => v === 0)
if (allZero || values.length < 2) {
return <span style={{ fontFamily: MONO, fontSize: '11pt', color: COLORS.slate300, fontWeight: 700, lineHeight: 1 }}>&mdash;</span>
}
const min = Math.min(...values)
const max = Math.max(...values)
const range = max - min || 1
const stepX = width / (values.length - 1)
const padY = 2
const innerH = height - padY * 2
const points = values.map((v, i) => {
const x = i * stepX
const y = padY + innerH - ((v - min) / range) * innerH
return { x, y }
})
const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ')
const last = points[points.length - 1]
// Area fill polygon for soft tone under the line
const area = `${path} L${last.x.toFixed(2)},${height} L0,${height} Z`
return (
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{ display: 'block', overflow: 'visible' }}>
<path d={area} fill={stroke} fillOpacity={0.08} />
<path d={path} fill="none" stroke={stroke} strokeWidth={1.2} strokeLinecap="round" strokeLinejoin="round" />
<circle cx={last.x} cy={last.y} r={1.6} fill={stroke} />
</svg>
)
}
export function PrintKPIHeroPage({ fmResults, lang, pageNum, totalPages, versionName }: SlideBase & { fmResults: FMResult[] }) {
const de = lang === 'de'
const kpis = computeAnnualKPIs(fmResults)
const k26 = kpis.find(k => k.year === 2026)
const k30 = kpis.find(k => k.year === 2030)
const breakEvenYear = kpis.find(k => k.ebit > 0)?.year
const series = (pick: (k: typeof kpis[number]) => number): number[] =>
[2026, 2027, 2028, 2029, 2030].map(y => {
const row = kpis.find(k => k.year === y)
return row ? pick(row) : 0
})
const tiles = k26 && k30 ? [
{ label: 'ARR', start: fmtEur(k26.arr), end: fmtEur(k30.arr), endColor: COLORS.violet600 },
{ label: de ? 'Kunden' : 'Customers', start: k26.customers.toLocaleString('de-DE'), end: k30.customers.toLocaleString('de-DE'), endColor: COLORS.violet600 },
{ label: de ? 'ARPU / Mo' : 'ARPU / mo', start: fmtEur(k26.arpu), end: fmtEur(k30.arpu), endColor: COLORS.slate900 },
{ label: de ? 'Mitarbeiter' : 'Employees', start: String(k26.employees), end: String(k30.employees), endColor: COLORS.slate900 },
{ label: de ? 'Bruttomarge' : 'Gross margin', start: `${k26.grossMargin}%`, end: `${k30.grossMargin}%`, endColor: COLORS.emerald700 },
{ label: 'EBIT', start: fmtEur(k26.ebit), end: fmtEur(k30.ebit), endColor: k30.ebit >= 0 ? COLORS.emerald700 : COLORS.red700 },
{ label: de ? 'Netto-Ergebnis' : 'Net income', start: fmtEur(k26.netIncome), end: fmtEur(k30.netIncome), endColor: k30.netIncome >= 0 ? COLORS.emerald700 : COLORS.red700 },
{ label: de ? 'Cash (Dez)' : 'Cash (Dec)', start: fmtEur(k26.cashBalance), end: fmtEur(k30.cashBalance), endColor: COLORS.emerald700 },
type Tile = { label: string; start: string; end: string; endColor: string; series: number[]; hideStart?: boolean; stroke?: string }
const tiles: Tile[] = k26 && k30 ? [
{ label: 'ARR', start: fmtEur(k26.arr), end: fmtEur(k30.arr), endColor: COLORS.violet600, series: series(k => k.arr), hideStart: k26.arr === 0 },
{ label: de ? 'Kunden' : 'Customers', start: k26.customers.toLocaleString('de-DE'), end: k30.customers.toLocaleString('de-DE'), endColor: COLORS.violet600, series: series(k => k.customers), hideStart: k26.customers === 0 },
{ label: de ? 'ARPU / Mo' : 'ARPU / mo', start: fmtEur(k26.arpu), end: fmtEur(k30.arpu), endColor: COLORS.slate900, series: series(k => k.arpu), hideStart: k26.arpu === 0 },
{ label: de ? 'Mitarbeiter' : 'Employees', start: String(k26.employees), end: String(k30.employees), endColor: COLORS.slate900, series: series(k => k.employees), hideStart: k26.employees === 0 },
{ label: de ? 'Bruttomarge' : 'Gross margin', start: `${k26.grossMargin}%`, end: `${k30.grossMargin}%`, endColor: COLORS.emerald700, series: series(k => k.grossMargin), hideStart: k26.grossMargin === 0, stroke: COLORS.emerald600 },
{ label: 'EBIT', start: fmtEur(k26.ebit), end: fmtEur(k30.ebit), endColor: k30.ebit >= 0 ? COLORS.emerald700 : COLORS.red700, series: series(k => k.ebit), hideStart: k26.ebit === 0, stroke: k30.ebit >= 0 ? COLORS.emerald600 : COLORS.red600 },
{ label: de ? 'Netto-Ergebnis' : 'Net income', start: fmtEur(k26.netIncome), end: fmtEur(k30.netIncome), endColor: k30.netIncome >= 0 ? COLORS.emerald700 : COLORS.red700, series: series(k => k.netIncome), hideStart: k26.netIncome === 0, stroke: k30.netIncome >= 0 ? COLORS.emerald600 : COLORS.red600 },
{ label: de ? 'Cash (Dez)' : 'Cash (Dec)', start: fmtEur(k26.cashBalance), end: fmtEur(k30.cashBalance), endColor: COLORS.emerald700, series: series(k => k.cashBalance), hideStart: k26.cashBalance === 0, stroke: COLORS.emerald600 },
] : []
return (
@@ -140,13 +176,22 @@ export function PrintKPIHeroPage({ fmResults, lang, pageNum, totalPages, version
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gridTemplateRows: '1fr 1fr', gap: '5mm', flex: 1, minHeight: 0 }}>
{tiles.map((t, i) => (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `3px solid ${COLORS.violet600}`, background: '#ffffff', padding: '5mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ fontFamily: MONO, fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: '4mm' }}>{t.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '3mm', marginBottom: 'auto' }}>
<div style={{ fontFamily: MONO, fontSize: '7.5pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: '3.5mm' }}>{t.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '3mm', marginBottom: '3.5mm' }}>
{!t.hideStart && (
<>
<span style={{ fontSize: '11pt', fontWeight: 600, color: COLORS.slate400, fontVariantNumeric: 'tabular-nums' }}>{t.start}</span>
<span style={{ fontFamily: MONO, fontSize: '11pt', color: COLORS.slate400, fontWeight: 700 }}></span>
</>
)}
<span style={{ fontSize: '24pt', fontWeight: 800, color: t.endColor, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.025em' }}>{t.end}</span>
</div>
<div style={{ marginTop: '3mm', paddingTop: '2mm', borderTop: `1px solid ${COLORS.slate100}`, fontFamily: MONO, fontSize: '6.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', fontWeight: 700 }}>2026 &middot; 2030</div>
<div style={{ marginTop: 'auto', display: 'flex', flexDirection: 'column', gap: '1.5mm' }}>
<Sparkline values={t.series} stroke={t.stroke ?? COLORS.violet600} />
<div style={{ paddingTop: '1.5mm', borderTop: `1px solid ${COLORS.slate100}`, fontFamily: MONO, fontSize: '6.5pt', color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.12em', fontWeight: 700, display: 'flex', justifyContent: 'space-between' }}>
<span>2026</span><span>2030</span>
</div>
</div>
</div>
))}
</div>
@@ -163,42 +208,67 @@ export function PrintTechStackPage({ lang, pageNum, totalPages, versionName }: S
const de = lang === 'de'
const cats = de ? [
{ name: 'Frontend', icon: ScanLine, items: ['Next.js 15', 'React 19', 'Tailwind CSS', 'Framer Motion', 'Dioxus (Rust)'] },
{ name: 'Backend', icon: Wrench, items: ['Go/Gin', 'Python/FastAPI', 'Rust/Axum', 'OpenAPI'] },
{ name: 'Storage', icon: Database, items: ['PostgreSQL 16', 'MongoDB', 'Qdrant Vector DB', 'Valkey (cache)'] },
{ name: 'KI / RAG', icon: Brain, items: ['LiteLLM', 'Qwen3-32B', 'DeepSeek-R1', 'Sentence-Transformers', 'LangGraph'] },
{ name: 'Code-Scanning', icon: Shield, items: ['Semgrep', 'Gitleaks', 'Syft', 'Trivy', 'CycloneDX'] },
{ name: 'Auth & SSO', icon: Lock, items: ['Keycloak', 'OIDC', 'OPA (policies)'] },
{ name: 'Kommunikation', icon: MessageSquare, items: ['Matrix (chat)', 'Jitsi (video)', 'Mailpit'] },
{ name: 'DevOps', icon: ShieldCheck, items: ['Gitea', 'Woodpecker CI', 'HashiCorp Vault', 'Orca', 'Docker Compose'] },
{ name: 'Frontend', icon: ScanLine, blurb: 'User-facing Oberflächen', items: ['Next.js 15', 'React 19', 'Tailwind CSS', 'Framer Motion', 'Dioxus (Rust)'] },
{ name: 'Backend', icon: Wrench, blurb: 'API & Business-Logik', items: ['Go/Gin', 'Python/FastAPI', 'Rust/Axum', 'OpenAPI'] },
{ name: 'Storage', icon: Database, blurb: 'Persistenter Zustand', items: ['PostgreSQL 16', 'MongoDB', 'Qdrant Vector DB', 'Valkey (cache)'] },
{ name: 'KI / RAG', icon: Brain, blurb: 'Inferenz & Retrieval', items: ['LiteLLM', 'Qwen3-32B', 'DeepSeek-R1', 'Sentence-Transformers', 'LangGraph'] },
{ name: 'Code-Scanning', icon: Shield, blurb: 'Schwachstellen-Erkennung', items: ['Semgrep', 'Gitleaks', 'Syft', 'Trivy', 'CycloneDX'] },
{ name: 'Auth & SSO', icon: Lock, blurb: 'Identität & Rechte', items: ['Keycloak', 'OIDC', 'OPA (policies)'] },
{ name: 'Kommunikation', icon: MessageSquare, blurb: 'Echtzeit-Kanäle', items: ['Matrix (chat)', 'Jitsi (video)', 'Mailpit'] },
{ name: 'DevOps', icon: ShieldCheck, blurb: 'Build & Ship', items: ['Gitea', 'Woodpecker CI', 'HashiCorp Vault', 'Orca', 'Docker Compose'] },
] : [
{ name: 'Frontend', icon: ScanLine, items: ['Next.js 15', 'React 19', 'Tailwind CSS', 'Framer Motion', 'Dioxus (Rust)'] },
{ name: 'Backend', icon: Wrench, items: ['Go/Gin', 'Python/FastAPI', 'Rust/Axum', 'OpenAPI'] },
{ name: 'Storage', icon: Database, items: ['PostgreSQL 16', 'MongoDB', 'Qdrant vector DB', 'Valkey (cache)'] },
{ name: 'AI / RAG', icon: Brain, items: ['LiteLLM', 'Qwen3-32B', 'DeepSeek-R1', 'Sentence-Transformers', 'LangGraph'] },
{ name: 'Code scanning', icon: Shield, items: ['Semgrep', 'Gitleaks', 'Syft', 'Trivy', 'CycloneDX'] },
{ name: 'Auth & SSO', icon: Lock, items: ['Keycloak', 'OIDC', 'OPA (policies)'] },
{ name: 'Communication', icon: MessageSquare, items: ['Matrix (chat)', 'Jitsi (video)', 'Mailpit'] },
{ name: 'DevOps', icon: ShieldCheck, items: ['Gitea', 'Woodpecker CI', 'HashiCorp Vault', 'Orca', 'Docker Compose'] },
{ name: 'Frontend', icon: ScanLine, blurb: 'User-facing surfaces', items: ['Next.js 15', 'React 19', 'Tailwind CSS', 'Framer Motion', 'Dioxus (Rust)'] },
{ name: 'Backend', icon: Wrench, blurb: 'API & business logic', items: ['Go/Gin', 'Python/FastAPI', 'Rust/Axum', 'OpenAPI'] },
{ name: 'Storage', icon: Database, blurb: 'Persistent state', items: ['PostgreSQL 16', 'MongoDB', 'Qdrant vector DB', 'Valkey (cache)'] },
{ name: 'AI / RAG', icon: Brain, blurb: 'Inference & retrieval', items: ['LiteLLM', 'Qwen3-32B', 'DeepSeek-R1', 'Sentence-Transformers', 'LangGraph'] },
{ name: 'Code scanning', icon: Shield, blurb: 'Vulnerability detection', items: ['Semgrep', 'Gitleaks', 'Syft', 'Trivy', 'CycloneDX'] },
{ name: 'Auth & SSO', icon: Lock, blurb: 'Identity & permissions', items: ['Keycloak', 'OIDC', 'OPA (policies)'] },
{ name: 'Communication', icon: MessageSquare, blurb: 'Real-time channels', items: ['Matrix (chat)', 'Jitsi (video)', 'Mailpit'] },
{ name: 'DevOps', icon: ShieldCheck, blurb: 'Build & ship', items: ['Gitea', 'Woodpecker CI', 'HashiCorp Vault', 'Orca', 'Docker Compose'] },
]
return (
<Page kicker="26" section={de ? 'ANHANG · TECH-STACK' : 'APPENDIX · TECH STACK'} title={de ? '8 Kategorien. Polyglott. 100 % Open Source.' : '8 categories. Polyglot. 100 % open source.'} subtitle={de ? 'Alle Komponenten mit kommerziell nutzbarer Lizenz (MIT, Apache-2.0, BSD, ISC, MPL-2.0, LGPL). Keine GPL/AGPL. Keine US-SaaS-Abhängigkeit.' : 'All components carry a commercially usable license (MIT, Apache-2.0, BSD, ISC, MPL-2.0, LGPL). No GPL/AGPL. No US SaaS dependency.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<Page kicker="26" section={de ? 'ANHANG · TECH-STACK' : 'APPENDIX · TECH STACK'} title={de ? '8 Kategorien. Polyglott. 100% Open Source.' : '8 categories. Polyglot. 100% open source.'} subtitle={de ? 'Alle Komponenten mit kommerziell nutzbarer Lizenz (MIT, Apache-2.0, BSD, ISC, MPL-2.0, LGPL). Keine GPL/AGPL. Keine US-SaaS-Abhängigkeit.' : 'All components carry a commercially usable license (MIT, Apache-2.0, BSD, ISC, MPL-2.0, LGPL). No GPL/AGPL. No US SaaS dependency.'} pageNum={pageNum} totalPages={totalPages} versionName={versionName}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gridTemplateRows: '1fr 1fr', gap: '4mm', flex: 1, minHeight: 0 }}>
{cats.map((c, i) => {
const Icon = c.icon
const num = String(i + 1).padStart(2, '0')
return (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.violet600}`, background: '#ffffff', padding: '4mm 5mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '3mm', marginBottom: '3mm' }}>
<div style={{ width: '8mm', height: '8mm', background: COLORS.violet50, borderRadius: '2pt', display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.violet600, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<Icon size={15} strokeWidth={1.5} />
<div key={i} style={{
border: `1px solid ${COLORS.violet200}`,
borderRadius: '4pt',
background: `linear-gradient(160deg, ${COLORS.violet50} 0%, #ffffff 55%)`,
padding: '4mm 4mm 3.5mm',
display: 'flex', flexDirection: 'column', alignItems: 'center',
boxShadow: '0 4px 12px rgba(124,58,237,0.08)',
WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact',
}}>
<div style={{
width: '14mm', height: '14mm', borderRadius: '3pt',
background: `linear-gradient(135deg, ${COLORS.violet400} 0%, ${COLORS.violet600} 100%)`,
boxShadow: '0 4px 10px rgba(124,58,237,0.28)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#ffffff', flexShrink: 0, marginBottom: '2.5mm',
WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact',
}}>
<Icon size={24} strokeWidth={1.6} />
</div>
<div style={{ fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15 }}>{c.name}</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0' }}>
<div style={{ fontFamily: MONO, fontSize: '6.5pt', fontWeight: 700, color: COLORS.violet600, textTransform: 'uppercase', letterSpacing: '0.22em', marginBottom: '0.5mm' }}>{num}</div>
<div style={{ fontSize: '12pt', fontWeight: 800, color: COLORS.slate900, lineHeight: 1.1, letterSpacing: '-0.005em', textAlign: 'center' }}>{c.name}</div>
<div style={{ fontSize: '7pt', fontStyle: 'italic', color: COLORS.slate500, lineHeight: 1.3, textAlign: 'center', marginTop: '0.8mm', marginBottom: '2.5mm' }}>{c.blurb}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1.2mm', justifyContent: 'center', marginTop: 'auto' }}>
{c.items.map((it, j) => (
<div key={j} style={{ fontFamily: MONO, fontSize: '8pt', color: COLORS.slate700, padding: '1.2mm 0', borderTop: j > 0 ? `1px solid ${COLORS.slate100}` : 'none', lineHeight: 1.3 }}>{it}</div>
<span key={j} style={{
fontFamily: MONO, fontSize: '7.5pt', fontWeight: 600,
color: COLORS.slate700,
background: COLORS.violet50,
border: `1px solid ${COLORS.violet200}`,
borderRadius: '999px',
padding: '0.8mm 2mm',
lineHeight: 1.15,
WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact',
}}>{it}</span>
))}
</div>
</div>
@@ -1,11 +1,13 @@
import { Language, PitchProduct } from '@/lib/types'
import { Page, Bullets, Callout, COLORS, DataTable } from './PrintLayout'
import { Page, Bullets, Callout, COLORS } from './PrintLayout'
import { LoopDiagram } from './PrintDiagrams'
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,
Lock, Heart, Banknote, ShoppingCart, Wifi, BookOpen, Landmark, Building2,
Factory, Cpu, CheckCircle2, Zap,
type LucideIcon,
} from 'lucide-react'
@@ -105,15 +107,16 @@ export function PrintUSPPage2({ lang, pageNum, totalPages, versionName }: SlideB
/* ===== REGULATORY LANDSCAPE ===== */
const CATEGORY_ICONS: LucideIcon[] = [Lock, Shield, Brain, Globe, ShieldCheck, Banknote, Heart, Users]
const RL_CATEGORIES_DE = [
{ name: 'Datenschutz', sample: 'DSGVO · ePrivacy · TTDSG · BDSG', count: 32 },
{ name: 'Cybersicherheit', sample: 'NIS2 · IT-SiG · BSIG · KRITIS-Verordnung', count: 47 },
{ name: 'Cybersicherheit', sample: 'NIS2 · IT-SiG · BSIG · KRITIS-VO', count: 47 },
{ name: 'KI-Regulierung', sample: 'AI Act · KI-Haftungsrichtlinie', count: 18 },
{ name: 'Digitale Märkte', sample: 'DMA · DSA · Data Act · Data Governance Act', count: 24 },
{ name: 'Produktsicherheit', sample: 'CRA · Maschinenverordnung · Produktsicherheitsgesetz', count: 41 },
{ name: 'Digitale Märkte', sample: 'DMA · DSA · Data Act · DGA', count: 24 },
{ name: 'Produktsicherheit', sample: 'CRA · MaschinenVO · ProdSG', count: 41 },
{ name: 'Finanzregulierung', sample: 'DORA · MiCA · FinmadiG · KWG', count: 53 },
{ name: 'Gesundheitsdaten', sample: 'MDR · IVDR · PatDG · Krankenhausgesetz', count: 28 },
{ name: 'Verbraucherschutz', sample: 'UWG · BGB · Geschäftsgeheimnisschutz · HinSchG', count: 36 },
{ name: 'Gesundheitsdaten', sample: 'MDR · IVDR · PatDG · KHG', count: 28 },
{ name: 'Verbraucherschutz', sample: 'UWG · BGB · GeschGehG · HinSchG', count: 36 },
]
const RL_CATEGORIES_EN = [
{ name: 'Data Privacy', sample: 'GDPR · ePrivacy · TTDSG · BDSG', count: 32 },
@@ -125,6 +128,7 @@ const RL_CATEGORIES_EN = [
{ name: 'Health Data', sample: 'MDR · IVDR · PatDG · Hospital Act', count: 28 },
{ name: 'Consumer Prot.', sample: 'UWG · BGB · Trade Secrets · HinSchG', count: 36 },
]
const INDUSTRY_ICONS: LucideIcon[] = [Building2, Factory, Heart, Banknote, ShoppingCart, Cpu, Wifi, Brain, ShieldCheck, BookOpen, Landmark]
const INDUSTRIES_DE = ['Alle Unternehmen', 'Maschinenbau', 'Gesundheit', 'Finanzsektor', 'E-Commerce', 'Technologie', 'IoT / Hardware', 'KI-Anbieter', 'Krit. Infrastruktur', 'Medien', 'Öffentl. Sektor']
const INDUSTRIES_EN = ['All companies', 'Manufacturing', 'Healthcare', 'Finance', 'E-Commerce', 'Technology', 'IoT / Hardware', 'AI Providers', 'Critical Infra.', 'Media', 'Public Sector']
@@ -151,32 +155,46 @@ export function PrintRegulatoryLandscapePage({ lang, pageNum, totalPages, versio
))}
</div>
<div style={{ marginTop: '5mm', flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1.5fr 1fr', gap: '6mm' }}>
{/* Categories */}
<div style={{ marginTop: '5mm', flex: 1, minHeight: 0, display: 'grid', gridTemplateColumns: '1.55fr 1fr', gap: '6mm' }}>
{/* Categories — 2x4 card grid with icons */}
<div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Acht Regulierungs-Kategorien' : 'Eight regulatory categories'}</div>
<DataTable
cols={[
{ header: de ? 'Kategorie' : 'Category', width: '32%' },
{ header: de ? 'Beispiele' : 'Examples' },
{ header: '#', width: '12%', numeric: true },
]}
rows={cats.map(c => [c.name, c.sample, c.count])}
dense
highlightFirstCol
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2mm' }}>
{cats.map((c, i) => {
const CIcon = CATEGORY_ICONS[i]
return (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, padding: '2mm 2.5mm', display: 'flex', alignItems: 'flex-start', gap: '2mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ width: '8mm', height: '8mm', background: COLORS.violet50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.violet600, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}><CIcon size={14} strokeWidth={1.6} /></div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '2mm' }}>
<div style={{ fontSize: '8.5pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.2 }}>{c.name}</div>
<div style={{ fontFamily: "'JetBrains Mono', ui-monospace, monospace", fontSize: '7pt', fontWeight: 700, color: COLORS.violet600, fontVariantNumeric: 'tabular-nums' }}>{c.count}</div>
</div>
<div style={{ fontSize: '6.5pt', color: COLORS.slate500, lineHeight: 1.3, marginTop: '0.5mm' }}>{c.sample}</div>
</div>
</div>
)
})}
</div>
</div>
{/* Industries */}
{/* Industries — cards with icons */}
<div>
<div style={{ fontSize: '8pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.1em', marginBottom: '2mm' }}>{de ? 'Zehn Branchen-Profile' : 'Ten industry profiles'}</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2mm' }}>
{industries.map((ind, i) => (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, padding: '2.5mm 3mm', fontSize: '8.5pt', color: COLORS.slate800, fontWeight: i === 1 ? 700 : 500 }}>
{i === 1 && <span style={{ fontSize: '6.5pt', color: COLORS.indigo600, textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, display: 'block', marginBottom: '1mm' }}>{de ? 'Kernfokus' : 'Core focus'}</span>}
{ind}
{industries.map((ind, i) => {
const IIcon = INDUSTRY_ICONS[i]
const isFocus = i === 1
return (
<div key={i} style={{ border: `1px solid ${isFocus ? COLORS.violet300 : COLORS.slate200}`, background: isFocus ? COLORS.violet50 : '#ffffff', padding: '2mm 2.5mm', display: 'flex', alignItems: 'center', gap: '2mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ width: '6mm', height: '6mm', display: 'flex', alignItems: 'center', justifyContent: 'center', color: isFocus ? COLORS.violet700 : COLORS.slate500, flexShrink: 0 }}><IIcon size={12} strokeWidth={1.7} /></div>
<div style={{ flex: 1, minWidth: 0 }}>
{isFocus && <div style={{ fontSize: '6pt', color: COLORS.violet700, textTransform: 'uppercase', letterSpacing: '0.1em', fontWeight: 700, lineHeight: 1 }}>{de ? 'Kernfokus' : 'Core focus'}</div>}
<div style={{ fontSize: '8pt', color: COLORS.slate800, fontWeight: isFocus ? 700 : 500, lineHeight: 1.2, marginTop: isFocus ? '0.5mm' : 0 }}>{ind}</div>
</div>
))}
</div>
)
})}
</div>
</div>
</div>
@@ -194,10 +212,7 @@ export function PrintRegulatoryLandscapePage({ lang, pageNum, totalPages, versio
/* ===== PRODUCT / MODULAR TOOLKIT ===== */
const MODULE_ICONS: LucideIcon[] = [
ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck,
AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare,
]
const MODULE_ICONS: LucideIcon[] = [ScanLine, ShieldCheck, FileText, ClipboardCheck, Users, UserCheck, AlertTriangle, Brain, Target, GraduationCap, TrendingUp, MessageSquare]
const MODULES_FULL_DE = [
{ name: 'Code Security', desc: 'SAST · DAST · SBOM · Container · Secrets · Pentesting', features: ['Bei jedem Push', 'Auto-Fix LLM', 'CI/CD-integriert'] },
{ name: 'CE-SW-Risikobeurteilung', desc: 'CE-Kennzeichnung für Maschinen mit Software-Anteil', features: ['Maschinen-VO', 'CRA-konform', 'Code-Basis-Analyse'] },
@@ -294,48 +309,64 @@ export function PrintHowItWorksPage({ lang, pageNum, totalPages, versionName }:
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}>
{/* 4-step rail: numbered violet circles on a horizontal connector line,
title + body underneath each. Replaces the floating-arrow StepStrip. */}
<div style={{ position: 'relative', marginTop: '4mm', flexShrink: 0 }}>
{/* connector line (behind the circles) */}
<div style={{ position: 'absolute', top: '7mm', left: '7mm', right: '7mm', height: '2px', background: `linear-gradient(90deg, ${COLORS.violet600} 0%, ${COLORS.violet400} 100%)`, opacity: 0.85, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
{/* 4-step rail: numbered violet circles on a horizontal connector line */}
<div style={{ position: 'relative', marginTop: '3mm', flexShrink: 0 }}>
<div style={{ position: 'absolute', top: '6mm', left: '6mm', right: '6mm', height: '2px', background: `linear-gradient(90deg, ${COLORS.violet600} 0%, ${COLORS.violet400} 100%)`, opacity: 0.85, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '5mm' }}>
{steps.map((s, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
{/* number circle on the rail */}
<div style={{ width: '14mm', height: '14mm', borderRadius: '50%', background: COLORS.violet600, color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14pt', fontWeight: 800, letterSpacing: '-0.01em', boxShadow: `0 0 0 4px ${COLORS.violet50}`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>{s.n}</div>
<div style={{ marginTop: '3mm', fontSize: '12pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15, letterSpacing: '-0.005em' }}>{s.t}</div>
<div style={{ marginTop: '2mm', fontSize: '8.5pt', color: COLORS.slate700, lineHeight: 1.5 }}>{s.d}</div>
<div style={{ width: '12mm', height: '12mm', borderRadius: '50%', background: COLORS.violet600, color: '#ffffff', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '12pt', fontWeight: 800, letterSpacing: '-0.01em', boxShadow: `0 0 0 4px ${COLORS.violet50}`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>{s.n}</div>
<div style={{ marginTop: '2.5mm', fontSize: '11pt', fontWeight: 700, color: COLORS.slate900, lineHeight: 1.15, letterSpacing: '-0.005em' }}>{s.t}</div>
<div style={{ marginTop: '1.5mm', fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.45 }}>{s.d}</div>
</div>
))}
</div>
</div>
{/* Fill space between steps and footer with a visual timeline */}
<div style={{ flex: 1 }} />
<div style={{ flexShrink: 0, marginBottom: '2mm' }}>
<div style={{ fontFamily: MONO, fontSize: '7.5pt', fontWeight: 700, color: COLORS.violet600, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: '4mm' }}>
{de ? 'Time-to-Value · Median 14 Tage · Worst Case 28 Tage' : 'Time-to-Value · Median 14 days · Worst case 28 days'}
</div>
{/* dotted timeline with 5 day markers */}
<div style={{ position: 'relative', height: '14mm' }}>
{/* the rail */}
{/* Day-marker timeline — directly under steps, ~3mm gap */}
<div style={{ flexShrink: 0, marginTop: '3mm' }}>
<div style={{ position: 'relative', height: '13mm' }}>
<div style={{ position: 'absolute', left: '7mm', right: '7mm', top: '5mm', height: '1.5px', background: `repeating-linear-gradient(90deg, ${COLORS.violet400} 0, ${COLORS.violet400} 2mm, transparent 2mm, transparent 4mm)`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', height: '100%' }}>
{days.map((d, i) => (
<div key={i} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative' }}>
{/* day marker pill */}
<div style={{ fontFamily: MONO, fontSize: '7pt', fontWeight: 700, color: COLORS.violet700, background: COLORS.violet50, padding: '1mm 3mm', borderRadius: '99pt', border: `1px solid ${COLORS.violet300}`, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>{d}</div>
{/* dot on rail */}
<div style={{ width: '3mm', height: '3mm', borderRadius: '50%', background: COLORS.violet600, marginTop: '1mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }} />
{/* label below */}
<div style={{ marginTop: '2mm', fontSize: '7.5pt', color: COLORS.slate700, textAlign: 'center', fontWeight: 600 }}>{labels[i]}</div>
<div style={{ marginTop: '1.5mm', fontSize: '7pt', color: COLORS.slate700, textAlign: 'center', fontWeight: 600 }}>{labels[i]}</div>
</div>
))}
</div>
</div>
</div>
{/* Time-to-value callout */}
<div style={{ flexShrink: 0, marginTop: '3mm' }}>
<Callout tone="accent" label={de ? 'Typische Time-to-Value' : 'Typical time-to-value'}>
{de
? 'Median 14 Tage · Worst Case 28 Tage. Vom Vertrag bis zur Audit-Bereitschaft typischerweise unter 30 Tagen.'
: 'Median 14 days · worst case 28 days. From contract to audit-readiness typically under 30 days.'}
</Callout>
</div>
{/* What you get on day N — 4-column benefit block, fills bottom third */}
<div style={{ flex: 1, minHeight: 0, marginTop: '4mm', display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
<div style={{ fontFamily: MONO, fontSize: '7pt', fontWeight: 700, color: COLORS.slate500, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: '2mm' }}>
{de ? 'Was Sie wann bekommen' : 'What you get, when'}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3mm' }}>
{([
[Shield, de ? 'Risikoanalysen automatisch ab Tag 3' : 'Risk analyses automatic from day 3'],
[FileText, de ? 'VVT / TOMs / DSFA generiert ab Tag 14' : 'RoPA / TOMs / DPIA generated from day 14'],
[CheckCircle2, de ? 'Audit-Trail vollständig ab Tag 30' : 'Audit trail complete from day 30'],
[Zap, de ? 'Continuous Scanning bei jedem Push' : 'Continuous scanning on every push'],
] as [LucideIcon, string][]).map(([Icon, t], i) => (
<div key={i} style={{ border: `1px solid ${COLORS.slate200}`, borderTop: `2px solid ${COLORS.violet600}`, padding: '2.5mm 3mm', display: 'flex', alignItems: 'flex-start', gap: '2.5mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ width: '7mm', height: '7mm', background: COLORS.violet50, display: 'flex', alignItems: 'center', justifyContent: 'center', color: COLORS.violet600, flexShrink: 0, WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}><Icon size={13} strokeWidth={1.7} /></div>
<div style={{ flex: 1, fontSize: '8pt', color: COLORS.slate800, fontWeight: 600, lineHeight: 1.35 }}>{t}</div>
</div>
))}
</div>
</div>
</Page>
)
}
@@ -347,42 +378,9 @@ export function PrintBusinessModelPage({ lang, pageNum, totalPages, versionName
const MONO = "'JetBrains Mono', ui-monospace, monospace"
const tiers = [
{
name: 'Starter',
target: de ? '< 25 Mitarbeiter · Basis-Module' : '< 25 employees · basic modules',
price: '€3.600',
unit: de ? '/ Jahr' : '/ year',
features: de
? ['DSGVO + Audit + DSR-Workflow', 'Compliance Scanner (CI/CD)', 'EU-Hosting · BSI C5', 'E-Mail-Support']
: ['GDPR + Audit + DSR workflow', 'Compliance Scanner (CI/CD)', 'EU hosting · BSI C5', 'Email support'],
tint: COLORS.violet400,
bg: COLORS.violet50,
featured: false,
},
{
name: 'Professional',
target: de ? '25250 Mitarbeiter · alle Module' : '25250 employees · all modules',
price: '€18.000',
unit: de ? '/ Jahr' : '/ year',
features: de
? ['Alle 12 Module', 'Priority-Support · Onboarding-Call', 'CE-Software-Risiko + Tender Matching', 'Dedicated CSM · 14-tägige Reviews', 'Custom-Integrationen (Jira, GitLab)']
: ['All 12 modules', 'Priority support · onboarding call', 'CE software risk + tender matching', 'Dedicated CSM · biweekly reviews', 'Custom integrations (Jira, GitLab)'],
tint: COLORS.violet600,
bg: `linear-gradient(180deg, ${COLORS.violet50} 0%, #ffffff 60%, ${COLORS.violet50} 100%)`,
featured: true,
},
{
name: 'Enterprise',
target: de ? '250+ Mitarbeiter · maßgeschneidert' : '250+ employees · custom',
price: de ? 'ab €50.000' : 'from €50k',
unit: de ? '/ Jahr' : '/ year',
features: de
? ['Alles aus Professional', 'SLA · Custom Contract', 'On-Premise / Air-Gap (Mac Mini/Studio)', 'Dedicated Customer Engineering', 'Multi-Region Audit-Trail']
: ['Everything in Professional', 'SLA · custom contract', 'On-premise / air-gap (Mac Mini/Studio)', 'Dedicated customer engineering', 'Multi-region audit trail'],
tint: COLORS.amber600,
bg: COLORS.amber50,
featured: false,
},
{ name: 'Starter', target: de ? '< 25 Mitarbeiter · Basis-Module' : '< 25 employees · basic modules', price: '€3.600', unit: de ? '/ Jahr' : '/ year', features: de ? ['DSGVO + Audit + DSR-Workflow', 'Compliance Scanner (CI/CD)', 'EU-Hosting · BSI C5', 'E-Mail-Support'] : ['GDPR + Audit + DSR workflow', 'Compliance Scanner (CI/CD)', 'EU hosting · BSI C5', 'Email support'], tint: COLORS.violet400, bg: COLORS.violet50, featured: false },
{ name: 'Professional', target: de ? '25250 Mitarbeiter · alle Module' : '25250 employees · all modules', price: '€18.000', unit: de ? '/ Jahr' : '/ year', features: de ? ['Alle 12 Module', 'Priority-Support · Onboarding-Call', 'CE-Software-Risiko + Tender Matching', 'Dedicated CSM · 14-tägige Reviews', 'Custom-Integrationen (Jira, GitLab)'] : ['All 12 modules', 'Priority support · onboarding call', 'CE software risk + tender matching', 'Dedicated CSM · biweekly reviews', 'Custom integrations (Jira, GitLab)'], tint: COLORS.violet600, bg: `linear-gradient(180deg, ${COLORS.violet50} 0%, #ffffff 60%, ${COLORS.violet50} 100%)`, featured: true },
{ name: 'Enterprise', target: de ? '250+ Mitarbeiter · maßgeschneidert' : '250+ employees · custom', price: de ? 'ab €50.000' : 'from €50k', unit: de ? '/ Jahr' : '/ year', features: de ? ['Alles aus Professional', 'SLA · Custom Contract', 'On-Premise / Air-Gap (Mac Mini/Studio)', 'Dedicated Customer Engineering', 'Multi-Region Audit-Trail'] : ['Everything in Professional', 'SLA · custom contract', 'On-premise / air-gap (Mac Mini/Studio)', 'Dedicated customer engineering', 'Multi-region audit trail'], tint: COLORS.amber600, bg: COLORS.amber50, featured: false },
]
return (
@@ -391,18 +389,7 @@ export function PrintBusinessModelPage({ lang, pageNum, totalPages, versionName
{/* 3 product cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '5mm', marginBottom: '5mm', position: 'relative' }}>
{tiers.map((t) => (
<div key={t.name} style={{
background: t.bg,
border: `${t.featured ? '2px' : '1px'} solid ${t.tint}`,
borderRadius: '6pt',
padding: '5mm',
display: 'flex',
flexDirection: 'column',
position: 'relative',
boxShadow: t.featured ? `0 6px 18px ${COLORS.violet600}25` : 'none',
WebkitPrintColorAdjust: 'exact',
printColorAdjust: 'exact',
}}>
<div key={t.name} style={{ background: t.bg, border: `${t.featured ? '2px' : '1px'} solid ${t.tint}`, borderRadius: '6pt', padding: '5mm', display: 'flex', flexDirection: 'column', position: 'relative', boxShadow: t.featured ? `0 6px 18px ${COLORS.violet600}25` : 'none', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
{/* Featured badge */}
{t.featured && (
<div style={{ position: 'absolute', top: '-3.5mm', left: '50%', transform: 'translateX(-50%)', fontFamily: MONO, fontSize: '7pt', fontWeight: 700, color: '#ffffff', background: COLORS.violet600, padding: '1mm 4mm', borderRadius: '99pt', textTransform: 'uppercase', letterSpacing: '0.18em', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>{de ? 'Beliebt' : 'Popular'}</div>
@@ -449,14 +436,37 @@ export function PrintBusinessModelPage({ lang, pageNum, totalPages, versionName
</div>
</div>
<div style={{ background: COLORS.emerald50, border: `1px solid ${COLORS.emerald600}`, borderRadius: '4pt', padding: '4mm 5mm', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ fontFamily: MONO, fontSize: '7pt', color: COLORS.emerald700, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: '2mm' }}>{de ? 'Netto-Effekt · KMU 50 MA / Jahr 1' : 'Net effect · SME 50 emp. / Y1'}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '3mm' }}>
<div style={{ fontSize: '26pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>+30k</div>
<div style={{ fontSize: '9pt', color: COLORS.emerald700, fontWeight: 600 }}>{de ? 'pro KMU / Jahr' : 'per SME / yr'}</div>
<div style={{ background: COLORS.emerald50, border: `1px solid ${COLORS.emerald600}`, borderRadius: '4pt', padding: '3mm 4mm', display: 'flex', flexDirection: 'column', WebkitPrintColorAdjust: 'exact', printColorAdjust: 'exact' }}>
<div style={{ fontFamily: MONO, fontSize: '7pt', color: COLORS.emerald700, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', marginBottom: '1.5mm' }}>{de ? 'Netto-Effekt · KMU 50 MA / Jahr 1' : 'Net effect · SME 50 emp. / Y1'}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '2.5mm' }}>
<div style={{ fontSize: '22pt', fontWeight: 800, color: COLORS.emerald700, lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.02em' }}>+30k</div>
<div style={{ fontSize: '8.5pt', color: COLORS.emerald700, fontWeight: 600 }}>{de ? 'pro KMU / Jahr' : 'per SME / yr'}</div>
</div>
<div style={{ fontSize: '8pt', color: COLORS.slate700, marginTop: '2mm', lineHeight: 1.4 }}>
{de ? 'Kunde spart €55k (Pentests, CE-Risiko, Compliance-Zeit), zahlt €25k. ROI ab Tag 1.' : 'Customer saves €55k (pentests, CE risk, compliance time), pays €25k. ROI from day 1.'}
<div style={{ fontSize: '7.5pt', color: COLORS.slate700, marginTop: '1.5mm', lineHeight: 1.4 }}>
{de ? 'Kunde spart €55k, zahlt €25k. ROI ab Tag 1.' : 'Customer saves €55k, pays €25k. ROI from day 1.'}
</div>
{/* Itemized breakdown */}
<div style={{ marginTop: '2.5mm', display: 'flex', flexDirection: 'column' }}>
{([
[de ? 'Pentests (kontinuierlich, inkl.)' : 'Pentests (continuous, incl.)', '+€13k'],
[de ? 'CE-Risiko (Code-basiert, inkl.)' : 'CE risk (code-based, incl.)', '+€9k'],
[de ? 'Compliance-Zeit (60%)' : 'Compliance time (60%)', '+€15k'],
[de ? 'Audit-Vorbereitung (auto)' : 'Audit prep (auto)', '+€9k'],
[de ? 'Legal-Stunden (40%)' : 'Legal hours (40%)', '+€5k'],
[de ? 'Schulungen (Academy inkl.)' : 'Training (Academy incl.)', '+€4k'],
] as [string, string][]).map(([l, v], i, arr) => (
<div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '2mm', padding: '1.2mm 0', borderBottom: i < arr.length - 1 ? `0.5px solid ${COLORS.emerald600}33` : 'none' }}>
<span style={{ fontSize: '8pt', color: COLORS.slate700, lineHeight: 1.3 }}>{l}</span>
<span style={{ fontSize: '8.5pt', fontWeight: 700, color: COLORS.emerald700, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{v}</span>
</div>
))}
</div>
<div style={{ marginTop: '2mm', fontSize: '7pt', color: COLORS.slate600, fontStyle: 'italic', lineHeight: 1.35 }}>
{de
? 'Plus Vermeidung von Bußgeldern (bis 4% Jahresumsatz) und gewonnene RFQs.'
: 'Plus avoided fines (up to 4% annual revenue) and won RFQs.'}
</div>
</div>
</div>