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 h1h3, 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 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.