Fundament: Branding Craftvia

- Farb-Tokens laut Brandbook §11.4 (Lotsenblau, Signalorange, Graphit, Hafengrau,
  Stahlgrau, Zink, Funktionsfarben) in globals.css + src/lib/brand.ts; helles Theme
- Inline-SVG-Logo src/components/brand/craftvia-logo.tsx (horizontal/signet,
  color/mono/reversed, App-Icon-Kachel, optionale Tagline); Certvia-/GEFIM-Logos entfernt
- Favicon/PWA-Icons aus dem Signet erzeugt (scripts/generate-brand-icons.ts),
  site.webmanifest (Craftvia, theme_color #082E5B)
- Inter selbst gehostet (next/font/local, lokale OFL-Datei), Open Sans/Poppins entfernt
- Metadaten, WebAuthn-RP-Name, Mail-/Dokument-CD auf Craftvia umgestellt
- docs/craftvia/BRANDING.md ersetzt docs/BRANDING-CERTVIA.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:39:52 +02:00
co-authored by Claude Opus 5
parent 8491c7f173
commit 9ec7fa5356
62 changed files with 549 additions and 1070 deletions
-149
View File
@@ -1,149 +0,0 @@
# Branding-Umstellung auf Certvia — Inventar & Umsetzung
> Branch `dev-branding-certvia` (Basis `dev`) · Quelle: `Aufgabenpaket-Branding-Certvia.md` + `Certvia-Assets/`
> Leitplanke: **GEFIM bleibt Dachmarke** („Ein Produkt von GEFIM"), nur die **Produktmarke** wird Certvia.
> Reines Branding — **keine Funktionsänderung**, kein Umbau von Logik oder Datenmodell.
---
## 1. Design-Tokens (verbindliche Referenz)
**Markenfarben** (Logo, Print, Verläufe — `--brand-*`):
| Rolle | Token | Hex |
|---|---|---|
| Violett (Marke) | `--brand-violet` | `#5d52a3` |
| Magenta (Akzent „via", Haken) | `--brand-magenta` | `#812d80` |
| Hellblau | `--brand-blue` | `#8dc4e0` |
| Anthrazit (Text/Headlines) | `--brand-anthracite` | `#3b3b3a` |
| Violett auf Dunkel | `--brand-violet-on-dark` | `#7d6fd6` |
| Magenta auf Dunkel | `--brand-magenta-on-dark` | `#d17bcf` |
**UI Dark-Theme** (unverändert — **ist** die Certvia-Produktpalette, nicht „korrigieren"):
`--ui-primary #7d6fd6` · `--ui-primary-deep #5d52a3` · `--ui-blue #8dc4e0` · `--ui-magenta #b45bb0` ·
`--bg-0 #0e1220` · `--bg-1 #141a2e` · `--elevated #1a2138` · `--panel rgba(30,38,64,.72)` ·
`--panel-brd rgba(120,135,180,.18)` · `--foreground #e8ecf7` · `--muted-foreground #8b93ad` ·
`--ok #39c07f` · `--warn #f0ad4e` · `--risk #ff6b6b` · Primär-Verlauf `linear-gradient(135deg,#5d52a3,#7d6fd6)`
· Fokus-Ring `rgba(125,111,214,.35)`.
Single Source of Truth: **`src/app/globals.css`** (CSS) und **`src/lib/brand.ts`** (JS/TS — für alles,
was CSS-Variablen nicht auflösen kann: Canvas-/PNG-Export, Metadaten, künftige Dokument-Exporte).
**Typografie:** Headlines/Wortmarke **Poppins**, Fließtext **Open Sans** — self-hosted (`src/app/fonts/`).
---
## 2. Inventar (Story S0)
### 2.1 Produktmarke → Certvia
| Datei | Fundstelle | Maßnahme | Story |
|---|---|---|---|
| `src/app/globals.css` | `--gefim-violet/-deep/-blue/-magenta`, Kommentare „GEFIM-Theme" | Umbenannt auf `--brand-*` / `--ui-*`, Kommentare auf Certvia | S1/S9 |
| `src/app/globals.css` | Flächenfarben direkt als Hex (`#0e1220`, `#141a2e`, …) | Über `--bg-0`/`--bg-1`/`--elevated` geführt, semantische Tokens referenzieren sie | S1 |
| `src/components/risk-modals.tsx` | `bg-[#0e1220]` (Rest-Risiko-Marker, Legende) | → `bg-[var(--bg-0)]` | S1 |
| `src/app/(app)/risks/page.tsx` | `bg-[#0e1220]/75`, `hover:bg-[#0e1220]` (Heatmap-Chip) | → Token `--chip-overlay` / `var(--bg-0)` | S1 |
| `src/components/dependency-graph.tsx` | `#8dc4e0`, `#ff6b6b`, `#141a2e`, `#0e1220`, `#4a5372`, `#8b93ad` | CSS-Vars wo möglich; PNG-Export nutzt `BRAND.ui` aus `src/lib/brand.ts` (html-to-image löst keine CSS-Vars auf) | S1 |
| `public/gefim-logo.png` | Produktlogo in Sidebar + Login | Verschoben nach `public/assets/logo/gefim-logo.png`, dient nur noch der **Dachmarke** | S2 |
| `src/app/(app)/layout.tsx` | Sidebar-Logo `/gefim-logo.png`, Alt-Text „GEFIM …" | `<CertviaLogo variant="lockup" theme="dark">` über den Branding-Resolver | S2/S8 |
| `src/app/login/page.tsx` | Logo `/gefim-logo.png`, Alt-Text | Certvia-Lockup + Tagline + Dachmarken-Fußnote | S2/S7 |
| `src/app/platform/login/page.tsx` | kein Logo | Certvia-Lockup + „Plattform-Administration" | S2/S7 |
| `src/app/(platform)/layout.tsx` | `ShieldCheck` + „ISMS · Plattform-Administration" | Certvia-Mark + „Certvia · Plattform-Administration" | S2/S3 |
| `src/app/layout.tsx` | `title: "ISMS-Tool"`, keine Icons/OG/Manifest | Vollständige Certvia-Metadaten inkl. Icons, Manifest, `themeColor` | S3 |
| `src/app/favicon.ico` | Next-Default-Favicon | Entfernt; Icons laufen über `public/favicon/` + `metadata.icons` | S2 |
| `messages/de.json`, `messages/en.json` | `common.appName` = „ISMS-Tool"/„ISMS Tool" | → „Certvia" (+ `appTagline`, `appByline`) | S3 |
| `src/server/mfa.ts` | TOTP-Issuer `"ISMS-Tool · Plattform"` | → `"Certvia · Plattform"` | S3 |
| `src/components/ui/button.tsx` | Kommentar „GEFIM-Verlauf" | → „Certvia-Verlauf" | S9 |
| `src/components/mockup-ui.tsx` | Kommentar-Referenz auf Mockup-Datei | Formulierung entschärft (Dateiname bleibt, s. u.) | S9 |
| `src/lib/control-titles.ts` | Kommentar „aus dem GEFIM-Mockup übernommen" | → neutral „aus dem UI-Mockup" | S9 |
| `src/app/(app)/settings/page.tsx` | Hinweis „Logo-Upload folgt in Phase 2" | Ergänzt um Certvia-Default-Aussage | S8 |
| `src/proxy.ts` | Matcher schloss `.webmanifest` nicht aus | `/site.webmanifest` lieferte die Login-HTML statt JSON → Endung ergänzt (siehe §5) | S3 |
### 2.2 Dachmarke GEFIM → bleibt bewusst erhalten
| Ort | Grund |
|---|---|
| `public/assets/logo/gefim-logo.png` | Dachmarken-Logo für „Ein Produkt von GEFIM" |
| `src/components/brand/powered-by-gefim.tsx` | Neue Komponente, die genau diesen Hinweis rendert (Login, Plattform-Login, Print-/Export-Fußzeile, Mail-Footer) |
| `docs/ISMS-*-GEFIM.html`, Verweise in `README.md`/`AGENTS.md`/`docs/SPEC.md` | Historische Mockups/Spezifikation — Projektartefakte, keine Produkt-UI |
| `prisma/seed.ts` (`ORG_NAME` „GEFIM Demo GmbH") | **Demo-Mandantendaten** (fiktiver Kunde), nicht der Produktname |
| `prisma/import-managed.ts` (`portal.gefim.example`) | Demo-Registerzeile, nicht der Produktname |
Nach der Umstellung liefert `rg -i "gefim"` ausschließlich diese Kategorien.
---
## 3. Umgesetzte Stories
| Story | Inhalt | Status |
|---|---|---|
| **S0** | Inventar (dieses Dokument) | ✅ |
| **S1** | Token-Zentralisierung `--brand-*` / `--ui-*`, `src/lib/brand.ts`, hartkodierte Farben ersetzt | ✅ |
| **S2** | Assets eingebunden, `<CertviaLogo>`, Favicon/PWA-Icons, Sidebar + beide Logins | ✅ |
| **S3** | App-Name/Meta/OG/Manifest/i18n/MFA-Issuer auf Certvia | ✅ |
| **S4** | Poppins + Open Sans self-hosted bestätigt (waren es bereits), Fallback-Stack + `swap` geprüft | ✅ |
| **S7** | Login, Plattform-Login, Change-Password, Enroll-MFA, Konto-deaktiviert, 404, 500 gebranded | ✅ |
| **S9** | Produkt-Referenzen bereinigt, Dachmarke belassen | ✅ |
| **S5** | Branding-Layer für Dokument-/Export-CD (`src/lib/brand.ts` + Print-Stylesheet) | ✅ (Andockpunkt) |
| **S8** | Branding-Resolver mit Certvia-Default, Custom überschreibt nur wenn gesetzt | ✅ |
| **S6** | Brandfähige E-Mail-Template-Basis (SMTP selbst folgt in Paket 4) | ✅ (Basis) |
---
## 4. Wo liegt was (neu)
| Pfad | Inhalt |
|---|---|
| `src/lib/brand.ts` | Marken-/UI-Farben als JS-Konstanten, Produktname/Tagline/Byline, Asset-Pfade, `resolveTenantBranding()` |
| `src/lib/document-brand.ts` | Dokument-CD (helles Theme, Kopf-/Fußzeilen-Bausteine) — Andockpunkt für DOCX/PDF |
| `src/lib/email-brand.ts` | Brandfähige HTML-/Text-Mail-Basis für Paket 4 |
| `src/components/brand/certvia-logo.tsx` | `<CertviaLogo variant="lockup\|mark" theme="light\|dark" />` als Inline-SVG |
| `src/components/brand/tenant-brand.tsx` | Mandanten-Marke mit Certvia-Fallback |
| `src/components/brand/powered-by-gefim.tsx` | Dachmarken-Hinweis inkl. GEFIM-Logo |
| `public/assets/logo/` | Certvia-SVGs/PNGs + GEFIM-Dachmarkenlogo |
| `public/favicon/`, `public/favicon.ico`, `public/site.webmanifest` | App-Icons und PWA-Manifest |
| `docs/branding/` | Aufgabenpaket + Logo-README aus dem Design-Paket |
| `src/app/globals.css` (`@media print`) | Druck-/Dokument-CD |
---
## 5. Beim Einbau aufgefallen (Abweichungen vom Aufgabenpaket)
- **Lockup-SVG trägt Poppins jetzt eingebettet.** `public/assets/logo/certvia-lockup-positiv.svg`
enthält Poppins 300 + 700 als woff2-Data-URI (≈22 KB). Damit erscheint die Wortmarke auch
außerhalb der App korrekt (Druck, Export, Mail) — der offene Punkt „Text in Pfade wandeln" aus
`docs/branding/README-Logo.md` ist damit erledigt. Geometrie und Farben sind unverändert.
In der App rendert `<CertviaLogo>` das Zeichen ohnehin inline und nutzt die App-Schrift direkt.
- **`/site.webmanifest` war nicht erreichbar.** Der Route-Gate-Matcher in `src/proxy.ts` nahm nur
`svg|png|jpg|ico` aus — für `.webmanifest` lieferte der Server die Login-Seite statt JSON, das
PWA-Manifest wäre also wirkungslos geblieben. Endung ergänzt. Bewusst **keine** Verzeichnis-
Ausnahme für `assets/`: das ist zugleich die App-Route des Asset-Inventars und bleibt hinter dem
Gate (nachgeprüft: `/assets`, `/assets/xyz` → 307 auf `/login`).
- **`src/app/favicon.ico` entfernt.** Die Datei-Konvention von Next.js und eine `public/favicon.ico`
schließen sich gegenseitig aus; die Icons laufen jetzt vollständig über `metadata.icons`.
- **404-/500-Seiten neu angelegt** (`src/app/not-found.tsx`, `src/app/error.tsx`) — es gab bisher
keine. Sie erscheinen für angemeldete Nutzer; anonyme Aufrufe fängt weiterhin das Login-Gate ab.
### Kontrast (WCAG AA, geprüft)
| Kombination | Kontrast | Bewertung |
|---|---|---|
| Magenta `#812d80` auf Weiß (Wortmarke/Print) | 8,0 : 1 | AA + AAA |
| Magenta `#d17bcf` auf Karte `#141a2e` | 6,1 : 1 | AA |
| Magenta `#d17bcf` auf Seitengrund `#0e1220` | 6,6 : 1 | AA |
---
## 6. Bewusste Abgrenzungen
- **UI-Palette unverändert.** Die Dark-Theme-Farben wurden nur konsolidiert/umbenannt, nicht geändert.
`--brand-violet` (`#5d52a3`) gilt für Logo/Print/Verläufe, `--ui-primary` (`#7d6fd6`) für Interaktion.
- **Kein Light-Mode.** Die App ist bewusst dark-only; `<CertviaLogo theme="light">` existiert für
Dokument-/Druck-/Mail-Kontexte, wird in der UI aber nicht verwendet.
- **Poppins 400** ist nicht als woff2 im Repo (vorhanden: 300/500/600/700). Aktuell nutzt nichts
Poppins 400 — Headlines laufen auf 600/700, die Wortmarke auf 300/700. Sollte 400 gebraucht werden,
muss die woff2 ergänzt werden; ein CDN-Fetch bleibt ausgeschlossen.
- **Logo-Upload je Mandant** braucht Objektspeicher (Admin Phase 2). Der Resolver ist bereits darauf
vorbereitet (`logoUrl` optional), das Feld existiert im Schema noch nicht — bewusst keine Migration.
- **DOCX/PDF-Export** existiert noch nicht. S5 liefert den Branding-Layer (Konstanten + Kopf-/Fußzeile
+ Print-CSS), an den das Export-Feature andockt.
@@ -1,107 +0,0 @@
# Umsetzungspaket — Vollständige Branding-Umstellung auf **Certvia**
> Claude-Code-Prompt für **einen** Entwickler. Basis-Branch **`dev`**, Feature-Branch **`dev/branding-certvia`** (PR-Ziel `dev`).
> Quelle der Wahrheit: **Certvia Brand-Board** (`Certvia-Brand-Board-final.html`) + Logo-SVGs (`certvia-mark-positiv/negativ/grau/anthrazit.svg`, `certvia-favicon.svg`, `certvia-lockup-positiv.svg`) aus `Certvia-Design-Uebergabe.zip`.
## 0. Ziel & Leitplanken
Die App vollständig auf den **Certvia-Styleguide** umstellen — Logo, Wortmarke, App-Name, Favicon/Icons, Typografie, Marken-/UI-Farben, Dokument-/Export-Design und alle Texte.
**Wichtige Leitplanken:**
- **GEFIM bleibt Dachmarke.** „**Ein Produkt von GEFIM**" (inkl. GEFIM-Logo) bleibt im Footer/Impressum/Export-Fußzeile erhalten. Nur die **Produktmarke** wird Certvia.
- **UI-Farben bleiben wie initial festgelegt** (Dark-Theme-Primär `#7d6fd6` etc.) — sie **sind** die Certvia-UI-Palette. Nicht „korrigieren". Marken-Violett `#5d52a3` gilt für Logo/Print/Verläufe, UI-Interaktiv bleibt `#7d6fd6`.
- **Keine Funktionsänderung** — reines Branding/Design. Kein Umbau von Logik/Datenmodell.
---
## 1. Design-Tokens (verbindliche Referenz)
**Markenfarben (Logo, Print, Verläufe, Akzent):**
| Rolle | Hex |
|---|---|
| Violett (Marke) | `#5d52a3` |
| Magenta (Akzent: „via", Haken, Highlights) | `#812d80` |
| Hellblau | `#8dc4e0` |
| Anthrazit (Text/Headlines) | `#3b3b3a` |
**UI Dark-Theme (unverändert, = Certvia-Produktpalette):**
`--violet:#7d6fd6` · `--violet-deep:#5d52a3` · `--blue:#8dc4e0` · `--magenta:#b45bb0` · `--bg0:#0e1220` · `--bg1:#141a2e` · `--panel:rgba(30,38,64,.72)` · `--panel-b:rgba(120,135,180,.18)` · `--elev:#1a2138` · `--txt:#e8ecf7` · `--muted:#8b93ad` · `--ok:#39c07f` · `--warn:#f0ad4e` · `--risk:#ff6b6b` · Primär-Verlauf `linear-gradient(135deg,#5d52a3,#7d6fd6)` · Fokus-Ring `rgba(125,111,214,.35)`.
**Typografie:** Headlines/Wortmarke **Poppins**, Fließtext **Open Sans** (bereits im Einsatz — bestätigen, **selbst hosten**, kein ungefragtes Google-Fonts-CDN).
**Logo/Marke:** C-Monogramm (offenes „C" Violett `#5d52a3` + Haken Magenta `#812d80`, Haken schließt bündig an die C-Öffnung). Wortmarke „Cert" (Poppins Light, Anthrazit) + „**via**" (Poppins Bold, Magenta). Auf dunklem Grund: C `#7d6fd6`, Haken `#d17bcf`, Text weiß.
---
## 2. Story B0 — Bestandsaufnahme / Branding-Inventar (zuerst!) (S)
**Als** Entwickler **möchte ich** alle Branding-Berührungspunkte finden, **damit** nichts übersehen wird.
- Repo-weit suchen und als kurze Inventarliste (Datei → Fundstelle → Maßnahme) dokumentieren:
```bash
rg -i "gefim" -l # Produktname vs. Dachmarke unterscheiden
rg -i "shield|logo|brand|favicon|wordmark|app[_-]?name|siteName|title" src public
rg -i "#5d52a3|#7d6fd6|#812d80|#8dc4e0|#b45bb0|#0e1220" src # hartkodierte Farben außerhalb der Tokens
rg -i "poppins|open sans|fonts.googleapis" src public
```
- **AK:** Inventarliste liegt vor; jede Fundstelle ist einer der Stories S1–S9 zugeordnet; klar getrennt „Produktmarke → Certvia" vs. „Dachmarke → GEFIM bleibt".
---
## 3. Stories
### S1 — Design-Tokens zentralisieren & Certvia-Palette verankern (M)
- Eine **zentrale Token-Quelle** (CSS-Variablen/`theme`/Tailwind-Config) als Single Source of Truth; hartkodierte Farben aus S0 durch Tokens ersetzen.
- Marken- vs. UI-Tokens klar benennen (`--brand-violet` `#5d52a3` vs. `--ui-primary` `#7d6fd6`), damit Logo/Print und UI nicht vermischt werden.
- **AK:** keine hartkodierten Markenfarben mehr in Komponenten; Umschalten zentral möglich; UI optisch unverändert (nur konsolidiert).
### S2 — Logo, Wortmarke & Icons (M)
- **App-Header/Sidebar**, **Login** (`/login`) und **Plattform-Login** (`/platform/login`): GEFIM-Schild → **Certvia-Logo** (Lockup C-Monogramm + „Certvia"). Dark-Variante nutzen.
- **Favicon** + **PWA/Manifest-Icons** (`public/`, `manifest.webmanifest`, `apple-touch-icon`) auf `certvia-favicon.svg` (C auf Violett-Tile) und generierte PNGs (32/180/192/512).
- `meta-theme-color` = `#5d52a3` (schon so auf der Website).
- SVG-Assets aus dem Design-Paket in `public/assets/logo/` bzw. `src/components/brand/` einbinden; **Logo als React-Komponente** `<CertviaLogo variant="lockup|mark" theme="light|dark" />`.
- **AK:** in allen App-Bereichen erscheint das Certvia-Logo; Favicon/Tab-Icon = Certvia; keine GEFIM-Schild-Reste in Produkt-UI.
### S3 — App-Name, Titel, Meta & i18n-Texte (S–M)
- App-Name/`<title>`/OG-/Twitter-Meta, PWA-Name, Sidebar-Kopf, „Über"/Version-Info → **Certvia**.
- i18n (`de`/`en`): alle Produktname-Strings auf „Certvia"; Tagline „Informationssicherheit. Endlich einfach." wo passend.
- **AK:** Browser-Tab, Meta, Manifest und sichtbare Produktnamen zeigen „Certvia"; „ein Produkt von GEFIM" bleibt erhalten.
### S4 — Typografie bestätigen & selbst hosten (S)
- Poppins (300/400/500/600/700) + Open Sans (400/600/700) **self-hosted** (woff2) statt CDN; Fallback-Stack; `font-display: swap`.
- **AK:** keine externen Font-Requests; Wortmarke „Cert**via**" korrekt (Light + Bold), Headlines Poppins, Fließtext Open Sans.
### S5 — Dokument-/Export-Corporate-Design (M–L)
- **DOCX/PDF/Print-Export** (Richtlinien, VDA-ISA-Katalog, Reports) auf **Certvia-Corporate-Design**: Deckblatt/Kopf mit Certvia-Logo + Farben, Fußzeile „Certvia — ein Produkt von GEFIM" + GEFIM-Logo, Poppins/Open Sans.
- Betrifft den (offenen) DOCX/PDF-Export — Branding-Layer so bauen, dass er beim Export-Feature andockt.
- **AK:** exportierte Dokumente tragen Certvia-Branding; GEFIM als Dachmarke in der Fußzeile.
### S6 — E-Mail-/Benachrichtigungs-Templates (S–M, falls vorhanden)
- Sobald SMTP/E-Mail (Paket 4) existiert bzw. für Einladungs-/Reset-Mails: Header/Footer/Farben/Logo auf Certvia; Absender-/Signatur-Text „Certvia — ein Produkt von GEFIM".
- Falls E-Mail noch nicht aktiv: nur die **Template-Basis** brandfähig vorbereiten.
- **AK:** vorhandene Mail-Templates gebranded oder brandfähige Basis vorbereitet.
### S7 — Auth-, Fehler- & Systemseiten (S)
- Login, Change-Password, deaktiviertes Konto, 404/500, Wartungs-/Status-Banner, Loading-/Empty-States: Certvia-Logo + Palette.
- **AK:** keine ungebrandeten/gemischten Screens mehr.
### S8 — Mandanten-Branding-Default = Certvia (S–M)
- Der Whitelabel-/Branding-Fallback je Mandant (Admin Phase 2 „Logo-Upload") ist **Certvia** als Default; Custom-Logo überschreibt nur, wenn gesetzt.
- **AK:** ohne Custom-Logo zeigt jeder Mandant Certvia; „ein Produkt von GEFIM" bleibt unabhängig davon.
### S9 — Restliche GEFIM-Referenzen bereinigen (S)
- Alle in S0 gefundenen **Produkt**-Referenzen „GEFIM" → „Certvia" (Kommentare, Alt-Texte, aria-labels, Klassennamen unkritisch). **Dachmarken-Hinweise bewusst belassen.**
- **AK:** `rg -i "gefim"` liefert nur noch Dachmarken-Kontext (Footer/Impressum/Export-Fußzeile/Assets `gefim/`).
---
## 4. QA / Definition of Done
- **Visuelles Review** der Kernscreens (Dashboard, Richtlinien, Assets, Risiken, Lieferanten, Aufgaben, Admin, beide Logins, Export, Fehlerseiten) hell **und** dunkel — Screenshots im PR.
- **Kontrast prüfen:** Magenta `#812d80` auf Weiß und `#d17bcf` auf Dunkel (WCAG AA für Text/aktive Elemente).
- `rg -i "gefim"` = nur Dachmarken-Kontext; keine externen Font-/Logo-Requests; keine hartkodierten Markenfarben.
- `npx tsc --noEmit` → `npm run lint` → `npm run build` grün; Demo-Umgebung lauffähig.
- **Reines Branding-PR** — keine funktionalen Diffs.
## 5. Reihenfolge
S0 (Inventar) → S1 (Tokens) → S2 (Logo/Icons) → S3 (Name/Meta/i18n) → S4 (Fonts) → S7 (Auth/Fehler) → S9 (Cleanup) → S5 (Export) → S8 (Mandanten-Default) → S6 (Mail, falls vorhanden).
## 6. Assets (mitliefern an den Entwickler)
Aus `Certvia-Design-Uebergabe.zip`: `logo/certvia-mark-positiv.svg`, `-negativ.svg`, `-grau.svg`, `-anthrazit.svg`, `certvia-favicon.svg`, `certvia-lockup-positiv.svg`, `logo/README-Logo.md`, `Brand-Board.html` (Farb-/Typo-Referenz) sowie `gefim-dachmarke/` (GEFIM-Logo für den „ein Produkt von GEFIM"-Hinweis).
> Hinweis: Falls PNG-Rastergrößen (Favicon/Apple-Touch/PWA 32/180/192/512) benötigt werden, kann ich sie aus dem SVG erzeugen und beilegen — sag kurz Bescheid.
-29
View File
@@ -1,29 +0,0 @@
# Certvia — Logo-Dateien
Logo = **C-Monogramm + Haken** (Entwurf C) in GEFIM-Farben. Alle Marken (außer Lockup) sind quadratisch (viewBox 64×64), vektoriell und frei skalierbar.
## Dateien
| Datei | Verwendung |
|---|---|
| `certvia-mark-positiv.svg` | Standard auf hellem Grund (C Violett, Haken Magenta) |
| `certvia-mark-negativ.svg` | Auf farbigem/dunklem Grund (weiß) |
| `certvia-mark-grau.svg` | Graustufen |
| `certvia-mark-anthrazit.svg` | Einfarbig Anthrazit (Dokumente/Druck s/w) |
| `certvia-favicon.svg` | App-Icon/Favicon (Monogramm auf violettem Tile) |
| `certvia-lockup-positiv.svg` | Wortmarke horizontal (Zeichen + „Certvia") |
## Farben
- **Violett (C):** `#5d52a3`
- **Magenta (Haken & „via"):** `#812d80`
- **Anthrazit (Wortmarke „Cert"):** `#3b3b3a`
- Auf dunklem Grund/UI: C `#7d6fd6`, Haken `#d17bcf`.
## Konstruktion / Regeln
- „C" ist **offen nach rechts**; der **Haken** liegt in der Öffnung („cert").
- **Kein Verlauf** im Zeichen. „C" nicht schließen, nicht verzerren, Seitenverhältnis wahren.
- **Schutzraum:** mind. Höhe des Haken-Elements rundum frei.
- Wortmarke „Cert" (Poppins Light/300, Anthrazit) + „**via**" (Poppins Bold/700, Magenta).
- Hinweis: SVG-Lockup nutzt Poppins per `font-family` — beim finalen Export ggf. **Text in Pfade wandeln**, damit die Schrift überall korrekt erscheint.
## Schrift
- **Poppins** (Wortmarke/Headlines), **Open Sans** (Fließtext).
+68
View File
@@ -0,0 +1,68 @@
# Craftvia — Branding in der Anwendung
Maßgeblich ist das Brandbook: [BRANDBOOK.md](BRANDBOOK.md) (§11.4 Farben, §11.5 Typografie).
Referenzbild: [assets/craftvia-brand-identity-v1.png](assets/craftvia-brand-identity-v1.png).
Dieses Dokument beschreibt, **wo** die Marke im Code verankert ist.
## Quellen der Wahrheit
| Was | Datei |
|---|---|
| CSS-Tokens (Browser) | `src/app/globals.css` — `--brand-*` (Kernfarben), `--ui-*` (UI-Rollen), shadcn-Tokens abgeleitet |
| JS-Konstanten (Metadaten, Mail, Dokumente) | `src/lib/brand.ts` — `BRAND`, `BRAND_COLORS`, `STATUS_COLORS`, `BRAND_FONTS` |
| Logo (Inline-SVG) | `src/components/brand/craftvia-logo.tsx` |
| Mandanten-Logo-Override | `src/components/brand/tenant-brand.tsx` + `resolveTenantBranding()` |
| E-Mail-Layout | `src/lib/email-brand.ts` |
| Dokument-/Druck-CD | `src/lib/document-brand.ts`, `@media print` in `globals.css` |
| Favicon/PWA-Icons | `scripts/generate-brand-icons.ts` → `public/favicon/*`, `public/favicon.ico` |
| Manifest | `public/site.webmanifest` (`theme_color` #082E5B) |
## Farben
| Rolle | Name | Hex | Token |
|---|---|---|---|
| Primär | Lotsenblau | `#082E5B` | `--brand-lotsenblau` → `--primary` |
| Akzent/CTA | Signalorange | `#FF6A00` | `--brand-signalorange` → `--ui-accent` (`bg-cta`) |
| Text | Graphit | `#34383D` | `--foreground` |
| Fläche | Hafengrau | `#F3F5F7` | `--background` |
| Sekundärtext | Stahlgrau | `#66727D` | `--muted-foreground` |
| Linien | Zink | `#D9E0E5` | `--border`, `--input` |
| Erfolg / Warnung / Fehler / Info | | `#23845D` / `#C87912` / `#C64242` / `#2F6FA3` | `--ok` / `--warn` / `--risk` / `--info` |
Regeln: Signalorange sparsam (eine Hauptaktion je Ansicht, aktive Elemente). Farbe nie
als einziges Statussignal — immer mit Text oder Icon. Keine Verläufe, keine Schlagschatten
(`--shadow-card` ist eine feine Kontur); die Utility-Namen `bg-grad*` zeigen Vollfarbe.
## Logo
`<CraftviaLogo />` — Props:
- `variant`: `"horizontal"` (Signet + Wortmarke, Default) | `"signet"`
- `tone`: `"color"` (Default) | `"mono"` (einfarbig Lotsenblau) | `"reversed"` (weiß, für dunkle Flächen)
- `tile` (nur Signet): App-Icon-Kachel in Lotsenblau mit weißem C und orangem Haken
- `tagline`: „Handwerk. Digital auf Kurs." unter der Wortmarke
- `height`: Renderhöhe in px
Konstruktion (viewBox 64×64, exportiert als `SIGNET`): kräftiges C (Kreisbogen r=19,
Strichstärke 11) mit Öffnung rechts oben; oranger Haken startet links außerhalb, durchquert
das C (dort per Maske freigestellt) und endet als Pfeil nach rechts oben. Wortmarke
„Craft" Lotsenblau + „via" Signalorange, Gewicht 800.
Nach Änderungen an der Geometrie Icons neu erzeugen:
```bash
npx tsx scripts/generate-brand-icons.ts
```
## Typografie
Inter (Brandbook §11.5), selbst gehostet über `next/font/local`
(`src/app/fonts/inter-variable.ttf`, SIL Open Font License) — kein Laden aus dem Netz.
TODO: auf woff2-Subsets umstellen (Dateigröße).
## Produktidentität
- Name: **Craftvia**, Tagline: **Handwerk. Digital auf Kurs.**
- In-Product-Assistent: **Lotse** (eigene Figur, getrennt vom Logo; Modul `lotse`)
- TOTP-Issuer: `Craftvia · Plattform` (`src/server/mfa.ts`), WebAuthn-RP-Name: `Craftvia`
- Mail-Absender-Default: `MAIL_FROM_NAME` → `Craftvia`
-5
View File
@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Certvia">
<rect x="0" y="0" width="64" height="64" rx="14" fill="#5d52a3"/>
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round"/>
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#f2a7ef" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#3b3b3a" stroke-width="6" stroke-linecap="round"/>
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#3b3b3a" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 361 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#8a8a90" stroke-width="6" stroke-linecap="round"/>
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#8a8a90" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#ffffff" stroke-width="6" stroke-linecap="round"/>
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#ffffff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="256" height="256" role="img" aria-label="Certvia">
<path d="M46 20 A17 17 0 1 0 46 44" fill="none" stroke="#5d52a3" stroke-width="6" stroke-linecap="round"/>
<path d="M27 32 l6 6 l12 -15" fill="none" stroke="#812d80" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 B

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#000" stroke-width="13" stroke-linejoin="miter"/>
</mask>
</defs>
<rect x="0" y="0" width="64" height="64" rx="14" fill="#082E5B"/>
<g transform="translate(9.6 9.6) scale(0.7)">
<path d="M36.5 16.14 A19 19 0 1 0 48.71 37.3" fill="none" stroke="#FFFFFF" stroke-width="11" stroke-linecap="butt" mask="url(#cut)"/>
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#FF6A00" stroke-width="7" stroke-linejoin="miter"/>
<path d="M52.7 12.7 L47.73 27.8 L37.37 16.96 Z" fill="#FF6A00"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 807 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#000" stroke-width="13" stroke-linejoin="miter"/>
</mask>
</defs>
<g transform="translate(2 2) scale(0.9375)">
<path d="M36.5 16.14 A19 19 0 1 0 48.71 37.3" fill="none" stroke="#082E5B" stroke-width="11" stroke-linecap="butt" mask="url(#cut)"/>
<path d="M6 28 L22 42 L44 21" fill="none" stroke="#FF6A00" stroke-width="7" stroke-linejoin="miter"/>
<path d="M52.7 12.7 L47.73 27.8 L37.37 16.96 Z" fill="#FF6A00"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 893 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

+9 -5
View File
@@ -1,11 +1,15 @@
{
"name": "Certvia",
"short_name": "Certvia",
"description": "Informationssicherheit. Endlich einfach.",
"theme_color": "#5d52a3",
"background_color": "#0e1220",
"name": "Craftvia",
"short_name": "Craftvia",
"description": "Handwerk. Digital auf Kurs.",
"lang": "de",
"start_url": "/dashboard",
"scope": "/",
"theme_color": "#082E5B",
"background_color": "#F3F5F7",
"display": "standalone",
"icons": [
{ "src": "/favicon/craftvia-app-icon.svg", "sizes": "any", "type": "image/svg+xml" },
{ "src": "/favicon/pwa-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon/pwa-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/favicon/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
+93
View File
@@ -0,0 +1,93 @@
/**
* Erzeugt Favicon/PWA-Icons aus dem Craftvia-Signet (src/components/brand/craftvia-logo.tsx,
* Geometrie `SIGNET`). Keine Netzwerkzugriffe; PNGs via `sharp` (liegt über Next.js bei).
*
* Lauf: npx tsx scripts/generate-brand-icons.ts
* Ausgabe: public/favicon/*.svg|png, public/favicon.ico (PNG-in-ICO, 32×32)
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
import { BRAND_COLORS } from "../src/lib/brand";
import { SIGNET } from "../src/components/brand/craftvia-logo";
const OUT = join(process.cwd(), "public", "favicon");
mkdirSync(OUT, { recursive: true });
function signetSvg(opts: { tile: boolean; padding?: number; radius?: number }): string {
const arc = opts.tile ? BRAND_COLORS.white : BRAND_COLORS.lotsenblau;
const check = BRAND_COLORS.signalorange;
const pad = opts.padding ?? 9.6;
const scale = (64 - 2 * pad) / 64;
const bg = opts.tile
? `<rect x="0" y="0" width="64" height="64" rx="${opts.radius ?? 14}" fill="${BRAND_COLORS.lotsenblau}"/>`
: "";
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<mask id="cut" maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
<rect x="0" y="0" width="64" height="64" fill="#fff"/>
<path d="${SIGNET.check}" fill="none" stroke="#000" stroke-width="${SIGNET.checkWidth + 6}" stroke-linejoin="miter"/>
</mask>
</defs>
${bg}
<g transform="translate(${pad} ${pad}) scale(${scale})">
<path d="${SIGNET.arc}" fill="none" stroke="${arc}" stroke-width="${SIGNET.arcWidth}" stroke-linecap="butt" mask="url(#cut)"/>
<path d="${SIGNET.check}" fill="none" stroke="${check}" stroke-width="${SIGNET.checkWidth}" stroke-linejoin="miter"/>
<path d="${SIGNET.arrow}" fill="${check}"/>
</g>
</svg>
`;
}
async function png(svg: string, size: number, file: string) {
await sharp(Buffer.from(svg), { density: 72 * (size / 64) * 2 })
.resize(size, size)
.png()
.toFile(join(OUT, file));
}
/** Minimaler ICO-Container mit einem eingebetteten PNG (von allen Browsern unterstützt). */
function icoFromPng(pngBuf: Buffer, size: number): Buffer {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0);
header.writeUInt16LE(1, 2);
header.writeUInt16LE(1, 4);
const entry = Buffer.alloc(16);
entry.writeUInt8(size >= 256 ? 0 : size, 0);
entry.writeUInt8(size >= 256 ? 0 : size, 1);
entry.writeUInt8(0, 2);
entry.writeUInt8(0, 3);
entry.writeUInt16LE(1, 4);
entry.writeUInt16LE(32, 6);
entry.writeUInt32LE(pngBuf.length, 8);
entry.writeUInt32LE(6 + 16, 12);
return Buffer.concat([header, entry, pngBuf]);
}
async function main() {
// Favicon: freistehendes Signet (transparent) als SVG — skaliert verlustfrei.
const plain = signetSvg({ tile: false, padding: 2 });
writeFileSync(join(OUT, "craftvia-signet.svg"), plain);
// App-Icon-Kachel (Lotsenblau) für PWA/Apple/OG.
const tile = signetSvg({ tile: true });
writeFileSync(join(OUT, "craftvia-app-icon.svg"), tile);
// Maskable: volle Fläche ohne Rundung, Signet in der Safe-Zone (80 %).
const maskable = signetSvg({ tile: true, padding: 14, radius: 0 });
await png(plain, 16, "icon-16.png");
await png(plain, 32, "icon-32.png");
await png(plain, 48, "icon-48.png");
await png(tile, 180, "apple-touch-icon-180.png");
await png(tile, 192, "pwa-192.png");
await png(tile, 512, "pwa-512.png");
await png(maskable, 512, "maskable-512.png");
const ico32 = await sharp(Buffer.from(plain), { density: 144 }).resize(32, 32).png().toBuffer();
writeFileSync(join(process.cwd(), "public", "favicon.ico"), icoFromPng(ico32, 32));
console.log("✓ Craftvia-Icons erzeugt in public/favicon (+ public/favicon.ico)");
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
+2 -2
View File
@@ -3,7 +3,7 @@ import { auth } from "@/server/auth";
import { dbForTenant, prisma } from "@/server/db";
import { resolvePasswordPolicy, describePasswordPolicy } from "@/lib/password-policy";
import { ForcePasswordChangeForm } from "@/components/force-password-change-form";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* Erzwungener Passwortwechsel beim ersten Login (Force-Change). Liegt außerhalb der
@@ -29,7 +29,7 @@ export default async function ChangePasswordPage() {
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={34} />
<CraftviaLogo variant="horizontal" height={34} />
<p className="mt-5 font-heading text-lg font-semibold">Passwort ändern</p>
<p className="mt-1 text-sm text-muted-foreground">
Ihr Konto wurde mit einem Initialpasswort angelegt. Bitte vergeben Sie jetzt ein eigenes Passwort, um fortzufahren.
+2 -2
View File
@@ -7,7 +7,7 @@ import { newTotpSecret, totpUri } from "@/server/mfa";
import { encryptSecret, decryptSecret } from "@/server/secret-crypto";
import { TenantMfaEnrollForm } from "@/components/tenant-mfa-enroll-form";
import { PasskeyManager } from "@/components/passkey-manager";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* SEC3-a: Pflicht-MFA-Einrichtung für Mandanten-Nutzer, wenn der Mandant MFA verlangt
@@ -37,7 +37,7 @@ export default async function EnrollMfaPage() {
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-md rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={34} />
<CraftviaLogo variant="horizontal" height={34} />
<p className="mt-5 font-heading text-lg font-semibold">Zwei-Faktor-Authentifizierung einrichten</p>
<p className="mt-1 text-sm text-muted-foreground">
Ihre Organisation verlangt eine Zwei-Faktor-Authentifizierung. Scannen Sie den QR-Code mit
+2 -4
View File
@@ -1,8 +1,7 @@
"use client";
import { Button } from "@/components/ui/button";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* Gebrandete Fehlerseite (Story S7). Zeigt bewusst keine technischen Details —
@@ -19,7 +18,7 @@ export default function Error({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8 text-center">
<CertviaLogo variant="lockup" theme="dark" height={34} className="mx-auto" />
<CraftviaLogo variant="horizontal" height={34} className="mx-auto" />
<p className="mt-6 font-heading text-lg font-semibold">Es ist ein Fehler aufgetreten</p>
<p className="mt-1 text-sm text-muted-foreground">
Die Aktion konnte nicht abgeschlossen werden. Bitte versuchen Sie es erneut oder wenden
@@ -31,7 +30,6 @@ export default function Error({
<Button onClick={reset} className="mt-6 w-full justify-center">
Erneut versuchen
</Button>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -5
View File
@@ -1,6 +1,5 @@
import Link from "next/link";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import { ForgotPasswordForm } from "@/components/auth-recovery-forms";
import { loginPathOf } from "@/server/auth-selfservice";
@@ -25,7 +24,7 @@ export default async function ForgotPasswordPage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={36} />
<CraftviaLogo variant="horizontal" height={36} />
<p className="mt-5 font-heading text-lg font-semibold">Passwort vergessen</p>
<p className="mt-1 text-sm text-muted-foreground">
{domain === "platform"
@@ -40,8 +39,6 @@ export default async function ForgotPasswordPage({
Zurück zur Anmeldung
</Link>
</p>
<PoweredByGefim className="mt-6 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+78 -269
View File
@@ -7,9 +7,9 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-open-sans);
--font-sans: var(--font-inter);
--font-mono: ui-monospace, monospace;
--font-heading: var(--font-poppins);
--font-heading: var(--font-inter);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -39,7 +39,7 @@
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
/* Semantische Zusatzflächen (dunkles Certvia-Theme) */
/* Semantische Zusatzflächen */
--color-panel: var(--panel);
--color-panel-brd: var(--panel-brd);
--color-elevated: var(--elevated);
@@ -47,6 +47,8 @@
--color-band: var(--band);
--color-band-text: var(--band-text);
--color-band-brd: var(--band-brd);
--color-cta: var(--ui-accent);
--color-cta-foreground: var(--ui-accent-foreground);
--color-ok: var(--ok);
--color-warn: var(--warn);
--color-risk: var(--risk);
@@ -61,133 +63,109 @@
}
/*
* Certvia — zentrale Token-Definition (Single Source of Truth für Farben).
* Craftvia — zentrale Token-Definition (Single Source of Truth für Farben).
* Quelle: docs/craftvia/BRANDBOOK.md §11.4. JS-Pendant: src/lib/brand.ts.
*
* Zwei bewusst getrennte Ebenen:
* --brand-* Markenfarben aus dem Certvia-Styleguide (Logo, Print, Verläufe,
* Dokument-/Export-CD). NICHT für UI-Flächen verwenden.
* --ui-* Produktpalette des dunklen App-Themes (Interaktion, Flächen).
* Sie ist die festgelegte Certvia-UI-Palette und wird nicht an die
* Markenfarben „angeglichen" — Marken-Violett #5d52a3 gilt fürs Logo,
* UI-Interaktiv bleibt #7d6fd6.
* Zwei Ebenen:
* --brand-* Kernfarben der Marke (Logo, Print, Dokumente).
* --ui-* Rollen im hellen App-Theme (Interaktion, Flächen), abgeleitet aus --brand-*.
*
* Die semantischen shadcn-Tokens (--primary, --background, …) leiten sich unten
* ausschließlich aus --ui-* / --bg-* ab; Hex-Werte stehen nur noch hier.
* Ein späterer Light-Mode kann über dieselben Token-Namen unter .light ergänzt werden.
* JS-seitiges Pendant (Canvas-/PNG-Export, Metadaten): src/lib/brand.ts.
* ausschließlich aus --brand-* / --ui-* ab; Hex-Werte stehen nur in diesem Block.
* Farbe nie als einziges Statussignal — immer mit Text/Icon kombinieren.
*/
:root {
/* ---- Marke (Logo / Print / Export) ---- */
--brand-violet: #5d52a3;
--brand-magenta: #812d80;
--brand-blue: #8dc4e0;
--brand-anthracite: #3b3b3a;
/* Marken-Varianten auf dunklem Grund (Styleguide) */
--brand-violet-on-dark: #7d6fd6;
--brand-magenta-on-dark: #d17bcf;
/* ---- Marke ---- */
--brand-lotsenblau: #082e5b;
--brand-signalorange: #ff6a00;
--brand-graphit: #34383d;
--brand-hafengrau: #f3f5f7;
--brand-stahlgrau: #66727d;
--brand-zink: #d9e0e5;
--brand-white: #ffffff;
/* ---- UI-Palette (dunkles Produkt-Theme) ---- */
--ui-primary: #7d6fd6;
--ui-primary-deep: #5d52a3;
--ui-blue: #8dc4e0;
--ui-magenta: #b45bb0;
--ui-primary-soft: rgba(125, 111, 214, 0.18);
--ui-primary-ring: rgba(125, 111, 214, 0.35);
/* ---- Funktionsfarben ---- */
--ok: #23845d;
--warn: #c87912;
--risk: #c64242;
--info: #2f6fa3;
/* ---- UI-Rollen (helles Produkt-Theme) ---- */
--ui-primary: var(--brand-lotsenblau);
--ui-primary-hover: #0b3c75;
--ui-primary-soft: rgba(8, 46, 91, 0.08);
--ui-primary-ring: rgba(8, 46, 91, 0.3);
/* Akzent/CTA: Signalorange — sparsam für die wichtigste Aktion je Ansicht. */
--ui-accent: var(--brand-signalorange);
--ui-accent-foreground: #ffffff;
/* Grundflächen */
--bg-0: #0e1220;
--bg-1: #141a2e;
--bg-2: #1a2138;
--bg-3: #1e2640;
--txt: #e8ecf7;
--txt-muted: #8b93ad;
--line: rgba(120, 135, 180, 0.18);
--bg-0: var(--brand-hafengrau);
--bg-1: var(--brand-white);
--bg-2: #eef1f4;
--txt: var(--brand-graphit);
--txt-muted: var(--brand-stahlgrau);
--line: var(--brand-zink);
/* Verläufe */
--grad: var(--grad-primary);
--grad-primary: linear-gradient(
135deg,
var(--ui-primary-deep) 0%,
var(--ui-primary) 100%
);
--grad-soft: var(--grad-primary);
--grad-critical: linear-gradient(90deg, var(--risk) 0%, var(--ui-magenta) 100%);
/* Keine Verläufe im Craftvia-CD: die Utility-Namen bleiben, zeigen aber Vollfarbe. */
--grad-primary: var(--ui-primary);
/* Status */
--ok: #39c07f;
--warn: #f0ad4e;
--risk: #ff6b6b;
--info: #5aa9e6;
/* Flächen (semantisch → leiten aus den Tokens oben ab) */
/* Flächen (semantisch) */
--background: var(--bg-0);
--foreground: var(--txt);
--card: var(--bg-1);
--card-foreground: var(--txt);
--popover: var(--bg-2);
--popover: var(--bg-1);
--popover-foreground: var(--txt);
--primary: var(--ui-primary);
--primary-foreground: #ffffff;
--secondary: var(--bg-2);
--secondary-foreground: #cfc9ee;
--secondary-foreground: var(--brand-lotsenblau);
--muted: var(--bg-2);
--muted-foreground: var(--txt-muted);
--accent: var(--bg-3);
--accent-foreground: #cfc9ee;
--accent: var(--ui-primary-soft);
--accent-foreground: var(--brand-lotsenblau);
--destructive: var(--risk);
--border: var(--line);
--input: rgba(120, 135, 180, 0.22);
--input: var(--line);
--ring: var(--ui-primary);
--chart-1: var(--ui-primary);
--chart-2: var(--ui-blue);
--chart-3: var(--ui-magenta);
--chart-1: var(--brand-lotsenblau);
--chart-2: var(--brand-signalorange);
--chart-3: var(--info);
--chart-4: var(--ok);
--chart-5: var(--warn);
--radius: 0.7rem;
--radius: 0.6rem;
/* Panels / Glas / erhöhte Flächen */
--panel: rgba(30, 38, 64, 0.72);
/* Panels / erhöhte Flächen */
--panel: rgba(255, 255, 255, 0.92);
--panel-brd: var(--line);
--elevated: var(--bg-2);
/* weiche Akzentkarte (früher #faf9fd) */
--surface-soft: rgba(125, 111, 214, 0.08);
/* Info-Band (früher #f2f0f9 / #e6e2f3 / #5a4e86) */
--band: rgba(125, 111, 214, 0.1);
--band-text: #c3bdec;
--band-brd: rgba(125, 111, 214, 0.22);
/* Chip auf farbiger Fläche (Risiko-Heatmap) — dunkler Grund, halbtransparent */
--chip-overlay: rgba(14, 18, 32, 0.75);
/* Graph (React Flow) — Linien/Labels, die als SVG-Attribute gesetzt werden */
--graph-edge: #4a5372;
--graph-edge-critical: var(--risk);
--surface-soft: var(--ui-primary-soft);
--band: rgba(47, 111, 163, 0.08);
--band-text: var(--info);
--band-brd: rgba(47, 111, 163, 0.25);
/* Sidebar / Topbar */
--sidebar: var(--bg-1);
--sidebar-foreground: #b6bdd4;
--sidebar-foreground: var(--brand-graphit);
--sidebar-primary: var(--ui-primary);
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: var(--ui-primary-soft);
--sidebar-accent-foreground: #cfc9ee;
--sidebar-accent-foreground: var(--brand-lotsenblau);
--sidebar-border: var(--line);
--sidebar-ring: var(--ui-primary);
--focus-ring: 0 0 0 3px var(--ui-primary-ring);
--radius-card: 16px;
--radius-ctrl: 10px;
--dot-grid: radial-gradient(rgba(140, 155, 200, 0.1) 1px, transparent 1px);
--glow:
radial-gradient(1200px 700px at 20% -10%, #1c2340 0%, transparent 60%),
radial-gradient(900px 600px at 100% 110%, #1a2138 0%, transparent 55%);
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 28px rgba(0, 0, 0, 0.28);
--radius-card: 12px;
--radius-ctrl: 8px;
/* Flaches CD: nur eine feine Kontur statt Schlagschatten. */
--shadow-card: 0 1px 2px rgba(8, 46, 91, 0.06);
}
body {
font-family: var(--font-sans), "Open Sans", system-ui, sans-serif;
font-family: var(--font-sans), Inter, system-ui, "Segoe UI", Roboto, Arial, sans-serif;
font-size: 14px;
background-color: var(--background);
background-image: var(--glow);
background-attachment: fixed;
}
@layer base {
@@ -204,149 +182,29 @@ body {
h2,
h3,
h4 {
font-family: var(--font-heading), "Poppins", sans-serif;
font-weight: 600;
font-family: var(--font-heading), Inter, system-ui, sans-serif;
font-weight: 700;
color: var(--brand-lotsenblau);
}
}
@layer utilities {
.bg-grad {
background: var(--grad-primary);
}
.bg-grad,
.bg-grad-soft {
background: var(--grad-primary);
}
.bg-grad-critical {
background: var(--grad-critical);
}
.shadow-card {
box-shadow: var(--shadow-card);
}
.dot-grid {
background-image: var(--dot-grid);
background-size: 26px 26px;
}
}
/* React Flow — Dark-Anpassungen + Glow für kritische Kanten (Teil B) */
.react-flow__edge.critical-edge .react-flow__edge-path {
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.7));
}
.react-flow__controls-button {
background: var(--elevated) !important;
border-color: var(--panel-brd) !important;
color: var(--muted-foreground) !important;
}
.react-flow__controls-button:hover {
background: var(--accent) !important;
}
.react-flow__controls-button svg {
fill: currentColor;
}
.react-flow__edge-text {
fill: var(--muted-foreground);
}
/* Gerenderte Richtlinien/Verfahren (Lesemodus) — Typografie ohne Plugin */
.policy-prose {
font-size: 13.5px;
line-height: 1.7;
color: var(--foreground);
}
.policy-prose h2 {
font-family: var(--font-heading, inherit);
font-size: 16px;
font-weight: 700;
margin: 1.6em 0 0.5em;
padding-top: 0.9em;
border-top: 1px solid var(--border);
scroll-margin-top: 1rem;
}
.policy-prose h2:first-child {
border-top: 0;
padding-top: 0;
margin-top: 0;
}
.policy-prose h3 {
font-family: var(--font-heading, inherit);
font-size: 14px;
font-weight: 700;
margin: 1.3em 0 0.4em;
}
.policy-prose h4 {
font-size: 13px;
font-weight: 700;
margin: 1em 0 0.3em;
}
.policy-prose p {
margin: 0.55em 0;
}
.policy-prose ul,
.policy-prose ol {
margin: 0.5em 0;
padding-left: 1.3em;
list-style: revert;
}
.policy-prose li {
margin: 0.25em 0;
}
.policy-prose strong {
font-weight: 700;
}
.policy-prose a {
color: var(--info);
text-decoration: none;
}
.policy-prose a:hover {
text-decoration: underline;
}
.policy-prose blockquote {
border-left: 3px solid var(--band-brd);
background: var(--band);
color: var(--band-text);
padding: 0.5em 0.9em;
margin: 0.8em 0;
border-radius: 0 8px 8px 0;
font-size: 12.5px;
}
.policy-prose table {
width: 100%;
border-collapse: collapse;
margin: 0.9em 0;
font-size: 12.5px;
}
.policy-prose th,
.policy-prose td {
border: 1px solid var(--border);
padding: 6px 10px;
text-align: left;
vertical-align: top;
}
.policy-prose th {
background: var(--elevated);
font-weight: 700;
}
.policy-prose code {
background: var(--elevated);
padding: 1px 5px;
border-radius: 4px;
font-size: 12px;
}
/* =========================================================================
* Dokument-/Druck-Corporate-Design (Story S5)
*
* Ausgaben außerhalb der App laufen im hellen Certvia-CD: Marken-Violett/
* Magenta auf Weiß, Poppins für Überschriften, Open Sans im Fließtext.
* Kopfzeile = Certvia-Lockup (das SVG trägt Poppins eingebettet, siehe
* public/assets/logo/certvia-lockup-positiv.svg), Fußzeile = Dachmarken-
* Hinweis „Certvia — ein Produkt von GEFIM" auf jeder Seite.
*
* Farb-/Textreferenz für den späteren DOCX/PDF-Export: src/lib/document-brand.ts.
* Dokument-/Druck-Corporate-Design (Einsatzberichte, Druckansicht)
* Farb-/Textreferenz für den PDF-Export: src/lib/document-brand.ts.
* ========================================================================= */
@page {
size: A4;
margin: 24mm 16mm 22mm;
margin: 20mm 16mm 20mm;
}
@media print {
@@ -361,82 +219,33 @@ body {
:root {
--background: #ffffff;
--foreground: var(--brand-anthracite);
--card: #ffffff;
--card-foreground: var(--brand-anthracite);
--muted-foreground: #6b6b70;
--primary: var(--brand-violet);
--border: #d9d7e4;
--panel: #ffffff;
--panel-brd: #d9d7e4;
--elevated: #f4f2fa;
--band: #f4f2fa;
--band-text: var(--brand-violet);
--band-brd: #d9d7e4;
--info: var(--brand-violet);
}
body {
background: #ffffff !important;
background-image: none !important;
color: var(--brand-anthracite);
color: var(--brand-graphit);
}
main {
padding: 0 !important;
}
/* Kopf: Certvia-Lockup, einmal am Dokumentanfang. */
body::before {
content: "";
display: block;
height: 13mm;
margin-bottom: 6mm;
padding-bottom: 4mm;
border-bottom: 1px solid #d9d7e4;
background: url("/assets/logo/certvia-lockup-positiv.svg") left center / auto 11mm no-repeat;
}
/* Fuß: Dachmarken-Hinweis, auf jeder Seite (fixed wiederholt beim Druck). */
/* Fuß: Produkthinweis auf jeder Seite. */
body::after {
content: "Certvia — ein Produkt von GEFIM";
content: "Erstellt mit Craftvia";
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-top: 2mm;
border-top: 1px solid #d9d7e4;
font-family: var(--font-sans), "Open Sans", sans-serif;
border-top: 1px solid var(--brand-zink);
font-size: 9pt;
color: #6b6b70;
color: var(--brand-stahlgrau);
}
.shadow-card,
.bg-grad,
.bg-grad-soft {
.shadow-card {
box-shadow: none !important;
background: #ffffff !important;
color: var(--brand-anthracite) !important;
}
.policy-prose {
color: var(--brand-anthracite);
font-size: 10.5pt;
}
.policy-prose h2,
.policy-prose h3 {
color: var(--brand-violet);
break-after: avoid;
}
.policy-prose table,
.policy-prose blockquote {
break-inside: avoid;
}
.policy-prose th {
background: #f4f2fa;
}
.policy-prose a {
color: var(--brand-violet);
text-decoration: none;
}
}
+2 -5
View File
@@ -1,7 +1,6 @@
import Link from "next/link";
import { prisma } from "@/server/db";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import { ResetPasswordForm } from "@/components/auth-recovery-forms";
import { checkInvitationToken } from "@/server/actions/auth-recovery";
import { describePasswordPolicy, resolvePasswordPolicy } from "@/lib/password-policy";
@@ -37,7 +36,7 @@ export default async function InvitePage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={36} />
<CraftviaLogo variant="horizontal" height={36} />
<p className="mt-5 font-heading text-lg font-semibold">Konto einrichten</p>
{valid ? (
@@ -57,8 +56,6 @@ export default async function InvitePage({
</Link>
</>
)}
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+12 -40
View File
@@ -5,30 +5,13 @@ import { getLocale } from "next-intl/server";
import { BRAND, BRAND_ASSETS, BRAND_COLORS } from "@/lib/brand";
import "./globals.css";
// Selbst gehostet über next/font/local (committete woff2 in ./fonts) — kein
// Google-Fonts-Fetch zur Build- oder Laufzeit. DSGVO-konform und macht den
// Produktiv-Build reproduzierbar/offline (next/font/google lud die Schriften
// sonst zur Build-Zeit von Google, was flakig fehlschlagen kann).
const openSans = localFont({
variable: "--font-open-sans",
// Inter (Brandbook §11.5), selbst gehostet über next/font/local — kein Google-Fonts-Fetch
// zur Build- oder Laufzeit (DSGVO, reproduzierbarer Offline-Build). Variable Font
// (Gewichte 100–900 in einer Datei). TODO(brand): auf woff2-Subsets umstellen (kleiner).
const inter = localFont({
variable: "--font-inter",
display: "swap",
src: [
{ path: "./fonts/open-sans-300.woff2", weight: "300", style: "normal" },
{ path: "./fonts/open-sans-400.woff2", weight: "400", style: "normal" },
{ path: "./fonts/open-sans-600.woff2", weight: "600", style: "normal" },
{ path: "./fonts/open-sans-700.woff2", weight: "700", style: "normal" },
],
});
const poppins = localFont({
variable: "--font-poppins",
display: "swap",
src: [
{ path: "./fonts/poppins-300.woff2", weight: "300", style: "normal" },
{ path: "./fonts/poppins-500.woff2", weight: "500", style: "normal" },
{ path: "./fonts/poppins-600.woff2", weight: "600", style: "normal" },
{ path: "./fonts/poppins-700.woff2", weight: "700", style: "normal" },
],
src: [{ path: "./fonts/inter-variable.ttf", weight: "100 900", style: "normal" }],
});
export const metadata: Metadata = {
@@ -37,35 +20,27 @@ export const metadata: Metadata = {
description: BRAND.description,
applicationName: BRAND.name,
manifest: "/site.webmanifest",
// Alle Icons aus dem Certvia-Asset-Bundle (public/favicon + public/assets/logo).
// Icons aus dem Craftvia-Signet erzeugt (scripts/generate-brand-icons.ts).
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: BRAND_ASSETS.favicon, type: "image/svg+xml" },
{ url: "/favicon/icon-16.png", type: "image/png", sizes: "16x16" },
{ url: "/favicon/icon-32.png", type: "image/png", sizes: "32x32" },
],
apple: [{ url: "/favicon/apple-touch-icon-180.png", sizes: "180x180" }],
apple: [{ url: BRAND_ASSETS.appleTouchIcon, sizes: "180x180" }],
},
appleWebApp: { capable: true, title: BRAND.name, statusBarStyle: "black-translucent" },
appleWebApp: { capable: true, title: BRAND.name, statusBarStyle: "default" },
openGraph: {
type: "website",
siteName: BRAND.name,
title: `${BRAND.name} — ${BRAND.tagline}`,
description: BRAND.description,
images: [{ url: "/assets/logo/certvia-lockup-positiv-1024.png", width: 1024, height: 259 }],
},
twitter: {
card: "summary_large_image",
title: `${BRAND.name} — ${BRAND.tagline}`,
description: BRAND.description,
images: ["/assets/logo/certvia-lockup-positiv-1024.png"],
images: [{ url: BRAND_ASSETS.ogImage, width: 512, height: 512 }],
},
};
export const viewport: Viewport = {
// Marken-Violett (wie auf der Certvia-Website), nicht die UI-Primärfarbe.
themeColor: BRAND_COLORS.violet,
themeColor: BRAND_COLORS.lotsenblau,
};
export default async function RootLayout({
@@ -76,10 +51,7 @@ export default async function RootLayout({
const locale = await getLocale();
return (
<html
lang={locale}
className={`${openSans.variable} ${poppins.variable} h-full antialiased`}
>
<html lang={locale} className={`${inter.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col bg-background text-foreground">
<NextIntlClientProvider>{children}</NextIntlClientProvider>
</body>
+2 -5
View File
@@ -8,8 +8,7 @@ import { verifyMfaPending, signLoginTicket, MFA_PENDING_COOKIE } from "@/server/
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* WS5 (Option C) — Two-Step-Login, Schritt 2: MFA. Erreichbar NUR mit gültigem,
@@ -57,7 +56,7 @@ export default async function LoginMfaPage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={40} />
<CraftviaLogo variant="horizontal" height={40} />
<p className="mt-5 font-heading text-lg font-semibold">Bestätigung (2. Schritt)</p>
<p className="mt-1 text-sm text-muted-foreground">
Geben Sie den 6-stelligen Code aus Ihrer Authenticator-App oder einen Recovery-Code ein.
@@ -81,8 +80,6 @@ export default async function LoginMfaPage({
<p className="mt-4 text-center text-[12.5px]">
<Link href="/login" className="text-muted-foreground hover:text-foreground">Zurück zur Anmeldung</Link>
</p>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+2 -5
View File
@@ -9,8 +9,7 @@ import { signMfaPending, signLoginTicket, MFA_PENDING_COOKIE } from "@/server/lo
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import { PasskeyLoginButton } from "@/components/passkey-login-button";
export default async function LoginPage({
@@ -78,7 +77,7 @@ export default async function LoginPage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={42} />
<CraftviaLogo variant="horizontal" height={42} />
<p className="mt-3 font-heading text-[13px] font-medium text-[var(--band-text)]">
{t("tagline")}
</p>
@@ -122,8 +121,6 @@ export default async function LoginPage({
{t("forgotPassword")}
</Link>
</p>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+2 -4
View File
@@ -1,14 +1,13 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/** Gebrandete 404-Seite (Story S7 — keine ungebrandeten Systemscreens). */
export default function NotFound() {
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8 text-center">
<CertviaLogo variant="lockup" theme="dark" height={34} className="mx-auto" />
<CraftviaLogo variant="horizontal" height={34} className="mx-auto" />
<p className="mt-6 font-heading text-4xl font-bold text-[var(--primary)]">404</p>
<p className="mt-2 font-heading text-lg font-semibold">Seite nicht gefunden</p>
<p className="mt-1 text-sm text-muted-foreground">
@@ -21,7 +20,6 @@ export default function NotFound() {
>
Zum Dashboard
</Button>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+2 -2
View File
@@ -5,7 +5,7 @@ import { prisma } from "@/server/db";
import { newTotpSecret, totpUri } from "@/server/mfa";
import { encryptSecret, decryptSecret } from "@/server/secret-crypto";
import { PlatformEnrollForm } from "@/components/platform-enroll-form";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* MFA-Einrichtung für Plattform-Administratoren (Pflicht beim ersten Login).
@@ -30,7 +30,7 @@ export default async function EnrollMfaPage() {
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-md rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={34} />
<CraftviaLogo variant="horizontal" height={34} />
<p className="mt-5 font-heading text-lg font-semibold">Zwei-Faktor-Authentifizierung einrichten</p>
<p className="mt-1 text-sm text-muted-foreground">
Für Plattform-Administratoren verpflichtend. Scannen Sie den QR-Code mit einer
+2 -5
View File
@@ -5,8 +5,7 @@ import { platformAuth, platformSignIn } from "@/server/platform-auth";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* Getrennter Login für Plattform-Administratoren (Phase-1-Härtung Paket 2).
@@ -41,7 +40,7 @@ export default async function PlatformLoginPage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={38} />
<CraftviaLogo variant="horizontal" height={38} />
<p className="mt-4 font-heading text-lg font-semibold">Plattform-Administration</p>
<h2 className="mt-1 text-sm font-normal text-muted-foreground">
Betreiberzugang — getrennt vom Mandanten-Login
@@ -84,8 +83,6 @@ export default async function PlatformLoginPage({
Passwort vergessen?
</Link>
</p>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+2 -5
View File
@@ -1,7 +1,6 @@
import Link from "next/link";
import { prisma } from "@/server/db";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import { ResetPasswordForm } from "@/components/auth-recovery-forms";
import { checkResetToken } from "@/server/actions/auth-recovery";
import { loginPathOf } from "@/server/auth-selfservice";
@@ -43,7 +42,7 @@ export default async function ResetPage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={36} />
<CraftviaLogo variant="horizontal" height={36} />
<p className="mt-5 font-heading text-lg font-semibold">Neues Passwort vergeben</p>
{valid ? (
@@ -74,8 +73,6 @@ export default async function ResetPage({
</Link>
</>
)}
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+2 -5
View File
@@ -2,8 +2,7 @@ import { redirect } from "next/navigation";
import { auth, signOut } from "@/server/auth";
import { setActiveTenant } from "@/server/actions/tenant-switch";
import { Button } from "@/components/ui/button";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
/**
* WS2 (Option C) — Mandantenauswahl nach dem Login, wenn eine Person mehreren
@@ -22,7 +21,7 @@ export default async function SelectTenantPage() {
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={36} />
<CraftviaLogo variant="horizontal" height={36} />
<p className="mt-5 font-heading text-lg font-semibold">Mandant wählen</p>
<p className="mt-1 text-sm text-muted-foreground">
Ihr Konto gehört mehreren Mandanten an. Bitte wählen Sie, in welchem Sie arbeiten möchten.
@@ -48,8 +47,6 @@ export default async function SelectTenantPage() {
<form action={async () => { "use server"; await signOut({ redirectTo: "/login" }); }} className="mt-5">
<Button type="submit" variant="ghost" className="w-full justify-center text-[13px]">Abmelden</Button>
</form>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
+2 -5
View File
@@ -1,6 +1,5 @@
import Link from "next/link";
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { PoweredByGefim } from "@/components/brand/powered-by-gefim";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import { confirmEmailChange } from "@/server/actions/auth-recovery";
import { loginPathOf } from "@/server/auth-selfservice";
@@ -32,7 +31,7 @@ export default async function VerifyEmailPage({
return (
<main className="flex flex-1 items-center justify-center p-6">
<div className="shadow-card w-full max-w-sm rounded-2xl border bg-card p-8">
<CertviaLogo variant="lockup" theme="dark" height={36} />
<CraftviaLogo variant="horizontal" height={36} />
<p className="mt-5 font-heading text-lg font-semibold">E-Mail-Adresse bestätigen</p>
<p
@@ -52,8 +51,6 @@ export default async function VerifyEmailPage({
>
Zur Anmeldung →
</Link>
<PoweredByGefim className="mt-7 border-t border-[var(--panel-brd)] pt-4" />
</div>
</main>
);
-119
View File
@@ -1,119 +0,0 @@
import { BRAND, BRAND_COLORS } from "@/lib/brand";
/**
* Certvia-Logo als Inline-SVG.
*
* Bewusst inline statt <Image src="…svg">: die Wortmarke setzt „Cert" in Poppins
* Light und „via" in Poppins Bold. Ein extern geladenes SVG hat keinen Zugriff auf
* die per next/font/local eingebundene Schrift (gehashter Family-Name) und würde
* auf eine System-Schrift zurückfallen. Inline greift `var(--font-poppins)`.
*
* Zeichen-Konstruktion (unverändert aus dem Design-Paket, viewBox 64×64):
* „C" nach rechts offen, der Haken liegt bündig in der Öffnung. Kein Verlauf.
*
* theme="light" → heller Grund (C Violett, Haken/„via" Magenta, „Cert" Anthrazit)
* theme="dark" → dunkler Grund (C #7d6fd6, Haken #d17bcf, „Cert" weiß)
*/
type LogoTheme = "light" | "dark";
const PALETTE: Record<LogoTheme, { arc: string; hook: string; word: string; accent: string }> = {
light: {
arc: BRAND_COLORS.violet,
hook: BRAND_COLORS.magenta,
word: BRAND_COLORS.anthracite,
accent: BRAND_COLORS.magenta,
},
dark: {
arc: BRAND_COLORS.violetOnDark,
hook: BRAND_COLORS.magentaOnDark,
word: BRAND_COLORS.white,
accent: BRAND_COLORS.magentaOnDark,
},
};
/** Das Monogramm allein — Pfade aus `certvia-mark-*.svg`. */
function Monogram({ arc, hook }: { arc: string; hook: string }) {
return (
<>
<path
d="M46 20 A17 17 0 1 0 46 44"
fill="none"
stroke={arc}
strokeWidth={6}
strokeLinecap="round"
/>
<path
d="M27 32 l6 6 l12 -15"
fill="none"
stroke={hook}
strokeWidth={5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</>
);
}
export function CertviaLogo({
variant = "lockup",
theme = "dark",
height = 40,
className,
title = BRAND.name,
}: {
variant?: "lockup" | "mark";
theme?: LogoTheme;
/** Renderhöhe in px; die Breite ergibt sich aus dem Seitenverhältnis. */
height?: number;
className?: string;
title?: string;
}) {
const c = PALETTE[theme];
if (variant === "mark") {
return (
<svg
viewBox="0 0 64 64"
width={height}
height={height}
role="img"
aria-label={title}
className={className}
>
<title>{title}</title>
<Monogram arc={c.arc} hook={c.hook} />
</svg>
);
}
return (
<svg
viewBox="0 0 380 96"
width={(height * 380) / 96}
height={height}
role="img"
aria-label={title}
className={className}
>
<title>{title}</title>
<g transform="translate(6,8) scale(1.25)">
<Monogram arc={c.arc} hook={c.hook} />
</g>
<text
x={112}
y={62}
fontSize={52}
letterSpacing={-1}
style={{ fontFamily: "var(--font-poppins), Poppins, 'Segoe UI', sans-serif" }}
>
<tspan fontWeight={300} fill={c.word}>
Cert
</tspan>
<tspan fontWeight={700} fill={c.accent}>
via
</tspan>
</text>
</svg>
);
}
+137
View File
@@ -0,0 +1,137 @@
import { useId } from "react";
import { BRAND, BRAND_COLORS } from "@/lib/brand";
/**
* Craftvia-Logo als Inline-SVG (Referenz: docs/craftvia/assets/craftvia-brand-identity-v1.png).
*
* Signet: kräftiges „C" in Lotsenblau, rechts oben geöffnet. Ein Haken in Signalorange
* startet links außerhalb, durchquert das C (das C ist dort freigestellt) und läuft als
* Pfeil nach rechts oben durch die Öffnung hinaus („auf Kurs").
* Wortmarke: „Craft" Lotsenblau + „via" Signalorange, fett; Tagline optional.
* Keine Schatten, keine Verläufe.
*
* Inline statt <img>: die Wortmarke nutzt die per next/font eingebundene App-Schrift.
*
* Varianten:
* variant="horizontal" (Default) | "signet"
* tone="color" (Default) | "mono" (einfarbig Lotsenblau) | "reversed" (weiß auf dunklem Grund)
* tile (nur signet): App-Icon — Lotsenblau-Kachel, weißes C, oranger Haken
*/
export type CraftviaLogoTone = "color" | "mono" | "reversed";
/** Geometrie des Signets (viewBox 0 0 64 64) — auch von scripts/generate-brand-icons.ts genutzt. */
export const SIGNET = {
/** C: Kreisbogen um (30,34), r=19, von -70° gegen den Uhrzeigersinn bis +10° (Öffnung rechts oben). */
arc: "M36.5 16.14 A19 19 0 1 0 48.71 37.3",
arcWidth: 11,
/** Haken: Schwanz links außerhalb → Tal → Richtung Öffnung. */
check: "M6 28 L22 42 L44 21",
checkWidth: 7,
/** Pfeilspitze nach rechts oben. */
arrow: "M52.7 12.7 L47.73 27.8 L37.37 16.96 Z",
} as const;
function palette(tone: CraftviaLogoTone, tile: boolean) {
if (tile) return { arc: BRAND_COLORS.white, check: BRAND_COLORS.signalorange, craft: BRAND_COLORS.white, via: BRAND_COLORS.signalorange, tagline: BRAND_COLORS.white };
switch (tone) {
case "mono":
return { arc: BRAND_COLORS.lotsenblau, check: BRAND_COLORS.lotsenblau, craft: BRAND_COLORS.lotsenblau, via: BRAND_COLORS.lotsenblau, tagline: BRAND_COLORS.lotsenblau };
case "reversed":
return { arc: BRAND_COLORS.white, check: BRAND_COLORS.white, craft: BRAND_COLORS.white, via: BRAND_COLORS.white, tagline: BRAND_COLORS.white };
default:
return { arc: BRAND_COLORS.lotsenblau, check: BRAND_COLORS.signalorange, craft: BRAND_COLORS.lotsenblau, via: BRAND_COLORS.signalorange, tagline: BRAND_COLORS.lotsenblau };
}
}
function Signet({ maskId, arc, check }: { maskId: string; arc: string; check: string }) {
return (
<>
<defs>
{/* Freistellung: das C ist dort unterbrochen, wo der Haken es durchquert. */}
<mask id={maskId} maskUnits="userSpaceOnUse" x="0" y="0" width="64" height="64">
<rect x="0" y="0" width="64" height="64" fill="#fff" />
<path d={SIGNET.check} fill="none" stroke="#000" strokeWidth={SIGNET.checkWidth + 6} strokeLinejoin="miter" />
</mask>
</defs>
<path d={SIGNET.arc} fill="none" stroke={arc} strokeWidth={SIGNET.arcWidth} strokeLinecap="butt" mask={`url(#${maskId})`} />
<path d={SIGNET.check} fill="none" stroke={check} strokeWidth={SIGNET.checkWidth} strokeLinejoin="miter" strokeLinecap="butt" />
<path d={SIGNET.arrow} fill={check} />
</>
);
}
export function CraftviaLogo({
variant = "horizontal",
tone = "color",
tile = false,
tagline = false,
height = 36,
className,
title = BRAND.name,
}: {
variant?: "horizontal" | "signet";
tone?: CraftviaLogoTone;
/** Nur für variant="signet": App-Icon-Kachel in Lotsenblau. */
tile?: boolean;
/** Tagline „Handwerk. Digital auf Kurs." unter der Wortmarke (nur horizontal). */
tagline?: boolean;
/** Renderhöhe in px; die Breite ergibt sich aus dem Seitenverhältnis. */
height?: number;
className?: string;
title?: string;
}) {
const maskId = `cv-mask-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
const isTile = variant === "signet" && tile;
const c = palette(tone, isTile);
if (variant === "signet") {
return (
<svg viewBox="0 0 64 64" width={height} height={height} role="img" aria-label={title} className={className}>
<title>{title}</title>
{isTile ? (
<>
<rect x="0" y="0" width="64" height="64" rx="14" fill={BRAND_COLORS.lotsenblau} />
<g transform="translate(9.6 9.6) scale(0.7)">
<Signet maskId={maskId} arc={c.arc} check={c.check} />
</g>
</>
) : (
<Signet maskId={maskId} arc={c.arc} check={c.check} />
)}
</svg>
);
}
const vbW = 292;
const vbH = tagline ? 82 : 64;
return (
<svg viewBox={`0 0 ${vbW} ${vbH}`} width={(height * vbW) / vbH} height={height} role="img" aria-label={title} className={className}>
<title>{title}</title>
<Signet maskId={maskId} arc={c.arc} check={c.check} />
<text
x={70}
y={48}
fontSize={46}
fontWeight={800}
letterSpacing={-1.2}
style={{ fontFamily: "var(--font-sans), Inter, system-ui, 'Segoe UI', Arial, sans-serif" }}
>
<tspan fill={c.craft}>Craft</tspan>
<tspan fill={c.via}>via</tspan>
</text>
{tagline && (
<text
x={72}
y={74}
fontSize={15}
fontWeight={600}
fill={c.tagline}
style={{ fontFamily: "var(--font-sans), Inter, system-ui, 'Segoe UI', Arial, sans-serif" }}
>
{BRAND.tagline}
</text>
)}
</svg>
);
}
-37
View File
@@ -1,37 +0,0 @@
import Image from "next/image";
import { BRAND, BRAND_ASSETS } from "@/lib/brand";
/**
* Dachmarken-Hinweis „Ein Produkt von GEFIM".
*
* Bewusste Leitplanke der Branding-Umstellung: Certvia ist die Produktmarke,
* GEFIM bleibt Dachmarke und erscheint weiterhin in Fußzeilen, auf den
* Anmeldeseiten sowie später in Export- und Mail-Fußzeilen.
* Siehe docs/BRANDING-CERTVIA.md.
*/
export function PoweredByGefim({
withLogo = true,
className,
}: {
withLogo?: boolean;
className?: string;
}) {
return (
<p
className={`flex items-center justify-center gap-2 text-[11px] text-muted-foreground ${className ?? ""}`}
>
<span>{BRAND.byline}</span>
{withLogo && (
<Image
src={BRAND_ASSETS.parentLogo}
alt={BRAND.parentBrand}
// Seitenverhältnis der Quelldatei (1000×353)
width={76}
height={27}
// Negativdarstellung auf dunklem Grund
className="opacity-80 brightness-0 invert"
/>
)}
</p>
);
}
+7 -9
View File
@@ -1,13 +1,12 @@
import { CertviaLogo } from "@/components/brand/certvia-logo";
import { CraftviaLogo } from "@/components/brand/craftvia-logo";
import type { TenantBranding } from "@/lib/brand";
/**
* Kopfbereichs-Marke eines Mandanten (Story S8).
* Kopfbereichs-Marke eines Mandanten.
*
* Default ist **Certvia**; ein mandanteneigenes Logo überschreibt nur, wenn es
* gesetzt ist. Der Upload je Mandant kommt mit Admin Phase 2 (Objektspeicher) —
* bis dahin liefert `resolveTenantBranding` konstant `logoUrl: null`, die
* Verzweigung ist aber bereits vorhanden und muss dann nicht nachgezogen werden.
* Default ist **Craftvia**; ein mandanteneigenes Logo überschreibt nur, wenn es
* gesetzt ist (TenantSettings.logoKey → Upload folgt). Bis dahin liefert
* `resolveTenantBranding` konstant `logoUrl: null`.
*/
export function TenantBrand({
branding,
@@ -20,8 +19,7 @@ export function TenantBrand({
}) {
if (branding.logoUrl) {
return (
// Mandanten-Logos liegen später im Objektspeicher (beliebige Domain) —
// daher bewusst <img> statt next/image ohne Remote-Pattern-Konfiguration.
// Mandanten-Logos liegen im Objektspeicher — daher bewusst <img> statt next/image.
// eslint-disable-next-line @next/next/no-img-element
<img
src={branding.logoUrl}
@@ -31,5 +29,5 @@ export function TenantBrand({
/>
);
}
return <CertviaLogo variant="lockup" theme="dark" height={height} className={className} />;
return <CraftviaLogo variant="horizontal" height={height} className={className} />;
}
+12 -90
View File
@@ -1,10 +1,9 @@
import { cn } from "@/lib/utils";
/**
* Basisbausteine im Look des UI-Mockups (docs/ISMS-Prototyp-GEFIM.html —
* historisches Projektartefakt, der Dateiname bleibt bewusst unverändert):
* PageHead (Crumb/Titel/Sub + Aktionen), KPI-Kapsel, C/I/A-Quadrate,
* Tag-Chip, Owner-Chip, Status-Pille.
* UI-Basisbausteine (Craftvia-CD): PageHead (Crumb/Titel/Sub + Aktionen), KPI-Kapsel,
* Tag-Chip, Owner-Chip, Status-Pille. Farbe nie als einziges Statussignal — Pillen
* tragen immer Text.
*/
export function PageHead({
@@ -56,80 +55,9 @@ export function KpiCard({
);
}
// Segmentbalken-Farben je Stufe 1–4 als Ampel: Grün → Gelb → Orange → Rot
const CIA_BAR_COLORS: Record<number, string> = {
1: "bg-[#2e9e6b]",
2: "bg-[#e5b000]",
3: "bg-[#e07d2e]",
4: "bg-[#d64c4c]",
};
/** Ein Schutzziel als 4er-Segmentbalken, gefüllt bis `level`. */
function CiaSegment({ level, label }: { level: number; label?: string }) {
const color = CIA_BAR_COLORS[level] ?? CIA_BAR_COLORS[1];
return (
<span className="inline-flex flex-col items-center gap-1">
<span className="flex gap-[2px]">
{[1, 2, 3, 4].map((seg) => (
<span
key={seg}
className={cn(
"h-4 w-[7px] rounded-[2px]",
seg <= level ? color : "bg-[rgba(120,135,180,0.2)]"
)}
/>
))}
</span>
{label && (
<small className="text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
{label}
</small>
)}
</span>
);
}
/**
* Schutzbedarf / Schadenshöhe als drei Segmentbalken (C/I/A). Mit `labels`
* werden VER/INT/VFB unter den Balken gezeigt (Detailkarten); ohne für
* kompakte Tabellenzellen.
*/
export function CiaBadge({
c,
i,
a,
labels = false,
}: {
c: number;
i: number;
a: number;
labels?: boolean;
}) {
return (
<span className="inline-flex items-center gap-2.5" title={`C${c} · I${i} · A${a}`}>
<CiaSegment level={c} label={labels ? "VER" : undefined} />
<CiaSegment level={i} label={labels ? "INT" : undefined} />
<CiaSegment level={a} label={labels ? "VFB" : undefined} />
</span>
);
}
/** VER/INT/VFB-Beschriftung passend zur Balkenbreite — einmal pro Tabelle als Überschrift. */
export function CiaLegend() {
return (
<span className="inline-flex items-center gap-2.5 text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
{["VER", "INT", "VFB"].map((l) => (
<span key={l} className="w-[34px] text-center">
{l}
</span>
))}
</span>
);
}
export function Tag({ children }: { children: React.ReactNode }) {
return (
<span className="inline-block rounded-md bg-[rgba(139,147,173,0.14)] px-2 py-0.5 text-[11px] font-bold text-[#b7bdd0]">
<span className="inline-block rounded-md bg-[var(--muted)] px-2 py-0.5 text-[11px] font-bold text-muted-foreground">
{children}
</span>
);
@@ -153,15 +81,15 @@ export function OwnerChip({ name, noOwnerLabel }: { name?: string | null; noOwne
);
}
// Status-Chips: ~16–20 % Alpha der Statusfarbe auf Dunkel (Referenz-Vorgabe)
// Status-Chips: ~12 % Alpha der Funktionsfarbe auf hellem Grund (Brandbook §11.4).
const PILL_TONES = {
ok: "bg-[rgba(57,192,127,0.16)] text-[var(--ok)]",
warn: "bg-[rgba(240,173,78,0.16)] text-[var(--warn)]",
orange: "bg-[rgba(240,140,60,0.16)] text-[#f0a35a]",
risk: "bg-[rgba(255,107,107,0.16)] text-[var(--risk)]",
info: "bg-[rgba(90,169,230,0.16)] text-[var(--info)]",
mut: "bg-[rgba(139,147,173,0.16)] text-muted-foreground",
violet: "bg-[rgba(125,111,214,0.2)] text-[#c3bdec]",
ok: "bg-[rgba(35,132,93,0.12)] text-[var(--ok)]",
warn: "bg-[rgba(200,121,18,0.12)] text-[var(--warn)]",
orange: "bg-[rgba(255,106,0,0.12)] text-[#b34a00]",
risk: "bg-[rgba(198,66,66,0.12)] text-[var(--risk)]",
info: "bg-[rgba(47,111,163,0.12)] text-[var(--info)]",
mut: "bg-[rgba(102,114,125,0.12)] text-muted-foreground",
violet: "bg-[rgba(8,46,91,0.1)] text-[var(--primary)]",
} as const;
export function Pill({
@@ -183,12 +111,6 @@ export function Pill({
);
}
/** Kritikalität 1–4 als Pille in Mockup-Farben (info/warn/risk). */
export function CriticalityPill({ level, label }: { level: number; label: string }) {
const tone = level >= 4 ? "risk" : level === 3 ? "warn" : level === 2 ? "info" : "ok";
return <Pill tone={tone}>{label}</Pill>;
}
export function SectTitle({ title, sub }: { title: string; sub?: string }) {
return (
<div>
+49 -68
View File
@@ -1,106 +1,88 @@
/**
* Certvia — Marken- und Produktkonstanten (JS-Pendant zu den CSS-Tokens in
* `src/app/globals.css`).
* Craftvia — Marken- und Produktkonstanten (JS-Pendant zu den CSS-Tokens in
* `src/app/globals.css`). Quelle: docs/craftvia/BRANDBOOK.md §11.4/§11.5.
*
* Hier steht nur, was CSS-Variablen NICHT auflösen können:
* - Metadaten (`<title>`, OG/Twitter, Manifest, TOTP-Issuer)
* - Canvas-/PNG-Export (html-to-image rendert außerhalb des CSS-Kontexts)
* - Dokument-/Export-Corporate-Design (DOCX/PDF, Print-Kopf/-Fußzeile)
* - Metadaten (`<title>`, OG, Manifest, TOTP-Issuer, WebAuthn-RP-Name)
* - Dokument-/Export-Corporate-Design (PDF-Berichte, Print)
* - E-Mail-Templates (Inline-Styles, kein CSS-Var-Support in Mail-Clients)
* - Inline-SVG-Logo (src/components/brand/craftvia-logo.tsx)
*
* Für alles, was im Browser gerendert wird, gelten die CSS-Tokens als Quelle —
* dieses Modul spiegelt sie nur, damit beide Seiten nicht auseinanderlaufen.
*
* Leitplanke: GEFIM bleibt Dachmarke. Produktmarke = Certvia.
*/
/** Markenfarben aus dem Certvia-Styleguide (Logo, Print, Verläufe). */
/** Kernfarben aus dem Craftvia-Brandbook (§11.4). */
export const BRAND_COLORS = {
violet: "#5d52a3",
magenta: "#812d80",
blue: "#8dc4e0",
anthracite: "#3b3b3a",
/** Varianten auf dunklem Grund */
violetOnDark: "#7d6fd6",
magentaOnDark: "#d17bcf",
white: "#ffffff",
/** Primär — Logo, Navigation, Headlines */
lotsenblau: "#082E5B",
/** Sekundär — „via", Haken-/Weg-Signet, CTA, aktive Elemente */
signalorange: "#FF6A00",
/** Dunkel — Fließtext, Footer */
graphit: "#34383D",
/** Hell — Flächen, Seitenhintergrund */
hafengrau: "#F3F5F7",
/** Neutral — Sekundärtext, Icons */
stahlgrau: "#66727D",
/** Linie — Trennlinien, Eingabefelder */
zink: "#D9E0E5",
white: "#FFFFFF",
} as const;
/**
* UI-Palette des dunklen Produkt-Themes. Bewusst getrennt von BRAND_COLORS:
* Marken-Violett #5d52a3 gilt fürs Logo, UI-Interaktiv bleibt #7d6fd6.
*/
export const UI_COLORS = {
primary: "#7d6fd6",
primaryDeep: "#5d52a3",
blue: "#8dc4e0",
magenta: "#b45bb0",
bg0: "#0e1220",
bg1: "#141a2e",
bg2: "#1a2138",
text: "#e8ecf7",
textMuted: "#8b93ad",
line: "rgba(120, 135, 180, 0.18)",
graphEdge: "#4a5372",
ok: "#39c07f",
warn: "#f0ad4e",
risk: "#ff6b6b",
info: "#5aa9e6",
/** Funktionsfarben (§11.4). Farbe nie als einziges Statussignal einsetzen. */
export const STATUS_COLORS = {
success: "#23845D",
warning: "#C87912",
error: "#C64242",
info: "#2F6FA3",
} as const;
/** Produktidentität — überall dort, wo der Name als Text erscheint. */
export const BRAND = {
/** Produktmarke */
name: "Certvia",
tagline: "Informationssicherheit. Endlich einfach.",
name: "Craftvia",
tagline: "Handwerk. Digital auf Kurs.",
description:
"Certvia — Informationssicherheits-Managementsystem für ISO/IEC 27001:2022 und TISAX/VDA-ISA.",
/** Dachmarke (bleibt erhalten, siehe docs/BRANDING-CERTVIA.md) */
parentBrand: "GEFIM",
/** Fußzeilen-/Signaturhinweis für UI, Export und E-Mail */
byline: "Ein Produkt von GEFIM",
"Craftvia — die Einsatz-App für Handwerks- und Montagebetriebe: Aufträge, Teams, Einsätze und Berichte in einem System.",
/** Name des In-Product-Assistenten (separat vom Logo, §Lotse) */
assistantName: "Lotse",
/** Kompakte Form für Dokument-Fußzeilen */
documentByline: "Certvia — ein Produkt von GEFIM",
documentByline: "Erstellt mit Craftvia",
} as const;
/** Asset-Pfade unter `public/` (Logo-Bundle aus dem Design-Paket). */
/** Asset-Pfade unter `public/` (aus dem Inline-SVG-Logo erzeugt). */
export const BRAND_ASSETS = {
markPositive: "/assets/logo/certvia-mark-positiv.svg",
markNegative: "/assets/logo/certvia-mark-negativ.svg",
markGrey: "/assets/logo/certvia-mark-grau.svg",
markAnthracite: "/assets/logo/certvia-mark-anthrazit.svg",
lockupPositive: "/assets/logo/certvia-lockup-positiv.svg",
favicon: "/assets/logo/certvia-favicon.svg",
/** Dachmarke */
parentLogo: "/assets/logo/gefim-logo.png",
} as const;
/** Schriftstapel für Kontexte ohne Tailwind/CSS-Vars (Export, E-Mail, Print). */
export const BRAND_FONTS = {
heading: "'Poppins', 'Segoe UI', system-ui, sans-serif",
body: "'Open Sans', system-ui, -apple-system, sans-serif",
favicon: "/favicon/craftvia-signet.svg",
appleTouchIcon: "/favicon/apple-touch-icon-180.png",
ogImage: "/favicon/pwa-512.png",
} as const;
/**
* Standard-Branding eines Mandanten (Story S8). Ohne eigenes Logo/Akzent zeigt
* jeder Mandant Certvia; ein späterer Logo-Upload (Admin Phase 2) überschreibt
* ausschließlich die gesetzten Felder.
* Schriftstapel für Kontexte ohne Tailwind/CSS-Vars (Export, E-Mail, Print).
* Primärschrift laut Brandbook: Inter. TODO(brand): Inter selbst hosten (woff2 in
* src/app/fonts) — bis dahin nutzt die App die vorhandenen selbst gehosteten Schriften.
*/
export const BRAND_FONTS = {
heading: "Inter, system-ui, 'Segoe UI', Roboto, Arial, sans-serif",
body: "Inter, system-ui, 'Segoe UI', Roboto, Arial, sans-serif",
} as const;
/**
* Standard-Branding eines Mandanten. Ohne eigenes Logo/Akzent zeigt jeder Mandant
* Craftvia; ein späterer Logo-Upload überschreibt ausschließlich die gesetzten Felder.
*/
export type TenantBranding = {
/** Produktname im Kopfbereich */
productName: string;
/** Custom-Logo des Mandanten; `null` ⇒ Certvia-Lockup */
/** Custom-Logo des Mandanten; `null` ⇒ Craftvia-Logo */
logoUrl: string | null;
/** Akzentfarbe; `null` ⇒ UI-Primärfarbe */
accent: string | null;
/** Dachmarken-Hinweis, unabhängig vom Mandanten-Branding */
byline: string;
};
const HEX_RE = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
/**
* Löst das effektive Branding eines Mandanten auf. Certvia ist der Default;
* Löst das effektive Branding eines Mandanten auf. Craftvia ist der Default;
* mandanteneigene Werte überschreiben nur, wenn sie gesetzt und plausibel sind.
*/
export function resolveTenantBranding(
@@ -112,6 +94,5 @@ export function resolveTenantBranding(
productName: BRAND.name,
logoUrl: logoUrl ? logoUrl : null,
accent: accent && HEX_RE.test(accent) ? accent : null,
byline: BRAND.byline,
};
}
+17 -28
View File
@@ -1,28 +1,23 @@
import { BRAND, BRAND_COLORS, BRAND_FONTS } from "@/lib/brand";
/**
* Branding-Layer für Dokument-Ausgaben (Story S5).
* Branding-Layer für Dokument-Ausgaben (Einsatzberichte, Druckansicht, PDF-Export).
*
* Ausgaben außerhalb der App-UI — Druckansicht, künftiger DOCX-/PDF-Export von
* Richtlinien, VDA-ISA-Katalog und Reports — laufen im **hellen** Corporate
* Design: Marken-Violett/Magenta auf Weiß, Poppins für Überschriften, Open Sans
* für Fließtext, Fußzeile mit dem Dachmarken-Hinweis.
*
* Bewusst ohne Abhängigkeit zu einer Export-Bibliothek: das Export-Feature
* (DOCX/PDF) existiert noch nicht. Dieses Modul liefert die Kopf-/Fußzeilen-
* Bausteine und die Farb-/Schriftreferenz, an die es dann andockt — damit das
* CD nicht ein zweites Mal definiert werden muss.
* Ausgaben außerhalb der App-UI laufen im hellen Craftvia-CD: Lotsenblau für
* Überschriften, Signalorange als sparsamer Akzent, Graphit für Fließtext.
* Bewusst ohne Abhängigkeit zu einer Export-Bibliothek — das Berichtsmodul dockt
* an diese Farb-/Schriftreferenz an.
*/
/** Farb-/Schriftreferenz für Dokumentausgaben (heller Grund). */
export const DOCUMENT_THEME = {
pageBackground: "#ffffff",
text: BRAND_COLORS.anthracite,
textMuted: "#6b6b70",
accent: BRAND_COLORS.violet,
accentStrong: BRAND_COLORS.magenta,
rule: "#d9d7e4",
tableHeaderBackground: "#f4f2fa",
pageBackground: BRAND_COLORS.white,
text: BRAND_COLORS.graphit,
textMuted: BRAND_COLORS.stahlgrau,
accent: BRAND_COLORS.lotsenblau,
accentStrong: BRAND_COLORS.signalorange,
rule: BRAND_COLORS.zink,
tableHeaderBackground: BRAND_COLORS.hafengrau,
headingFont: BRAND_FONTS.heading,
bodyFont: BRAND_FONTS.body,
} as const;
@@ -30,7 +25,7 @@ export const DOCUMENT_THEME = {
export type DocumentHeader = {
/** Produktmarke — steht im Kopf jeder Ausgabe */
brand: string;
/** Dokumenttitel, z. B. „Richtlinie Zugriffskontrolle" */
/** Dokumenttitel, z. B. „Einsatzbericht A-2026-0012" */
title: string;
/** Organisation des Mandanten */
organisation?: string;
@@ -41,10 +36,8 @@ export type DocumentHeader = {
};
export type DocumentFooter = {
/** „Certvia — ein Produkt von GEFIM" */
/** „Erstellt mit Craftvia" */
byline: string;
/** Logo-Pfad der Dachmarke für Ausgaben, die Bilder einbetten können */
parentLogo: string;
/** Optionaler Vertraulichkeitsvermerk */
confidentiality?: string;
};
@@ -54,18 +47,14 @@ export function documentHeader(input: Omit<DocumentHeader, "brand">): DocumentHe
return { brand: BRAND.name, ...input };
}
/** Fußzeilen-Daten — der Dachmarken-Hinweis ist nicht abschaltbar. */
/** Fußzeilen-Daten einer Dokumentausgabe. */
export function documentFooter(confidentiality?: string): DocumentFooter {
return {
byline: BRAND.documentByline,
parentLogo: "/assets/logo/gefim-logo.png",
confidentiality,
};
return { byline: BRAND.documentByline, confidentiality };
}
/**
* Einzeiler für Ausgabeformate, die nur eine Textfußzeile kennen
* (CSV-Kopfkommentar, Plaintext-Mail, DOCX-Footer ohne Bild).
* (CSV-Kopfkommentar, Plaintext, PDF-Footer ohne Bild).
*/
export function documentFooterLine(confidentiality?: string): string {
const f = documentFooter(confidentiality);
+10 -9
View File
@@ -5,7 +5,7 @@ import { DOCUMENT_THEME } from "@/lib/document-brand";
* Brandfähige E-Mail-Template-Basis (Story S6).
*
* SMTP und der Einladungs-/Reset-Flow kommen erst mit Paket 4 — dieses Modul
* liefert vorab das Certvia-Gerüst, damit die Templates dann nur noch Inhalt
* liefert das Craftvia-Gerüst, damit die Templates nur noch Inhalt
* einsetzen müssen und das CD nicht neu erfunden wird.
*
* Bewusste Einschränkungen für E-Mail-Clients:
@@ -39,7 +39,7 @@ export type EmailContent = {
* E-Mail-Clients nicht.
*/
function baseUrl(): string {
return (process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "https://app.certvia.de").replace(
return (process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "http://localhost:3000").replace(
/\/+$/,
""
);
@@ -58,13 +58,13 @@ export function emailSender(address: string): string {
return `${BRAND.name} <${address}>`;
}
/** Signatur-/Fußzeilentext — enthält immer den Dachmarken-Hinweis. */
/** Signatur-/Fußzeilentext. */
export function emailSignature(): string {
return `${BRAND.name} — ${BRAND.byline}`;
return `${BRAND.name} — ${BRAND.tagline}`;
}
/**
* Rendert eine Mail im Certvia-CD (heller Grund, Marken-Violett, Poppins-Kopf).
* Rendert eine Mail im Craftvia-CD (Hafengrau-Grund, Lotsenblau, Signalorange-CTA).
* Rückgabe ist ein vollständiges HTML-Dokument.
*/
export function renderHtmlEmail(content: EmailContent): string {
@@ -80,7 +80,7 @@ export function renderHtmlEmail(content: EmailContent): string {
const action = content.action
? `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:24px 0;">
<tr><td style="background:${BRAND_COLORS.violet};border-radius:8px;">
<tr><td style="background:${BRAND_COLORS.signalorange};border-radius:8px;">
<a href="${escapeHtml(content.action.url)}" style="display:inline-block;padding:12px 22px;font-family:${t.headingFont};font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;">${escapeHtml(content.action.label)}</a>
</td></tr>
</table>`
@@ -93,12 +93,13 @@ export function renderHtmlEmail(content: EmailContent): string {
return `<!doctype html>
<html lang="de">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(content.subject)}</title></head>
<body style="margin:0;padding:0;background:#f4f2fa;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f2fa;padding:28px 12px;">
<body style="margin:0;padding:0;background:${BRAND_COLORS.hafengrau};">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:${BRAND_COLORS.hafengrau};padding:28px 12px;">
<tr><td align="center">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;background:${t.pageBackground};border-radius:14px;overflow:hidden;border:1px solid ${t.rule};">
<tr><td style="padding:26px 30px 0;">
<img src="${url}/assets/logo/certvia-lockup-positiv.svg" alt="${BRAND.name}" height="34" style="height:34px;width:auto;display:block;border:0;">
<img src="${url}/favicon/pwa-192.png" alt="${BRAND.name}" height="34" width="34" style="height:34px;width:34px;display:inline-block;vertical-align:middle;border:0;border-radius:8px;">
<span style="display:inline-block;vertical-align:middle;margin-left:10px;font-family:${t.headingFont};font-size:22px;font-weight:800;color:${BRAND_COLORS.lotsenblau};">Craft<span style="color:${BRAND_COLORS.signalorange};">via</span></span>
</td></tr>
<tr><td style="padding:22px 30px 4px;">
<h1 style="margin:0 0 14px;font-family:${t.headingFont};font-size:20px;font-weight:600;color:${t.text};">${escapeHtml(content.heading)}</h1>
+1 -1
View File
@@ -90,7 +90,7 @@ export function resetMailConfigCache(): void {
cached = null;
}
/** Absenderzeile `Certvia <no-reply@certvia.de>`. */
/** Absenderzeile `Craftvia <no-reply@craftvia.de>`. */
export function mailFrom(config: MailConfig): string {
return `${config.fromName} <${config.from}>`;
}
+2 -1
View File
@@ -9,6 +9,7 @@ import type {
AuthenticationResponseJSON,
AuthenticatorTransportFuture,
} from "@simplewebauthn/types";
import { BRAND } from "@/lib/brand";
/**
* SEC3-b: WebAuthn/Passkey-Basis. Kapselt die RP-/Origin-Konfiguration (aus AUTH_URL bzw.
@@ -17,7 +18,7 @@ import type {
* (Cookie) und Persistenz.
*/
export const RP_NAME = "Certvia";
export const RP_NAME = BRAND.name;
/** Cookie-Name der kurzlebigen Login-Challenge (pre-session Passkey-Login). */
export const LOGIN_CHALLENGE_COOKIE = "wa_login_challenge";