# Designsprache
Die geschriebene Quelle der Wahrheit für das Aussehen und Verhalten beider Oberflächen von
BWBH: die Marketing-Site (`apps/web`) und die regulierte App (`apps/dashboard`). Sie
erklärt **warum** etwas so aussieht und **wann** man was nimmt.
Was sie **nicht** tut: zeigen. Dafür ist `/design` da (BF-232), die klickbare Galerie, die
jede Komponente in allen Zuständen rendert. Wo beide denselben Sachverhalt berühren,
verlinken sie aufeinander statt sich zu wiederholen. Doppelt gepflegte Wahrheit läuft
garantiert auseinander.
> **Stand:** Tokens und Primitives liegen seit BF-230 gemeinsam in `@bwb/ui` und werden von
> beiden Apps verbraucht. Die früheren Kopien je App gibt es nicht mehr. Was hier steht,
> gilt damit ohnehin für beide.
---
## 1. Grundhaltung
Drei Sätze, aus denen sich fast alles Weitere ableitet.
**BWBH ist ein Werkzeug, kein Schaufenster.** Es wird überflogen und bedient, nicht
gelesen. Zusammenfassung vor Detail; Zustand steckt in der Form (Pille, Chip, Farbstreifen),
nicht nur in einer Zahl.
**Gesetzliche Pflichten sind kein Designspielraum.** Wo eine Frist aus §128a stammt, muss
das erkennbar bleiben und darf nicht neben selbst gesetzten Terminen verschwimmen. Farbe,
Beschriftung und Wortwahl tragen diese Unterscheidung mit.
**Der Nutzer ist eine Hausverwaltung, keine Entwicklerin.** Benannt wird, was die Person
wiedererkennt, nicht wie das Datenmodell heißt. „Objekt", nicht „Bauwerk-Entity";
„Prüfbericht", nicht „Dokument vom Typ Befund".
---
## 2. Farbe
*→ Live in der Galerie: `/design#tokens` (BF-232).*
Definiert als CSS-Variablen in `@bwb/ui/tokens.css`, über `@theme inline` in Tailwind
gehoben (also lösen `bg-bg`, `text-ink`, `border-border`, `text-accent` auf).
| Token | Wert | Wofür, und wofür nicht |
| --- | --- | --- |
| `--bg` | `#faf8f4` | Seitengrund. Warmes Papier, bewusst kein klinisches Grau. |
| `--ink` | `#14110d` | Fließtext und Überschriften. Warmes Fast-Schwarz, nie reines `#000`. |
| `--card` | `#ffffff` | Erhöhte Flächen: Karten, Leiste, Dialoge. |
| `--border` | `#e0d9cf` | Alle Trennlinien. Ein einziger Rahmenton, keine Abstufungen. |
| `--muted` | `#70685e` | Sekundärtext, Beschriftungen, inaktive Symbole. |
| `--soft` | `#f2ede5` | Hover- und Aktivflächen. |
| `--accent` | `#1b4dff` | **Die einzige tauschbare Farbe.** Primäraktion, Links, Fokus, aktiver Zustand. |
| `--accent-ink` | `#ffffff` | Text auf Akzentfläche. |
| `--urgent` | `#cc2020` | **Reserviert.** Siehe unten. |
| `--danger` | `#b3352d` | Fehlerrot der Oberfläche. Siehe unten. |
| `--positive` | `#0f7a3d` | Erledigt, in Ordnung, bestanden. |
| `--warning` | `#8a5300` | Achtung ohne Frist: unsichere Einstufung, fehlende Angabe. |
| `--info` | `#1f4bb8` | Neutrale Statusaussage. Bewusst dunkler als `--accent`. |
| `--progress` | `#5b46c9` | In Bearbeitung, läuft. |
Die Werte in dieser Tabelle sind eine **Abschrift**; die Quelle ist `tokens.css`. Wer sie
nachschlagen will, öffnet besser `/design#tokens` — die Galerie liest sie seit BF-385 aus
dem laufenden Schema statt sie zu führen, und zeigt deshalb auch die dunklen Werte.
(Diese Tabelle stand zwei Werte falsch, bevor jemand das Nachrechnen anfing.)
### `--muted` und `--urgent` mussten wandern (BF-385/BF-355)
`--muted` stand auf `#7a7268` und erreichte auf `--soft` nur **4,06:1** — unter AA, und
`--soft` ist kein Sonderfall, sondern die Fläche jeder Hover- und Aktivzeile. `--urgent`
stand auf `#dc2626` und lag auf `--soft` bei **4,14:1**, also durfte ausgerechnet die
Frist-Warnung dort nicht als Text stehen. Beides waren Rechenfehler auf dem Papier: die
Werte waren gegen `--bg` gemessen und die dritte Fläche war nie geprüft worden.
Die Regel, die daraus folgt: **ein Textton muss gegen alle drei Flächen halten** —
`--bg`, `--card` **und** `--soft`. Gemessen wird im Browser, nicht im Kopf; der Wächter
dafür ist `apps/dashboard/e2e/token-kontrast.spec.ts`.
### `--urgent` ist reserviert
Rot bedeutet in BWBH genau eine Sache: **eine Frist ist verstrichen oder ein Zustand
verlangt jetzt eine Handlung.** Es ist keine Serienfarbe, keine Akzentvariante und kein
Gestaltungsmittel.
Der Grund ist nicht Ästhetik. Wenn Rot auch für „Kategorie 4" oder „Hervorhebung" benutzt
wird, lernt der Nutzer, es zu überlesen, und übersieht dann die eine überfällige Frist, für
die das Produkt existiert. Jede Verwendung von `--urgent` muss die Frage beantworten: *Was
genau soll die Person deswegen tun?*
### `--danger` ist das andere Rot (BF-385)
`--urgent` war jahrelang das einzige Rot, und deshalb landete auf ihm alles Rote: rund
**45 Formularfehler** („Pflichtfeld", „Datei zu groß", „Adresse ungültig"). Damit war die
Reservierung im Quelltext nur noch eine Behauptung — sie stand im Kommentar und war in der
Oberfläche längst gebrochen, und der einzige Ort, an dem Rot wirklich *jetzt handeln*
heißen soll, war einer von sechsundvierzig.
Erwogen und verworfen: `--urgent` einfach auch für Fehler freigeben (dann ist die
Reservierung ehrlich, aber wertlos — genau der Gewöhnungseffekt, gegen den sie da ist)
und Fehler unbunt setzen (ein Formularfehler *muss* auffallen). Die Gabelung kostet ein
Token und hält beide Aussagen intakt.
| | trägt | Beispiel |
| --- | --- | --- |
| `--urgent` | **gesetzliche** Dringlichkeit | Frist verstrichen, Gefahr im Verzug, Objekt überfällig |
| `--danger` | Fehler der **Oberfläche** | Pflichtfeld leer, Aktion fehlgeschlagen, Löschen |
Die Faustregel: `--urgent` beantwortet *„was muss ich am Gebäude tun?"*, `--danger`
*„was habe ich gerade falsch gemacht?"*. Ein Formularfehler verschwindet, wenn man ihn
behebt; eine verstrichene Frist nicht.
### Fremde Paletten sind gegen fremde Grundfarben gerechnet (BF-355)
`--positive` gab es bis BF-385 nicht; drei Ansichten griffen stattdessen zu Tailwinds
mitgelieferten Farben (`text-emerald-600`, `bg-emerald-500`). Die sind sauber gewählt —
gegen Tailwinds eigene Grundfarben. Auf `--card` erreichte `emerald-600` **3,35:1**, im
dunklen Schema **4,45:1**: in beiden durchgefallen. Aufgefallen ist es nie, weil Grün nach
„alles in Ordnung" aussieht und niemand eine bestandene Prüfung nachmisst.
Dasselbe galt für die drei übrigen Töne des Statusvokabulars (`lib/status-tone.ts`):
`amber-600` lag bei **3,00:1**, `blue-600` verfehlte `--soft` mit 4,44. Ausgerechnet der
Ton, der „schau hier hin" sagt, war der am schlechtesten lesbare. Das Vokabular hat seit
BF-385 einen vollständigen Tokensatz: `--danger` · `--warning` · `--info` · `--progress` ·
`--positive` · `--muted`.
Die Regel: **keine Farbklasse aus einer fremden Palette im Produktcode.** Wer eine Farbe
braucht, die es nicht als Token gibt, legt das Token an und misst es. Die einzige bewusste
Ausnahme steht in `status-badge.tsx` und sagt dort, warum.
### Akzent sparsam
Der Akzent trägt die **eine** Primäraktion je Ansicht. Zwei blaue Knöpfe nebeneinander
heben sich gegenseitig auf. Alles Weitere ist eine Sekundärfläche (`bg-card` mit
`border-border`) oder ein reiner Textlink.
### Diagrammfarben (BF-228)
Das System hatte keine Diagrammpalette, nur `--accent`, das reservierte `--urgent` und
Neutralwerte. Die folgenden Werte sind **berechnet und mit einem Validator geprüft**, nicht
ausgesucht. Wer sie ändert, prüft neu; „sieht gut aus" ist hier keine Begründung.
**Bestand nach Welle, eine Rampe, weil die Reihenfolge Bedeutung trägt.** Welle 1 → Welle 2
→ nicht pflichtig ist eine Folge abnehmenden gesetzlichen Drucks; sie umzustellen würde die
Aussage ändern. Also *eine* Farbe in monotonen Helligkeitsstufen, keine vier
Identitätsfarben:
| Segment | Wert | |
| -- | -- | -- |
| Welle 1 (2027) | `#1742d3` | Akzent, dunkelste Stufe |
| Welle 2 (2030) | `#4a7cff` | |
| nicht pflichtig | `#63a0ff` | hellste Stufe |
| **unklar** | `#c2870a` | **außerhalb der Rampe**: kein „weniger verpflichtet", sondern „wir wissen es nicht" |
Geprüft als Rampe (`--ordinal`): monotone Helligkeit, Stufenabstand ≥ 0.06, helles Ende
2,62:1 auf Weiß, ein Farbton.
**Beschriftung auf der Marke folgt der Marke, nicht dem Haus-Stil.** Weiß auf `#1742d3`
ergibt 7,62:1, auf `#63a0ff` nur **2,62:1**; die Zahl steht da und ist unlesbar. Die
Gewinnerfarbe kippt also mitten in der Rampe: Weiß auf der dunkelsten Stufe, Ink auf den
übrigen. Die Mittelstufe wanderte deshalb von `#3b71ff` (Ink 4,47:1, knapp unter der
4,5:1-Grenze für kleinen Text) auf `#4a7cff` (5,04:1), und die Rampe wurde danach neu
validiert. Eine Farbe wird nicht wegen Lesbarkeit verschoben, ohne die Prüfungen zu
wiederholen, die sie ursprünglich bestanden hat. Die Kategoriengrenze `#63a0ff` ↔ `#c2870a` separat als
Kategorienpaar: ΔE 29,4 normal, 28,5 protan.
**Fristen nach Horizont, Statusfarben statt Serienfarben.** „Überfällig" ist ein Zustand,
keine Kategorie:
| Zustand | Wert | |
| -- | -- | -- |
| überfällig | `var(--urgent)` | Hier legitim: verstrichene gesetzliche Frist |
| ≤ 90 Tage | `#c2870a` | |
| ≤ 12 Monate | `#1b4dff` | `--accent` |
| später | `#7a7268` | Unbunt, „nichts zu tun" ist keine Identität. Eigener Wert, **nicht** `--muted` |
Seit BF-385 ist „überfällig" ein **Verweis** auf `--urgent`, kein zweites `#dc2626`
daneben. Solange beide Werte zufällig gleich waren, fiel das nicht auf; als `--urgent` auf
`#cc2020` wanderte, wären die verstrichene Frist im Balken und die verstrichene Frist im
Text zwei verschiedene Rottöne geworden, nebeneinander auf derselben Seite.
„Später" ist umgekehrt **kein** Verweis auf `--muted`, obwohl der Wert von dort stammt: das
ist eine Balkenfläche (3:1), `--muted` ist Text (4,5:1). Die beiden dürfen auseinanderlaufen
und tun es seit BF-385 auch.
Die drei bunten Werte bestehen die Kategorienprüfung (schlechtestes benachbartes Paar
`#c2870a` ↔ `#dc2626`: ΔE 8,4 deutan, 18,7 normal, alle ≥ 3:1 Kontrast). **Zwei nähere
Bernsteintöne fielen vorher durch:** `#d97706` an der Normalsicht-Schwelle (14,4) und
`#b45309` katastrophal unter Deuteranopie (2,8, praktisch identisch mit Rot). Genau deshalb
wird gerechnet und nicht geschätzt.
`#7a7268` fällt durch die Chroma-Prüfung, und das ist beabsichtigt: der Validator prüft
Kategorienpaletten, und ein unbunter „kein Handlungsbedarf"-Wert leistet keine
Identitätsarbeit über den Farbton. Er trägt immer Beschriftung und Zahl.
**Zustandsstufen des Portfolios** (`--chart-zustand-*`, seit BF-385 Token statt roher
Hexwerte im Ring-Bauteil). Das ist eine **Rangfolge von Dringlichkeit**, keine kategoriale
Palette: benachbarte Stufen *sollen* sich ähneln, weil sie benachbart sind. Deshalb gelten
hier die Abstandsregeln oben ausdrücklich nicht, und deshalb trägt jede Stufe in der
Oberfläche immer Wort **und** Zahl, auch bei null.
| Stufe | Wert | |
| -- | -- | -- |
| Gefahr im Verzug | `#dc2626` | **Nicht** `--urgent`: siehe unten |
| Frist verstrichen | `#ec835a` | War Rot, fiel bei ΔE 14,3 gegen „Gefahr" durch |
| Noch abwendbar | `#fab219` | |
| Offen | `#3b82f6` | |
| Keine Grundlage | `#a89f93` | Bewusst unbunt: kein Zustand, sondern sein Fehlen |
| In Ordnung | `#0ca30c` | |
**In beiden Schemata dieselben Werte** — anders als jede andere Palette hier. Der Grund ist
auch der, warum „Gefahr im Verzug" nicht auf `--urgent` verweist: im Dunklen wird `--urgent`
zu `#f2635c`, und direkt daneben liegt `#ec835a`. Das ist genau die Kollision, wegen der
„verstrichen" überhaupt von Rot auf Orange gewechselt ist. Eine Rangfolge braucht
gleichbleibende Abstände *zwischen ihren Stufen*, ein Textton braucht Kontrast *zum Grund*;
ein Verweis würde das erste dem zweiten opfern.
**Pflicht bei jeder Verwendung:** Legende ab zwei Serien, zusätzlich direkte Beschriftung,
2 px Abstand zwischen gestapelten Segmenten, Tabellenansicht als Rückfallebene. Identität
nie allein über Farbe. Das ist die Bedingung, unter der die Kontrast-WARNung für `#63a0ff`
überhaupt zulässig ist.
**Dunkel ist seit BF-234 ebenfalls geprüft**, mit eigenen Werten, siehe unten. Die Werte
oben gelten für Hell.
### Dunkles Schema
Seit BF-234 vorhanden, gesteuert über `data-theme="dark"` auf `<html>`. **Eigene Stufen,
keine automatische Invertierung.** Eine Invertierung macht aus dem warmen Papier ein
schmutziges Grau und aus `--urgent` ein stumpfes Braun.
| Token | Wert | gemessen |
| --- | --- | --- |
| `--bg` | `#17140f` | warmes Nachtbraun, kein Blaustich |
| `--ink` | `#f5f1ea` | 16,31:1 auf bg |
| `--card` | `#232019` | 1,13:1 gegen bg (erhöht) |
| `--soft` | `#1e1a14` | zwischen bg und card |
| `--border` | `#4b4237` | 1,87:1 gegen bg |
| `--muted` | `#a99f92` | 7,05:1 auf bg |
| `--accent` | `#6b8cff` | 5,97:1 auf bg |
| `--accent-ink` | `#0d0b08` | 6,38:1 auf accent |
| `--urgent` | `#f2635c` | 5,87 / 5,20 / 5,54:1 gegen bg / card / soft |
| `--danger` | `#ef8079` | 7,03 / 6,22 / 6,62:1 gegen bg / card / soft |
| `--positive` | `#6cd39a` | 9,99 / 8,84 / 9,42:1 gegen bg / card / soft |
| `--warning` | `#d9a441` | 8,17 / 7,23 / 7,70:1 gegen bg / card / soft |
| `--info` | `#7fa0ff` | 7,32 / 6,48 / 6,90:1 gegen bg / card / soft |
| `--progress` | `#a795f5` | 7,22 / 6,39 / 6,81:1 gegen bg / card / soft |
**Flächen sind monoton gestuft: bg < soft < card.** Im ersten Entwurf lag `soft` über
`card`; die Karte wäre dunkler gewesen als der Streifen, auf dem sie liegt, und die
Tiefenwirkung stünde auf dem Kopf.
**`--urgent` ist der Grund, warum hier nichts automatisiert wird.** `#dc2626` erreicht auf
`#17140f` nur 3,4:1 und säuft ab; Rot muss aber Warnung bleiben und darf nicht zur
Dekoration verblassen. Geprüft gegen alle drei Flächen, nicht nur gegen Weiß.
**Nur über `[data-theme]`, kein `@media (prefers-color-scheme)` in der Token-Datei.** Sonst
stünde derselbe Farbblock zweimal dort und würde auseinander driften. Die Systemvorgabe
löst das Inline-Skript (`@bwb/ui/theme-script`) auf, das ohnehin nötig ist, damit beim
ersten Rendern nicht kurz das falsche Schema aufblitzt. Preis: ohne JavaScript bleibt es
hell, die harmlose Richtung, denn Hell ist der vollständig geprüfte Standardzustand.
Weil das Skript `data-theme` setzt, bevor React hydriert, trägt `<html>` in beiden Apps
`suppressHydrationWarning` (BF-250). Ohne das meldet React bei jedem Seitenaufruf einen
Attribut-Unterschied.
#### Diagramme im Dunklen
Die Diagrammfarben sind seit BF-234 **Tokens in `@bwb/ui`**, keine Hex-Werte im TSX: Sie
müssen sich mit dem Schema ändern. Hart kodiert erreichte die dunkelste Blaustufe auf der
dunklen Karte **2,13:1** und war schlicht nicht mehr da.
**Die ordinale Rampe kippt.** Im Hellen ist Welle 1 die *dunkelste* Stufe, im Dunklen die
*hellste*. „Stärkste Stufe" heißt auf dunklem Grund hell, sonst verschwindet ausgerechnet
die wichtigste Kategorie. Die Reihenfolge bleibt monoton (L 0,53 → 0,35 → 0,21), nur der
Anker wechselt.
| | dunkel | gegen Karte | Beschriftung |
| --- | --- | --- | --- |
| Welle 1 | `#a9c0ff` | 9,03:1 | dunkle Tinte, 10,46:1 |
| Welle 2 | `#7a9cff` | 6,21:1 | dunkle Tinte, 7,19:1 |
| nicht pflichtig | `#5877e0` | 3,97:1 | dunkle Tinte, 4,60:1 |
| unklar | `#d9a441` | 7,23:1 | dunkle Tinte, 8,37:1 |
| überfällig | `#f2635c` | 5,20:1 | entfällt |
| ≤ 90 Tage | `#d9a441` | 7,23:1 | entfällt |
| ≤ 12 Monate | `#6b8cff` | 5,28:1 | entfällt |
| später | `#8d8377` | 4,37:1 | entfällt |
Im Dunklen tragen **alle** Marken dunkle Tinte, anders als im Hellen, wo die Gewinnerfarbe
mitten in der Rampe kippt.
Der Umschalter sitzt auf der Kontoseite unter „Erscheinungsbild" (BF-225). Er war dort
zunächst ein ehrlicher Platzhalter, solange die dunkle Rampe nicht definiert war: ein
Umschalter ohne Rampe schaltet auf ein kaputtes Produkt um. Mit BF-234 ist die Rampe
definiert und der Umschalter wirksam.
---
## 3. Typografie
*→ Live in der Galerie: `/design#typografie` (BF-232).*
Drei Schriften, per `next/font` eingebunden, selbst gehostet (kein externer Font-Host, die
CSP von `apps/web` erlaubt keinen).
| Rolle | Schrift | Variable | Wofür |
| --- | --- | --- | --- |
| Überschriften | Funnel Display | `--font-funnel-display` | `h1`–`h3`, Laufweite `-0.02em` |
| Fließtext / UI | Funnel Sans | `--font-funnel-sans` | alles Übrige |
| Daten | Departure Mono | `--font-departure-mono` | Eyebrows, Beschriftungen, **Zahlen, Datumsangaben, IDs** — versal |
| Code | Systemmono | `--font-code` | Pfade, URLs, Codespannen, CSV-Muster — **nie** versal |
Der groß gesetzte, allein stehende **Zählwert** trägt die Textschrift (`.stat`) — siehe
unten, „Warum es die vierte Klasse gibt".
Departure Mono (v1.500, **SIL OFL 1.1**, Helena Zhang) kommt nicht über Google Fonts,
sondern liegt als `woff2` in `packages/ui/src/fonts/` — **einmal**, von beiden Apps über
`next/font/local` geladen. Die Lizenz liegt als `DepartureMono-LICENSE.txt` daneben; die
OFL verlangt, dass sie die Schrift begleitet.
### Wann Mono
Mono ist kein Stilmittel, sondern eine Aussage: *Das hier ist ein Wert, kein Fließtext.*
Zahlen, Datumsangaben, Fristen, Aktenzeichen. Wo Ziffern untereinander stehen, zusätzlich
`font-variant-numeric: tabular-nums`, damit die Spalte ausrichtet.
### Die drei Komponenten-Klassen
- **`.eyebrow`**: Mono, `0.6875rem`, Versalien, Laufweite `0.14em`, `--muted`. Die kleine
Zeile über einer Überschrift, die den Kontext benennt. Nie länger als drei Wörter.
- **`.mono`**: Departure Mono, **Versalien** (`text-transform: uppercase`), `tnum`. Für
Werte im Fließtext: Zahlen, Datumsangaben, Fristen, Token-Namen, Größen.
- **`.code`**: Systemmono, **Schreibung unangetastet**. Für alles, was jemand **liest oder
kopiert**: Codespannen, Dateipfade, Einladungs-URLs, CSV-Muster, Klassennamen.
- **`.stat`**: Funnel Sans mit `tabular-nums`. Für den **groß gesetzten, allein stehenden
Zählwert** — die Kennzahlenzeile, die Kacheln der Übersicht, die Zähler auf `/maengel`.
**Warum es die vierte Klasse gibt (BF-417).** Departure Mono setzt eine **geschlitzte
Null**. In `2025`, `31.12.2027` oder `1 585` ist das richtig und unauffällig: der Schlitz
unterscheidet die Null vom großen O, und genau dafür ist er da. Steht die Ziffer dagegen
**allein und groß**, gibt es kein O, von dem sie sich unterscheiden müsste — der Schlitz
trägt keine Information mehr und kostet nur noch Lesbarkeit. Auf der Kennzahlenzeile las
sich `0` als `Ø`: ausgerechnet die *gute* Nachricht dieses Produkts (null überfällige
Fristen) sah aus wie eine Leermenge.
⚠️ **Keine Rücknahme von BF-408 und keine Entscheidung gegen die Schrift.** Die Grenze
läuft nicht zwischen Schriften, sondern zwischen **Verwendungen** — dieselbe Trennung wie
`.mono` gegen `.code`:
> Ein Wert **in** einer Angabe (Jahr, Datum, Betrag, Aktenzeichen, `489 Tage`) → `.mono`.
> Ein **Zählwert**, der allein und groß steht und `0` sein kann → `.stat`.
`tabular-nums` bleibt in beiden Fällen: dass eine Spalte ausrichtet, war nie die Leistung
der Mono-Schrift, sondern die der Ziffernvariante.
**Und die Paragraphenzitierung gehört in keine versale Fläche (BF-417).** Das kleine `a`
in **§128a** gehört zur Paragraphenbezeichnung — es unterscheidet §128a von §128 und ist
keine Schreibvariante. In `.eyebrow`/`.mono` wurde daraus `§128A`, also eine ungültige
Zitierung der einen Norm, um die es in diesem Produkt geht. Die Regel lautet deshalb: die
Beschriftung trägt die Versalien, **die Norm steht eine Zeile tiefer im Fließtext**.
Durchgesetzt wird das im Browser (`e2e/typografie-und-verlauf.spec.ts`) und nicht über die
Kopie — `text-transform` ändert nur die Darstellung, im DOM steht weiterhin `§128a`, und
welche Zeichenkette in einer versalen Fläche landet, entscheidet erst die Komponente.
**Warum es die dritte Klasse gibt (BF-408).** `.mono` trug bis dahin auch Codespannen,
Pfade und Einladungs-URLs. Mit der Versalien-Regel würde daraus
`OUTPUTFILETRACINGINCLUDES` — und eine versal gesetzte Einladungs-URL funktioniert nicht
mehr. Die Regel ist deshalb nicht „Mono ist versal", sondern:
> **Departure Mono ist eine Beschriftungsschrift und wird nie eine Codeschrift.**
**Und sie trägt nie Fett (BF-418).** Die Schrift liefert genau *einen* Schnitt. Trifft
`font-semibold` darauf, hat der Browser keinen fetten Schnitt zum Nachschlagen — er
**synthetisiert** einen, indem er die Glyphe verbreitert und versetzt mehrfach zeichnet.
Bei einer Proportionalschrift fällt das kaum auf; bei einer **Pixelschrift** zerstört es
genau das, was sie ausmacht: die Kanten liegen nicht mehr auf ganzen Pixeln, aus einer
scharfen Ziffer wird ein Klumpen. `.mono` und `.eyebrow` tragen deshalb
`font-synthesis-weight: none`.
⚠️ Eine Gewichtsangabe in `@layer components` hätte **nicht** gereicht — Tailwind-Utilities
stehen in einer späteren Kaskadenschicht und gewinnen. Die Synthese abzuschalten wirkt
unabhängig davon, wer später was setzt. Betonung in Mono kommt über **Größe und Farbe**,
nicht über Gewicht.
⚠️ Die Entscheidung zwischen beiden hängt nicht am Aussehen, sondern an der **Verwendung**:
wird der Inhalt *gelesen* (ein Wert, den das Auge aufnimmt) oder *übernommen* (ein Text,
den jemand abtippt, kopiert oder in eine Adresszeile setzt)? Im zweiten Fall `.code`.
### Zeilenlänge
Fließtext bei rund 65 Zeichen halten. Überschriften bekommen `text-wrap: balance`.
---
## 4. Abstand, Radius, Höhe
Tailwinds Standardskala, mit zwei Festlegungen:
- **`--radius: 0.5rem`** ist der Standardradius. `--radius-sm` für Chips und Symbolknöpfe,
`--radius-lg` für Dialoge. Nichts ist vollständig rund außer Avataren.
- **Abstände kommen vom Layout, nicht vom Element.** Geschwister werden mit `flex`/`grid`
plus `gap` gesetzt, nicht mit `margin` pro Element. Margins kollabieren still oder
verdoppeln sich, `gap` nicht.
Breite Inhalte (Tabellen, Diagramme, Code) bekommen `overflow-x: auto` auf einem eigenen
Container. **Der Seitenkörper scrollt nie waagrecht.**
### Die Bühne scrollt, nicht die Seite (BF-416)
Die Regel aus §4 gilt in der Galerie an **einer** Stelle, nicht an vielen: die
Specimen-Bühne (`components/design/specimen.tsx`) trägt `overflow-x-auto`. Jedes neue
Specimen erbt das, ohne dass jemand daran denken muss — und genau daran verrottet eine
Galerie sonst: der nächste Mensch fügt ein breites Beispiel ein und denkt nicht an 390 px.
⚠️ **Specimen werden dafür nicht kleiner gemacht.** Die Galerie zeigt die Primitiven in
ihrer echten Größe; ein Knopf, der auf dem Telefon anders aussähe als in der App, wäre eine
Lüge über das Designsystem. Breites darf scrollen, es darf nur nicht die Seite mitziehen.
Gemessen statt vermutet: der Überläufer war die **Wortmarke in `text-[34px]`** — 504 px in
einem 390-px-Fenster. Eine Quellenlese hatte sie nicht gefunden (alle Raster stehen
mobil-zuerst, der Rahmen ist sauber); gefunden hat sie `e2e/design-mobil.spec.ts`, das bei
390 px zusichert, dass `document.documentElement.scrollWidth` die Fensterbreite nicht
überschreitet — und die überstehenden Elemente gleich mitnennt, damit die rote Zeile einen
Anhaltspunkt trägt.
Zwei Dinge, die dabei nur mit Messung sichtbar waren:
- **Markdown-Tabellen brauchen `display: block`.** `w-full` hindert eine Tabelle nicht am
Überlaufen — ihre Spalten bestimmen die Mindestbreite. In der gerenderten Schriftfassung
gibt es keinen Ort für einen Wrapper, also bekommt die Tabelle selbst
`block` + `overflow-x-auto`.
- **Ein Importpfad in einem Flex-Element schrumpft nicht.** `CopyPath` steht in *jedem*
Specimen; ohne `max-w-full min-w-0` und `break-all` am Text schob er allein die ganze
Seite in den Querscroll. `break-all` und nicht `truncate`: ein abgeschnittener Pfad sieht
vollständig aus und ist es nicht.
---
## 5. Komponenten
*→ Live in der Galerie: `/design#primitives` (BF-232), jedes Primitive in allen Zuständen.*
### Primitives
Radix-basiert, an die Marken-Tokens angepasst. Seit BF-230 gemeinsam in `@bwb/ui`; die
früheren `components/ui/` je App gibt es nicht mehr.
| Primitive | Wo | Anmerkung |
| --- | --- | --- |
| `button` | beide | Varianten über `class-variance-authority` |
| `card` | beide | identisch in beiden Apps |
| `input` | beide | identisch in beiden Apps |
| `label` | beide | vereinheitlicht in BF-230 |
| `select` | beide | |
| `accordion`, `textarea` | nur web | |
| `avatar`, `dropdown-menu`, `separator`, `tooltip` | nur dashboard | |
| `dialog` | nur dashboard | neu in BF-224 |
**Ein Primitive kennt keinen Anwendungskontext.** Keine Session, kein Mandant, kein Import
aus `app/`. Zustand kommt über Props herein und über Callbacks heraus. Sobald ein Primitive
nach App-Kontext greift, ist es keines mehr.
Ein Primitive, das über den Sammel-Export erreichbar ist und Radix berührt, trägt sein
eigenes `"use client"`: der Sammel-Export zieht sonst jedes Primitive in jeden Graphen, der
eines davon importiert.
### Zustände, die jede interaktive Komponente braucht
Ruhe · Hover · **Fokus** · deaktiviert · Ladezustand · Fehler.
Jeder Wechsel zwischen diesen Zuständen ist ein Übergang, kein Sprung, und läuft auf
`--dauer-sofort` (§11). Der Zustand selbst muss aber ohne die Bewegung lesbar sein: Wer
die Seite gerade neu geladen hat, sieht keinen Übergang und trotzdem den richtigen Zustand.
Fokus ist nicht verhandelbar: `:focus-visible` ist global auf `2px solid var(--accent)`
mit `2px` Versatz gesetzt. Wer ihn lokal entfernt, muss ihn lokal ersetzen.
---
## 6. Muster
*→ Live in der Galerie: `/design#muster` (BF-232).*
### Seitenkopf
`PageHeader` mit Eyebrow, Titel und optionalem Intro. Jede Seite hat genau einen.
### Leere Zustände
Ein leerer Zustand sagt **drei** Dinge: dass nichts da ist, ob das normal ist, und was man
tun kann. „Keine Daten" sagt keines davon.
Zwei verschiedene Leerzustände sind nicht derselbe:
- *„Sie haben noch kein Objekt angelegt"* → mit Handlungsaufforderung.
- *„Kein prüfpflichtiges Objekt vorhanden"* → **ohne**, denn das ist kein Problem, das der
Nutzer beheben soll.
### Zahlen in der Navigation
Nur dort, wo die Zahl eine Handlung auslöst: überfällig, unklar, offen. **Nie eine Null.**
Eine dauerhaft sichtbare `0` trainiert Leute darauf, die Zahlen zu überlesen, die zählen.
Eine Gesamtzahl ohne Handlung („47 Dokumente") ist Orientierung und bleibt grau.
### Berechtigungen in der Oberfläche
**Eine Oberfläche darf nichts anbieten, was der Server verweigert.** Wer Aufträge sehen,
aber keinen anlegen darf, sieht die Liste und *keinen* Anlegen-Knopf, nicht einen
deaktivierten. Ein ausgegrauter Knopf ist eine Frage ohne Antwort.
Umgesetzt in `lib/rbac.ts`: das Leserecht eines Bereichs und das Recht seiner Aktion sind
getrennte Einträge.
### Bestätigung vor destruktiven Aktionen
Löschen, Widerrufen, Entfernen fragen zurück und benennen dabei **konkret**, was passiert
(„Zugriff von Maria Huber auf Zieglergasse 14 entziehen?"), nicht „Sind Sie sicher?".
Die bestätigende Aktion trägt **`--danger`**, nicht `--urgent` (BF-385): Löschen ist ein
Eingriff in der Oberfläche, keine gesetzliche Frist.
---
## 7. Datenvisualisierung
*→ Live in der Galerie: `/design#dataviz` (BF-232).*
Ausführlich in der `dataviz`-Systematik; hier das für BWBH Festgelegte.
**Kein Diagramm ohne Frage, die es beantwortet.** Steht die Frage nicht in der Überschrift
oder Bildunterschrift, gehört das Diagramm nicht auf die Seite.
**Kategoriale Palette** — der generische Vorrat für Serien, die Identität tragen; gegen
die sechs Checks geprüft (Helligkeitsband, Chroma, CVD-Trennung, Normalsicht, Kontrast).
> **Das ist NICHT die Wellen-Palette.** Bis BF-385 stand hier „z. B. §128a-Welle", und
> §2 führte gleichzeitig `--chart-welle-*` mit ganz anderen Werten — zwei Tabellen, beide
> für dieselbe Sache beschriftet, und die falsche war die auffälligere. Die §128a-Wellen
> zeichnet das Produkt aus den Token in §2 „Diagrammfarben"; die vier Reihenfarben unten
> benutzt heute **keine** Ansicht. Sie sind der Vorrat für die erste, die mehrere
> gleichrangige Serien zeigen muss — und werden in dem Moment zu Token.
| | hell | dunkel |
| --- | --- | --- |
| 1 | `#2f5bff` | `#6b86f5` |
| 2 | `#0a9396` | `#12968f` |
| 3 | `#c2660a` | `#c67d1c` |
| 4 | `#a3348f` | `#c04fa8` |
Reihenfolge ist fest und wird **nie** durchrotiert. Eine neunte Serie bekommt keine
erzeugte Farbe, sondern wird zu „Sonstige" zusammengefasst.
**Statusfarben sind getrennt** von Serienfarben und werden nie als „Serie 4" recycelt. Sie
tragen immer Symbol **und** Beschriftung, nie Farbe allein.
**Nie zwei Y-Achsen.** Zwei Messgrößen unterschiedlicher Größenordnung werden zwei
Diagramme.
Legende ab zwei Serien immer vorhanden, dazu direkte Beschriftung. Identität hängt nie
allein an der Farbe.
---
## 8. Sprache
*→ Live in der Galerie: `/design#schreibweise` (BF-232).*
**Deutsch, förmliches „Sie", durchgängig.** Auch in Fehlermeldungen und Knopfbeschriftungen.
**Das Produkt heißt Bauwerksbuchhaltung, kurz BWBH.** Ausgeschrieben beim ersten Auftreten
in einem Text, danach BWBH. Nicht „die App", nicht „das Tool", nicht „BWB".
**Keine Geviertstriche.** Wo ein Gedanke abgesetzt gehört, tun es Komma, Doppelpunkt,
Klammer oder ein neuer Satz. Ein `—` mitten im Satz liest sich wie diktiert und nicht wie
geschrieben.
Die Regel stand hier seit jeher und wurde trotzdem 69-mal gebrochen (BF-396). Zwei Gründe,
beide lehrreich:
- **Der Wächter sah nur ein Viertel der Anwendung.** `e2e/typografie-und-verlauf.spec.ts`
prüfte vier Routen und nur `main` — Anmeldung, Registrierung und der Posteingang des
Fachpartners standen nicht darin, Seitenleiste und Bänder lagen ausserhalb von `main`.
Er meldete Ruhe für Bildschirme, die er nie angesehen hatte. Jetzt: mehr Routen, `body`
statt `main`, und ein eigener **anonymer** Block für die öffentlichen Seiten (mit
gespeicherter Sitzung sieht man `/login` nie so wie ein Neuer — die Lehre aus BF-389).
- **Der naheliegende Ersatz ist falsch.** „ — " durch „, " zu tauschen erzeugt im Deutschen
einen Kommafehler, sobald ein Hauptsatz folgt („Bitte wenden Sie sich an uns, wir klären
das gemeinsam"). Richtig ist **Punkt und Grossbuchstabe** als Vorgabe — das ist immer
grammatisch, auch wenn ein kurzes Satzfragment entsteht — und Komma, Doppelpunkt oder
Semikolon nur dort, wo wirklich eine Apposition folgt. Wer das automatisiert, liest
danach jede geänderte Zeile.
Das alleinstehende `—` als Platzhalter für „kein Wert" bleibt: das ist Typografie, keine
Prosa. Der Test unterscheidet die beiden über die Leerzeichen ringsum.
**Pflicht- und Wahlfelder sind BEIDE gekennzeichnet** (BF-396). Ein Stern in `--danger` am
Pflichtfeld, ein graues „(optional)" am freiwilligen, und einmal darunter die Legende „Mit *
gekennzeichnete Felder sind erforderlich". Drei Werte, EINE Quelle
(`registrierung.pflichtStern` / `.pflichtLegende` / `.optionalSuffix`) — dieselbe
Kennzeichnung taucht in Anmelde-, Kunden- und Fachpartnerformular auf, und dreimal getippt
sähe sie irgendwann verschieden aus. Genau das war der Fall: ein Feld trug „(optional)" fest
im deutschen Text und damit in Schwarz, alle anderen den grauen Zusatz. Dasselbe Wort in zwei
Farben liest sich als Bedeutungsunterschied.
Der Stern ist `aria-hidden`: das Feld trägt sein `required` bereits, ein Screenreader sagt
„erforderlich" ohnehin, und ein vorgelesenes „Stern" wäre die zweite, schlechtere Ansage.
**Bedienhöhen kommen aus dem System, nicht aus dem Gefühl.** `Button` (Vorgabe), `Input` und
`Select` stehen auf `h-10` (40 px); `Button size="sm"` auf `h-9`. Ein neues Bedienelement,
das über oder zwischen solchen Feldern sitzt, nimmt dieselbe Höhe. Ein Zwischenschritt mit
44 px (Apples Untergrenze für Tippziele) sass beim Reiterschalter der Anmeldung sichtbar zu
hoch — an dieser Stelle wiegt die Einheitlichkeit schwerer, und 40 px sind bei rund 200 px
Breite ein bequemes Ziel. Wo ein Ziel wirklich klein wäre (ein reiner Textlink), hilft
Polster statt einer Sondergrösse.
**Aller sichtbare Text lebt in `content/de.ts`**, nicht im JSX. Das ist keine Formalie: es
lässt Copy ändern, ohne Komponenten anzufassen, und macht sichtbar, wie das Produkt
eigentlich klingt.
**Ein Bedienelement sagt, was passiert.** „Auftrag zurückziehen", nicht „Absenden". Danach
bestätigt die Rückmeldung in derselben Sprache: „Auftrag zurückgezogen."
**Fehlermeldungen erklären zwei Dinge:** was schiefging und wie man es behebt. Keine
Entschuldigungen, keine Vagheit, keine technischen Innereien.
> „Das Passwort muss mindestens 12 Zeichen lang sein."
> nicht „Validierungsfehler."
**Fachbegriffe in der Sprache der Branche.** Objekt, Frist, Mangel, Prüfbericht,
Sachverständiger, Hausverwaltung. Nicht Entity, Deadline, Issue.
---
## 9. Barrierefreiheit
Nicht optional, und der größte Teil ist billig:
- **Fokus sichtbar**, überall (siehe oben).
- **Kontrast:** Fließtext mindestens 4.5:1, große Schrift und Bedienelemente 3:1.
- **Tastatur:** jede Aktion erreichbar; die Reihenfolge folgt der sichtbaren Anordnung.
Bei der Navigationszeile heißt das: Chevron → Beschriftung → Aktion.
- **Identität nie nur über Farbe.** Ein roter Punkt braucht ein Wort daneben.
- **Zugängliche Namen an Symbolknöpfen.** Ein Knopf mit reinem Symbol trägt ein
`aria-label`, das die Aktion benennt, nicht den Bereich („Objekt anlegen", nicht „Objekte").
- **`prefers-reduced-motion`** wird respektiert, seit BF-254 global in
`@bwb/ui/tokens.css` und damit in **beiden** Apps. Vorher stand die Zusage hier,
eingelöst war sie nur in `apps/web`; das Dashboard hat die Einstellung ignoriert.
Wie und warum auf `0.01ms` statt `0`: §11.
---
## 10. Die Navigationsleiste
Das jüngste und meistdiskutierte Muster (BF-222/BF-223), deshalb eigens festgehalten.
**Jede Bereichszeile hat drei getrennte Klickziele**, keine verschachtelten:
```
[ › ] [ Objekte 12 ] [ + ]
│ │ │
│ │ └── Hauptaktion des Bereichs
│ └─────────────────────── Bereich öffnen
└────────────────────────────────────── auf- und zuklappen
```
Der Grund, warum das drei Elemente sind und nicht ein Link mit Zusatzflächen: ein `<a>`
darf keine `<button>` enthalten, und ein einziges Ziel würde den Nutzer zwingen, erst eine
Listenseite zu besuchen, um dort den Knopf zu drücken, den er meint.
Weitere Festlegungen:
- Der Aufklappzustand wird **pro Browser** gemerkt (`localStorage`). Beim ersten Besuch ist
nur der Bereich der aktuellen Seite offen. Alles aufzuklappen macht aus neun Einträgen
eine Wand aus dreißig Links, also genau das, was das Zuklappen verhindern soll.
- Die Aktion prüft ihr **eigenes** Recht, getrennt vom Leserecht des Bereichs.
- Die Marke steht oben, darunter der Mandant (er rahmt alles Weitere), unten das Konto.
- Das **Impersonation-Banner** bleibt ein voll breiter Streifen über dem Inhalt und
wandert unter keinen Umständen in ein Menü. Es ist der einzige sichtbare Hinweis, dass
gerade in fremdem Namen gehandelt wird.
---
## 11. Bewegung
*Angehängt statt eingeordnet: eine neue §3 hätte alles danach verschoben und damit die
Anker gebrochen, auf die die Galerie zeigt (`components/design/design-md.ts`).*
### Wozu Bewegung da ist
**Bewegung erklärt eine Veränderung, sie schmückt sie nicht.** Sie beantwortet genau zwei
Fragen: *Woher kam das?* und *Gehört das zusammen?* Ein Dialog, der aus dem Nichts
erscheint, zwingt zum Suchen; einer, der kurz aufzieht, sagt, dass er über allem liegt und
wieder verschwindet. Wo eine Bewegung keine dieser beiden Fragen beantwortet, gehört sie
weg.
Daraus folgt der Rest fast von selbst. BWBH wird bedient, nicht bestaunt (§1). Wer
fünfzig Objekte durchsieht, sieht dieselbe Animation fünfzig Mal, und was beim ersten Mal
elegant war, ist beim zwanzigsten Mal Wartezeit.
### Die eine Kurve, die drei Dauern
Definiert in `@bwb/ui/tokens.css`:
| Token | Wert | Wofür |
| --- | --- | --- |
| `--ease-out` | `cubic-bezier(0.22, 1, 0.36, 1)` | **Die einzige Kurve.** |
| `--dauer-sofort` | `120ms` | Zustand am Element selbst: Hover, Fokus, Aktiv |
| `--dauer-normal` | `200ms` | Ein- und Ausblenden, Aufklappen, Wechsel im Inhalt |
| `--dauer-ruhig` | `320ms` | Flächen, die den Blick führen: Dialog, Panel |
`--ease-out` beschleunigt sofort und bremst aus. Eine Oberfläche, die auf einen Klick
antwortet, soll sich bereits bewegt haben und dann zur Ruhe kommen, nicht erst anfahren.
Ein Ease-in lässt jede Reaktion träger wirken, als sie ist.
**Jede Tailwind-`transition-*`-Klasse trägt diese Kurve und 200 ms bereits von selbst**
(`--default-transition-timing-function` und `--default-transition-duration` im
`@theme`-Block). Wer nichts angibt, liegt also richtig. Eine Dauer wird nur genannt, wenn
sie von 200 ms abweicht.
Drei Stufen sind bewusst grob. Ein Zwischenwert löst keine Gestaltungsfrage, er erzeugt
nur eine weitere Variante, die niemand später wiederfindet.
### Was sich bewegen darf
Nur `opacity` und `transform`. Beide laufen auf dem Compositor und kosten kein Layout.
**Nicht animiert werden `height`, `width`, `top` und Verwandte.** Jeder Frame erzwingt ein
neues Layout, und auf einer Liste mit vielen Zeilen ruckelt genau das. Die eine Ausnahme
ist das Aufklappen des Accordions, wo Radix die Zielhöhe als Variable liefert und die
Alternative eine gemessene Höhe im JavaScript wäre.
### Was sich nicht bewegt
**`--urgent` blinkt nicht und pulsiert nicht.** Eine verstrichene gesetzliche Frist ist ein
Zustand, keine Aufmerksamkeitsheischerei. Etwas, das blinkt, wird nach zwei Tagen
weggesehen, und §2 hat bereits festgehalten, warum wir uns das bei Rot nicht leisten
können.
**Zahlen in der Navigation bewegen sich nicht bei jedem Rendern.** Eine Zählerbewegung
sagt „hier ist gerade etwas passiert". Läuft sie auch, wenn nichts passiert ist, sagt sie
das Gegenteil von dem, was sie soll.
**Statutorische Zustände tragen ihre Aussage nie in der Bewegung.** Dieselbe Regel wie bei
der Farbe (§2, §7): Bewegung ist immer die Zugabe zu Beschriftung und Form, nie ihr
Ersatz. Wer die Animation nicht sieht, weil er die Seite gerade neu geladen hat, mit
reduzierter Bewegung arbeitet oder schlicht wegsah, muss denselben Sachverhalt aus dem
Text ablesen können.
### Bewegungsreduktion
`prefers-reduced-motion: reduce` wird in `@bwb/ui/tokens.css` global beantwortet und gilt
damit für **beide** Apps.
Dauern gehen dort auf `0.01ms`, nicht auf `0`. Eine Dauer von exakt null unterdrückt in
manchen Browsern das `transitionend`- und `animationend`-Ereignis, und Radix hängt sein
Aufräumen daran: Die Komponente bliebe im Übergangszustand stehen. `0.01ms` ist für das
Auge sofort und feuert das Ereignis trotzdem.
Das ist keine Abwertung der Einstellung, sondern ihre Erfüllung: Wer sie setzt, will keine
Bewegung sehen, nicht eine kaputte Oberfläche bekommen.
### Übergänge aus fremder Quelle
[transitions.dev](https://transitions.dev/) ist eine Sammlung fertiger CSS-Rezepte (MIT,
keine Laufzeitabhängigkeit, Installation als Agent-Skill). Als **Bezugsquelle** brauchbar,
als Baustein nicht: Es gibt nichts einzubinden, was danach von allein wirkt.
Übernommen wird nur, was den Regeln oben genügt, und jede Übernahme wird auf die Tokens
dieses Systems umgeschrieben statt mit eigenen Dauern und Kurven danebengelegt. Sonst
entsteht ein zweites Bewegungsvokabular neben diesem, und das ist genau der Zustand, den
BF-230 bei den Farben gerade erst beendet hat.
---
## Verwandtes
- `/design`: die klickbare Galerie (BF-232). Erreichbar **lokal ohne Anmeldung**, auf
**Staging und in Vorschauen mit jeder angemeldeten Sitzung**, in der **Produktion nur
für `neuklick_operator`** (`admin:access`). Die mittlere Stufe kam nach, weil die Galerie
ausgerechnet dort mit 404 antwortete, wo an der Designsprache weitergearbeitet wird;
die Weiche steht in `apps/dashboard/app/(design)/layout.tsx`.
- `apps/dashboard/lib/rbac.ts`: Navigation und Rechte, eine Quelle
- `apps/dashboard/lib/create-intents.ts`: was angelegt werden kann, von wem, wohin
- `docs/knowledge/`: fachliche Vertiefung je Bereich
---
## Marke (BF-394)
Das Zeichen heißt **Kern**: eine volle Fläche mit ausgespartem Fenster und einem
zurückgenommenen Kern. Es ersetzt den Platzhalter — das weiße B im blau gerundeten
Rechteck —, der bis dahin an jeder sichtbaren Stelle stand.
### Die Maße
Drei konzentrische Quadrate auf einer Kantenlänge von 156. **Es wird nie nachgezeichnet,
sondern aus diesen Maßen gesetzt.**
| Element | Maß | Anteil | Farbe |
| --- | --- | --- | --- |
| Fläche | 156 × 156, Radius 3 | 1,000 | Zeichenfarbe |
| Fenster | 68 × 68, Radius 2, zentriert | 0,436 | Grund |
| Kern | 42 × 42, Radius 2, zentriert | 0,269 | Zeichenfarbe bei 50 % |
#### Die vierte Kopie: der Mailkopf (BF-482)
E-Mail-HTML kann weder SVG noch die Primitiven aus `@bwb/ui` — der Mailkopf **zeichnet das
Zeichen mit verschachtelten Tabellenrahmen nach** (`packages/email/src/templates.ts`). Das
ist die eine erlaubte Nachbildung; sie existiert, weil es keine Alternative gibt.
⚠️ **Was daran schiefging, und wie es zu erkennen ist.** Der Kasten stand bei 32 px neben
15 px Schrift. Die Grössenregel lautet: **Kastenhöhe = Versalhöhe der Wortmarke =
0,675 × Schriftgrösse**, also 10 px — das Zeichen war gut dreimal zu gross und las sich als
Symbol MIT Schriftzug daneben statt als eine Wortmarke. Die INNEREN Verhältnisse waren
dabei die ganze Zeit richtig (0,250 / 0,438 gegen Soll 0,269 / 0,436); falsch war
ausschliesslich der Massstab.
Jetzt 20 px Schrift, 14 px Kasten: `4 (Fläche) + 1 (Fenster) + 4 (Kern) + 1 + 4`. Ganze
Pixel sind Pflicht — Rahmenbreiten in Mail-HTML sind ganzzahlig, und ein halber Pixel wird
je Programm anders gerundet. Der Kern trägt `KERN_DECKUNG` **eingebrannt**, weil
Teiltransparenz in Mailprogrammen unzuverlässig ist: Akzent zu 50 % auf Weiss ist `#8da6ff`.
⚠️ **Die Fassung muss der Seitenleiste folgen.** Beide zeigen `positiv`; wer die eine
wechselt, wechselt die andere mit, sonst tragen Anwendung und Mail zwei verschiedene Marken.
Die Farbe im Mailkopf ist die einzige Stelle, an der sie auseinanderlaufen können, ohne dass
etwas bricht.
⚠️ **Und die Lehre über den Wächter.** `apps/dashboard/e2e/mail-marke.spec.ts` gab es seit
BF-398 und war die ganze Zeit grün: er misst die vier Ränder, also ob Fenster und Kern
**mittig** sitzen. Sie sassen mittig — bei jeder Grösse. *Ein Zeichen kann perfekt zentriert
und dabei dreimal zu gross sein.* Die Markenregel ist eine BEZIEHUNG zur Schrift daneben,
und eine Beziehung misst man nur an beiden Seiten; der Test hat dafür jetzt einen zweiten
Fall.
| Ring | 13 breit | 0,083 | Grund |
**Der Kern steht bei genau 50 Prozent — gerechnet, nicht gewählt.** Er muss gegen zwei
Seiten gleichzeitig bestehen: gegen das helle Fenster und gegen die Fläche. Bei 50 % steht
er 2,33:1 gegen das Weiß und 2,54:1 gegen die Fläche, und das ist der Bestwert — heller
schwächt die eine Seite, dunkler die andere. Die zuerst gezeigten 34 % lagen bei 1,75:1
gegen das Weiß; in Kleingrößen war der Kern damit weg.
Der Radius ist mit 1,9 % bewusst fast nichts. Der alte Platzhalter lag bei 25 % und wirkte
deshalb wie ein Symbol aus dem Baukasten.
**Im Produkt ist das Fenster ausgespart, nicht weiß gefüllt** (`fill-rule="evenodd"`).
Damit trägt das Zeichen automatisch den Ton der Fläche, auf der es liegt, und die Farbe
läuft über `currentColor` statt über drei Fassungen im Quelltext. Nur in freistehenden
Dateien — Favicon, Vorschaubild, Bildwelt — wird das Fenster ausdrücklich gefüllt: dort
gibt es keinen Grund, den es annehmen könnte.
### Die eine Größenregel
**Die Kastenhöhe ist immer gleich der Versalhöhe der Wortmarke.** Funnel Display hat laut
OS/2-Tabelle eine Versalhöhe von 810 auf 1200 Einheiten, also **0,675 der Schriftgröße**.
Bei 20 px Schrift ist der Kasten 13,5 px hoch.
Es gibt keine zweite Größe. `Mark` und `Logo` nehmen deshalb **keine eigene Größe
entgegen** — sie leiten sie aus der Schriftgröße ab, und die setzt der Aufrufer über eine
gewöhnliche Textklasse. *Wer eine feste Pixelgröße setzen kann, setzt irgendwann eine
falsche.*
- Abstand Zeichen zu Wort: **0,30 × Kastenhöhe**, in beiden Sperrungen derselbe Anteil.
Er hängt am Kasten, nicht an der Versalhöhe: der Kasten ist eine volle Fläche und drängt
anders an die Buchstaben als eine Kontur.
- Gestapelt: Kastenhöhe = Höhe des zweizeiligen Wortblocks (zwei Versalhöhen plus
Zeilenabstand).
- Schutzraum: eine volle Kastenhöhe rundum.
- Untergrenzen: Zeichen allein ab 16 px, Wortmarke ab 120 px Breite, im Druck ab 32 mm.
#### Die Falle beim Ausrichten
**Der Kasten wird an den Versalien ausgerichtet, nicht an der Zeilenbox.** Funnel Display
hat einen Ascender von 1200 und einen Descender von 300 auf 1200 Einheiten. Setzt man die
Zeilenhöhe auf die Versalhöhe, liegt die Grundlinie **0,0375 em unter** der Unterkante der
Zeilenbox. Wer den Kasten gegen die Zeilenbox zentriert oder bündig setzt, setzt ihn genau
um diesen Betrag zu hoch — bei 40 px Schrift um 1,5 px, und das sieht man.
Richtig: den Text um 0,0375 em anheben und beide am oberen Rand ausrichten. Danach fällt
die Oberkante des Kastens auf die Versalhöhe und die Unterkante auf die Grundlinie.
Der Wert hängt an den Metriken **dieser** Schrift. Wird die Wortmarke je getauscht, wird er
neu aus der Fontdatei gelesen und nicht geschätzt.
### Fassungen
Eine Konstruktion, vier Farbpaare. **Eine fünfte entsteht nicht dadurch, dass jemand eine
Farbe tauscht.**
| Fassung | Zeichenfarbe | Grund | Wo |
| --- | --- | --- | --- |
| Positiv | Akzent | Karte oder Seitengrund | heller Grund, Standard |
| Tinte | Tintenton | Karte oder Seitengrund | Dokument, Rechnung, Mail |
| Negativ | Papierton | Nachtbraun | dunkles Schema, Bildwelt |
| Einfarbig | die eine Farbe | Bedruckstoff | Gravur, Prägung, Stempel, 1c-Druck |
**Einfarbig ist die einzige Fassung mit vollem Kern.** Gravur, Prägung und einfarbiger
Druck können keinen Tonwert ohne Raster, und ein gerasterter Kern in dieser Größe wird zu
Schmutz. Überall sonst gelten die 50 %.
### Sperrungen
Drei, keine vierte: **waagrecht**, **gestapelt**, **nur Zeichen**. Die Kurzform BWBH gibt
es nicht — der Name wird nicht abgekürzt.
**Die Seitenleiste trägt die gestapelte Fassung.** Die Versalien machen den Namen rund ein
Drittel breiter als die frühere Gemischtschreibung: waagrecht bräuchte er bei 15 px etwa
215 px, und `w-64` lässt nach Innenabstand rund 224 px. Zu knapp, um es stehen zu lassen.
### Die 16-Pixel-Fassung ist eine eigene Zeichnung
Aus dem 156er-Raster skaliert fielen Fenster und Kern auf 6,97 und 4,31 px — beides mitten
zwischen zwei Pixel, und beides verwischt zu Grau. Die 16er wird deshalb **auf ganze Pixel
gesetzt**: Fenster 8, Kern 4, Ring 2. Die Anteile weichen minimal ab (0,500 statt 0,436
und 0,250 statt 0,269) — dafür steht das Zeichen im Reiter gestochen. Das ist die eine
erlaubte Abweichung, und sie ist im Erzeugungslauf als solche kommentiert.
### Fehlanwendungen
- Das Zeichen nachzeichnen, statt es aus den Maßen zu setzen.
- Eine feste Pixelgröße am Zeichen setzen, statt die Schriftgröße zu setzen.
- Den Kasten gegen die Zeilenbox ausrichten (siehe oben: 0,0375 em zu hoch).
- Den Kern auf einen anderen Wert als 50 % stellen — außer in der einfarbigen Fassung.
- Eine fünfte Fassung durch Farbtausch erzeugen.
- Den Namen abkürzen.
- Das Fenster im Produkt weiß füllen, statt es auszusparen.
- Die 16er aus dem großen Raster skalieren.
### Die Dateien entstehen aus dem Quelltext
`pnpm marke` erzeugt sämtliche Ableitungen nach **`apps/dashboard/public/marke/`** (von
dort bietet `/design` sie an) und legt die Symbolstrecken beider Apps gleich mit an ihre
Plätze. Der Lauf leert zuerst und liefert zweimal hintereinander dasselbe Ergebnis.
**Die Sperrungen gibt es nur als SVG.** Ein PNG davon backt die Schrift des erzeugenden
Rechners ein — auf einem ohne Funnel Display, und das ist jeder CI-Läufer, entstünde eine
eingecheckte Bilddatei mit **fremder** Wortmarke. Das ist genau der Weg, auf dem ein
zweites Zeichen in die Welt kommt. Wer ein Rasterbild braucht, exportiert es aus dem SVG
auf einem Rechner mit der Schrift oder wandelt vorher in Pfade. Die Zeichenfassungen ohne
Text bleiben als PNG — dort gibt es keine Schrift.
**`favicon.ico` gehört dazu, obwohl `icon.svg` moderner ist.** Browser fragen den
klassischen Pfad unabhängig weiter ab; fehlt die Datei, gibt es eine 404 — und ein
fehlgeschlagener Favicon-Abruf wird hartnäckig zwischengespeichert. Sie enthält 16, 32 und
48, wobei die 16er aus ihrer eigenen Zeichnung kommt und nicht aus dem Raster skaliert ist.
**Was nicht als Datei greifbar ist, baut beim nächsten Angebot, beim nächsten Deck und bei
der nächsten Druckerei jemand nach — und ab dann gibt es zwei Zeichen.**
Die Maße stehen in **`packages/ui/src/marke-geometrie.ts`** — einer Datei ohne JSX, damit
sowohl das Primitive als auch der Erzeugungslauf sie einlesen können. Eine Zahl, die an
zwei Stellen steht, ist eine Zahl, die irgendwann an einer Stelle geändert wird; genau das
war im ersten Entwurf der Fall, während zwei Kommentare das Gegenteil behaupteten.
### Der Versatz ist eine Funktion, keine Zahl
`versatzFuer(zeilenhoehe)` rechnet ihn aus den Schriftmetriken. Der erste Entwurf notierte
den Wert der waagrechten Sperrung (0,0375 em) als Konstante und benutzte ihn in der
gestapelten mit — dort gilt eine andere Zeilenhöhe, und richtig sind 0,0975. Die erste
Zeile stand deshalb 0,22 em zu tief.
Ebenso ist die Zeilenhöhe der gestapelten Sperrung nicht frei: die Kastenhöhe soll die
Blockhöhe sein, und die ist `Zeilenhöhe + Versalhöhe`. Mit 0,795 ergibt das 1,47 em = zwei
Versalhöhen plus 0,12. Eine andere Zeilenhöhe bricht die Kastenformel — unsichtbar.