00Contents
  1. 01Principles
  2. 02Logo
  3. 03Colour
  4. 04Type
  5. 05Surfaces and rules
  6. 06Density
  7. 07Motion and layers
  8. 08Components
  9. 09Voice
  10. 10Downloads

Design language · Version 3.1 · 04.10.2026

Paper, ink and one burgundy.

This is how the BauLynq app looks and behaves — Designsprache v3, the drafting studio. Every value on this page is read from frontend/src/index.css and shown as a live specimen, in light and dark.

Typeface

Geist

13 px UI body · Geist Mono for identifiers

Themes

5

light · dark · material · contrast · dim

Radii

8 · 6 · 4

sheet · control · tag, in pixels

Controls

32 px

buttons, fields and table rows

01 · Principles

A drafting studio, not a brochure.

BauLynq is a tool for people who check numbers. The skin is warm paper, hairlines and one ink — quiet enough that the figures and the status carry the screen.

The studio

Decided with the owner on 2026-10-03. It borrows the drafting-table look of the KEHBIMVISION tools and keeps BauLynq's own colours. It covers the app at app.baulynq.com only — the marketing site and the promo films have their own identity (see Downloads).

Paper, not white

The canvas is warm paper (#f7f2ea); the sheets that carry work sit on it in off-white (#fffdf9). Dark is warm graphite, never blue-black.

Rules, not shadows

An edge is the ink mixed into transparent at one of six fixed strengths. Shadows are kept for things that float: menus, dialogs, the palette.

Quiet figures

13 px body, two ink tiers, tabular figures. Colour appears only when it means something — a status, an action, a focus.

Three colours, three jobs

The part most likely to be gotten wrong. Burgundy is the action and the place you are. Teal is where your attention is. Neutral ink is the value you picked. Ask of anything “selected”: is it a place (a page, a tab) or a value (a filter, a row)?

Burgundy — the action and the place

primary #8E2222, #E5726A on dark. The one primary action per view (the create or commit button) and the ⌘K search — and, since 2026-10-03, the selected page or tab: the active rail row, the tab and its underline, the selected view switch. Money, decision and blockage may use it as text or tint.

Neu anlegenEntscheidung offen

Teal — focus and “changed”

accent #0e6e7e, #5cc3cf on dark. The focus ring, text selection, links and “changed” markers; the edge of a toggled-on filter chip. Never an action, never a page.

FokusLink: Beleg öffnen
Status: Offen

Neutral — a chosen value

A selected row or card, an active filter, a checked box, a toggle: a neutral tint of the ink (8 %) and weight 500. Hover is 5 %. A value you picked, not a place you are.

AR-2026-010Mai289.410 €
AR-2026-011Juni317.854 €
Colour otherwise means status only — soft tints through SoftBadge tone={toneForStatus(…)}, never full-opacity badges. The ring is in Colour.

Place or value?

One table settles every “selected” in the product. The tab recipe lives in one file, components/tabStyles.ts (tabClass, DAISY_TAB_ACTIVE, SEGMENT_*), and the design guard allowlists the files that carry it. The burgundy token flips to the AA-lifted value in the dark family, so nothing hard-codes a hex.

What is selectedRecipeColour
Rail row, mobile nav item, module or section link, contents rowbg-primary/10 text-primary font-medium; the rail adds a 2 px barBurgundy
Page tab — underline strip, masthead tabs, SectionTabstext-primary font-medium and a 2 px inset underline; inactive ink-2, hover inkBurgundy
Sheet tab — the LV's trade tabsLifted to the sheet, burgundy label and 2 px underline, a neutral count chipBurgundy
View switcher — SegmentedToggle, tone="nav" (default)Raised segment on a recessed track, label text-primary font-mediumBurgundy
Filter or form value — tone="choice", a filter chipRaised segment with an ink label; a chip that is on takes the 8 % tint and border-accent/50Neutral, teal edge
Selected row or card, checkbox, toggle, radiobg-base-content/[0.08]Neutral
Focus ring, link, text selection, “changed” markerThe global 2 px ring, underline, 25 % tintTeal

One primary per view

A view has one filled burgundy button — the way to create the thing the view lists. An alternative route to the same result (an import next to “Neu”) is the flat outline.

Do

Neue RechnungImportieren
  • One primary, leftmost in the toolbar.
  • Neutral actions are the outline with a leading icon.
  • Burgundy for the selected page or tab — a place.

Don't

Neue RechnungImportieren
  • Two filled burgundies: neither one says “this is the main thing”.
  • A burgundy filter chip, row or card to mean “selected”: that picks a value, not a place.

Rules that do not bend

Do

  • Reach for a token by its job, never restate a percentage.
  • Show a status as a tone: SoftBadge tone=, with its word.
  • Lead a screen with a sentence about the work: “Fünf Dinge blockieren Geld.”
  • Set identifiers in .bl-id; money in Geist, tabular, no decimals in a list.
  • Design for all five themes and for a 390 px phone.

Don't

  • Add a third grey. Quietness comes from size and weight.
  • Hand-roll hex, shadows or bg-white; no font-mono.
  • Paint a selected filter, row or card burgundy — or use border-primary/30 or bl-soft-primary as a selected state.
  • Use rounded-full beyond avatars, dots and count badges.
  • Nest a card in a card: one sheet per page, sections divided by a hairline.
  • Animate a figure someone is reading.

03 · Colour

Warm paper, two inks, one burgundy.

Colour is delivered as DaisyUI theme variables plus a handful of product tokens. All values below are the live ones; the specimens are painted from them.

Surfaces — paper, sheet, hairline, ink

Three surfaces and one ink. The canvas is not white — which is why a page sheet needs only a 1 px edge. base-300 is the opaque warm hairline for the few dividers that must not be translucent; almost every edge is a rule token (Surfaces and rules).

Light — warm paper

PaperCanvas#f7f2ea

base-200, --bl-surface

SheetSheet#fffdf9

base-100: page, cards, dialogs

LineHairline#e6e0d4

base-300

InkInk#16161a

base-content

Dark — warm graphite

PaperCanvas#141311

base-200, --bl-surface

SheetSheet#1c1b19

base-100

LineHairline#35322d

base-300

InkInk#eeeae3

base-content

The five themes

light is the default. material shares its paper (and has softer chart hues). dark is warm graphite. contrast is true black with white ink; dim is a muted slate. The dark family lifts burgundy to #E5726A (dim: #EE8880) so it clears AA as text on its sheet, and puts near-black ink on every bright fill. The selected rail row and tab read the token, so they lift with it. Flip the specimen below through all five — the page theme is the sun / moon in the bar.

Specimen theme

InrX · Rechnungen · In Prüfung

Drei Rechnungen warten auf Freigabe.

Zuletzt geändert von Markus Kern · Beleg AR-2026-011 öffnen

Neue Rechnung Exportieren Abbrechen
FreigegebenIn PrüfungÜberfälligEntwurfBlockiertGemessen
ÜbersichtPositionenVerlauf
AR-2026
Variablelightdarkmaterialcontrastdim
primary#8E2222#E5726A#8E2222#E5726A#EE8880
accent#0E6E7E#5CC3CF#0E6E7E#5CC3CF#5CC3CF
base-100 sheet#FFFDF9#1C1B19#FFFDF9#000000#232830
base-200 paper#F7F2EA#141311#F7F2EA#111111#1C2128
base-300 hairline#E6E0D4#35322D#E6E0D4#2A2A2A#2F3641
base-content ink#16161A#EEEAE3#16161A#FFFFFF#E6E9EE
success#006133#41BB88#006133#17C964#17C964
warning#985E00#EFAF4C#985E00#F5A524#F5A524
error#B83B36#F77469#B83B36#F31260#F77469
info#0972B2#51A9EF#0972B2#14B4FF#14B4FF

Light's signal colours are pulled down in lightness so raw text-success, text-warning and text-error clear 4.5 : 1 on the sheet. The dark family uses brighter hues and near-black *-content on every fill (#0A0B0D; warning #150A00).

Two ink tiers — and no third

Tier 1 is the ink; tier 2 is the AA-safe quiet grey. Quietness comes from size and weight, never from more grey. In class names: text-base-content (or text-ink) and text-ink-2. Never an opacity grey below /70 on text; the legacy /40…/65 classes are remapped to tier 2 in the stylesheet so they stay legible.

Light

Abschlagsrechnung Mai 2026

Rohbau Nord · Freigabe am 12.06.2026 durch M. Kern

TierValueon sheeton paper
1 · ink#16161a17.8 : 116.2 : 1
2 · ink-2#5b574f7.1 : 16.5 : 1

Dark

Abschlagsrechnung Mai 2026

Rohbau Nord · Freigabe am 12.06.2026 durch M. Kern

TierValueon sheeton paper
1 · ink#eeeae314.4 : 115.5 : 1
2 · ink-2#a8a2996.8 : 17.3 : 1

Action and focus colours against the sheet: burgundy 8.6 : 1 (light) and 5.7 : 1 (dark); teal 5.8 : 1 (light) and 8.3 : 1 (dark). A selected rail row is burgundy on its own 10 % tint over the paper: 6.6 : 1 (light), 5.4 : 1 (dark), 5.6 : 1 (dim, #EE8880).

The status ring

Six tones, one meaning each (lib/tone.ts). A tone is a 3 px tick at the left of a row (--bl-tone-*) and, where a column reports it, a pill on the matching tint (--bl-tint-*). Beton is a marker colour only — legible as a 3 px bar, not as 12 px text — so printed text uses --bl-tone-beton-text. In the dark family tints are low-alpha mixes of the tone itself.

ToneMeansLight · tick / tintDark · tickLightDark
gruenfreigegeben, bezahlt#006133
#E8FCEE
#41BB88 FreigegebenFreigegeben
bernsteinin Prüfung, Frist läuft#985E00
#FFF3DE
#EFAF4C In PrüfungIn Prüfung
rotüberfällig, abgelehnt#B83B36
#FFEDE9
#F77469 ÜberfälligÜberfällig
betonEntwurf, kein Status#CBC5BA
#EFEADF
#4A463F
text #5B574F / #A8A299
EntwurfEntwurf
oxGeld, Entscheidung, Blockade#8E2222
#F2DEDE
#E5726A
dim #EE8880
BlockiertBlockiert
inka measured fact#16161A
#E3E7EE
#EEEAE3 GemessenGemessen

Dark tints: color-mix(in oklab, tone 16%, transparent) — beton mixes #9BA2AB at 12 %, ink mixes #F2F4F6 at 10 %. Dim lowers the rot tint to 12 % so error text on it stays 4.5 : 1. Neutral (beton, ink) means “not asked / not granted / a plain fact” — never merge it with a status.

Chart series

Six categorical slots, desaturated and distinct from the status colours so charts read as a separate data layer. Fixed order, never cycled — only neighbours touch. A status colour is never “series 4”; marks carry colour, label text wears ink. material has its own softer six (#57927D, #5F8EA9, #B1935A, #AF6B64, #8773A9, #6E8D7E); contrast and dim share one muted set (#8DB5A7, #92B2C5, #CAB68F, #C99A96, #ADA0C5, #9CB1A7).

Light

1#A22626
2#0E6E7E
3#007A4B
4#4A6FA5
5#9B6000
6#9A5B8F

Dark

1#D8574D
2#5CC3CF
3#19A572
4#7EA8E8
5#C57E00
6#C58AB9

04 · Type

One face, one density.

13 px is the UI body: a tool, not a brochure. Three weights, one scale, figures that line up.

Faces and weights

Geist (variable, Latin + Latin-extended, so äöüß and € are real glyphs) carries the voice and the figures. Geist Mono carries identifiers and nothing else (.bl-id; never font-mono). Both are self-hosted from /fonts — no third-party font request, which is also why the CSP can stay at font-src 'self'. Body text is set at letter-spacing: -0.005em with Geist's ss01 and cv11 alternates.

Geist

Aa Bb Cc Äö Üß € 0123456789

Die Schlussrechnung ist freigegeben und zahlbar.

Geist Mono — identifiers

AB-2026-033
OZ 02.140

R-2026-0147 · SHA-256 · /inrx/rechnungen

WeightSampleWhere
400RegularBody text, field values, table cells.
500MediumHeadlines, titles, buttons, badges, figures, the active tab or row.
600SemiboldKickers and table heads (11 px, uppercase, 0.06 em).

The scale

Tailwind's names are re-mapped, so text-sm is the UI body. Arbitrary sizes on the same scale are allowed: 11 / 12 / 13 / 14 / 15 / 18 / 22 / 24 px.

11 pxbl-kicker · tagsOffen zur Freigabe — labels, badges, table heads, keycaps
12 pxtext-xs · bl-metaZuletzt geändert von Markus Kern — notes, hints, dense (btn-xs) controls
13 pxtext-sm · bodyDie Schlussrechnung ist freigegeben und zahlbar — buttons, fields, table cells
14 pxtext-baseRechnung suchen oder Befehl eingeben — the palette input, running text
15 pxtext-lg · bl-titleNachtrag anlegen — section and dialog titles, 500
18 pxtext-xlDrei Rechnungen warten — sub-headline, phone masthead
22 pxarbitrary317.854 € — stat figures
24 pxtext-2xlFünf Dinge blockieren Geld. — the page headline

Figures and identifiers

The face is chosen by what a string is, not by whether it contains digits. Money and quantities are values and belong to the sentence around them: Geist, tabular-nums, no decimals in a list — cents only where a document reconciles (an invoice total, the Mengen grid). Identifiers are read character by character: Geist Mono, .bl-id.

Nr.BezeichnungNetto, in a listTotal, on the invoice
AR-2026-011Abschlagsrechnung Juni317.854 €317.854,20 €
AR-2026-010Abschlagsrechnung Mai289.410 €289.409,85 €
NT-2026-007Nachtrag Fassade17.760 €17.760,00 €
A kicker is Geist 600, 11 px, 0.06 em, uppercase, ink-2 — not mono. It is the section label and the masthead's scope line; add burgundy only for that scope line.

05 · Surfaces and rules

Rules, not shadows.

The app is a paper canvas, a rail on it, and one sheet beside the rail: topbar and page together. A 1 px edge, an 8 px radius, a barely-there lift. Sections divide with a hairline, not with more cards.

The shell — canvas, rail, one sheet

The root is the paper canvas (--bl-surface = base-200). The rail sits on it: no fill, no right border. Beside the rail is one sheet (.bl-sheet: sheet colour, 1 px --bl-rule, 8 px radius, the sheet shadow, 6 px in from the canvas edge) that holds both the topbar, on a bottom hairline, and the scrolling page. The page frame inside it — PageSurface, DataTable — is flat: it draws no card of its own, and its sections divide with border-t. Below lg (1024 px) there is no rail and no margin: the sheet is the whole screen and a four-tab bottom nav takes over.

  • Rail 208 px · 56 px collapsed
  • Sheet 6 px from the canvas
  • Topbar 48 px · 56 px below lg
  • Page gutter --page-gutter 14 px · 0 below lg

Radii — 8, 6, 4

Pick by role, not by taste. rounded-xl and rounded-2xl are re-mapped to 8 px so a stray one cannot out-round the shell. rounded-full is for avatars, dots and count badges only; rounded-3xl (24 px) is the mobile bottom sheet's top edge only.

8 px — sheetrounded-lg · page sheet, card, dialog, popover, menu
6 px — controlrounded-md · buttons, fields, selects, tabs' chips
ZeileSuchen
4 px — tagrounded / rounded-sm · badges, keycaps, checkboxes
Freigegebenesc

The rule tokens

Every edge is the ink (--color-base-content) mixed into transparent at a fixed strength, so one definition serves every theme. Reach for a token by its job — never restate the percentage at a call site. The line in each row is the token itself, drawn at 1 px.

--bl-rule-faint7 %Table rows, toolbar rule, the gaps in a figure strip.
--bl-rule10 %Sheet edges, dividers, dialog header and footer hairlines.
--bl-rule-card12 %A clickable sheet (bl-card) at rest.
--bl-rule-button18 %The outline button's edge.
--bl-rule-hover30 %A card or outline button under the pointer.
--bl-rule-control50 % · 38 % darkInputs, selects, checkboxes — 3 : 1 against sheet and paper.
Hover states follow the same ladder: cards darken their rule (no shadow), rows take bg-base-content/[0.05], outline buttons take base-200 and the hover rule. Selected or active is /[0.08].

Shadows — only for what floats

shadow-sm is the sheet lift: a hairline plus a barely-there drop. shadow-md … 2xl all resolve to the floating-menu shadow, reserved for menus, popovers and dialogs. The dark themes swap both for a 1 px edge, because shadow on near-black is invisible.

Light

Sheet

--bl-shadow-sheet

Menu

--bl-shadow-menu

Dark

Sheet

1 px edge

Menu

edge + deep drop

The backdrop behind a dialog, drawer or the palette is --bl-backdrop: 25 % ink with a 2 px blur on paper, and 55 % black in the dark themes (light ink would lift the page into a grey haze). Never bg-black/40.

Primitives — name the surface, never re-spell it

Sheet

bl-panel — the page sheet

Popover

bl-popover — menus, dialogs, the palette

Card — hover me

bl-card — clickable; its rule darkens

Offen
Zahlbar
Frist

bl-strip — hairline gaps, for figures

bl-grid-paper — the 16 px drafting grid

K

bl-icon-tile · bl-keycap

Also: bl-title (15 / 500), bl-meta (12, ink-2), bl-kicker (11 uppercase eyebrow), bl-id (Geist Mono), and bl-tick — the 3 px inset tone marker at a row's left edge, an inset shadow so it never shifts a column.

06 · Density

Thirty-two pixels, no more.

A Bauleiter reads forty rows at a time. Controls and rows share one height so a toolbar, a form row and a table line up.

Controls and rows

btn-sm is the default button size: 32 px, 13 px type. A bare .input / .select — no size modifier — is 32 px too (only DaisyUI's --size moves, so an explicit h-10 on a touch-first form still wins), and table rows are 32 px. btn-xs (24 px, 12 px type) is for dense rows only; badges are 22 px (or 20 px compact).

Anlegenbutton · 32
Feldfield · 32
Zeilebtn-xs · 24
In Prüfungbadge · 22
Freigegebencompact · 20
table row · 32

Icons and gaps

Icons are lucide-react, imported directly: size-4 (16 px) at strokeWidth={1.75}. The rail's icons are 18 px at stroke 2; the empty-state tile's glyph is size-5. Gaps are 12–16 px; a dialog's header is px-5 py-3, its body px-5 py-4.

16 · 1.75 20 · 1.75 (empty-state tile)

07 · Motion and layers

Motion explains a change.

One scale for the whole app, named by the job it does. If an animation explains nothing, it is decoration and does not ship.

The four durations — try them

Press play: each dot travels the track on its own token. The reduced-motion guard lives at the token layer — one media query collapses all four to 1 ms — so anything written in these tokens is automatically safe. This page obeys it too.

--bl-dur-tap 90 msPressed or hover — must read as instant. A press: Halten
--bl-dur-ui 160 msControl state, tint, tick, badge.
--bl-dur-enter 220 msSomething arrives: overlay, popover, row.
--bl-dur-move 300 msSomething travels: sheet, drawer, reorder.

The three easings

Easings say what the thing is doing: arriving decelerates, leaving accelerates, travelling settles long.

Arriving--bl-ease-out · cubic-bezier(0.22, 1, 0.36, 1)Overlays, popovers, rows.
Leaving--bl-ease-in · cubic-bezier(0.4, 0, 1, 1)The backdrop, mostly — exits rarely animate.
Travelling--bl-ease-move · cubic-bezier(0.16, 1, 0.3, 1)Sheets, drawers, maximizing a dialog.

Rules

Do

  • Animate transform and opacity only — they never trigger layout.
  • Let entrances animate; React unmounts exits at once, and a faked delay feels slow.
  • Write durations as tokens: duration-[var(--bl-dur-ui)].
  • Stagger first paint only, capped at six rows.

Don't

  • Animate height, width or top.
  • Count a figure up. Money and status appear; they do not tick.
  • Write a bare duration-300 or 0.22s — it opts out of the reduced-motion guard.
  • Re-stagger a list on refetch.

Layering — a fixed scale

Never invent z-[10000]-style values. Overlays sit above the chrome so their backdrop dims the rail and the topbar. z-10 / z-20 are local stacking inside a context (a sticky table head) and are not steps on this scale.

LayerzExamples
Page contentautoeverything in <main>
Mobile module sheetz-30bottom sheet; under the chrome so the tab bar stays tappable
Shell chromez-40topbar, rail, bottom nav
Chrome popoversz-50topbar menus, notification panel, banners
Overlaysz-[60]Modal, Drawer, ConfirmDialog, the ⌘K palette
Lightboxz-[70]full-screen image viewer
Boot splashz-[9998]AppPreloader only

08 · Components

A small kit, built from tokens.

Live specimens, painted from the tokens above. They copy the visual recipe of the app's components, not their markup. Use the bar to see the whole gallery in any of the five themes.

Gallery theme

Buttons

Flat, 32 px, weight 500, a leading icon. One filled primary per view. Neutral actions are the outline. Ghost is for icon-only buttons and a dialog's Cancel. Destructive is error-coloured text, and only the confirm step is filled.

Primary
Neue RechnungDeaktiviert
Outline
ExportierenFilterFokusFreigeben
Ghost
Abbrechen
Destructive
LöschenEndgültig löschen
Dense (xs)
ZeilePosition

Fields

32 px, 13 px, a 6 px radius and a --bl-rule-control edge (3 : 1). A bare .input or .select is 32 px by itself, and the text of a raw .input, .select or .textarea is 13 px — not the browser's 16, not smaller. Focus is one quiet teal ring — not the double ring DaisyUI ships. An invalid field turns its edge error-red and says why in 12 px. A caller that needs another border colour on a field uses the important suffix (border-accent/50!): the field voice is unlayered and outranks a plain utility.

Nachtrag FassadeErscheint auf dem Aufmaßblatt.
17.760€Fokus: teal, 35 % ring.
TT.MM.JJJJBitte ein Datum eingeben.
Rohbau
Suchen
PrüfenFreiAn

Badges

Soft tints only, 11 px / 500, a 4 px radius, 22 px tall (20 px compact). Anything that is a status takes a tone from the ring: SoftBadge tone={toneForStatus(row.status)}. color= stays for categorical chips — a kind, a priority, a code.

Status ring
FreigegebenIn PrüfungÜberfälligEntwurfBlockiertGemessen
Compact (20)
BezahltFrist 3 TEntwurf
Categorical
PrimarySecondarySuccessWarningErrorInfoAccentNeutral

Tabs and view switchers

A selected page or tab is burgundy (owner decision 2026-10-03): a text-primary font-medium label and a 2 px burgundy underline, inset so it never shifts layout. Inactive is ink-2, full ink on hover; 32 px, 13 px, icon + label; focus is the inset teal ring. The recipe is one file, components/tabStyles.ts. A page's view switcher lives under its headline (Masthead), not in the topbar.

Underline strip
ÜbersichtPositionenVerlauf
Sheet tabs
View switch (nav)
Choice (filter, form)
Status: Offen
The segmented control is a 32 px recessed track on paper with a raised sheet for the selected segment. As a view switch (SegmentedToggle, tone="nav", the default) the selected label is burgundy. As a filter or a form choice (tone="choice") it stays ink — it picks a value, not a place. The lifted sheet tabs (.sheet-tab, the LV's trade strip) wear the same burgundy label and underline on the sheet colour, with a neutral count chip that takes a 14 % burgundy tint when selected.

Figure strip

Four cells on hairline gaps (bl-strip): an 11 px label, a 22 px / 500 figure, a 12 px note. No icon chips. A figure takes a colour only when it means “this is the bad one”.

Zur Freigabe

12

3 älter als 14 Tage

Zahlbar

317.854 €

AR-2026-011 · netto

Überfällig

17.760 €

1 Nachtrag, 9 Tage

Freigegeben 05/26

289.410 €

4 Rechnungen

Masthead

A kicker for scope and state, then a sentence about the work — “Fünf Dinge blockieren Geld.” beats “Dashboard”. 24 px / 500 with tight tracking (18 px on a phone), one line where it fits. It is the page's only <h1>. The screen's own non-creation actions sit right; its view switcher sits under it, and its selected view is burgundy. From xl the identity can dock into the topbar (next).

InrX · Abrechnung & Freigaben

Fünf Dinge blockieren Geld.

Zwei Aufmaßblätter warten auf Freigabe, ein Nachtrag ist überfällig.

Exportieren

Docked masthead

“If there is nothing on the topbar far left, move the whole page data up.” From xl (1280 px), when the topbar's left holds only the back arrow — no scope switcher, no crumb trail — the masthead's identity (kicker, record number and status, the 24 px headline) moves into that empty band, so the page starts right under the topbar instead of one band lower. The subtitle, the actions, the tabs and the figure strip stay in the page. The specimens are drawn at real size and scaled to fit the stage.

Docked — the dashed box is the slot: the grid's left cell beside the arrow, so the text can never reach the centred search.
Not docked — a scope pill is present (every /inrx… and /aufgaben… page), so the masthead keeps its own band.

Viewport

≥ 1280 px

xl; below it, and on a phone, nothing changes

Slot

≥ 288 px

else the masthead stays in the page band

Lines

≤ 2 · ≤ 2 · 1

headline · kicker · record-number row

Height

≤ 76 px

identity only; always saves space over the inline band

The docked stack is tight on purpose: a 13 px kicker line over a 26 px headline line is 40 px, so a one-line kicker over a one-line headline fits the 48 px row exactly and the bar does not grow. A wrapped line grows the bar by its extra lines only; the extra height over 48 px is published as --bl-dock-extra and is part of --topbar-height (calc(3rem + var(--bl-dock-extra))), so a fill page still ends at the sheet's bottom. The arrow, the search and the right cluster keep their 48 px row at the top — they never move between pages. No state exists in which the text is cramped or clipped: it either fits the rules or stays inline.

Journal table

Lists are journals. A paper head band with an 11 px uppercase head under a --bl-rule hairline, 13 px body, 32 px rows, --bl-rule-faint between rows, hover 5 %. A row states its tone as a 3 px tick at the left edge, and the status column repeats it in words. The creation button sits far left; filters and search sit far right.

Neue RechnungStatus: AlleSuchen
Nr.BezeichnungZeitraumNettoStatus
AR-2026-011Abschlagsrechnung Juni01.06.–30.06.2026317.854 €In Prüfung
AR-2026-010Abschlagsrechnung Mai01.05.–31.05.2026289.410 €Freigegeben
NT-2026-007Nachtrag Fassade—17.760 €Überfällig
AR-2026-009Abschlagsrechnung April01.04.–30.04.2026244.176 €Bezahlt
AR-2026-012Abschlagsrechnung Juli——Entwurf
Summe869.200 €

Empty state

A bl-grid-paper frame with an icon tile, a bl-title that says what is missing, and a hint that says what would create the first record. When the same primary already sits above, the action here is the outline.

Noch keine Rechnungen

Eine Rechnung entsteht aus freigegebenen Aufmaßblättern. Legen Sie die erste an.

Erste Rechnung anlegen

Dialogs

A bl-popover window that stays draggable (grab the header — the grip icon says so), resizable (corner grip), and expandable (the maximize button, or a double-click on the header). Below 640 px it becomes a bottom sheet. Header px-5 py-3 on a hairline, title in bl-title, footer hairline with the primary action last. Its icon chip is neutral unless a severity colour is passed — burgundy is the action, not the dialog's decoration. ConfirmDialog and GateDialog inherit it.

Normal — grip, maximize, close; corner resize.
Expanded — restore replaces maximize.
Below 640 px — bottom sheet.
  • Phone sheet: 24 px top radius, a drag handle, no grip or maximize, buttons stacked full-width with the primary on top.
  • Width is the content's, never more: sm for confirmations and a few fields, md for one column, lg for two or three columns, xl and up for tables and card grids.
  • Backdrop: --bl-backdrop with a 2 px blur.
  • Keep the exact size-tier class strings — a unit test asserts them.

Rail

The rail sits on the canvas and is module-scoped: it lists the current module's screens in the order the work happens — Projekt-Setup, Ausführung, Abrechnung. It is collapsible: 208 px with icon and label, 56 px with icons only, remembered per browser (bl-rail-collapsed); the sheet's margin follows through --bl-rail. The mark, the row icons and the collapsed buttons share one axis, so nothing jumps when it folds.

InrX

MKMarkus KernManager
Expanded — 208 px

MKMarkus KernManager
Collapsed — 56 px
  • Brand block: an h-12 row level with the topbar; the mark at h-6, the wordmark 13 / 600, the company's name beneath it at 11 px in ink-2 once it is known (only Manager and up can read it, so for others it is the wordmark alone). It links to the hub.
  • Module header: icon and name in bl-title, a plain label. The rail carries no back link — the topbar's arrow is the only way up.
  • Selected row: burgundy — bg-primary/10 text-primary font-medium and a 2 px bar at the left edge. Hover is a neutral 5 %.
  • Counts sit right and only when really read (money in burgundy); collapsed they become a tiny badge on the icon, and a group heading becomes a 20 px hairline.
  • Collapsed rows are 36 px icon buttons; the label is the tooltip.
  • Icons 18 px at stroke 2; the toggle's PanelLeftClose is 16 px at 1.75.

Topbar and back arrow

The topbar lives inside the sheet: 48 px (56 below lg) on a bottom hairline. From md it is a three-column grid — 1fr · search · 1fr, the search at most 560 px and 40 % of the row — so the ⌘K field is centred in the sheet's width, measured 0.0 px off at 1024, 1280, 1440 and 1920 px, rail open or collapsed. Left: the back arrow, then the scope switcher and crumbs. Right: the page's own actions, the notification bell and “Mehr”. There is no avatar: the account menu moved to the rail.

Zurück
Datensätze, Seiten und Befehle suchenK
A sub-screen (a record, a detail route) — the arrow reads “Zurück” and goes to the parent.
Alle Module
Datensätze, Seiten und Befehle suchenK
A module's top-level screen, or a standalone page — the arrow reads “Alle Module” and goes to the hub.
ScreenThe arrow goes toLabel
A page that registers its own backPath or onBackwhere it says — this wins over everything belowits own
A sub-screenits parent screenZurück
A module's top-level screen — a rail row, a module dashboard, /verwaltungthe hub, /Alle Module
A standalone page — project record, Abläufe, workspace, Docs, Changelog, Benachrichtigungenthe hub, /Alle Module
The hub, the 404, loginno arrow—
A full screen never carries a Close button — it gets this arrow. The arrow is a ghost square button, btn btn-ghost btn-sm btn-square: text-ink-2, hover:text-accent, a 16 px arrow at stroke 1.75, the tooltip is its label, a 40 px touch target below lg, the teal focus ring. It navigates, so the unsaved-changes gate still asks Save, Discard or Stay. Resolved in one place, backTargetFor() in app/modules.ts.

Account menu — the rail footer

Like tomweb's account block, the account menu is the footer row of the rail: a 28 px round avatar, name, role and a chevron — collapsed, the avatar alone with a tooltip. It opens a bl-popover (240 px) upward, portalled to <body> and positioned from the trigger so a 56 px rail never clips it. Items: change password, the Claude connector, and sign out in the error colour. Roving focus, Esc, arrows, Home and End; Tab closes; focus returns to the trigger. The topbar's right cluster has no avatar and the footer has no settings gear — one affordance per target. Below lg it is the last row of the bottom nav's Module sheet.

MKMarkus KernManager
Signing out stops a running task timer first, then clears the session and goes to the login. A name that is not known falls back to the e-mail, then to the word “Konto”.

Drawer

The dialog's chrome on a side panel, for read-mostly detail. Its inner edge carries a drag strip for width; it is also expandable — a maximize toggle (or a double-click on the header) turns it into a floating sheet. It does not drag to move. Below 640 px it is a bottom sheet like Modal.

09 · Voice

Write like a Bauleiter.

German, direct, technically literate. Short sentences, exact nouns, no adjective doing a number's job. v3 changes how the app looks, not what it says.

Tone

Do

  • „Von der Ausschreibung bis zur Übergabe geht nichts verloren."
  • Name the artefact: Leistungsverzeichnis, Aufmaß, Nachtrag, Abschlagsrechnung.
  • Cite the rule where it earns trust: VOB/B § 16.
  • Headlines of one or two clauses; sub-copy of one sentence.
  • Use „Sie". Address a Bauleiter, not a persona.

Don't

  • „Revolutionäre KI-gestützte Cloud-Lösung."
  • Anglicisms where a German word exists — Abrechnung, not Billing.
  • Exclamation marks. Ever.
  • Claims without a figure behind them.
  • Emoji, on any surface.

Naming

BauLynq — one word, capital B and L, everywhere. Module names end in a capital X (InrX) with no space or hyphen. The app is German-first: every string goes through i18n in both languages, and records keep their German names.

10 · Downloads

The brand pack is not the app spec.

The app's tokens are v3. The pack below is the marketing identity: logo files, Office templates and the brand-v2 manual.

Brand pack — logo, marketing colours, Office templates

These files are the brand pack, not the app UI spec. They were built for the marketing identity (brand v2: Plus Jakarta Sans, cool greys, the website's colours). The app has looked different since v3 — use this page and the token file above for anything inside the app. The logo files are still the logo.

Nothing here has been regenerated for v3.

Using the tokens

<link rel="stylesheet" href="/designlanguage/assets/tokens/baulynq-tokens.css">

<div data-theme="dark">…</div>   /* any element can carry a theme */

.sheet {
  background: var(--color-base-100);
  border: 1px solid var(--bl-rule);
  border-radius: var(--radius-box);
  box-shadow: var(--bl-shadow-sheet);
}
.figure {
  font-size: 22px; font-weight: 500;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

In the app, use the Tailwind classes and primitives (bg-base-100, text-ink-2, bl-panel) rather than the raw variables; frontend/src/index.css is where the values change.