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/uiund 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, Laufweite0.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 mittabular-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 und0sein 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.5remist der Standardradius.--radius-smfür Chips und Symbolknöpfe,--radius-lgfür Dialoge. Nichts ist vollständig rund außer Avataren.- Abstände kommen vom Layout, nicht vom Element. Geschwister werden mit
flex/gridplusgapgesetzt, nicht mitmarginpro Element. Margins kollabieren still oder verdoppeln sich,gapnicht.
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-fullhindert 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 selbstblock+overflow-x-auto. - Ein Importpfad in einem Flex-Element schrumpft nicht.
CopyPathsteht in jedem Specimen; ohnemax-w-full min-w-0undbreak-allam Text schob er allein die ganze Seite in den Querscroll.break-allund nichttruncate: 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.tsprüfte vier Routen und nurmain— Anmeldung, Registrierung und der Posteingang des Fachpartners standen nicht darin, Seitenleiste und Bänder lagen ausserhalb vonmain. Er meldete Ruhe für Bildschirme, die er nie angesehen hatte. Jetzt: mehr Routen,bodystattmain, und ein eigener anonymer Block für die öffentlichen Seiten (mit gespeicherter Sitzung sieht man/loginnie 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-motionwird respektiert, seit BF-254 global in@bwb/ui/tokens.cssund damit in beiden Apps. Vorher stand die Zusage hier, eingelöst war sie nur inapps/web; das Dashboard hat die Einstellung ignoriert. Wie und warum auf0.01msstatt0: §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 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ürneuklick_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 inapps/dashboard/app/(design)/layout.tsx.apps/dashboard/lib/rbac.ts: Navigation und Rechte, eine Quelleapps/dashboard/lib/create-intents.ts: was angelegt werden kann, von wem, wohindocs/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.