/*
  Oberflaeche des Inolares CRM.

  Aufbau dieser Datei:

    1. Grundlagen        Reset, Body, Fokus, Hilfsklassen
    2. Shell             Kopfzeile, Navigation, Hauptbereich
    3. Seitengeruest     Titelzeile, Panels, Raster
    4. Komponenten       Button, Badge, Tag, Stat, DataRow
    5. Muster            Tabelle, Liste, Balken, Timeline, Board, Kalender
    6. Formulare         Felder, Meldungen, Benutzermenue, Anmeldung, Leerzustand

  Regeln, an die sich alles hier haelt (Kalk 1.1, inolares-branding):

  - Nur Tokens, keine Rohwerte. Farbe, Abstand, Ecke, Schrift und Dauer
    kommen aus tokens.css. Ausnahme sind einzelne Pixelmasse, die der Entwurf
    ausdruecklich so vorgibt (Rasterhoehe im Kalender, Trefferflaechen).
  - Die Klassen tragen die Gestalt, das Markup bleibt frei von style-Attributen.
    Einzige Ausnahme: Werte, die aus Daten kommen (Balkenbreite, Lage eines
    Kalendereintrags) – die stehen als CSS-Variable am Element.
  - Rechteckig ist der Normalfall: Karten, Felder, Tabellen, Hinweise, Dialoge.
    Eine runde Ecke tragen nur Knoepfe, Tags und Abzeichen, genau eine, oben
    rechts, Radius halbe Hoehe. Radio und Foto eines Menschen sind ein Stadion.
  - Schatten nur in drei Stufen: der feste Kopf (1), eine Karte, die auf eine
    Handlung hin 2 px steigt (2), Dialog und Flyout (3). Ruhende Karten haben
    keinen.
  - Lime ist Handlung und Markierung, nie Text und nie Diagramm. Wo ein Akzent
    Text ist, steht Tanne.
  - Nichts bewegt sich von selbst. Einzige Schleife ist der Ladeindikator.
  - Kleinste Schrift 13 px. Labels in 13/500 ohne Versalien.
  - Breakpoints aus Kalk: unter 640 px Telefon, unter 1000 px Schublade statt
    Seitenleiste, unter 1280 px schmal.
*/

/* ---- 1. Grundlagen ------------------------------------------------------- */

* { box-sizing: border-box; }

/* `hidden` heisst verborgen, ohne Ausnahme. Das `display: none` hinter [hidden]
   steht im Stylesheet des Browsers und verliert gegen jede Autorenregel — also
   auch gegen ein harmloses `display: flex` an derselben Klasse. Genau so stand
   der Diktierknopf sichtbar da, wo er nichts tun kann.

   Das !important ist hier nicht der Notausgang, sondern die Aussage: eine
   Layoutregel beschreibt, wie ein Element aussieht, wenn es da ist, und darf
   nicht darueber entscheiden, ob es da ist. Wer ein Element zeigen will, nimmt
   das Attribut weg (so machen es dictate.js und keys.js). Deshalb steht die
   Regel einmal hier und nicht je Klasse. */
[hidden] { display: none !important; }

/* Dichte der Listenzeilen. Das Mockup kennt spacious/comfortable/dense; die
   Anwendung faehrt spacious und hat hier die eine Stelle zum Umstellen.

   Und die zwei Masse der Shell, von denen der Hauptbereich abhaengt: die Hoehe
   der Kopfzeile und die Breite der Navigation. Beide stehen hier, weil sie an
   zwei Stellen gebraucht werden — am Element selbst und als Innenabstand von
   .main, der unter ihnen liegt. Die Breite der Seitenleiste und der Rand des
   Inhalts sind die des Kalk-App-Musters (256 px, 32 px). */
:root {
    --row-pad: 12px;
    --topbar-h: var(--kopf-h);
    --nav-w: 256px;
    --page-inset: var(--s-8);
}

html, body { margin: 0; height: 100%; }

body {
    background: var(--ground);
    color: var(--fg);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    -webkit-font-smoothing: antialiased;
    text-wrap: pretty;
}

/* Ueberschriften halbfett (600) und enger gesetzt, wie die Kalk-Stufen sie
   vorgeben. Die Laufweite gilt fuer die Stufen um 22 px; der Seitentitel setzt
   seine eigene. */
h1, h2, h3, h4 {
    margin: 0;
    color: var(--fg);
    font-family: var(--font-display);
    font-weight: var(--fw-display);
    line-height: var(--lh-display);
    letter-spacing: -0.02em;
    hyphens: manual;
}

/* Links in Tanne. Ein Link im Text ist unterstrichen (Kalk, k-link): Tanne
   allein hebt sich von Schwarz nicht ab, die Linie traegt die Aussage. Links
   mit Klasse sind Bauteile (Zeile, Kachel, Menue) und bringen ihre Gestalt
   selbst mit. */
a {
    color: var(--accent-ink);
    text-decoration: none;
}

a:not([class]) {
    font-weight: 500;
    text-decoration: underline;
    text-decoration-color: var(--linie-stark);
    text-decoration-thickness: 1.5px;
    text-underline-offset: 4px;
    transition: text-decoration-color var(--dur-fast) var(--ease);
}

a:not([class]):hover { text-decoration-color: currentColor; }

/* Der Fokus steht in tokens.css: 2 px Tanne mit 2 px Abstand, auf Tanne
   Kalkweiss. */

.skip-link {
    position: absolute;
    left: var(--s-4);
    top: -3rem;
    z-index: 60;
    padding: var(--s-2) var(--s-4);
    background: var(--fg);
    color: var(--bg);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    transition: top var(--dur-fast) var(--ease);
}

.skip-link:focus { top: var(--s-4); }

/* Fuer Bedienelemente, die es geben muss, aber nicht zu sehen sein soll — der
   Absende-Knopf der Suche in der Kopfzeile. Ohne ihn ist ein Formular auf das
   Absenden mit Enter angewiesen, und das ist laut HTML nur der Notfallpfad
   ("implicit submission"): Tastaturen ohne Enter-Semantik und
   Bedienungshilfen finden dann kein Ziel. clip-path statt display:none, weil ein
   verstecktes Element sonst nicht fokussierbar waere. */
.offscreen {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* Text, der in einer festen Spalte nicht umbrechen darf.

   display: block ist nicht kosmetisch: text-overflow wirkt nur auf Blockboxen.
   Als reines <span> schnitt die Zelle mitten im Wort ab, weil dann das
   overflow: hidden der <td> geklippt hat und nie die Ellipse. */
.ell {
    display: block;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.icon { display: inline-block; flex: 0 0 auto; vertical-align: middle; }

/* ---- 2. Shell ----------------------------------------------------------- */

/* Die Shell ist der Grund (Kalkweiss). Darauf liegen der feste Kopf in Weiss und
   die Seitenleiste in Tanne, wie im Kalk-App-Muster. .main ist der eine
   Scroll-Container und traegt oben und links so viel Innenabstand, wie Kopf und
   Leiste hoch bzw. breit sind; der Inhalt laeuft beim Scrollen unter den Kopf. */
.shell {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100dvh;
    max-height: 100vh;
    overflow: hidden;
    background: var(--ground);
}

/* Kopfzeile: weiss, ueber die ganze Breite, mit Schatten 1 (Kalk: fester Kopf). */

.topbar {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 40;
    display: flex;
    align-items: center;
    gap: var(--s-4);
    height: var(--topbar-h);
    padding: 0 var(--page-inset);
    background: var(--surface);
    box-shadow: var(--schatten-1);
}

.topbar__burger {
    display: none;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: 0 0 auto;
    cursor: pointer;
    background: var(--grau-100);
    border: none;
    color: var(--fg);
}

.topbar__burger:hover { background: var(--line); }

/* Der Schriftzug: 24px hoch, Schutzraum rundum die Hoehe des I (Kalk). Rechts
   steht er hier, oben und unten gibt ihn die Kopfzeile, links ihr
   Innenabstand. Schwarz auf Weiss, nie in einer Pille, nie in einem Kasten. */
.brand {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    height: 24px;
    margin-right: var(--s-4);
}

.brand__mark { height: 24px; width: auto; }

.searchbox {
    flex: 1 1 160px;
    min-width: 0;
    max-width: 420px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    gap: var(--s-3);
    height: var(--target-min);
    padding: 0 var(--s-4);
    border: 1.5px solid var(--border);
    background: var(--surface);
    cursor: text;
    color: inherit;
}

.searchbox:hover { border-color: var(--fg); }
.searchbox:focus-within { border-color: var(--fg); outline: 2px solid var(--focus); outline-offset: 2px; }
.searchbox .icon { color: var(--fg-muted); }

/* Das Eingabefeld traegt keinen eigenen Rahmen: den hat .searchbox, und das Feld
   sitzt darin. `appearance: none` nimmt type="search" das Kreuz von Safari, das
   im Kasten neben dem ⌘K-Hinweis stehen wuerde. Der Fokus steht am Kasten
   (:focus-within), deshalb hier keiner. */
.searchbox__input {
    flex: 1;
    min-width: 0;
    border: none;
    background: none;
    outline: none;
    appearance: none;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
}

.searchbox__input::placeholder { color: var(--fg-muted); }
.searchbox__input::-webkit-search-cancel-button { display: none; }

.kbd {
    margin-left: auto;
    font: 500 var(--fs-13)/1 var(--font-text);
    color: var(--fg-muted);
}

.topbar__actions {
    margin-left: auto;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: var(--s-4);
}

.topbar__divider { width: 1px; height: 22px; background: var(--line); flex: 0 0 auto; }

.iconbtn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    flex: 0 0 auto;
    cursor: pointer;
    background: none;
    border: none;
    color: var(--fg-muted);
}

.iconbtn:hover { color: var(--fg); }

/* Loeschen in einer Tabellenzeile: dieselbe Groesse wie die uebrigen Icons,
   aber in Fehlerfarbe — der Sinn steht im aria-label, die Farbe warnt. */
.iconbtn--danger { color: var(--fehler-text); }
.iconbtn--danger:hover { color: var(--weiss); background: var(--fehler); }

.iconbtn--search { display: none; }

.iconbtn--cta {
    display: none;
    background: var(--accent);
    color: var(--accent-on);
    border-radius: 0 18px 0 0; /* ein Knopf: die Ecke, halbe Hoehe */
}

.iconbtn--cta:hover { background: var(--accent-hover); color: var(--accent-on); }

/* Der Avatar ist ein Quadrat, kein Kreis: Kalk kennt keine Kreise, und die
   Ecke gehoert den Handlungen. Blatt mit Tanne, wie das Piktogramm einer
   Kalk-Karte. */
.avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: 0 0 auto;
    background: var(--accent-soft);
    color: var(--accent-ink);
    font: 500 var(--fs-13)/1 var(--font-text);
}

/* Verbindungsanzeige. Nicht Teil des Entwurfs: sie zeigt, ob die Live-Verbindung
   des Stacks steht – im Betrieb die schnellste Antwort auf "kommen noch Daten?".
   Der Punkt ist ein kleines Quadrat, aus demselben Grund wie der Avatar, und
   traegt die Markenfarbe des Zustands; das Wort daneben sagt ihn mit. */
.conn {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
    white-space: nowrap;
}

.conn__dot {
    width: 8px;
    height: 8px;
    background: var(--fg-muted);
    flex: 0 0 auto;
}

.conn[data-state="live"] .conn__dot { background: var(--erfolg-mark); }
.conn[data-state="polling"] .conn__dot { background: var(--warnung-mark); }
.conn[data-state="offline"] .conn__dot { background: var(--fehler-mark); }

.conn[data-state="connecting"] .conn__dot,
.conn[data-state="reconnecting"] .conn__dot { display: none; }

.conn .loading-mark { display: none; }
.conn[data-state="connecting"] .loading-mark,
.conn[data-state="reconnecting"] .loading-mark { display: block; }

/* Der Ladeindikator aus Kalk: ein Ring, der sich in 800 ms linear dreht – die
   einzige Drehung und die einzige Schleife, die Kalk kennt. Er laeuft nur,
   solange er zu sehen ist (app.js setzt data-running), und steht bei
   reduzierter Bewegung still (tokens.css). */
.loading-mark {
    flex: 0 0 auto;
    width: 16px;
    height: 16px;
    border: 2px solid color-mix(in srgb, currentColor 20%, transparent);
    border-top-color: currentColor;
    border-radius: 50%;
    color: var(--fg);
}
.loading-mark[data-running="true"] { animation: loading-spin var(--t-lang) linear infinite; }
@keyframes loading-spin { to { transform: rotate(360deg); } }

/* Mobile Suchzeile, direkt unter der Kopfzeile und gebaut wie sie: weiss, mit
   Schatten 1. Sie liegt ueber dem Inhalt statt ihn zu verschieben, und
   verschwindet mit data-search="closed". */

.searchbar {
    position: absolute;
    top: var(--topbar-h);
    left: 0;
    right: 0;
    z-index: 40;
    display: none;
    align-items: center;
    gap: var(--s-3);
    height: 60px;
    padding: 0 var(--page-inset);
    background: var(--surface);
    border-top: 1px solid var(--line);
    box-shadow: var(--schatten-1);
}

.shell[data-search="open"] .searchbar { display: flex; }

/* Der Knopf zeigt, dass die Zeile ihm gehoert – im Entwurf wechselt er auf
   die Akzentfarbe, solange die Suche offen ist. */
.shell[data-search="open"] .iconbtn--search { color: var(--accent-ink); }

.searchbar__field {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex: 1;
    min-width: 0;
    height: var(--target-min);
    padding: 0 var(--s-4);
    border: 1.5px solid var(--border);
    background: var(--surface);
}

.searchbar__field:focus-within { border-color: var(--fg); outline: 2px solid var(--focus); outline-offset: 2px; }
.searchbar__field .icon { color: var(--fg-muted); }

.searchbar__cancel {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 64px;
    height: var(--target-min);
    flex: 0 0 auto;
    cursor: pointer;
    background: none;
    border: none;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

.searchbar__cancel:hover { color: var(--accent-ink); }

/* ---- Vorschläge beim Tippen --------------------------------------------- */

/* Der Kasten haengt unter dem Feld, also braucht das Feld den Bezugsrahmen.
   Ohne das waere es .topbar (position: absolute), und die Liste stuende auf
   einem schmalen Fenster neben dem Feld statt darunter. Die mobile .searchbar
   ist selbst absolut gesetzt und damit schon Bezugsrahmen – sie steht hier
   nicht, denn ein `relative` hier machte sie wieder zur Flex-Zeile der Shell. */
.searchbox { position: relative; }

/* Verborgen ist es im Markup, und dabei bleibt es, bis public/js/suggest.js
   Vorschlaege eingesetzt hat — dafuer sorgt die globale [hidden]-Regel, hier
   steht deshalb kein eigenes display: none. Weiss, rechteckig, Schatten 3: so
   sieht jede aufklappende Flaeche aus (Kalk: Dialog, Flyout). */
.suggest {
    position: absolute;
    top: calc(100% + var(--s-2));
    left: 0;
    right: 0;
    z-index: 50;
    background: var(--surface);
    box-shadow: var(--schatten-3);
    overflow: hidden;
}

.suggest__list { display: flex; flex-direction: column; }

/* Zwei Zeilen je Vorschlag: der Titel und der Grund des Treffers. Beide
   beschnitten statt umgebrochen — eine Liste, deren Zeilen unterschiedlich hoch
   sind, laesst sich mit den Pfeilen nicht ablesen.

   `border-bottom: none` gegen die Regel fuer jeden Link (`a:hover` faerbt den
   unteren Rand): unterstrichen wird ein Wort, keine Zeile ueber die ganze
   Breite. Die Trennlinie steht deshalb als border-top am Nachbarn. */
.suggest__row {
    display: flex;
    flex-direction: column;
    gap: 1px;
    padding: var(--s-2) var(--s-4);
    border-bottom: none;
    color: var(--fg);
}

.suggest__row + .suggest__row { border-top: 1px solid var(--line); }

.suggest__title {
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.suggest__meta {
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Eine Markierung fuer Maus und Tastatur: suggest.js setzt die Klasse, damit die
   gewaehlte Zeile dieselbe ist, egal woher die Wahl kommt. */
.suggest__row:hover,
.suggest__row--on { background: var(--ground); }

.suggest__row--on .suggest__title { color: var(--accent-ink); }

/* Zwei benannte Gruppen gibt es nur in der Palette („Befehle", „Treffer"). Die
   Ueberschrift ist keine Zeile der Liste: sie bekommt deshalb den kleineren Satz
   der Augenbraue und keine Trennlinie. */
.suggest__group { display: flex; flex-direction: column; }

.suggest__group + .suggest__group {
    border-top: 1px solid var(--line);
}

.suggest__grouptitle {
    margin: 0;
    padding: var(--s-3) var(--s-4) var(--s-1);
    font: 500 var(--fs-13)/1.4 var(--font-text);
    color: var(--fg-muted);
}

/* ---- Die Befehlspalette -------------------------------------------------- */

/* Das Overlay von ⌘K. Wie bei .keyhelp bringt <dialog> im modalen Modus Fokus,
   Escape und die oberste Ebene selbst mit; hier steht nur, wie es aussieht.

   Oben statt in der Mitte (margin-top statt auto): waehrend die Liste waechst und
   schrumpft, wandert ein mittig stehender Kasten mit — und der Blick sitzt auf
   dem Feld, das dann nicht stillsteht. */
.palette {
    width: min(560px, calc(100vw - 2 * var(--s-6)));
    margin: 12vh auto auto;
    padding: 0;
    overflow: hidden;
    background: var(--surface);
    color: var(--fg);
    border: none;
    box-shadow: var(--schatten-3);
}

/* Der Vorhang hinter einem Dialog und hinter der Schublade: Schwarz zu 40 %,
   wie hinter dem Kalk-Dialog. */
.palette::backdrop { background: color-mix(in srgb, var(--schwarz) 40%, transparent); }

.palette__field {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-4);
    border-bottom: 1px solid var(--line);
}

.palette__field .icon { color: var(--fg-muted); }

/* Kein eigener Rahmen: der Kasten *ist* das Feld, wie bei .searchbox. Kein
   Fokusring am Feld – der Dialog ist der Fokus, und die gewaehlte Zeile markiert
   .suggest__row--on: der Fokus bleibt im Feld, sonst kaeme der naechste
   Buchstabe nicht mehr an. */
.palette__input {
    flex: 1;
    min-width: 0;
    border: none;
    background: none;
    outline: none;
    appearance: none;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
}

.palette__input::placeholder { color: var(--fg-muted); }
.palette__input::-webkit-search-cancel-button { display: none; }

.palette__close {
    flex: 0 0 auto;
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

.palette__close:hover { color: var(--accent-ink); }

/* Verborgen ist es im Markup und bleibt es, bis suggest.js etwas eingesetzt hat –
   die globale [hidden]-Regel erledigt das, hier steht deshalb kein display: none.
   Die Hoehe ist begrenzt, weil die Liste mit Befehlen und Treffern laenger wird
   als ein Fenster: gescrollt wird dann in ihr und nicht im Overlay, damit das Feld
   stehen bleibt. */
.palette__panel {
    max-height: min(50vh, 420px);
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* ---- Glocke: die Zahl der ungelesenen Benachrichtigungen ----------------- */

/* Das Abzeichen sitzt am Symbol, deshalb ist der Knopf der Bezugsrahmen. */
.iconbtn--bell { position: relative; }

.bell__count {
    position: absolute;
    top: -4px;
    right: -6px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    background: var(--accent);
    color: var(--accent-on);
    border-radius: 0 10px 0 0; /* ein Abzeichen: die Ecke, halbe Hoehe */
    font: 500 var(--fs-13)/1 var(--font-text);
    font-variant-numeric: tabular-nums;
}

/* Rumpf, Navigation, Hauptbereich */

.body { position: relative; flex: 1; min-height: 0; display: flex; }

.scrim {
    display: none;
    position: absolute;
    inset: 0;
    z-index: 20;
    background: color-mix(in srgb, var(--schwarz) 40%, transparent);
    border: none;
    padding: 0;
}

.shell[data-drawer="open"] .scrim { display: block; }

/* Die Navigation: die Seitenleiste in Tanne, wie im Kalk-App-Muster. Sie steht
   unter dem Kopf, vom Kopf bis zur Unterkante. Die Eintraege sind Kalkweiss zu
   75 %, der aktive voll, mit einem Hauch Flaeche und dem Lime-Strich links –
   Lime als aktive Markierung. Den Fokus auf Tanne setzt .k-dunkel (tokens.css)
   auf Kalkweiss. */
.sidenav {
    position: absolute;
    top: var(--topbar-h);
    bottom: 0;
    left: 0;
    z-index: 30;
    display: flex;
    flex-direction: column;
    width: var(--nav-w);
    background: var(--tanne);
    color: var(--kalk);
}

.sidenav__list {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--s-3) var(--s-2);
    overflow-x: hidden;
    overflow-y: auto;
}

.sidenav__item,
.sidenav__section-toggle {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--target-min);
    flex-shrink: 0;
    padding: 0 var(--s-3);
    border: none;
    background: transparent;
    color: color-mix(in srgb, var(--kalk) 75%, transparent);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    cursor: pointer;
    text-align: left;
    white-space: nowrap;
    transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}

.sidenav__item:hover, .sidenav__section-toggle:hover { color: var(--kalk); background: color-mix(in srgb, var(--kalk) 6%, transparent); }

.sidenav__item[aria-current="page"] {
    background: color-mix(in srgb, var(--kalk) 10%, transparent);
    color: var(--kalk);
    font-weight: 500;
    box-shadow: inset 3px 0 0 var(--lime);
}

.sidenav__label { flex: 1; overflow: hidden; text-overflow: ellipsis; }

.sidenav__section { margin-top: var(--s-3); border-top: 1px solid color-mix(in srgb, var(--kalk) 15%, transparent); padding-top: var(--s-2); }
.sidenav__section-toggle { list-style: none; }
.sidenav__section-toggle::-webkit-details-marker { display: none; }
.sidenav__chevron { transition: transform var(--dur-fast) var(--ease); }
.sidenav__section[open] .sidenav__chevron { transform: rotate(180deg); }
.sidenav__section-items { display: flex; flex-direction: column; gap: 2px; }
.sidenav__section-items .sidenav__item { font-size: var(--fs-13); }
.sidenav__section:has([aria-current="page"]) > summary { color: var(--kalk); }
.shell[data-nav="collapsed"] .sidenav__chevron { display: none; }

.sidenav__foot {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    height: 40px;
    padding: 0 var(--s-4);
    cursor: pointer;
    background: none;
    border: none;
    border-top: 1px solid color-mix(in srgb, var(--kalk) 15%, transparent);
    color: color-mix(in srgb, var(--kalk) 75%, transparent);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    white-space: nowrap;
    overflow: hidden;
}

.sidenav__foot:hover { color: var(--kalk); }

/* Standard am Schreibtisch, ausgeklappt: Pfeil nach links und „Einklappen". */
.sidenav__foot .icon--expand,
.sidenav__foot .icon--close,
.sidenav__footlabel--drawer { display: none; }

/* Eingeklappt: nur der Pfeil, und der zeigt in die Richtung, in die es geht.
   Die Breite steht als Variable an der Shell, weil .main sie als Innenabstand
   mitfuehrt. */
.shell[data-nav="collapsed"] { --nav-w: 56px; }
.shell[data-nav="collapsed"] .sidenav__label,
.shell[data-nav="collapsed"] .sidenav__footlabel,
.shell[data-nav="collapsed"] .sidenav__foot .icon--collapse { display: none; }
.shell[data-nav="collapsed"] .sidenav__foot .icon--expand { display: inline-block; }

/* Der eine senkrechte Scroll-Container der Anwendung. Nichts darin scrollt
   senkrecht mit – siehe .scroll-x.

   Der Innenabstand oben und links ist der Platz von Kopf und Leiste plus der
   Rand des Inhalts (32 px, Kalk-App-Muster): der Inhalt beginnt unter der
   Kopfzeile und rechts der Navigation, laeuft beim Scrollen aber unter die
   Kopfzeile – deshalb padding und kein margin, der Scroll-Container selbst
   reicht bis zur Oberkante. */
.main {
    flex: 1;
    min-width: 0;
    min-height: 0;
    /* Die Kopfzeile liegt absolut ueber diesem Container: ohne den Abstand landen
       Anker und fokussierte Felder beim Scrollen unter ihr. */
    scroll-padding-top: calc(var(--topbar-h) + var(--s-4));
    overflow-x: hidden;
    overflow-y: auto;
    padding: calc(var(--topbar-h) + var(--page-inset))
             var(--page-inset)
             var(--s-16)
             calc(var(--nav-w) + var(--page-inset));
}

/* pagenav.js setzt den Fokus programmatisch auf den neuen Inhalt (tabindex -1), damit
   Screenreader und Tastatur oben anfangen. Ein Ring um die ganze Flaeche waere
   dabei nur Laerm: er ist kein Bedienelement. */
#content:focus,
#content:focus-visible { outline: none; }

/* Waagerecht scrollende Flaeche: die breiten Tabellen, das Board, das
   Kalenderraster. Alle drei sind breiter als das Fenster, wenn es schmal wird.

   overflow-y: hidden ist Pflicht und nicht Kosmetik. CSS rechnet eine
   visible-Achse auf auto hoch, sobald die andere nicht visible ist: ein blosses
   overflow-x: auto macht die Flaeche also auch zum senkrechten Scroll-Container,
   und dann scrollt der Inhalt in sich statt mit der Seite. Abgeschnitten wird
   dadurch nichts – die Hoehe dieser Flaechen richtet sich nach ihrem Inhalt.

   Klasse, nicht data-Attribut: Gestalt gehoert an Klassen (siehe Kopf). */
.scroll-x {
    overflow-x: auto;
    overflow-y: hidden;
}

/* Scrollbalken in Neutralen – das Fenster selbst scrollt nicht. Kein Gruen:
   der Akzent ist fuer Knoepfe und Signale, ein Balken ist keines von beiden. */
.main::-webkit-scrollbar,
.scroll-x::-webkit-scrollbar { width: 10px; height: 10px; -webkit-appearance: none; }
.main::-webkit-scrollbar-track,
.scroll-x::-webkit-scrollbar-track { background: transparent; }
.main::-webkit-scrollbar-thumb,
.scroll-x::-webkit-scrollbar-thumb { background: var(--border); }
.main::-webkit-scrollbar-thumb:hover,
.scroll-x::-webkit-scrollbar-thumb:hover { background: var(--fg-muted); }

/* ---- 3. Seitengeruest --------------------------------------------------- */

.page {
    display: flex;
    flex-direction: column;
    gap: var(--s-6);
    max-width: var(--content-width);
}

.page--wide { max-width: none; }
.page--lead { gap: var(--s-8); }

.page__head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--s-6);
    flex-wrap: wrap;
}

.page__head--top { align-items: flex-start; }

.page__titles { display: flex; flex-direction: column; gap: var(--s-2); }

.page__title {
    font: var(--fw-display) var(--fs-28)/var(--lh-display) var(--font-display);
    letter-spacing: -0.025em;
    color: var(--fg);
}

/* Das Label ueber dem Seitentitel: 13/500 in Stein, ohne Versalien (Kalk). */
.eyebrow {
    font: 500 var(--fs-13)/1.4 var(--font-text);
    color: var(--fg-muted);
}

.lead { margin: 0; font: 400 var(--fs-17)/var(--lh-text) var(--font-text); color: var(--fg); max-width: var(--measure); }

.actions { display: flex; gap: var(--s-3); flex-wrap: wrap; }
.recordmore { position: relative; }
.recordmore > summary { list-style: none; }
.recordmore > summary::-webkit-details-marker { display: none; }
.recordmore__items { position: absolute; top: calc(100% + var(--s-2)); right: 0; z-index: 25; display: grid; gap: var(--s-2); padding: var(--s-3); min-width: 220px; max-width: calc(100vw - 2 * var(--page-inset)); background: var(--surface); box-shadow: var(--schatten-3); }
.recordmore__items .btn { width: 100%; justify-content: flex-start; }
.recordmore__items .inlineform { display: block; }

/* Filtering changes the working set. Search and table preferences stay outside
   the disclosure so routine lookup never needs an extra click. */
.listtools { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.listtools .searchform { margin-left: 0; flex: 1; }
.listtools .searchform__field { flex: 1; }
.listtools + .filters { margin-top: calc(-1 * var(--s-3)); }
.filters { border-bottom: 1px solid var(--line); }
.filters__summary {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-height: var(--target-min);
    padding: var(--s-2) 0;
    cursor: pointer;
    list-style: none;
    font: 500 var(--fs-15)/var(--lh-text) var(--font-display);
}
.filters__summary::-webkit-details-marker { display: none; }
.filters__selection { flex: 1; min-width: 0; color: var(--fg-muted); font-weight: 400; overflow-wrap: anywhere; }
.filters__chevron { transition: transform var(--dur-fast) var(--ease); }
.filters[open] .filters__chevron { transform: rotate(180deg); }
.filters__choices { display: grid; gap: var(--s-3); padding: var(--s-3) 0 var(--s-4); }
.filters .chips__label { min-width: 112px; }
.boardhelp { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.boardhelp summary { cursor: pointer; width: fit-content; padding: var(--s-2) 0; }
.boardnote:has(.boardsay:empty) { margin-block: calc(-1 * var(--s-3)); }
.boardnote .boardsay:empty { display: none; }

::selection { background: var(--blatt); color: var(--schwarz); }
input, textarea { caret-color: var(--accent-ink); }
/* Erscheinen (Kalk): Deckkraft und 6 px Weg nach oben, mittlere Dauer. */
.filters__choices, .sidenav__section-items, .usermenu__panel, .colpick__panel {
    animation: disclose var(--dur) var(--ease);
}
@keyframes disclose { from { opacity: 0; transform: translateY(var(--versatz)); } }

/* Der Pfad. Er steht als erstes Element in .main, nicht in .page: gerendert wird
   er in der Shell (base.html.twig), damit es genau einen gibt. Der Abstand nach
   unten ist derselbe, den .page zwischen ihren Kindern haelt – sonst saesse der
   Seitentitel naeher am Pfad als an allem anderen. */
.breadcrumb {
    margin-bottom: var(--s-4);
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

.breadcrumb__list {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-3);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Das Trennzeichen ist Gestaltung und steht deshalb im CSS: im Markup waere es
   ein Element, das eine Vorlesehilfe mitzaehlt. */
.breadcrumb__step + .breadcrumb__step::before {
    content: "/";
    margin-right: var(--s-2);
    color: var(--fg-muted);
}

.breadcrumb__list { gap: var(--s-2); }

/* Die Brotkrumen aus Kalk: Schritte in Stein, ohne Unterstrich, beim Zeigen
   Schwarz; der letzte Schritt ist die Seite selbst, Schwarz und halbfett. */
.breadcrumb a { color: var(--fg-muted); font-weight: 400; text-decoration: none; }
.breadcrumb a:hover { color: var(--fg); }
.breadcrumb [aria-current] { color: var(--fg); font-weight: 500; }

.identity { display: flex; align-items: flex-start; gap: var(--s-4); }
.identity__text { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
/* Die Kennzeichen unter dem Namen einer Identitaet (Kontakt, Benutzer). */
.identity__badges { display: flex; gap: var(--s-3); margin-top: var(--s-2); flex-wrap: wrap; }

/* Ein Panel ist eine Kalk-Karte: Weiss auf Kalkweiss, rechteckig, ohne Rahmen
   und ohne Schatten in Ruhe. Der Tonschritt zum Grund ist die Grenze. Steht ein
   Panel in einem anderen, ist der Tonschritt null – dann traegt es die
   Haarlinie. */
.panel {
    background: var(--surface);
    padding: var(--s-6);
}

.panel .panel { border: 1px solid var(--line); }

/* Die helle Markenflaeche: Blatt, ohne Rand. Lime bleibt der Handlung. */
.panel--accent { background: var(--accent-soft); }

/* Panel, in dem eine Tabelle waagerecht scrollt: rechts kein Innenabstand,
   den tragen die Zellen – sonst reisst der Rand beim Scrollen ab. */
/* Das Scrollen selbst kommt von .scroll-x im Markup, hier nur die Polsterung:
   rechts keine, damit die Tabelle bis an die Kante laufen kann. */
.panel--table { padding: var(--s-4) 0 var(--s-3) var(--s-6); }
/* Was in so einem Panel keine Tabelle ist, braucht den rechten Rand trotzdem:
   Kopfzeile mit Notiz, Hinweistexte, das Formular unter der Liste. Ohne diese
   Regel standen „0 Paare", „90 Tage" und das Feld „Schlagwort" buendig auf der
   Panelkante. 32 px wie der rechte Innenabstand der letzten Zelle (.table
   td:last-child), damit Notiz und Spalte eine Kante haben. */
.panel--table > :is(.panel__head, .panel__text, .form, .chips, .empty, .notice) { margin-right: var(--s-8); }

/* Ein Panel, dessen letzte Zeile an der Unterkante sitzt. Es ist ein Flex-Stapel,
   damit das `margin-top: auto` der Chipreihe wirkt: in einem Blockfluss ist ein
   automatischer Rand null, und die Reihe klebte an den Balken statt am Boden.
   Gebraucht wird das nur, wenn das Panel hoeher als sein Inhalt ist – das
   uebernimmt .grid-2--even. */
.panel--column { display: flex; flex-direction: column; }

/* Ein Panel, das im Raster die ganze Zeile nimmt statt einer Spalte. Fuer den
   Fall, dass sein Inhalt eine Tabelle ist, deren Spalten in 300px keine
   Auskunft mehr sind: das Panel „Aenderungen" zeigte in der dritten
   Rasterspalte nur Wann und Wer, waehrend seine Zeilenhoehe aus der Spalte
   „Felder" kam, die man nicht sah.
   In einer Spalte (mobil) ist die Regel wirkungslos, dort gibt es nur
   eine. */
.panel--span { grid-column: 1 / -1; }

.panel__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    margin-bottom: var(--s-4);
}

.panel__head--wide { margin-bottom: var(--s-6); }

.panel__title { font: var(--fw-display) var(--fs-22)/var(--lh-display) var(--font-display); letter-spacing: -0.02em; color: var(--fg); }
.panel__meta { font: 500 var(--fs-13)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg-muted); }
.panel__note { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.panel__link { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); }

.panel__foot { padding: var(--s-6) var(--s-8) 0 0; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Die Zwischenueberschrift eines Panels, das mehrere Abschnitte traegt. Genau
   ein Aufrufer braucht sie: der Akquise-Block des Wochenberichts hat vier
   Abschnitte, und zwei davon rechnen ueber verschiedene Zeitraeume – ohne
   Ueberschrift liest man die Kette je Quelle als Wochenwert (Ticket #94).
   Kleiner als .panel__title, denn sie ordnet unter und ersetzt nicht. */
.panel__subhead {
    margin: var(--s-8) 0 var(--s-4);
    font: var(--fw-display) var(--fs-19)/var(--lh-display) var(--font-display);
    color: var(--fg);
}

.panel__text { margin: var(--s-4) 0 var(--s-6); font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }

/* Der Satz unter einer Tabelle, der sagt, was die Zahlen bedeuten – und was sie
   nicht bedeuten. Gedaempft, weil er nicht die Aussage ist, sondern ihre Grenze. */
.panel__text--note { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

.stack { display: flex; flex-direction: column; gap: var(--s-6); }

/* Der Abschnitt um die Kacheln: Ueberschrift, darunter das Raster. Der Abstand
   ist der kleinere von .panel__head und nicht der Abstand der Seite – die
   Ueberschrift gehoert zu den Kacheln, sie steht nicht daneben. */
.stats { display: flex; flex-direction: column; gap: var(--s-4); }
.stats__title { font: var(--fw-display) var(--fs-22)/var(--lh-display) var(--font-display); letter-spacing: -0.02em; color: var(--fg); }
.stats__head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); flex-wrap: wrap; }

/* auto-fit und keine feste Spaltenzahl: die Kacheln waren einmal fuenf, dann
   drei, jetzt vier. Bei 1440px stehen vier neben einander, bei 390px unter
   einander, und keine Zahl braucht eine eigene Regel. */
.grid-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
    gap: var(--s-4);
}

.grid-2 {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
    gap: var(--s-6);
    align-items: start;
}

.grid-2--tight { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* Zwei Spalten, die auf derselben Linie enden (Meeting-Beschluss zur Uebersicht).
   Ohne die Regel gilt das `align-items: start` von .grid-2, und die kuerzere
   Spalte hoert dort auf, wo ihr Inhalt aufhoert. Nebenwirkung ist keine: unter rund
   710px Inhaltsbreite (2 x 340px plus gap) bleibt eine Spalte, jedes Panel steht in
   seiner eigenen Zeile, und stretch hat nichts zu strecken. */
.grid-2--even { align-items: stretch; }

/* ---- 4. Komponenten ----------------------------------------------------- */

/* Knopf nach Kalk: Satzschrift 500, eine runde Ecke oben rechts mit dem Radius
   der halben Hoehe, mindestens 44 px hoch (--target-min). Die kompakte Fassung
   .btn--sm (32 px, Kalk S) steht in Tabellenzeilen und Chipreihen; sie ist die
   Fassung, die ui.button setzt. Druck aendert nur die Farbe, nichts versetzt
   sich. Konturen sind Innenlinien (1,5 px), damit Kontur und Flaeche gleich
   hoch sind. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    min-height: var(--target-min);
    padding: var(--s-2) var(--s-6);
    font: 500 var(--fs-15)/1.2 var(--font-text);
    border: 0;
    border-radius: var(--ecke-knopf);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color var(--dur-fast) var(--ease),
                color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
}

.btn:disabled { background: var(--grau-100); color: var(--kiesel); box-shadow: none; cursor: not-allowed; }

.btn--sm { min-height: 32px; padding: var(--s-1) var(--s-4); font-size: var(--fs-13); border-radius: var(--ecke-knopf-s); }

/* Primaer: Lime mit Schwarz – die Handlung der Seite. */
.btn--primary { background: var(--accent); color: var(--accent-on); }
.btn--primary:hover:not(:disabled),
.btn--primary:active:not(:disabled) { background: var(--accent-hover); }

/* Sekundaer: die Kontur aus Kalk (ghost), 1,5 px Schwarz. Beim Zeigen wird sie
   zur dunklen Flaeche. */
.btn--secondary { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 1.5px var(--line-strong); }
.btn--secondary:hover:not(:disabled) { background: var(--grau-600); color: var(--kalk); box-shadow: none; }

/* Leise: nur das Wort; beim Zeigen eine helle Flaeche. */
.btn--ghost { background: transparent; color: var(--fg); }
.btn--ghost:hover:not(:disabled) { background: var(--grau-100); }

/* Abzeichen nach Kalk: 24 px hoch, 13/500, eine Ecke oben rechts (halbe Hoehe).
   Neutral ist Grau 100; die Zustaende tragen ihre Flaeche und ihre Textfarbe.
   Erfolg ist Blatt mit dem Erfolgsgruen – Lime erscheint hier nie. */
.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    height: 24px;
    padding: 0 var(--s-3) 0 var(--s-2);
    border-radius: var(--ecke-tag);
    font: 500 var(--fs-13)/1.2 var(--font-text);
    white-space: nowrap;
    background: var(--grau-100);
    color: var(--fg);
}

.badge--accent { background: var(--accent-soft); color: var(--accent-ink); }
.badge--ok { background: var(--erfolg-soft); color: var(--erfolg-text); }
.badge--warn { background: var(--warnung-soft); color: var(--warnung-text); }
.badge--alarm { background: var(--fehler-soft); color: var(--fehler-text); }

/* Die Statusmarke aus Kalk: ein Strich von 2 px statt eines Punktes – Kalk kennt
   keine Kreise, und eine Linie ist die Form, die Zustaende tragen. */
.badge__dot { width: 12px; height: 2px; background: currentColor; flex: 0 0 auto; }

/* Schlagwort ohne Ton: die Gestalt des Kalk-Tags, eine Innenlinie in Kante,
   24 px, die Ecke oben rechts. */
.tag {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    height: 24px;
    padding: 0 var(--s-3);
    box-shadow: inset 0 0 0 1.5px var(--border);
    border-radius: var(--ecke-tag);
    font: 500 var(--fs-13)/1.2 var(--font-text);
    color: var(--fg);
    white-space: nowrap;
}

/* In einer Tabellenzelle steht das Schlagwort unter dem Titel. Mit 24px waere die
   Zeile hoeher als ihre Nachbarn – 20 px tragen denselben Text in der kleinsten
   Stufe; die Ecke bleibt die halbe Hoehe. */
.tag--sm {
    height: 20px;
    padding: 0 var(--s-2);
    border-radius: 0 10px 0 0;
    color: var(--fg-muted);
}

/* Kennzahl-Kachel nach dem Kalk-App-Muster: Weiss auf Kalkweiss, rechteckig,
   ohne Rahmen. Die Zahl steht proportional in Display 600 (Kalk: Kennzahlen),
   darunter Bezeichnung und Einordnung. */
.stat {
    display: flex;
    flex-direction: column;
    gap: var(--s-1);
    padding: var(--s-4) var(--s-6);
    background: var(--surface);
    color: var(--fg);
    overflow-wrap: anywhere;
}

/* Eine Kachel, die verlinkt. Sie soll aussehen wie ihre Nachbarn und sich wie ein
   Link verhalten: keine Unterstreichung, keine Linkfarbe. Auf die Handlung hin
   hebt sie sich 2 px mit Schatten 2 – die Geste der Kalk-Karte. */
.statlink { display: block; text-decoration: none; color: inherit; }
.statlink .stat { transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
.statlink:hover .stat { transform: translateY(calc(-1 * var(--anheben))); box-shadow: var(--schatten-2); }

/* Eine Kachel mit zwei Zielen: die Zahl fuehrt auf ihre Menge, die Fussnote auf
   die Teilmenge daneben. Beide sind Links, deshalb kein .statlink um alles herum —
   ein Link im Link gibt es im Markup nicht. */
.stat__link { display: block; text-decoration: none; color: inherit; }
.stat__link:hover .stat__label { color: var(--accent-ink); text-decoration: underline; text-underline-offset: 4px; }
.stat__link .stat__value,
.stat__link .stat__label { display: block; }

.stat__note--link { display: inline-block; text-decoration: none; }
.stat__note--link:hover { color: var(--accent-ink); text-decoration: underline; }

/* Die Zahl in Display 600, proportionale Ziffern: eine grosse Kennzahl steht
   allein und braucht keine Spalte. 28 px wie die Kennzahl im Kalk-App-Muster. */
.stat__value {
    font: var(--fw-display) var(--fs-28)/1.1 var(--font-display);
    font-variant-numeric: proportional-nums;
    color: var(--fg);
    letter-spacing: -0.02em;
}
.stat__label { font: 500 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }
.stat__note { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

.datarow {
    display: flex;
    align-items: center;
    gap: var(--s-4);
    padding: var(--s-3) 0;
    border-bottom: 1px solid var(--line);
}

.datarow:last-child { border-bottom: none; }
.datarow__label { flex: 1; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }
.datarow__value { font: 500 var(--fs-15)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg); text-align: right; }

.chips { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }

/* Eine Chipreihe als letzte Zeile eines Panels. Zwei Werte, zwei Aufgaben: das
   padding ist der Mindestabstand zu den Balken (im Panel gibt es kein gap, das ihn
   traegt), das `margin-top: auto` schiebt die Reihe an die Unterkante, sobald das
   Panel hoeher ist als sein Inhalt. Beides braucht .panel--column am Panel – ohne
   Flex-Stapel ist der automatische Rand null. */
.chips--panel { margin-top: auto; padding-top: var(--s-6); }

.chips__label {
    font: 500 var(--fs-13)/1.4 var(--font-text);
    color: var(--fg-muted);
    margin-right: var(--s-2);
}

/* Chips sind Filter: der Kalk-Tag als Bedienelement. Innenlinie in Kante,
   gewaehlt Schwarz mit Kalkweiss. 32 px hoch wie die kompakten Knoepfe daneben
   (Kalk S), und deshalb mit deren Ecke – Radius halbe Hoehe. Mobil wachsen sie
   auf die Trefferflaeche. */
.chip {
    display: inline-flex;
    align-items: center;
    height: 32px;
    padding: 0 var(--s-3);
    cursor: pointer;
    white-space: nowrap;
    border: 0;
    box-shadow: inset 0 0 0 1.5px var(--border);
    border-radius: var(--ecke-knopf-s);
    background: transparent;
    color: var(--fg);
    font: 500 var(--fs-13)/1.2 var(--font-text);
    transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease), box-shadow var(--dur-fast) var(--ease);
}

.chip:hover { box-shadow: inset 0 0 0 1.5px var(--fg); }

.chip[aria-current="true"] {
    background: var(--schwarz);
    color: var(--kalk);
    box-shadow: none;
}

/* Suchfeld ueber einer Liste. Es sitzt in derselben Zeile wie die Chips und
   wandert mit margin-left:auto nach rechts – im Entwurf steht die Filterleiste
   links, die Suche am rechten Rand. */
.searchform {
    display: flex;
    align-items: center;
    /* Umbruch wie bei jeder anderen Reihe aus eigenstaendigen Teilen (`.chips`,
       `.actions`, `.paper__bar`). Der Zeitraumfilter des Verlaufs traegt zwei
       Datumsfelder und einen Knopf, und keines davon schrumpft unter seinen
       Inhalt: 417,7 px Eigenbreite in einem 358 px breiten Inhaltsbereich, der
       letzte Knopf endete bei x 454,7 -- `.main` scrollte bei 390 px 65 px
       waagerecht (CLO-159, F8 der Browser-Abnahme). Waagerechter Scroll der
       Seite ist ein Fehler, siehe „Gescrollt wird an genau einer Stelle". */
    flex-wrap: wrap;
    gap: var(--s-2);
    margin-left: auto;
}

/* Die Reihe ueber einer Liste ist kompakt: Feld, Datumsfelder und Knopf sind
   32px hoch wie die Chips daneben (Kalk S), nicht die 44px eines Formularfeldes
   – hier filtert man, man traegt nichts ein. Deshalb die drei Regeln unter dem
   Feld. */
.searchform__field {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    height: 32px;
    padding: 0 var(--s-3);
    border: 1.5px solid var(--border);
    background: var(--surface);
    transition: border-color var(--dur-fast) var(--ease);
}

/* Der Fokus steht an der Huelle, nicht am Feld darin (das hat `outline: none`):
   die Huelle ist das, was wie ein Feld aussieht. */
.searchform__field:focus-within { border-color: var(--fg); outline: 2px solid var(--focus); outline-offset: 2px; }
.searchform__field .icon { color: var(--fg-muted); }

/* Das Kreuz, mit dem Chrome und Safari ein type="search" verzieren, sitzt in
   diesem Rahmen schief; die Leerung geschieht ohnehin ueber "Alle". */
.searchform__input {
    width: 22ch;
    border: 0;
    background: transparent;
    color: var(--fg);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
}

.searchform__input:focus { outline: none; }
.searchform__input::-webkit-search-cancel-button { display: none; }

.searchform .btn { min-height: 32px; padding: var(--s-1) var(--s-4); font-size: var(--fs-13); border-radius: var(--ecke-knopf-s); }
.searchform .control { width: auto; min-height: 32px; padding: var(--s-1) var(--s-3); font-size: var(--fs-13); }

/* Gespeicherte Ansichten: „Ansicht speichern" und „Als Standard" sitzen am
   rechten Rand der Ansichtsreihe, auf demselben Platz wie das Suchfeld einer
   Liste – deshalb dieselbe Feldhuelle (.searchform__field) und dieselbe
   Ausrichtung. Eigen ist nur, dass hier ein POST steht und keine Suche; in der
   Verwaltung steht dieselbe Form in einer Tabellenzelle, dort ohne margin. */
.viewform { display: flex; align-items: center; gap: var(--s-2); margin-left: auto; }
.rowactions .viewform { margin-left: 0; }

.viewform__input {
    width: 18ch;
    border: 0;
    background: transparent;
    color: var(--fg);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
}

.viewform__input:focus { outline: none; }

/* Spalten waehlen (Migration 0034). Ein <details> wie das Benutzermenue: der
   Browser klappt es auf, kein Skript ist beteiligt. Das Panel liegt absolut ueber
   der Tabelle, damit es die Zeilen nicht verschiebt – eine Liste, die beim
   Aufklappen um 300 px springt, verliert die Zeile, die man ansah.

   Der Kasten haengt an der **rechten** Kante seines Auslaesers. Beide Ausloeser
   (Listenwerkzeuge, Kopf der Kacheln) stehen am rechten Rand ihrer Zeile; mit
   `left: 0` wuchs der Kasten von dort nach rechts aus dem Fenster -- auf
   /kontakte bei 1440 px bis x 1630, und .main scrollte waagerecht. Nach links
   hat er die ganze Zeile. */
.colpick { position: relative; }

.colpick__trigger { display: inline-flex; align-items: center; gap: var(--s-2); cursor: pointer; list-style: none; }
.colpick__trigger::-webkit-details-marker { display: none; }
.colpick[open] .colpick__trigger { color: var(--accent-ink); }
.colpick__panel {
    position: absolute;
    top: calc(100% + var(--s-3));
    right: 0;
    z-index: 25;
    min-width: 260px;
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    padding: var(--s-6);
    background: var(--surface);
    box-shadow: var(--schatten-3);
}

.colpick__list { display: flex; flex-direction: column; gap: var(--s-2); }

.colpick__row { display: flex; align-items: center; gap: var(--s-2); }

.colpick__pick {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    cursor: pointer;
}

/* Der erste Zettel in der Zeile nimmt den Platz, die zwei Pfeile stehen rechts. */
.colpick__row .colpick__pick { flex: 1; min-width: 0; }

.colpick__move {
    flex: 0 0 auto;
    width: 24px;
    height: 24px;
    border: 0;
    background: var(--grau-100);
    color: var(--fg);
    font: 400 var(--fs-13)/1 var(--font-text);
    cursor: pointer;
}

.colpick__move:hover { background: var(--line); }

.colpick__sort { display: flex; flex-direction: column; gap: var(--s-2); }

.colpick__label {
    font: 500 var(--fs-13)/1.4 var(--font-text);
    color: var(--fg-muted);
}

.colpick__hint { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

.colpick__actions { display: flex; gap: var(--s-3); }


/* ---- 5. Muster ---------------------------------------------------------- */

/* Tabelle. Das Mockup baut die Listen als CSS-Grid; hier sind es echte
   Tabellen – Screenreader und spaeteres Sortieren brauchen das.

   Spaltenbreiten stehen als style am <col>. Das ist die eine Ausnahme von der
   Regel oben, und zwar eine gewollte: <col> ist der Mechanismus, den HTML dafuer
   hat, und die Breite steht dort neben ihrer Spalte statt als
   col:nth-child(4) irgendwo im Blatt. table-layout: fixed haelt sie ein.

   Die Mindestbreite gehoert nicht dazu – sie ist eine Zahl je Tabelle und steht
   deshalb hier. Unterhalb davon scrollt .panel--table waagerecht, statt die
   Prozentspalten zu zerdruecken. Der Wert ist die Summe der festen Spalten
   geteilt durch ihren Anteil an der Tabelle. */

.table { width: 100%; border-collapse: collapse; table-layout: fixed; }

/* Kontakte: **keine Mindestbreite hier**. Sie haengt an der Spaltenauswahl des
   Benutzers und steht deshalb als style am <table> (Migration 0034,
   store.ListLayoutFor) – bei allen sechs Spalten sind es dieselben 1040 px wie
   vorher, bei drei entsprechend weniger. Eine Zahl im CSS waere die zweite
   Wahrheit, und die falsche: die Tabelle scrollte dann bei jeder Auswahl so weit
   wie bei der groessten.

   Die drei anderen Listen haben ihre Spalten weiter fest; accounts (Formel 1013
   -> eingetragen 1050),
   quotes (1125 -> 1012) und users (1000 -> 900) sind von Hand nachjustiert. */
.table--accounts { min-width: 1050px; }
/* Eine Summenspalte ist keine weitere Kategorie. Ohne Trennung stiessen in der
   Arbeitsuebersicht "UNTERNEHMEN" (eine Vorgangsart) und "VORGÄNGE" (deren
   Summe) aneinander: beide Ueberschriften fuellen ihre 107 px ganz aus und lasen
   sich als ein Wort. Die Haarlinie sagt gleichzeitig, was die Spalte ist. */
.table th.sum,
.table td.sum { border-left: 1px solid var(--line); }

/* 140 px Status: „Angenommen" braucht mit Punkt, Innenabstand und dem Zellenrand
   von 32 px rund 135 px -- in 120 px stand „Angenom…". */
.table--quotes { min-width: 1032px; }
.table--users { min-width: 900px; }
/* Die drei Tabellen der Auswertung. Die Spalten der ersten tragen je Zelle zwei
   Angaben ("3 · 636.000 €") – Anzahl allein sagt nicht, ob es um 60.000 € oder
   6 Mio geht, und je Ausgang zwei Spalten waeren vierzehn.

   Die Uebergangsquote ist die schmalste: vier Spalten, alle nur Zahlen. Eigene
   Klasse und nicht .table--funnel, obwohl sie danebensteht – zwei Tabellen mit
   derselben Klasse sind fuer jede Probe eine Tabelle. */
.table--funnel { min-width: 910px; }
.table--transitions { min-width: 560px; }
.table--owners { min-width: 700px; }
/* Der Trichter je Quelle: Quelle, Kohorte, eine Spalte je Phase (je Zelle
   "6 · 60 %"), offen, Quote, gewonnenes Volumen. Er waechst mit der Phasenliste
   und ist deshalb die breiteste Tabelle der Auswertung. */
.table--sources { min-width: 1000px; }
/* Die drei Tabellen der Arbeitsuebersicht. Die erste hat eine Spalte je
   Aktivitaetsart und waechst deshalb mit der Werteliste: sieben Arten, ein Konto
   und eine Summe. Ohne Mindestbreite fiele sie auf 390 px in sich zusammen --
   dieselbe Falle wie bei den Angebotspositionen. */
.table--work { min-width: 920px; }
.table--outcomes { min-width: 630px; }
.table--duties { min-width: 520px; }
/* Die zwei eigenen Tabellen des Wochenberichts. Die uebrigen drei Bloecke der
   Seite tragen die Klassen der Auswertung, weil es dieselben Zahlen sind.

   Eigene Klassen und nicht .table--work bzw. .table--funnel: die Spalten sind
   andere -- der Wochenblock zeigt Vorgaenge, Fristen und Nachgetragene statt
   einer Spalte je Aktivitaetsart, und der Trichter hier keine Waehrungsspalten
   je Ausgang. Ohne Mindestbreite fielen beide auf 390 px in sich zusammen, und
   die Kopfzeile schoebe sich uebereinander. */
.table--weekly-work { min-width: 620px; }
.table--weekly-funnel { min-width: 720px; }
/* Die zwei Tabellen der Akquise im Wochenbericht. Sie haben eine colgroup und
   trotzdem keine Untergrenze: 550 px bzw. 440 px feste Spalten und eine
   Prozentspalte davor, und unterhalb der Summe bleibt der Woche bzw. der Gruppe
   **nichts** -- gemessen −20 px Inhaltsbreite bei 390 px, also weniger als ihr
   Innenabstand. Vorher lief der Zeilenkopf darueber hinweg, seit er brechen darf
   waren es 422 px hohe Zeilen mit einem Zeichen je Zeile. Beide Zahlen nach der
   Formel oben: 550/0,66 und 440/0,60 (CLO-156, F5 der Browser-Abnahme). */
.table--acquisition { min-width: 840px; }
.table--acquisition-groups { min-width: 740px; }
/* Das Panel „Zusammengefuehrt" einer Detailseite. Es sitzt in einer Spalte von
   335 px, und ohne Untergrenze teilte table-layout: fixed die auf vier Spalten:
   64 px Inhalt fuer den **Namen** des aufgeloesten Datensatzes. Die colgroup
   gibt ihm 42 %, die drei anderen tragen eine ID, eine ID und ein Datum;
   310 px fest / 0,58 = 534,5 -> 540 px. */
.table--merged { min-width: 640px; }
/* Die Liste der API-Schluessel. Sieben Spalten, und bis CLO-157 die einzige
   Liste ohne colgroup und ohne Untergrenze: auf 390 px teilte table-layout:
   fixed 356 px auf sieben und dem Namen blieben **28 px** Inhalt -- „Scraper
   Ausschreibungen" ist dort 153,5 px breit, also 125,5 px zu viel, und die
   .scroll-x um die Liste kam nie zum Zug.

   Die Zahl nach der Formel oben und nicht nach Gefuehl: 680 px feste Spalten
   (Zustand 110, Letzte Benutzung 170, Ablauf 100, Ausgestellt von 160,
   Widerrufen 140) geteilt durch ihren Anteil 0,63 = 1079,4. Damit bekommt der
   Name 237 px und die Bereiche 162 px -- beide ueber ihrem gemessenen Bedarf
   (173,5 bzw. 92,2 px), und bei 1440 px scrollt die Liste weiter nicht. */
.table--api-keys { min-width: 1080px; }
/* Die fuenf Tabellen der Schuldenliste (CLO-158, F7 der Browser-Abnahme). Sie
   standen als benannte Ausnahme in PageControllerTest::WITHOUT_A_LOWER_BOUND und
   quetschten bei 390 px messbar: 64, 64, 47, 36 und **−20 px** Inhaltsbreite in
   der ersten Spalte. Die −20 px sind der Fall, der die Reihe erklaert — weniger
   als der eigene Innenabstand heisst, die Spalte existiert nicht mehr.

   Je Tabelle **eine** Frage, und je Tabelle eine andere Antwort: welche Spalte
   kann nicht brechen? Eine Zahl, ein Abzeichen (`nowrap`), ein `.tag`, ein Knopf
   mit Wort und ein Zeitpunkt koennen es nicht und stehen deshalb fest; ein
   Zeilenkopf, ein Wert und ein Satz duerfen es und nehmen Prozente. Die
   Untergrenze ist die Summe der festen Spalten geteilt durch ihren Anteil --
   dieselbe Formel wie oben. Die Bedarfszahlen dahinter sind gemessen
   (`table-layout: auto`, `width: max-content`, colgroup aus), nicht geschaetzt.

   Und eine Obergrenze fuer alle fuenf: der Rahmen ist bei 1440 px 1138 px breit
   (1170 px bei den Schlagwoertern). Eine Untergrenze darueber liesse die Tabelle
   auch am Schreibtisch scrollen, wo sie heute passt -- genau der Grund, aus dem
   die Liste der API-Schluessel eine colgroup bekam. */

/* Loeschvorschau, Panel „Was mit jedem Bezug passiert": Zeilen (Zahl, Bedarf
   72 px -> 75) und Was passiert (Abzeichen „bleibt, verliert den Bezug", Bedarf
   229 px -> 230) stehen fest, Bezug (26 %) und Warum (44 %) wachsen mit.
   305 px fest / 0,30 = 1016,7 -> 1020 px. Damit bekommt der Bezug 265 px, also
   genau die Breite, die er bei 1440 px heute hat. */
.table--erasure-refs { min-width: 1020px; }
/* Dieselbe Seite, Panel „Was automatisch geschwaerzt wird": nur Feld steht fest
   (Bedarf 86 px -> 90). Datensatz (34 %), Jetzt (28 %) und Danach (26 %) tragen
   Werte und duerfen brechen; die Anteile folgen ihrem Bedarf (246 / 189 /
   179 px). 90 px fest / 0,12 = 750 px, und bei 750 px liegt jede der drei ueber
   ihrem Bedarf. */
.table--erasure-fields { min-width: 750px; }
/* Der Dubletten-Report. Zwei Spalten koennen nicht brechen: Art ist ein `.tag`
   mit `nowrap` (Bedarf 111 px -> 115) und die letzte traegt zwei Knoepfe mit Wort
   (Bedarf 288 px -> 290) -- ein abgeschnittener Knopf ist der Fund, der diese
   Oberflaeche schon einmal gekostet hat („ndern"). Die zwei Namen (je 16 %) und
   der Verdacht (22 %) duerfen brechen. 405 px fest / 0,46 = 880,4 -> 890 px. */
.table--duplicates { min-width: 890px; }
/* Die Liste „Zusammengefuehrt" darunter (Migration 0059): Art und Knopf fest
   (115 + 140 px), drei Textspalten mit 74 % -- 255 / 0,26 = 981 -> 900 px, weil
   die Namen brechen duerfen. */
.table--merges { min-width: 900px; }
/* Der Bericht des Imports. Schmaler als die Listen: vier Spalten, und die zwei
   breiten nehmen Prozente. */
.table--import { min-width: 620px; }
/* Und die Laeufe darunter -- eigene Klasse, weil es andere Spalten sind: zwei
   Tabellen mit derselben Klasse sind fuer jede Probe eine Tabelle. Drei stehen
   fest, weil ihr Inhalt nicht brechen kann: Wann (Zeitpunkt, Bedarf 126 px ->
   130), Stand (Abzeichen „zurueckgenommen", 140 -> 145) und Zuruecknehmen (Knopf
   mit Wort, 157 -> 160). Was (16 %), Ergebnis (24 %) und Von (12 %) brechen.
   435 px fest / 0,48 = 906,3 -> 910 px. */
.table--import-runs { min-width: 910px; }
/* Die Schlagwoerter, und die einzige der fuenf **ohne** neue colgroup: ihre erste
   Spalte hat absichtlich keine Breite und nimmt, was die drei festen uebrig
   lassen. Damit gibt es keinen Anteil, durch den die Formel oben teilen koennte
   -- die Untergrenze ist hier feste Summe **plus** Bedarf: 390 px (130 + 110 +
   150) + 170 px fuer das Abzeichen des Schlagworts = 560 px. Bei 560 px bleiben
   ihm 170 px, bei 356 px Rahmen waren es −20. */
.table--tags { min-width: 560px; }

/* Die Mailvorlagen (Migration 0055): Bezeichnung 30 %, Betreff waechst, dazu
   120 px Zeitpunkt und 110 px Handgriffe. Bei 560 px bleiben der Bezeichnung
   168 px und dem Betreff 162 px -- beide brechen als Text, keine faellt unter
   null. Der Betreff traegt zusaetzlich die Ellipse (.ell). */
.table--mail-templates { min-width: 560px; }

/* Die Wahl der Vorlage ueber dem Mailformular (contact-email.html.twig): ein
   eigenes GET-Formular, Auswahlfeld und Knopf in einer Zeile, der Knopf auf der
   Grundlinie des Feldes. Kein `.form`, damit es nicht wie ein zweiter
   Schreibweg aussieht -- es aendert die Adresse, nicht den Bestand. */
.templatepick { display: flex; align-items: flex-start; gap: var(--s-3); margin-bottom: var(--s-4); }
.templatepick .field { flex: 1; min-width: 0; }
.templatepick .btn { margin-top: calc(var(--fs-13) * var(--lh-text) + var(--s-2)); }
/* Die sechste Tabelle derselben Bauform, ausserhalb der Schuldenliste, weil die
   Zusammenfuehrung keine Seitenroute ist (zwei Kennungen in der Adresse,
   CLO-162). Panel „Feld fuer Feld": vier Spalten, keine davon fest -- Feld ist
   ein Zeilenkopf, Ziel und „Wird aufgeloest" sind Werte, „Es gilt" eine
   Chipreihe, alle vier duerfen brechen. Die Formel oben hat damit keine feste
   Summe; die Untergrenze ist der groesste Bedarf geteilt durch seinen Anteil:
   145,4 px / 0,25 = 581,6 -> 590 px bei einer colgroup aus 21 / 21 / 25 / 33 %.
   Gegenprobe am Seed-Paar Weber/Hoffmann (table-layout: auto, max-content):
   Feld 106,3, Chips 193,7 px -- bei 590 px bekommen sie 124 und 194,7. Ohne die
   Zahl teilte table-layout: fixed die 322 px des Rahmens gleichmaessig: 57 px
   Inhalt je Spalte bei 390 px, Zeilen 93 bis 100 px hoch. */
.table--merge { min-width: 590px; }
/* Das Audit-Log. Die Spalte "Felder" traegt je Zeile eine Liste alt -> neu und
   ist deshalb die breiteste; die Mindestbreite haelt sie lesbar, den Rest
   uebernimmt .scroll-x um die Tabelle. */
.table--changes { min-width: 760px; }
/* Der Papierkorb: sechs gleiche Spalten (keine colgroup), die letzte traegt
   einen Knopf mit Wort. Ohne Mindestbreite fiele die Tabelle auf 390px in sich
   zusammen -- dieselbe Falle wie bei den Angebotspositionen. „Wiederherstellen"
   ist als Kalk-Knopf auf dem Telefon 144 px breit, die Zelle braucht dazu ihre
   24 px Rand: 168 px je Spalte, mal sechs, also 1020 px (bei 880 schnitt die
   Zelle die Ecke des Knopfes ab). */
.table--trash { min-width: 1020px; }
.table--changes td { vertical-align: top; }
/* Die Webhooks (Migration 0054), dieselbe Bauform wie die API-Schluessel. Die
   Liste: vier feste Spalten (Zustand 130, Ereignisse 100, Letzte Zustellung 190,
   Offen/tot 150 = 570 px), System 24 %, die Adresse nimmt den Rest -- bei 1080 px
   sind das 259 und 251 px, genug fuer einen Hostnamen samt Pfad in Ziffernschrift
   mit Ellipse. Das Protokoll: sechs feste Spalten (150 + 200 + 110 + 70 + 80 +
   140 = 750 px), der Fehler nimmt den Rest; 1000 px lassen ihm 250. */
.table--webhooks { min-width: 1080px; }
.table--deliveries { min-width: 1000px; }
/* Die drei Tabellen der Laeufe (Migration 0062): Uebersicht je Art, das
   Protokoll mit acht Spalten, die Fehlerzeilen eines Laufs. Dieselbe Bauform
   wie die Zustellungen -- Mindestbreite im .scroll-x, sonst quetscht das
   schmale Fenster die Zahlen uebereinander. */
.table--run-overview { min-width: 760px; }
.table--runs { min-width: 1040px; }
.table--run-errors { min-width: 1000px; }

/* Ein geaendertes Feld: Name, alter Wert, Pfeil, neuer Wert. Kein Grid, weil die
   Werte sehr verschieden lang sind – eine feste Spalte fuer den alten Wert
   liesse bei "Berlin -> Hamburg" die halbe Zeile leer und bei einer Notiz
   nichts uebrig. */
.fieldlist { display: flex; flex-direction: column; gap: var(--s-2); }
.fieldlist li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s-3); }
.fieldlist__name { font: 500 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.fieldlist__from { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); text-decoration: line-through; }
.fieldlist__arrow { color: var(--fg-muted); }
.fieldlist__to { font: 500 var(--fs-13)/1.4 var(--font-text); }

/* Tabellenkopf nach Kalk: Label 13/500 in Stein, keine Versalien, Haarlinie. */
.table th {
    text-align: left;
    padding: 0 var(--s-6) var(--s-3) 0;
    border-bottom: 1px solid var(--line);
    font: 500 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
    white-space: nowrap;
}

.table td {
    padding: var(--row-pad) var(--s-6) var(--row-pad) 0;
    border-bottom: 1px solid var(--line);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    overflow: hidden;
}

/* Ein `<th scope="row">` ist ein **Wert** und keine Spaltenueberschrift.
   `white-space: nowrap` aus `.table th` ist fuer eine Ueberschrift richtig — sie
   steht einmal und ist so lang, wie ihr Wort ist. Fuer einen Zeilenkopf ist sie
   falsch: er traegt einen Namen, ein Label oder eine Woche, und `.table th` hat
   im Unterschied zu `.table td` **kein** `overflow: hidden`. Ein zu langer Wert
   kuerzt dort also nicht und bricht nicht, er **legt sich ueber die
   Nachbarspalte** — „Charité CFM GmbH" 153,7 px in einer Zelle von 64 px, im
   Panel „Zusammengefuehrt", und bis +279,9 px auf /kontakte/{slug}/loeschen
   (CLO-156, F5 der Browser-Abnahme).

   Deshalb hier und nicht am Fundort: der Fall haengt nicht an einer Tabelle,
   sondern an der Bauform. `overflow-wrap: anywhere` steht dazu, weil ein Wert
   ohne Wortgrenze sonst dieselbe Ueberlagerung behaelt — dieselbe Antwort wie
   an `.cell-strong` (CLO-153).

   Ein `overflow: hidden` steht hier **nicht**: mit einer Umbruchgelegenheit an
   jeder Stelle gibt es nichts mehr, was die Zelle verlassen koennte, und ein
   Schnitt ohne Ellipse waere der Schaden, den CLO-153 gerade weggenommen hat.
   Wer in einem Zeilenkopf kuerzen will, schreibt `.ell` hinein — dessen
   `nowrap` schlaegt beide Zeilen hier, wie in der Liste.
   */
.table th[scope="row"] {
    white-space: normal;
    overflow-wrap: anywhere;
}

.table th:last-child,
.table td:last-child { padding-right: var(--s-8); }

/* Ein Abzeichen ist breiter als seine Spalte -- und dann klippt es das
   overflow: hidden der Zelle darueber mitten im Wort. Genau der Fall, vor dem
   der Kommentar an .ell warnt: ein inline-flex ist keine Blockbox, also greift
   text-overflow nicht an ihm, und es fehlt jedes Zeichen, das „hier fehlt
   etwas" sagt („Hauptunterneh").

   Deshalb zwei Zeilen statt drei Einzelfaelle: die Obergrenze nimmt dem
   Abzeichen die Ueberbreite, und die Ellipse steht an seinem Text -- ui.badge
   legt ihn in eine .ell. Das ist das Netz, nicht der Plan: eine Beschriftung,
   die abgeschnitten nichts mehr sagt, wird kurz gefasst.

   Der Zeilenkopf steht seit CLO-156 mit dabei, und er ist der Grund, dass hier
   nicht `td` steht: ein `.badge` traegt sein eigenes `nowrap`, also nimmt ihm
   die Erlaubnis zu brechen nichts von seiner Breite. Im Panel „Feld fuer Feld"
   der Zusammenfuehrung sass das Abzeichen „verschieden" neben dem Feldnamen in
   64 px und lief mit ihm ueber die Nachbarspalte. */
.table :is(td, th) .badge { max-width: 100%; }
/* Dieselbe Obergrenze fuer das Schlagwort ohne Ton: `.tag` traegt dasselbe
   `nowrap`, und `ui.tag` legt seine Beschriftung seit CLO-163 in dieselbe
   `.ell`. Gemessen im Panel „Laeufe" des Imports mit einem 64 Zeichen langen
   Dateinamen: 362,7 px in einer Zelle von 122 px (390 px) bzw. 157 px
   (1440 px), still geschnitten um 216,8 bzw. 182,2 px — kein Zeichen sagte,
   dass etwas fehlt. */
.table :is(td, th) .tag { max-width: 100%; }
/* Und die Reihe der Schlagwoerter braucht dieselbe Grenze, sonst greift die am
   Abzeichen nicht: `.tagbadges` ist inline-flex und wird so breit wie ihr
   breitestes Abzeichen -- die 100 % darin sind dann die des Abzeichens selbst.
   Gemessen mit einem Schlagwort von 40 Zeichen (store.maxTagName) in der
   Kontaktliste: 247,5 px in 209 px Inhalt (1440 px) bzw. 205 px (390 px),
   still geschnitten, ohne Ellipse (CLO-163). */
.table :is(td, th) .tagbadges { max-width: 100%; }

/* Zahlenspalten in 13 px wie in der Kalk-Tabelle, mit Tabellenziffern: die
   Stellen stehen untereinander. Satzschrift und nicht die Mono der Kalk-Tabelle,
   weil diese Zellen auch Woerter tragen („· Annahme der Phase", „Mio €") und die
   Spaltenbreiten darauf gemessen sind – in der Mono schnitt die Zelle sie ab.
   Geist hat die Tabellenziffern als Feature (tnum). */
.table .num { text-align: right; font: 400 var(--fs-13)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg); white-space: nowrap; }
.table .num--muted { color: var(--fg-muted); }
.table .right { text-align: right; }

.table tbody tr:last-child td { border-bottom: none; }

/* Die Zeile, die zum Formular wird (`?adresse=`, `?beteiligter=`, `?position=`).
   `overflow: visible` gegen die Regel oben: `.table td` schneidet ab, und ein
   Auswahlfeld mit offener Liste steht dann halb hinter dem Rand. Der rechte
   Innenabstand faellt weg, damit das Formular die ganze Breite der Zeile hat —
   es ist keine Zelle mehr, sondern eine Flaeche. */
.table__form td {
    overflow: visible;
    padding-right: 0;
    background: var(--accent-soft);
}

/* Kein Hover auf einer offenen Zeile: sie ist kein Ziel, sie ist die Arbeit. */
.table--rows tbody tr.table__form:hover { background: var(--accent-soft); }

/* Die markierte Zeile der Positionstabelle. Dort steht das Formular **unter** der
   Tabelle (sie hat eine Mindestbreite von 520 px, und ein Formular darin lag auf
   390 px zur Haelfte hinter dem Rand) — die Markierung sagt, zu welcher Zeile es
   gehoert. */
.table tbody tr.table__editing td { background: var(--accent-soft); }

.table--rows tbody tr { transition: background var(--dur-fast) var(--ease); }
.table--rows tbody tr:hover { background: var(--ground); }
.table--rows tbody tr:hover .rowlink { color: var(--accent-ink); }

/* Die ganze Zeile ist anklickbar, der Link traegt sie. So bleibt genau ein
   Tabstop je Zeile und die Tastatur kommt ohne Sonderbehandlung durch. */
.rowlink { color: inherit; }

/* Der Wert einer Zelle. Er darf **ueberall** brechen, und das ist die zweite
   Antwort auf denselben Fall wie bei .ell: eine E-Mail-Adresse hat keine
   Wortgrenze, an der ein Umbruch ansetzen koennte, also lief sie in den
   Panels der Detailseite (81 px Inhalt bei 1440 px, 85 px bei 390 px) ueber
   die Zelle hinaus und wurde von deren overflow: hidden geschnitten --
   „m.weber@cfm.ch" ohne jedes Zeichen, das „hier fehlt etwas" sagt
   (CLO-153, F4 der Browser-Abnahme).

   Warum hier ein Umbruch und in der Liste eine Ellipse: in einem Panel ist
   die Adresse der **Inhalt** der Zeile -- auf 81 px gekuerzt bliebe
   „m.weber@cfm.ch…", und das ist keine Adresse mehr. Eine Zeile hoeher ist
   billiger als ein halber Wert. In einer Liste zaehlt die gleiche Zeilenhoehe
   fuers Ueberfliegen, und daneben stehen Spalten, die die Zeile identifizieren
   -- dort gilt weiter .ell mit dem vollen Wert im title.

   Die zwei Regeln stossen sich nicht: white-space: nowrap aus .ell
   unterdrueckt jede Umbruchgelegenheit, also auch die hier erlaubte. Eine
   .cell-strong.ell bleibt deshalb einzeilig mit Ellipse.
   */
.cell-strong {
    display: block;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    font-weight: 500;
    color: var(--fg);
    overflow-wrap: anywhere;
}
/* Auf der Zelle selbst (Summenzeile im tfoot) zerstoerte display: block die
   Tabellenzelle: Wert und Linie sassen neben der Zeile statt in ihr. */
td.cell-strong, th.cell-strong { display: table-cell; }

.cell-sub { display: block; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
/* Nummern, Daten und Kennungen in der Zelle. Der Name ist aelter als Kalk: die
   Schrift ist die Satzschrift mit Tabellenziffern, nicht die Mono – die Zellen
   tragen auch Woerter, und ihre Breiten sind darauf gemessen. */
.cell-mono { font: 400 var(--fs-13)/1.4 var(--font-text); font-variant-numeric: tabular-nums; }
.cell-mono--sm { display: block; font: 400 var(--fs-13)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg-muted); }

.cell-person { display: flex; align-items: center; gap: var(--s-3); min-width: 0; }

.initials {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    flex: 0 0 auto;
    background: var(--grau-100);
    color: var(--fg);
    font: 500 var(--fs-13)/1.4 var(--font-text);
}

.initials--lg {
    width: 64px;
    height: 64px;
    background: var(--accent-soft);
    color: var(--accent-ink);
    font-size: var(--fs-22);
}

.initials--sm { width: 24px; height: 24px; font-size: var(--fs-13); }

/* Name mit Avatar in einer Tabellenzelle. Der Name kuerzt sich (.ell), das
   Quadrat nicht – es hat sein Mass und behaelt es. */
.withavatar { display: flex; align-items: center; gap: var(--s-3); min-width: 0; }
.withavatar .initials { flex: none; }

/* Eine Reihe Schlagwoerter, die umbricht: die Spalten, die eine Import-Art
   kennt. Dieselbe Anordnung wie .identity__badges, aber ein eigener Name – das eine
   sind Zustaende eines Datensatzes, das andere eine Aufzaehlung. */
.tags { display: flex; flex-wrap: wrap; gap: var(--s-3); margin: var(--s-4) 0; }

/* Die Schlagwoerter eines Datensatzes (Migration 0022).

   Drei Anordnungen, drei Orte, und alle drei zeigen dasselbe Abzeichen:

   .tagbadges  in einer Tabellenzelle und im Kopf einer Detailseite. Inline, weil
               es dort neben Text steht; der Abstand nach oben trennt es von der
               Zeile darueber, ohne die Zeilenhoehe der Nachbarn zu aendern.
   .tagrow     im Panel, wo jedes Abzeichen sein Formular zum Wegnehmen traegt.
   .tagpicks   die Farbwahl der Verwaltung: dieselbe Reihe, nur mit Radioknopf.

   Keine eigene Farbe, keine eigene Form: ein Schlagwort *ist* ein .badge.
   */
.tagbadges { display: inline-flex; flex-wrap: wrap; gap: var(--s-2); margin-top: var(--s-1); }

.tagrow { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.tagrow__item { display: inline-flex; align-items: center; gap: var(--s-1); }

.tagpicks { display: flex; flex-wrap: wrap; gap: var(--s-4); }

.tagpick { display: inline-flex; align-items: center; gap: var(--s-2); cursor: pointer; }

/* Eine Datenzeile, deren Wert breiter ist als eine Spalte: Beschriftung oben,
   Inhalt darunter. Fuer die Spaltenlisten des Imports – zehn Schlagwoerter passen
   nicht rechtsbuendig in eine halbe Zeile. */
.datarow--stacked { flex-direction: column; align-items: stretch; gap: 0; }
.datarow--stacked .datarow__value, .datarow--stacked .tags { text-align: left; margin: var(--s-2) 0 var(--s-3); }

/* Ein Feldfehler in einer Zeile des Import-Berichts. Als Block, weil eine Zeile
   mehrere haben kann, und in Alarmfarbe, weil er dieselbe Aussage traegt wie ein
   Feldfehler im Formular. */
.rowerror { display: block; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fehler-text); }
.rowerror code { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg); }
/* Was eine Importzeile ausser ihrem Datensatz bewirkt (Unternehmen, Notiz): eine
   Auskunft, kein Fehler -- deshalb die ruhige Farbe neben .rowerror. */
.rownote { display: block; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Eine Zahl, auf die man reagieren soll: die offenen Projekte ohne naechsten
   Schritt. Als Link in Alarmfarbe – dieselbe Aussage wie das Kennzeichen auf der
   Karte, an einer anderen Stelle. */
.alarm { color: var(--fehler-text); }

/* Liste mit Trennlinien: Aufgaben, Termine, verknuepfte Projekte */

.list { display: flex; flex-direction: column; }

.list__row {
    display: flex;
    /* Eine Reihe aus eigenstaendigen Teilen darf umbrechen. Die Aufgabenzeile traegt neben
       dem Titel ein Kaestchen, ein Abzeichen, das Ergebnisfeld und die Frist:
       16 + 62 + 110 + 64 px plus vier Abstaende = 316 px, und die Zeile hat bei
       390 px 298. Ohne Umbruch bekam der Titel **0 px** und stand mit einem Wort
       je Zeile in 350 bis 557 px hohen Zeilen; die Frist ragte 20 px ueber den
       Rand der Zeile (CLO-161). Mit Umbruch rutschen Ergebnisfeld und Frist in
       eine zweite Zeile, sobald dem Titel weniger bleibt als seine Basis. */
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--s-4);
    padding: var(--s-4) 0;
    border-bottom: 1px solid var(--line);
}

.list__row:last-child { border-bottom: none; }
.list__row--center { align-items: center; }

/* Basis 170 px, nicht 0: mit `flex: 1` schrumpft der Titel vor allen anderen auf
   nichts, und die Zeile bricht nie. 90 px waere die Breite, unter der eine
   Textspalte aufhoert, eine zu sein (PageControllerTest::CRAMPED_FROM_COLUMNS) --
   als Basis war das zu wenig: in der Aufgabenspalte von /aktivitaeten (510 px
   Inhalt) blieben dem Titel bei 1440 px 108 px, die Zeile brach nicht, und ein
   Titel stand in drei, sein Beiwerk in sechs Zeilen. Mit 170 px weichen
   Ergebnisfeld und Frist in die zweite Zeile, sobald der Titel weniger bekaeme;
   bei 390 px (298 px Zeile) bleiben Kaestchen, Titel und Abzeichen in der ersten
   (16 + 170 + 70 + 2 x 16 = 288). Was der Titel darueber hinaus bekommt, waechst
   mit. */
.list__main { display: flex; flex-direction: column; gap: 2px; flex: 1 1 170px; min-width: 0; }
/* overflow-wrap: ein Titel ohne Wortgrenze („Angebotsdurchsprache") lief bei
   390 px aus seiner Spalte in den Namen rechts daneben (Termine der Woche). */
.list__title { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); overflow-wrap: anywhere; }
.list__meta { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
/* margin-left: auto haelt die Frist rechts, auch wenn sie in der zweiten Zeile
   neben dem Ergebnisfeld steht; in der ersten fuellt der Titel, und der Rand
   tut nichts. */
.list__aside { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); white-space: nowrap; margin-left: auto; }

/* Frist **und** angesetzte Dauer in einer Zeile („01.06.2026, 09:30 · 45 min"):
   das ist zu lang fuer nowrap. Umbrochen wird nur zwischen den beiden Angaben –
   innerhalb halten geschuetzte Leerzeichen zusammen (ui.duration). Dass der
   Titel daneben Platz behaelt, regelt der Umbruch der Zeile (.list__row), nicht
   diese Regel. */
.list__aside--wrap { white-space: normal; text-align: right; }

.list__when { display: flex; flex-direction: column; gap: 2px; width: 78px; flex: 0 0 auto; }
.list__day { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--accent-ink); }
.list__time { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }

.check {
    width: 20px;
    height: 20px;
    margin-top: 1px;
    flex: 0 0 auto;
    border: 1.5px solid var(--border);
    background: var(--surface);
    cursor: pointer;
    padding: 0;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

.check:hover { border-color: var(--fg); }

/* Gehakt: das Kalk-Kaestchen, Schwarz mit Haken in Kalkweiss. Der Haken ist ein
   gedrehtes L aus zwei Raendern – kein Icon, ein <use> im Kaestchen waere nur ein
   Fleck. Gesetzt wird der Zustand ueber aria-checked, damit die Zeile auch
   vorgelesen richtig ist. */
.check[aria-checked="true"] {
    background: var(--schwarz);
    border-color: var(--schwarz);
    position: relative;
}

.check[aria-checked="true"]::after {
    content: "";
    position: absolute;
    left: 5.5px;
    top: 1.5px;
    width: 5px;
    height: 9px;
    border: solid var(--kalk);
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
}

/* ---- Benachrichtigungen: die Zeile ist ein Formular ---------------------- */

/* Der Klick auf den Titel markiert als gelesen und leitet weiter, und das ist ein
   POST (CLAUDE.md, Regel 3). Die Zeile erbt .list__row und legt nur ab, was ein
   <form> von sich aus mitbringt. */
.notify__row { margin: 0; }

/* Der Titel ist der Absende-Knopf dieses Formulars und soll aussehen wie der
   Titel jeder anderen Zeile — deshalb der Reset und keine .btn-Klasse. */
.notify__title {
    padding: 0;
    border: none;
    background: none;
    cursor: pointer;
    text-align: left;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
}

.notify__title:hover { color: var(--accent-ink); }

/* Ungelesenes hebt sich ab, ohne die Zeile umzubauen: der Lime-Strich links
   (Kalk: aktive Markierung, wie in der Seitenleiste) und Blatt als Flaeche. Das
   Abzeichen „Neu" sagt dasselbe in Worten — Farbe allein traegt keine Aussage.
   Eine Innenlinie und kein Rand: ein Rand verschoebe den Inhalt gegen die
   gelesenen Zeilen. */
.list__row--unread {
    background: var(--accent-soft);
    box-shadow: inset 3px 0 0 var(--lime);
}

/* Gruppen in der Aufgabenliste */

.group { display: flex; flex-direction: column; margin-bottom: var(--s-8); }
.group:last-child { margin-bottom: 0; }

.group__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    padding-bottom: var(--s-3);
    border-bottom: 1px solid var(--line);
}

.group__label {
    font: 500 var(--fs-13)/1.4 var(--font-text);
    color: var(--fg);
}

.group__count { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }

/* Balken: Pipeline-Wert nach Phase */

.bars { display: flex; flex-direction: column; gap: var(--s-6); }

.bar {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 2fr) minmax(0, 1fr);
    align-items: center;
    gap: var(--s-6);
}

.bar__label { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }
/* Balken nach Kalk: rechteckig, die Spur Grau 100, die Fuellung die erste
   Reihenfarbe (Moos) – Lime steht nie in einem Diagramm. Er zeichnet sich einmal
   lang (800 ms, Einlaufkurve), wenn er ins Bild kommt. */
.bar__track { display: block; height: 12px; background: var(--grau-100); }
.bar__fill { display: block; height: 12px; background: var(--chart-1); width: var(--bar-pct, 0%); }
.bar__fill[data-reveal="true"] { transform-origin: left; animation: bar-reveal var(--dur-slow) var(--ease-in); }
@keyframes bar-reveal { from { transform: scaleX(0); } }
.bar__value { text-align: right; font: 400 var(--fs-13)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg); }

/* Zeitreihen: dieselben Balken, nur viele. Der engere Abstand ist der Unterschied
   zwischen einer lesbaren Reihe und einer Seite Balken – eine Reihe aus 24
   Monaten scrollt sonst aus dem Bild. Die Beschriftung ist kuerzer als bei einer
   Phase ("Aug 2026" statt "Qualifizierung · 12"), deshalb bekommt der Balken die
   Breite. */
.bars--series { gap: var(--s-3); }
.bars--series .bar { grid-template-columns: minmax(0, 1.3fr) minmax(0, 2.2fr) minmax(0, 1.4fr); }

/* Der laufende Abschnitt ist unvollstaendig: ein Balken, der halb so hoch ist,
   weil der Monat halb vorbei ist, wird sonst als Einbruch gelesen. Die Schraffur
   sagt es am Balken, die Beschriftung daneben in Worten – Farbe allein waere
   keine Auskunft fuer jemanden, der sie nicht unterscheidet. */
.bar--running .bar__fill {
    background: repeating-linear-gradient(135deg,
        var(--chart-1) 0 4px,
        var(--grau-100) 4px 8px);
}

/* Timeline: Verlauf am Kontakt, Aktivitaeten im Team */

.timeline { display: flex; flex-direction: column; gap: var(--s-6); }

.timeline__item { display: grid; grid-template-columns: 22px 1fr; gap: var(--s-6); align-items: start; }

.timeline__rail {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-2);
    padding-top: 3px;
    color: var(--accent-ink);
}

.timeline__line { width: 1px; flex: 1; min-height: 26px; background: var(--line); }

.timeline__body { display: flex; flex-direction: column; gap: var(--s-2); padding-bottom: var(--s-2); }
.timeline__head { display: flex; align-items: baseline; gap: var(--s-4); flex-wrap: wrap; }
.timeline__title { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); font-weight: 500; color: var(--fg); }
.timeline__date { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }
.timeline__text { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); max-width: var(--measure); overflow-wrap: anywhere; }

/* Änderungsvermerk („geändert am … von …", ui.editMark). Gedämpft, in derselben
   Zeile wie Text bzw. Beiwerk: eine Fußnote, die den Inhalt nicht verdrängt. In
   .list__meta ist die ganze Zeile schon gedämpft, in .timeline__text nicht. */
.editmark { color: var(--fg-muted); }

/* Board: Projekte nach Phase */

.board {
    display: grid;
    grid-template-columns: repeat(5, minmax(216px, 1fr));
    align-items: stretch;
    overscroll-behavior-x: contain;
    background: var(--ground);
    border: 1px solid var(--line);
}

.projectnav { gap: var(--s-2); flex-wrap: wrap; }
.projectnav .chip[aria-current="page"] { background: var(--schwarz); color: var(--kalk); box-shadow: none; }
.projectbrief { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-6); padding-block: var(--s-4); border-block: 1px solid var(--line); }
.projectbrief > div { flex: 1 1 18rem; min-width: 0; }
.projectbrief__next { margin-block: var(--s-2); font-size: var(--fs-19); font-weight: 500; overflow-wrap: anywhere; }
.projectbrief__facts { display: flex; flex-wrap: wrap; gap: var(--s-6); margin: 0; }
.projectbrief__facts dt { color: var(--fg-muted); font-size: var(--fs-13); }
.projectbrief__facts dd { margin: var(--s-1) 0 0; overflow-wrap: anywhere; }
.projectrelations { display: flex; flex-wrap: wrap; gap: var(--s-6); padding-bottom: var(--s-4); border-bottom: 1px solid var(--line); }
.projectrelations > div { flex: 1 1 14rem; min-width: 0; overflow-wrap: anywhere; }
.projectrelations__label { margin: 0 0 var(--s-2); color: var(--fg-muted); font-size: var(--fs-13); font-weight: 400; }
.projectrelations__people { display: grid; gap: var(--s-3); margin: 0; padding: 0; list-style: none; }
.projectrelations__people span { display: block; color: var(--fg-muted); font-size: var(--fs-13); }
.projectrelations__manage { display: inline-block; margin-top: var(--s-3); font-size: var(--fs-13); text-decoration: underline; }

.board__col {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s-4);
    padding: var(--s-6);
    border-left: 1px solid var(--line);
}

.board__col:first-child { border-left: none; }

.board__colhead {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding-bottom: var(--s-4);
    border-bottom: 2px solid var(--line);
}

.board__coltop { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s-3); }

.board__collabel {
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    font-weight: 500;
    font-size: var(--fs-13);
    color: var(--fg);
}

.board__colcount { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }
.board__colsum { font: 400 var(--fs-13)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg); }

.board__card {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    background: var(--surface);
    padding: var(--s-4);
    cursor: pointer;
    transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
}

/* Die Kalk-Karte: weiss auf Kalkweiss, ohne Rahmen. Auf die Handlung hin hebt sie
   sich 2 px mit Schatten 2. */
.board__card:hover { transform: translateY(calc(-1 * var(--anheben))); box-shadow: var(--schatten-2); }

.board__title { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); font-weight: 500; color: var(--fg); overflow-wrap: anywhere; }
.board__client { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); overflow-wrap: anywhere; }

.board__figures {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s-3);
    padding-top: var(--s-3);
    border-top: 1px solid var(--line);
    white-space: nowrap;
}

.board__value { font: 500 var(--fs-15)/1.4 var(--font-text); font-variant-numeric: tabular-nums; color: var(--fg); }
.board__bgf { min-width: 0; overflow: hidden; text-overflow: ellipsis; font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }
.board__foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); }
.board__date { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }
.board__empty { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* "+ 12 weitere" am Fuss einer Spalte. Derselbe gedaempfte Kleinsatz wie der
   Leerzustand, aber ein eigener Name: das eine sagt "hier ist nichts", das andere
   "hier ist mehr, als das Board zeigt". */
.board__more { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Der Leerzustand gilt nur, solange die Spalte leer ist. Beim Ziehen wandert die
   Karte in die Spalte, bevor gespeichert ist – ohne diese Zeile stuende "Kein
   Projekt in dieser Phase" unter einer Karte.

   Auf .board__card und nicht auf [data-dnd-item]: die Karten der Spalte
   "Gewonnen" sind keine Ziehobjekte, und
   mit dem alten Selektor stuende ihr Leerzustand unter ihnen. */
.board__col:has(.board__card) .board__empty { display: none; }

/* Der naechste Schritt auf der Karte. Zwei Zustaende, und der zweite ist der
   Grund fuer die Zeile: ohne offenen Vorgang bewegt sich ein Projekt nicht mehr,
   und das soll man einer Spalte ansehen, ohne jede Karte zu oeffnen. Deshalb hier
   Farbe statt Grauschrift – es ist die einzige Fehlmeldung auf der Karte. */
.board__next {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    min-width: 0;
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

.board__next svg { flex: none; }
.board__next time { margin-left: auto; font: 500 var(--fs-13)/1.4 var(--font-text); }
.board__next--none { color: var(--fehler-text); }

/* Die Liegezeit ab der Schwelle ihrer Phase: Warnfarbe, nicht Alarm. Ein
   fehlender naechster Schritt ist eine Unterlassung, eine lange Liegezeit erst
   ein Verdacht — und beide Zeilen stehen uebereinander, also darf nur eine
   davon rot sein. */
.board__next--stale { color: var(--warnung-text); }

/* Die Spalte des Ausgangs. Gefaerbt ist nur ihr Kopf: eine ganze Spalte in Gruen
   zoege den Blick vom offenen Bestand weg, und der ist die Arbeit. */
.board__col--won .board__colhead { border-bottom-color: var(--erfolg-mark); }
.board__col--won .board__colsum { color: var(--erfolg-text); }

/* "in diesem Monat" unter dem Spaltenkopf: die Spalte zeigt einen Zeitraum,
   waehrend ihre Nachbarn einen Bestand zeigen. */
.board__colnote { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* ---- Ziehen und Ablegen (public/js/dnd.js) ------------------------------- */

/* Hinweis und Meldung ueber dem Board. Die Meldung haelt ihre Zeile frei, auch
   wenn sie leer ist: sonst rutscht das Board beim ersten Ziehen nach unten. */
/* Hinweis und Ansage des Ziehens stehen in einem Block: die Seite gibt ihren
   Abstand damit einmal, nicht dreimal. Vorher lag das Board 110 px tiefer, und
   auf einem 900 px hohen Fenster kostet das eine Kartenreihe. */
.boardnote {
    display: grid;
    gap: var(--s-2);
}

.boardnote > p { margin: 0; }

.boardhint {
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

.boardhint kbd {
    font: 400 var(--fs-13)/1.4 var(--font-mono);
    padding: 1px var(--s-2);
    background: var(--grau-100);
    color: var(--fg);
}

.boardsay {
    min-height: 1.4em;
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--accent-ink);
}

.boardsay[data-tone="error"] { color: var(--fehler-text); font-weight: 500; }

/* Der Weg aus einer Fehlermeldung: im Board gibt es kein Feld zum Nachtragen,
   nur diesen Link (public/js/dnd.js). Er nimmt die Farbe der Zeile, damit er zum
   Satz gehoert statt daneben zu stehen — unterstrichen bleibt er, sonst ist er
   als Link nicht zu erkennen. */
.boardsay a { color: inherit; text-decoration: underline; }

/* Karten sind ziehbar: der Greifzeiger sagt es, und touch-action gibt dem Finger
   das Blaettern zurueck – waagerecht zieht die Geste, senkrecht scrollt die
   Seite. Ohne diese Zeile entscheidet der Browser, und der entscheidet scrollen. */
[data-dnd-item] { touch-action: pan-y; cursor: grab; }
[data-dnd-item]:active { cursor: grabbing; }

/* Das Original waehrend des Ziehens: es bleibt an seinem kuenftigen Platz stehen
   und zeigt als Umriss, wohin die Karte fallen wird. */
.dnd-ghost {
    opacity: .38;
    outline: 1.5px dashed var(--border);
    outline-offset: -1.5px;
}

/* Die Kopie am Zeiger. Leicht gekippt – der Griff soll sich vom Raster loesen,
   sonst sieht das Ziehen aus wie ein Rendering-Fehler. Sie liegt ueber allem und
   traegt deshalb Schatten 3 (Kalk: Flyout). */
.dnd-fly {
    position: fixed;
    z-index: 60;
    margin: 0;
    pointer-events: none;
    box-shadow: var(--schatten-3);
    transform-origin: 12% 12%;
    rotate: 1.4deg;
    scale: 1.02;
}

/* Aufgenommen mit der Tastatur: kein fliegender Griff, dafuer ein Ring, der so
   deutlich ist wie der Fokus. */
.dnd-picked {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

/* Zielspalte. Der Ton ist derselbe wie bei einer Erfolgsmeldung – hier passiert
   gleich etwas Gewolltes. */
.dnd-over { background: var(--erfolg-soft); }

/* Waehrend des Schreibens: die Karte liegt schon, ist aber noch nicht bestaetigt. */
.dnd-saving { opacity: .6; }

/* Nach dem Nachladen ist die Karte ein neues Element an einer neuen Stelle.
   Der kurze Ring fuehrt das Auge dorthin. */
.dnd-landed { outline: 2px solid transparent; outline-offset: 2px; animation: dnd-landed var(--dur) var(--ease) 2 alternate; }

@keyframes dnd-landed {
    from { outline-color: transparent; }
    to   { outline-color: var(--focus); }
}

/* Waehrend ein Zug laeuft, markiert niemand Text – ein blau markierter Kartentitel
   unter dem Zeiger sieht aus wie ein Fehler. */
[data-dnd-active="1"] { user-select: none; }
[data-dnd-active="1"] [data-dnd-item] { cursor: grabbing; }

@media (prefers-reduced-motion: reduce) {
    .dnd-fly { rotate: none; scale: 1; }
}

/* Kalender: Wochenraster mit absolut gesetzten Eintraegen */

.cal__head,
.cal__grid {
    display: grid;
    grid-template-columns: 56px repeat(5, minmax(110px, 1fr));
    min-width: 700px;
}

.cal__grid { border-top: 1px solid var(--line); }

.cal__day {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 0 var(--s-3) var(--s-4);
    border-left: 1px solid var(--line);
}

.cal__dayname { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); font-weight: 500; color: var(--fg); }
.cal__daydate { font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }

.cal__hours { display: flex; flex-direction: column; }

/* 62px ist die Rasterhoehe einer Stunde – die Lage der Eintraege rechnet damit
   (CalendarController::HOUR_PX). Wer sie aendert, aendert beides. */
.cal__hour { height: 62px; padding-top: var(--s-1); font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }

.cal__col { position: relative; overflow: hidden; border-left: 1px solid var(--line); }
.cal__slot { display: block; height: 62px; border-bottom: 1px solid var(--line); }

/* Ein Termin im Raster. Ueberschneiden sich zwei, teilen sie die Spalte in Spuren:
   --event-lanes ist die Zahl der Spuren dieses Tages, --event-lane die Spur dieses
   Termins. Ohne das liegen gleichzeitige Termine uebereinander, und man liest
   keinen von beiden. Beide Werte rechnet der Controller – er hat die Zeitpunkte;
   die Standardwerte (eine Spur, Spur 0) gelten fuer den Normalfall. */
.cal__event {
    --event-lane: 0;
    --event-lanes: 1;

    position: absolute;
    top: var(--event-top);
    height: var(--event-height);
    left: calc(6px + var(--event-lane) * (100% - 12px) / var(--event-lanes));
    width: calc((100% - 12px) / var(--event-lanes));
    display: flex;
    flex-direction: column;
    gap: 2px;
    overflow: hidden;
    padding: var(--s-2) var(--s-3);
    background: var(--accent-soft);
    border-left: 2px solid var(--moos);
}

/* Als Link soll der Block ein Block bleiben: kein Unterstrich, keine Linkfarbe –
   die Klasse traegt die Gestalt, das <a> nur das Ziel. */
a.cal__event { text-decoration: none; }
a.cal__event:hover { border-left-color: var(--tanne); }

.cal__eventmeta { font: 500 var(--fs-13)/1.4 var(--font-text); line-height: 1.2; color: var(--accent-ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.cal__eventtitle {
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    line-height: 1.25;
    font-weight: 500;
    color: var(--fg);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ---- Ziehen im Raster (public/js/dnd.js) --------------------------------- */

/* Im Raster zieht die Geste auch senkrecht – das ist ihr Sinn. Deshalb hier
   touch-action: none statt des pan-y der Karten: der Finger soll die Uhrzeit
   aendern und nicht die Seite scrollen. */
[data-dnd="calendar"] [data-dnd-item] { touch-action: none; cursor: grab; }

/* Der Griff fuer die Dauer sitzt auf der Unterkante. Sichtbar wird er erst unter
   dem Zeiger: ein Raster mit 30 Griffen darin waere unruhig. Die Trefferflaeche
   ist trotzdem immer da – 8px, nicht 1px Rand. */
.cal__grip {
    position: absolute;
    inset: auto 0 0 0;
    height: 8px;
    cursor: ns-resize;
}

.cal__grip::after {
    content: "";
    position: absolute;
    inset: auto var(--s-3) 2px;
    height: 2px;
    background: var(--moos);
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease);
}

.cal__event:hover .cal__grip::after,
.cal__event:focus-visible .cal__grip::after,
.cal__grip:hover::after { opacity: .9; }

/* Der Block in der Hand. Er wandert selbst (keine Kopie), liegt ueber allem und
   nimmt keine Zeigerereignisse an – sonst faende elementFromPoint immer ihn und
   nie die Spalte, in die er soll. */
.dnd-lifted {
    z-index: 20;
    pointer-events: none;
    border-left-color: var(--tanne);
    box-shadow: var(--schatten-3);
}

/* ---- 6. Formulare, Anmeldung, Leerzustaende ----------------------------- */

/* Ein Formular ist eine Spalte aus Feldern mit einer Knopfzeile am Fuss. Wo
   zwei kurze Felder nebeneinander passen (Ort/PLZ, Datum/Frist), nimmt die
   Seite .form__grid – das Raster bricht selbst um, statt an einem Breakpoint. */

.form { display: flex; flex-direction: column; gap: var(--s-6); }
/* Zwei Formulare hintereinander sind zwei Dinge: „Angebotsblatt als PDF ablegen"
   stand ohne Abstand ueber dem Feld „Datei", „Logo von der Website holen" ueber
   „Logo hochladen". Der Abstand ist der eines Formularblocks. */
.panel > form + form { margin-top: var(--s-6); }

.form__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
    gap: var(--s-4);
}

.formextras { border-top: 1px solid var(--line); }
.formextras > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: var(--target-min);
    gap: var(--s-3);
    cursor: pointer;
    list-style: none;
    font-weight: 500;
}
.formextras > summary::-webkit-details-marker { display: none; }
.formextras[open] > summary .icon { transform: rotate(180deg); }
.formextras[open] > .form__grid { padding-top: var(--s-3); }

.field { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }

.field__label {
    font: 500 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg);
}

.field__req { margin-left: 2px; color: var(--accent-ink); }

/* Ein Feld nach Kalk (Input, Select, Textarea): rechteckig, Kante 1,5 px in
   --border (3,8 : 1 auf Weiss), Weiss, mindestens 44 px hoch, 15 px Schrift.
   Der Fokus ist der Ring aus tokens.css (2 px Tanne), dazu wird die Kante
   Schwarz. Unter 1000 px steigt die Schrift auf 17 px: iOS zoomt beim Fokus in
   jedes Feld unter 16 px. */
.control {
    width: 100%;
    min-height: var(--target-min);
    padding: var(--s-2) var(--s-4);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    background: var(--surface);
    border: 1.5px solid var(--border);
    transition: border-color var(--dur-fast) var(--ease);
}

.control:hover { border-color: var(--fg); }
.control:focus-visible { border-color: var(--fg); }
.control::placeholder { color: var(--fg-muted); opacity: 1; }
.control:disabled { background: var(--grau-100); border-color: var(--linie-stark); color: var(--fg-muted); cursor: not-allowed; }

.control--area {
    min-height: 96px;
    padding: var(--s-3) var(--s-4);
    line-height: var(--lh-text);
    resize: vertical;
}

/* Das Auswahlfeld traegt den Winkel aus Kalk (k-select) statt des Pfeils des
   Systems. Ein Bild aus der eigenen Herkunft: die CSP erlaubt `self`, kein
   `data:`. Rechts bleibt Platz fuer ihn. */
.control--select {
    appearance: none;
    padding-right: var(--s-12);
    background: var(--surface) url("/img/select.svg") no-repeat right var(--s-4) center / 12px 12px;
}

.field--error .control { border-color: var(--fehler-mark); }

.field__hint,
.field__error {
    margin: 0;
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
}

.field__hint { color: var(--fg-muted); }
.field__error { color: var(--fehler-text); }

/* Ein Wert im Raster, der kein Feld ist (form.fixed) – die Quelle eines
   bestehenden Datensatzes. Dieselbe Höhe wie .control, damit die Zeile im Raster
   auf derselben Grundlinie sitzt wie die Felder daneben; kein gesenkter Grund und
   keine Haarlinie, denn hier schreibt niemand hinein. */
.field__value {
    display: flex;
    align-items: center;
    min-height: var(--target-min);
    margin: 0;
    font: 500 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
}

/* Spracheingabe (public/js/dictate.js). Der Block steht im Markup mit hidden und
   wird erst sichtbar, wenn der Browser Spracherkennung kann – dass das `display`
   hier den Ausgangszustand nicht aufhebt, garantiert die eine [hidden]-Regel in
   Abschnitt 1.

   Waehrend der Aufnahme steht der Knopf in der Fehlerfarbe auf ihrer Flaeche:
   dass zugehoert wird, muss man sehen, ohne auf Text zu warten. Er pulst nicht –
   in Kalk bewegt sich nichts von selbst; der Zustand ist Farbe und Wort. */
.dictate {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-width: 0;
}

.dictate__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--s-1);
    padding: var(--s-1) var(--s-2);
    margin-left: calc(var(--s-2) * -1);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
    background: none;
    border: 0;
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease), background-color var(--dur-fast) var(--ease);
}

.dictate__btn:hover { color: var(--fg); }

.dictate__status {
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.dictate--on .dictate__btn { color: var(--fehler-text); background: var(--fehler-soft); }
.dictate--on .dictate__status { color: var(--fehler-text); }

/* In der Suchzeile sitzt nur das Icon, zwischen Feld und Tastenhinweis. */
.dictate--compact { flex: 0 0 auto; }
.dictate--compact .dictate__btn { margin: 0; padding: var(--s-1); }

/* Kaestchen im Formular. Nicht .check – das ist der Knopf zum Abhaken einer
   Aufgabe in der Liste (Abschnitt 5). */
/* Eine Gruppe Kaestchen unter einer Beschriftung (API-Schluessel, „Bereiche").
   <fieldset> traegt im Browser einen Rahmen und eine eigene Innenabstand-Regel;
   beides weg, damit die Gruppe aussieht wie jedes andere Feld – die Semantik ist
   der Grund fuer das Element, nicht sein Aussehen. */
.fieldset {
    display: flex;
    flex-direction: column;
    gap: var(--s-3);
    margin: 0;
    padding: 0;
    border: 0;
}

.checkbox { display: flex; align-items: flex-start; gap: var(--s-3); }

.checkbox__input { margin: 1px 0 0; cursor: pointer; }

.checkbox__label { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); cursor: pointer; }

/* Kaestchen und Radio nach Kalk, fuer jedes Feld der Anwendung. Das Kaestchen ist
   ein Quadrat von 20 px mit Kante, gewaehlt Schwarz mit Haken in Kalkweiss; das
   Radio ist ein stehendes Stadion (16 × 24), gewaehlt mit schwarzem Kern. Den
   Haken traegt ein Bild aus der eigenen Herkunft (CSP: kein `data:`). */
input[type="checkbox"],
input[type="radio"] {
    appearance: none;
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    margin: 0;
    border: 1.5px solid var(--border);
    background: var(--surface);
    vertical-align: middle;
    cursor: pointer;
    transition: background-color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}

input[type="checkbox"]:hover,
input[type="radio"]:hover { border-color: var(--fg); }

input[type="checkbox"]:checked {
    border-color: var(--schwarz);
    background: var(--schwarz) url("/img/check.svg") no-repeat center / 12px 12px;
}

input[type="radio"] { width: 16px; height: 24px; border-radius: var(--stadion); }
input[type="radio"]:checked { border-color: var(--schwarz); background: var(--schwarz); box-shadow: inset 0 0 0 3px var(--weiss); }

input[type="checkbox"]:disabled,
input[type="radio"]:disabled { background-color: var(--grau-100); border-color: var(--linie-stark); cursor: not-allowed; }

/* Gesetzt und gesperrt (Lesekopie sperrt jedes Feld) bleibt als gesetzt erkennbar:
   ohne diese Regel faerbt :disabled die Flaeche grau, der weisse Haken verschwindet
   und das Kaestchen sieht ungesetzt aus. Stein haelt den Haken sichtbar. */
input[type="checkbox"]:checked:disabled,
input[type="radio"]:checked:disabled { background-color: var(--stein); border-color: var(--stein); }

/* Meldung: Flash der Sitzung, Fehlerkasten eines Formulars, fehlgeschlagene
   Anmeldung. Eine Gestalt, drei Anlaesse – der Hinweis aus Kalk: rechteckig,
   16 px Innenabstand, die Flaeche des Zustands und seine Textfarbe. Ohne Ton
   ist es die Information (Blau). Zustand nie nur ueber Farbe: das Icon und der
   Satz sagen es mit. */
.flashes { display: flex; flex-direction: column; gap: var(--s-3); margin-bottom: var(--s-6); }

.notice,
.offline {
    display: flex;
    align-items: flex-start;
    gap: var(--s-3);
    padding: var(--s-4);
    background: var(--hinweis-soft);
    color: var(--hinweis);
    font: 400 var(--fs-15)/1.45 var(--font-text);
    overflow-wrap: anywhere;
}

.notice__icon, .offline__icon { margin-top: 2px; color: inherit; }
.notice__body, .offline__body { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; }
.notice__title, .offline__title { margin: 0; font: 600 var(--fs-15)/1.45 var(--font-text); color: inherit; }
.notice__list { margin: 0; padding-left: var(--s-6); font: 400 var(--fs-15)/1.45 var(--font-text); color: inherit; }
.notice__list li { margin-top: var(--s-1); }

/* Jeder Eintrag springt zu seinem Feld. Die Verweise tragen die Farbe des
   Kastens und ihre Unterstreichung dauerhaft, damit sie als Sprungpunkt zu
   erkennen sind, ohne gegen ihn anzuschreien. */
.notice__list a { color: inherit; text-decoration: underline; text-underline-offset: 4px; }
.notice__list a:hover { color: var(--fg); }

.notice--ok { background: var(--erfolg-soft); color: var(--erfolg-text); }
.notice--error { background: var(--fehler-soft); color: var(--fehler-text); }

/* Dritter Ton: ein Hinweis, der keinen Fehlschlag meldet und keinen Erfolg –
   „dieser Datensatz ist archiviert". */
.notice--warn { background: var(--warnung-soft); color: var(--warnung-text); }

/* Das Kennzeichen der Staging-Umgebung (CLO-78, components/stage.html.twig): die
   Gestalt der Warnung, dazu nur der Abstand nach unten, den sonst .flashes
   traegt -- es steht vor den Meldungen und ist keine. */
.stage { margin-bottom: var(--s-6); }

/* Der Hinweis der Lesekopie (CLO-118, base.html.twig #offline-banner). Die
   Gestalt der Meldung im Warn-Ton, aber eigene Klassen: der Inhalt einer
   <template> steht im DOM, und `.notice--warn` faende sonst auf jeder Seite
   zuerst ihn (die Regeln oben gruppieren .notice und .offline). Er steht vor
   den Meldungen und traegt denselben Abstand. Der Zeitpunkt ist fett: er ist
   die Auskunft, der Rest ist Erklaerung. */
.offline { background: var(--warnung-soft); color: var(--warnung-text); margin-bottom: var(--s-6); }
.offline time { font-weight: 600; }

/* Ohne Netz sind die Formulare gesperrt (app.js setzt disabled je Feld). Der
   Zustand am Shell-Element nimmt den Schreibknoepfen zusaetzlich den Akzent:
   ein gruener Knopf, der nichts tut, saehe aus wie ein Fehler der Seite. */
.shell[data-offline="true"] .btn--primary,
.shell[data-offline="true"] .topbar__cta,
.shell[data-offline="true"] .iconbtn--cta { opacity: 0.5; pointer-events: none; }

.notice__text, .offline__text { margin: 0; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: inherit; }

/* Der Knopf in der Box traegt die Farbe der Box, nicht seine eigene: ein grauer
   Rand im roten Kasten saehe aus wie ein zweiter Gegenstand. */
.notice .btn--secondary { color: inherit; box-shadow: inset 0 0 0 1.5px currentColor; }
.notice .btn--secondary:hover:not(:disabled) { background: var(--surface); color: inherit; box-shadow: inset 0 0 0 1.5px currentColor; }

/* Der Weg aus einer Meldung. Als eigene Zeile im .notice__body, damit der Knopf
   nicht neben dem Satz klebt – .notice__body ist eine Spalte mit Abstand. */
.notice__action { margin: 0; }

/* Knopfzeile. Der zerstoerende Knopf steht rechts aussen, mit Abstand zu
   Speichern – dort trifft ihn niemand versehentlich. */
.formactions {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
    padding-top: var(--s-6);
    border-top: 1px solid var(--line);
}

.formactions__destructive { margin-left: auto; }

/* Ein Formular, das nur einen Knopf traegt: Archivieren und Abhaken sind
   Schreibvorgaenge und brauchen deshalb ein <form> mit CSRF-Feld, sollen aber in
   einer Knopfzeile stehen wie ein Knopf. */
.inlineform { display: inline-flex; margin: 0; }

/* Das Auswahlfeld einer Listenzeile – heute nur das Ergebnis am Haekchen
   (Migration 0032). Es traegt .control und damit Grund, Haarlinie und Fokusring
   eines Feldes, aber nicht seine Masse: eine Zeile hat schon Titel, Kennzeichen
   und Frist, und ein 36px-Feld ueber die ganze Spalte waere der lauteste
   Gegenstand darin. Die Breite kommt vom Inhalt, gedeckelt, damit ein langer
   Wert die Frist nicht aus der Zeile draengt. */
.rowselect {
    width: auto;
    max-width: 10rem;
    min-height: 32px;
    padding: 0 var(--s-8) 0 var(--s-2);
    background-position: right var(--s-2) center;
    font-size: var(--fs-13);
}

/* Die Anrufliste (Ticket #92). Eine Zeile traegt drei Bloecke: wer angerufen
   wird, die waehlbaren Nummern, und das Formular, mit dem der Anruf festgehalten
   wird.

   **Umbrechend und ohne Mindestbreite** – das ist die ganze Antwort auf 390 px.
   Eine Tabelle mit sechs Spalten braeuchte dort einen waagerechten Scroll oder
   gequetschte Spalten; hier legt sich das Formular auf eine eigene Zeile
   (flex-basis 100 %), und Nummern und Notiz brechen fuer sich um. Kein
   .scroll-x, weil nichts breiter wird als sein Rahmen. */
.calllist__row { flex-wrap: wrap; align-items: center; }

.calllist__dial { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* Immer eine eigene Zeile, auf jeder Breite. Zwei Zeilen je Anruf sind
   vorhersehbar; ein Formular, das ab einer bestimmten Breite in dieselbe Zeile
   rutscht, springt beim Drehen des Telefons. */
.calllist__form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2);
    flex: 1 1 100%;
    margin: 0;
}

/* Datum und Notiz tragen .control und damit Grund, Haarlinie und Fokusring eines
   Feldes – aber die Masse einer Listenzeile, wie .rowselect daneben. */
.calllist__date {
    width: auto;
    min-height: 32px;
    padding: 0 var(--s-2);
    font-size: var(--fs-13);
}

/* Eine Zeile hoch: es ist eine Notiz und kein Bericht. Wachsen darf sie von
   Hand, und sie nimmt den Platz, der zwischen den Knoepfen uebrig ist. */
.calllist__note {
    flex: 1 1 12rem;
    min-width: 8rem;
    height: 32px;
    min-height: 32px;
    padding: var(--s-1) var(--s-2);
    font-size: var(--fs-13);
    resize: vertical;
}

/* Die Meldung der Zeile steht unter ihrem Formular und nicht in einer
   Flash-Meldung am Kopf der Seite: bei 25 Zeilen ist „welche denn?" die erste
   Frage, die ein Satz da oben aufwirft. */
.calllist__status {
    flex: 1 1 100%;
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

.calllist__status[data-tone="error"] { color: var(--fehler-text); }

/* Festgehalten: die Zeile bleibt stehen und tritt zurueck. Sie zu entfernen
   waere schneller und liesse einen Fehlgriff spurlos verschwinden – so steht das
   Ergebnis noch da, und der Fokus ist schon in der naechsten Zeile. */
.calllist__row[data-call-done] { opacity: .55; }
.calllist__row[data-call-busy] .calllist__form { opacity: .55; }

/* Zwei Wege an einer Tabellenzeile: aendern und loeschen. Rechtsbuendig und mit
   kleinem Abstand, damit das rote Kreuz nicht am Knopf klebt. */
.rowactions {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: var(--s-2);
}

/* Im Verlauf stehen die beiden Wege links unter dem Text – dort, wo die Zeile
   beginnt, und nicht am rechten Rand einer Spalte, die es hier nicht gibt. */
.rowactions--left { justify-content: flex-start; margin-top: var(--s-2); }

/* Ein Formular-Panel hat keine Zeilen, sondern Felder: der Innenabstand steht
   deshalb hier und nicht am .panel. */
.panel--form { padding: var(--s-6); }

.btn--danger { background: transparent; color: var(--fehler-text); box-shadow: inset 0 0 0 1.5px var(--fehler); }

.btn--danger:hover:not(:disabled) {
    background: var(--fehler);
    color: var(--weiss);
    box-shadow: none;
}

/* Benutzermenue hinter dem Avatar. Das Aufklappen macht <details>, deshalb
   steht hier nur die Lage – und das Verstecken des Dreiecks, das der Avatar
   nicht braucht. */
.usermenu { position: relative; display: flex; align-items: center; flex: 0 0 auto; }

.usermenu__trigger { cursor: pointer; list-style: none; }
.usermenu__trigger::-webkit-details-marker { display: none; }
.usermenu[open] .usermenu__trigger { color: var(--accent-ink); }

.usermenu__panel {
    position: absolute;
    top: calc(100% + var(--s-3));
    right: 0;
    z-index: 50;
    min-width: 220px;
    display: flex;
    flex-direction: column;
    padding: var(--s-2) 0;
    background: var(--surface);
    box-shadow: var(--schatten-3);
}

.usermenu__who {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--s-2) var(--s-6) var(--s-3);
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--s-2);
}

.usermenu__name { font: 500 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }
.usermenu__role { font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

.usermenu__item {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-6);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    white-space: nowrap;
}

.usermenu__item:hover { background: var(--ground); }

/* Anmeldung und Fehlerseiten. Eigene Seite ohne Shell (base_bare.html.twig): eine
   Karte in der Mitte, sonst nichts – es gibt an dieser Stelle genau eine Sache zu
   tun. Spalte, nicht Zeile: in Staging steht das Banner (components/stage.html.twig)
   vor der Karte und nahm sonst die halbe Breite neben ihr ein. */
.bare { display: flex; flex-direction: column; min-height: 100dvh; background: var(--ground); }
.bare > .stage { margin: 0; }

.login {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s-12) var(--s-4);
}

.login__card {
    width: 100%;
    max-width: 384px;
    display: flex;
    flex-direction: column;
    gap: var(--s-6);
    padding: var(--s-12) var(--s-8);
    background: var(--surface);
}

/* Der Schriftzug: dieselbe Datei wie in der Kopfzeile, Schwarz auf Weiss, hier
   28px hoch. Der Schutzraum steht als Abstand nach unten. */
.login__mark { align-self: flex-start; height: 28px; width: auto; margin-bottom: var(--s-1); }

.login__intro { display: flex; flex-direction: column; gap: var(--s-2); }
.login__title { font: var(--fw-display) var(--fs-28)/var(--lh-display) var(--font-display); letter-spacing: -0.025em; }
.login__lead { margin: 0; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.login__submit { width: 100%; margin-top: var(--s-2); }
/* "Mit Microsoft anmelden" (CLO-60): ein zweiter Weg unter dem Formular, als
   Nebenknopf -- das Passwort bleibt der erste. Das "oder" trennt die zwei, eine
   Haarlinie darueber. */
.login__alt { display: flex; flex-direction: column; gap: var(--s-2); margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--line); }
.login__or { align-self: center; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Leerzustand einer Liste, nach Kalk: Kalkweiss als Flaeche, das Piktogramm im
   stehenden Stadion aus Blatt und Tanne. Steht im Panel an der Stelle der
   Zeilen, damit eine leere Liste nicht wie ein Fehler aussieht. */
.empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-12) var(--s-6);
    text-align: center;
    background: var(--ground);
}

.empty__mark {
    display: grid;
    place-items: center;
    width: 32px;
    height: 48px;
    margin-bottom: var(--s-2);
    border-radius: var(--stadion);
    background: var(--blatt);
    color: var(--tanne);
}
.empty__title { margin: 0; font: 500 var(--fs-17)/var(--lh-text) var(--font-text); color: var(--fg); }
.empty__hint { margin: 0; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); max-width: 46ch; }
.empty .btn { margin-top: var(--s-2); }

/* Blaetterwerk unter einer Liste. Es sitzt im Panel unter den Zeilen und traegt
   deshalb dieselbe Trennlinie wie eine Zeile – sonst klebt es am letzten
   Eintrag. */
.pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    flex-wrap: wrap;
    padding: var(--s-3) var(--s-6);
    border-top: 1px solid var(--line);
}

.pager__count,
.pager__pos { margin: 0; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.pager__links { display: flex; align-items: center; gap: var(--s-2); }

/* Der Rand der Liste: kein Link, sondern ein abgeblendeter Platzhalter. Als
   <span> bleibt die Breite der Zeile gleich, egal auf welcher Seite man steht –
   ein verschwindender Knopf laesst den Rest springen. */
.is-disabled {
    opacity: 0.45;
    cursor: default;
    pointer-events: none;
}

/* ---- Die Tastatur (public/js/keys.js) ----------------------------------- */

/* Eine Taste auf dem Bildschirm – eine Klasse fuer beide Orte, an denen sie
   vorkommt: als Hinweis in einem Knopf und als Zeile im Overlay. Es ist dieselbe
   Sache, und zwei Klassen dafuer waeren zwei Gestalten derselben Taste. */
.keyhint {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 var(--s-1);
    font: 400 var(--fs-13)/1 var(--font-mono);
    color: var(--fg);
    background: var(--grau-100);
}

/* Im Menue steht die Taste rechts aussen, wie die Uhrzeit in einer Listenzeile. */
.usermenu__item .keyhint { margin-left: auto; }

/* Der Eintrag „Tastenkürzel" ist ein Knopf und kein Link: dahinter liegt keine
   Adresse, sondern ein Overlay. Ein <button> bringt seine eigene Gestalt mit,
   und die muss hier weg. */
button.usermenu__item {
    width: 100%;
    background: none;
    border: none;
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    text-align: left;
    cursor: pointer;
}

/* Das Overlay mit der Liste. <dialog> im modalen Modus bringt Fokus, Escape und
   die oberste Ebene selbst mit; hier steht nur, wie es aussieht. */
.keyhelp {
    width: min(760px, calc(100vw - 2 * var(--s-6)));
    max-height: calc(100dvh - 2 * var(--s-6));
    overflow-y: auto;
    padding: var(--s-6);
    background: var(--surface);
    color: var(--fg);
    border: none;
    box-shadow: var(--schatten-3);
}

.keyhelp::backdrop { background: color-mix(in srgb, var(--schwarz) 40%, transparent); }

.keyhelp__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-4);
    margin-bottom: var(--s-6);
}

.keyhelp__title { margin: 0; font: var(--fw-display) var(--fs-22)/var(--lh-display) var(--font-display); letter-spacing: -0.02em; color: var(--fg); }

.keyhelp__cols {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(230px, 100%), 1fr));
    gap: var(--s-6);
}

.keyhelp__grouptitle {
    margin: 0 0 var(--s-3);
    font: 500 var(--fs-13)/1.4 var(--font-text);
    color: var(--fg-muted);
}

/* Zwei Spalten aus einer Definitionsliste: Taste links, Bedeutung rechts. Das
   Raster liegt auf der <dl>, damit beide Spalten ueber alle Zeilen ausgerichtet
   sind – mit einem Flexbox-Paar je Zeile waeren sie es nicht. */
.keyhelp__list {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: baseline;
    gap: var(--s-2) var(--s-4);
    margin: 0;
}

.keyhelp__keys { display: flex; gap: var(--s-1); }
.keyhelp__what { margin: 0; font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

.keyhelp__note {
    margin: var(--s-6) 0 0;
    padding-top: var(--s-4);
    border-top: 1px solid var(--line);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

/* ---- Schmal (< 1280px, Kalk l) ------------------------------------------- */

@media (max-width: 1279px) {
    .kbd { display: none; }
}

/* ---- Mobil (< 1000px, Kalk m): Schublade statt Seitenleiste ------------- */

@media (max-width: 999px) {
    /* 58px: 24px Wortmarke plus ihr Schutzraum von 17px oben und unten. Der
       Rand des Inhalts ist der mobile aus Kalk (20 px). */
    :root { --topbar-h: 58px; --page-inset: 20px; }
    .topbar { gap: var(--s-2); }
    .topbar__actions { gap: var(--s-2); }
    .topbar__burger { display: flex; width: 44px; height: 44px; }
    .topbar__divider { display: none; }
    .brand { margin-right: var(--s-2); }
    .searchbox { display: none; }
    .iconbtn { width: 44px; height: 44px; }
    .iconbtn--search { display: flex; }
    .iconbtn--cta { display: flex; border-radius: var(--ecke-knopf); }
    .iconbtn--bell { display: none; }
    /* Der beschriftete Knopf weicht dem Icon-Knopf: beide zusammen sprengen
       die Kopfzeile, und alles rechts davon rutscht aus dem Bild. */
    .topbar__cta { display: none; }
    .avatar { width: 40px; height: 40px; }

    /* Der Tastenhinweis im Knopf verschwindet, wo kein Knopf Platz hat und in
       aller Regel keine Tastatur steckt. Im Overlay bleibt er stehen: wer die
       Liste aufruft, hat eine. */
    .btn .keyhint { display: none; }

    /* Die Verbindungsanzeige ist unsere Zutat, im Entwurf gibt es sie nicht –
       und in der Zeile bleiben mobil keine 19px fuer sie uebrig. Absolut
       gesetzt kostet sie keine Breite und liegt als Abzeichen auf dem Avatar.
       Der Rand in Weiss trennt das Quadrat vom Avatar. */
    .topbar__actions { position: relative; }
    .conn { position: absolute; right: -2px; top: 4px; }
    .conn__text { display: none; }
    .conn__dot { width: 11px; height: 11px; border: 2px solid var(--surface); }

    /* Ohne Navigation neben dem Inhalt: links nur der Seitenrand. */
    .main { padding-left: var(--page-inset); padding-bottom: var(--s-12); }

    /* Zeitreihen auf dem Telefon: Beschriftung und Zahl in einer Zeile, der
       Balken darunter ueber die ganze Breite. Dreispaltig blieben fuer
       "Aug 2026 · laufend" rund 70px, und die Beschriftung waere drei Zeilen
       hoch -- der Balken, um den es geht, waere der schmalste Teil der Zeile. */
    .bars--series .bar {
        grid-template-columns: 1fr auto;
        gap: var(--s-1) var(--s-3);
    }
    /* Ausdruecklich gesetzt und nicht der automatischen Platzierung ueberlassen:
       ein Element, das zwei Spalten braucht, springt dort in die naechste Zeile
       und schiebt die Zahl in eine dritte. */
    .bars--series .bar__label { grid-area: 1 / 1; }
    .bars--series .bar__value { grid-area: 1 / 2; }
    .bars--series .bar__track { grid-area: 2 / 1 / 3 / -1; }

    /* Navigation liegt als Schublade ueber dem Inhalt – dieselbe Leiste in Tanne,
       ueber dem Vorhang (.scrim). Die Schublade beginnt wie am Schreibtisch unter
       der Kopfzeile: .sidenav traegt top/bottom schon. */
    .sidenav {
        display: none;
        width: 256px;
    }

    .shell[data-drawer="open"] .sidenav { display: flex; }

    /* Der gemerkte eingeklappte Zustand gilt nur am Schreibtisch: als
       Schublade ist die Navigation immer voll breit und beschriftet. Die
       Variable an der Shell wird hier ueberschrieben, damit .main nicht mit
       56px rechnet – gebraucht wird sie mobil ohnehin nicht. */
    .shell[data-nav="collapsed"] { --nav-w: 256px; }
    .shell[data-nav="collapsed"] .sidenav__label { display: initial; }

    .sidenav__list { gap: var(--s-1); }
    .sidenav__item { min-height: 48px; }
    .sidenav__foot { height: 52px; }

    /* Der Fussknopf schliesst hier, statt zu klappen (app.js) – also X und
       „Schließen", in beiden Werten von data-nav.

       Das [data-nav] ohne Wert steht da fuer die Spezifitaet: die Regeln des
       eingeklappten Schreibtischs sind dreiklassig, ein einfaches
       .sidenav__footlabel kaeme dagegen nicht an. Gleiche Spezifitaet, spaeter
       im Blatt – das gewinnt. */
    .shell[data-nav] .sidenav__foot .icon--collapse,
    .shell[data-nav] .sidenav__foot .icon--expand,
    .shell[data-nav] .sidenav__footlabel { display: none; }
    .shell[data-nav] .sidenav__foot .icon--close { display: inline-block; }
    .shell[data-nav] .sidenav__footlabel--drawer { display: initial; }

    .page { gap: var(--s-6); }
    .page--lead { gap: var(--s-8); }
    .page__title { font: var(--fw-display) var(--fs-22)/var(--lh-display) var(--font-display); letter-spacing: -0.02em; }

    .panel { padding: var(--s-6); }
    .panel--table { padding: var(--s-6) 0 var(--s-4) var(--s-6); }
    .panel--table > :is(.panel__head, .panel__text, .form, .chips, .empty, .notice) { margin-right: var(--s-6); }
    .panel__foot { padding-right: var(--s-6); }
    .table th:last-child,
    .table td:last-child { padding-right: var(--s-6); }

    .grid-stats, .grid-2 { gap: var(--s-6); }
    .grid-2--tight { grid-template-columns: minmax(0, 1fr); }
    .board { grid-template-columns: repeat(5, minmax(250px, 1fr)); }
    .chip { height: var(--target-min); border-radius: var(--ecke-knopf); }
    .control { font-size: var(--fs-17); }

    /* Unter 16 px zoomt iOS beim Fokus in ein Feld hinein und laesst die Seite
       vergroessert stehen. Nicht nur .control: auch die Felder der Kopfzeile, der
       Palette, der Listenfilter und der Zeilen tragen ihre eigene Schrift. Das
       Typpraefix hebt die Regel ueber .bulkbar__action, das weiter unten steht. */
    :is(input, select, textarea):is(
        .searchbox__input, .palette__input, .searchform__input, .viewform__input,
        .rowselect, .calllist__date, .calllist__note, .bulkbar__action
    ),
    .searchform .control { font-size: var(--fs-17); }

    /* Die Spaltenwahl nimmt hier die ganze Zeile. Der Kasten haengt an der
       linken Kante seines Auslaesers, ist rund 300px breit und traegt kein
       max-width -- sitzt der Chip als vierter seiner Reihe bei x 95, endet der
       Kasten jenseits von 390px, und .main scrollt waagerecht. Waagerechter
       Scroll der Seite ist ein Fehler, und es ist der vierte Fundort desselben.

       Die Zeile als Bezug und keine Zahl: eine Obergrenze aus 100vw minus
       Rand haenge an der Position des Chips, und die kennt CSS hier nicht --
       eine Zeile weiter oder ein Chip mehr, und der Kasten ragte wieder heraus.
       Der Auslaeser bleibt der Chip: <details> ist ein Block, das <summary>
       darin ein inline-flex und damit weiter so breit wie seine Beschriftung. */
    .colpick { flex: 1 0 100%; }
    .colpick__panel { min-width: 0; width: 100%; }

    /* Menueeintraege auf Fingergroesse – die Felder haben ihre 44px schon
       (--target-min an .control). */
    .usermenu__item { min-height: 48px; }
    .usermenu__panel { min-width: 240px; }

    /* Gestapelt gibt es kein „rechts aussen" mehr, und die Knopfzeile darf den
       Loeschknopf nicht als letzten Fingerweg anbieten. */
    .formactions__destructive { margin-left: 0; }

    .login { padding: var(--s-6) var(--s-4); }
    .login__card { padding: var(--s-8) var(--s-6); }

    .empty { padding: var(--s-8) var(--s-4); }
}

/* ---------------------------------------------------------------------------
   Positionen und Druckansicht eines Angebots

   Zwei Tabellen und ein Blatt. Die Positionstabelle steht in einem Panel der
   Detailseite, das Blatt ist eine eigene Seite (.paper) — dieselbe Klasse .table
   traegt beide, nur die Mindestbreiten unterscheiden sich.
   --------------------------------------------------------------------------- */

/* Telefon (< 640px, Kalk s). Wortmarke in Mindestgroesse und alle Bedienelemente
   der Kopfzeile passen nicht in eine Zeile: die Suche bekommt eine eigene, statt
   die Wortmarke zu verkleinern oder Handgriffe zu verstecken. */
@media (max-width: 639px) {
    :root { --topbar-h: 112px; }
    .topbar { flex-wrap: wrap; align-content: center; gap: var(--s-2); padding: var(--s-2) var(--s-3); }
    .topbar .brand { margin-right: auto; order: 1; }
    .topbar__burger { order: 0; }
    .topbar__actions { display: contents; }
    .topbar .usermenu { order: 2; }
    .topbar .searchbox { order: 3; display: flex; flex: 1 1 calc(100% - 60px); max-width: none; height: var(--target-min); margin: 0; }
    .topbar .iconbtn--cta { order: 4; }
    .topbar .iconbtn--search, .topbar .conn { display: none; }
    .topbar .usermenu__panel { right: var(--s-3); top: 56px; }
    .filters .chips__label { flex-basis: 100%; }
    .listtools .searchform { flex-basis: 100%; }
    /* Das „Mehr"-Menue haengt an seinem Knopf, und der steht in der umbrechenden
       Knopfzeile mal links, mal rechts: an der rechten Kante verankert ragte es
       links heraus, an der linken (so stand es hier) rechts -- bei 390 px bis
       x 486. Bezug ist deshalb die Zeile: .actions wird der Positionsrahmen, das
       Menue spannt ihre ganze Breite und liegt unter ihr. */
    .actions { position: relative; }
    .recordmore { position: static; }
    .recordmore__items { left: 0; right: 0; max-width: none; }
    .grid-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s-3); }
    .stat { padding-left: var(--s-3); padding-right: var(--s-3); padding-bottom: var(--s-4); }
    .stat__value { font-size: var(--fs-22); }
    .stat__label { font-size: var(--fs-13); }
    .page--lead { gap: var(--s-6); }
    .page__head .lead { font-size: var(--fs-15); }
    .stats__head { align-items: flex-start; }
    .stats__head .colpick { flex-basis: auto; }
    .stats__head .colpick__panel { width: calc(100vw - 2 * var(--page-inset)); }
}

/* Fuenf Spalten, vier davon fest (Menge, Einzelpreis, Summe, Handgriffe: 476 px).
   Was bleibt, ist die Leistung -- mit 584 px waren das 108 px, und bei 390 px
   stand „Aufnahme und Kennzeichnung Datenpunkte" mitten im Wort gebrochen in
   sechs Zeilen. 760 px lassen ihr 284 px; das Panel scrollt waagerecht. */
.table--items { min-width: 760px; }
.table--sheet { min-width: 480px; }
/* Die Betraege des Blatts in der Mono, wie in der PDF-Datei (pdf/quote-sheet.css):
   dort gibt es keine Tabellenziffern, und Bildschirm und Datei sollen dasselbe
   Blatt zeigen. */
.table--sheet .num { font-family: var(--font-mono); }

/* Die Summenzeile ist die Aussage der Tabelle: eine Linie darueber, kraeftiger
   gesetzt. tfoot bekommt sie hier und nicht in .table, weil nur diese zwei
   Tabellen eine haben. */
.table--items tfoot th,
.table--items tfoot td,
.table--sheet tfoot th,
.table--sheet tfoot td {
    padding-top: var(--s-4);
    border-top: 1px solid var(--line-strong);
    border-bottom: none;
    font-size: var(--fs-15);
    color: var(--fg);
}
/* `.table .num` schlaegt die Zeilenregel an Spezifitaet: die Summe stand sonst
   in 13 px neben einem Zeilenkopf in 15 px. */
.table--sheet tfoot .num { font-size: var(--fs-15); line-height: var(--lh-text); }

/* Das Blatt. Die Flaeche ist Papier, auch auf dem Bildschirm: was hier zu sehen
   ist, kommt so aus dem Drucker. */
.paper {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-6);
    min-height: 100dvh;
    padding: var(--s-6) var(--s-4);
    background: var(--ground);
}

/* Die einzige Bedienung auf dem Blatt — und das Einzige, was nicht mitgedruckt
   wird. */
.paper__bar {
    display: flex;
    /* Umbruch wie bei jeder anderen Knopfreihe (`.actions`): die Leiste der
       Auskunft traegt drei Knoepfe und stand auf 390 px 52 px ueber dem Rand --
       waagerechter Scroll auf einer Seite, die keinen braucht. */
    flex-wrap: wrap;
    gap: var(--s-3);
    width: 100%;
    max-width: 820px;
}

/* Papierkanten sind gerade – auch auf dem Bildschirm, denn das Blatt zeigt,
   was aus dem Drucker kommt. */
.sheet {
    width: 100%;
    max-width: 820px;
    padding: var(--s-12) var(--s-8);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 0;
}

.sheet__head {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-6);
    justify-content: space-between;
    align-items: flex-start;
    padding-bottom: var(--s-6);
    border-bottom: 1px solid var(--line);
}

.sheet__brand { font: var(--fw-display) var(--fs-22)/var(--lh-display) var(--font-display); letter-spacing: -0.02em; color: var(--fg); }
.sheet__from { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Drei Angaben, die auf jedem Angebot oben rechts stehen: Nummer, Datum, Frist.
   Als <dl>, weil es Begriff und Wert sind.

   Und mit Umbruch, aus demselben Grund wie `.paper__bar` darueber: die drei
   Begriff/Wert-Paare schrumpfen nicht unter ihren Inhalt, zusammen 382,3 px in
   einem Blatt, das bei 390 px 292 px Inhalt hat. Das Dokument scrollte deshalb
   waagerecht (431/390) -- die Druckansicht war damit **wieder** die einzige
   Seite, die als Ganzes scrollt. Beim ersten Mal wurde die Tabelle behoben und
   die Kopfdaten daneben nicht (CLO-159, F8 der Browser-Abnahme). */
.sheet__meta { display: flex; flex-wrap: wrap; gap: var(--s-6); }
.sheet__meta dt {
    font: 500 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}
.sheet__meta dd { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }

.sheet__to { padding: var(--s-6) 0 var(--s-6); }
.sheet__label {
    font: 500 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}
.sheet__account { font: 500 var(--fs-17)/var(--lh-text) var(--font-text); color: var(--fg); }
.sheet__project { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }

.sheet__title { font: var(--fw-display) var(--fs-28)/var(--lh-display) var(--font-display); letter-spacing: -0.025em; color: var(--fg); margin-bottom: var(--s-6); }

.sheet__hint { padding-top: var(--s-4); font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Auskunft nach Art. 15 DSGVO: je Quelle ein Abschnitt, je Zeile ein Satz
   Feld/Wert. Als <dl> und nicht als Tabelle je Zeile — die Felder einer Zeile
   sind eine Liste und keine Matrix, und zwoelf Quellen mit je einer Tabelle
   scrollten auf dem Telefon zwoelfmal. */
.sheet__block { padding-top: var(--s-6); }
.sheet__subtitle {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2);
    align-items: baseline;
    font: var(--fw-display) var(--fs-22)/var(--lh-display) var(--font-display);
    color: var(--fg);
}
.sheet__context {
    padding-top: var(--s-4);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    font-weight: 500;
    color: var(--fg);
}
.sheet__record {
    display: grid;
    grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
    gap: 2px var(--s-4);
    padding-top: var(--s-2);
}
.sheet__record dt { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
/* Eine UUID, ein Dateiname oder ein jsonb hat keine Wortgrenze, an der ein
   Umbruch von selbst passiert — ohne das steht der Wert aus dem Blatt heraus. */
.sheet__record dd { font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); overflow-wrap: anywhere; }

.sheet__foot {
    margin-top: var(--s-8);
    padding-top: var(--s-6);
    border-top: 1px solid var(--line);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
}

/* Der zweite Faktor: QR-Code und Wiederherstellungscodes.

   Der QR-Code steht auf Weiss: er ist dunkel auf hell, und so liest ihn jede
   Kamera. Die weisse Flaeche bringt das SVG selbst mit
   (src/Security/QrCodeSvg.php); hier steht nur das Polster, damit die Ruhezone
   nicht am Rand der Karte klebt. */
.qr {
    display: flex;
    justify-content: center;
    padding: var(--s-3);
    background: var(--weiss);
}

.qr__code { width: 228px; max-width: 100%; height: auto; }

.qr__secret { margin-top: var(--s-3); text-align: center; }

.qr__secret code,
.codes__item code { font: 400 var(--fs-15)/1.4 var(--font-mono); color: var(--fg); }

/* Zehn Codes, zweispaltig, damit sie ohne Scrollen auf einen Blick abzuschreiben
   sind. Auf einem schmalen Fenster wird eine Spalte daraus (auto-fit). */
.codes {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(160px, 100%), 1fr));
    gap: var(--s-2);
    list-style: none;
    margin: 0;
    padding: 0;
}

.codes__item {
    padding: var(--s-2) var(--s-3);
    background: var(--ground);
    text-align: center;
}

/* Der Druck. Alle Regeln hier sind Weglassungen oder Papiermasse — was das Blatt
   *ist*, steht oben, damit der Bildschirm nicht luegt. */
@media print {
    /* Eine gewoehnliche Seite sitzt in der Shell mit fester Bildschirmhoehe, und
       .main scrollt dort: gedruckt wuerde nur die erste Bildschirmhoehe. Auf
       Papier fliesst der Inhalt, und Navigation und Kopfzeile haben dort nichts
       verloren. */
    html, body { height: auto; }
    .shell { display: block; height: auto; max-height: none; overflow: visible; }
    .main { display: block; overflow: visible; padding: 0; }
    .topbar, .sidenav, .voice-launcher, .voice, .searchbar, .skip-link { display: none; }

    .paper { display: block; min-height: 0; padding: 0; background: var(--weiss); }
    .paper__bar { display: none; }

    .sheet {
        max-width: none;
        padding: 0;
        border: none;
    }

    /* Der Rahmen, in dem das Blatt auf dem Telefon scrollt, darf auf Papier nicht
       schneiden: dort hat die Tabelle ihren Platz. */
    .scroll-x { overflow: visible; }

    /* Eine Position soll nicht auf zwei Seiten stehen. */
    .table--sheet tr { break-inside: avoid; }
    /* Die Kopfzeile wiederholt sich auf jeder Seite — das macht thead von selbst,
       sobald der Druck die Tabelle bricht. */
    .table--sheet thead { display: table-header-group; }
}

/* ---- Die Sprachsteuerung (public/js/voice.js) ---------------------------- */

/* Der Knopf der Sprachsteuerung schwebt rechts unten: ein Kalk-Knopf in Tanne
   (sekundaer – Lime bleibt der einen Handlung der Seite), mit Mikrofon und Namen,
   der Ecke oben rechts und Schatten 2, weil er ueber dem Inhalt liegt. */
.iconbtn--voice { width: auto; height: var(--target-min); }
.voice-launcher {
    position: fixed;
    right: var(--page-inset);
    bottom: var(--s-6);
    z-index: 54;
    display: flex;
    gap: var(--s-2);
    padding: 0 var(--s-6) 0 var(--s-4);
    border: 0;
    border-radius: var(--ecke-knopf);
    background: var(--tanne);
    color: var(--kalk);
    box-shadow: var(--schatten-2);
    font: 500 var(--fs-15)/1.2 var(--font-text);
}
.voice-launcher:hover,
.iconbtn--voice-on { background: var(--tanne-hell); color: var(--kalk); }
.voice-launcher[data-panel="open"] { visibility: hidden; }

/* Der Knopf haengt fest 24 px ueber dem Rand und deckte das Seitenende ab (der
   Innenabstand von .main ist 64 bzw. mobil 48 px, der Knopf braucht 24 + 44).
   Er steht ausserhalb von .shell, deshalb haengt die Regel am body. Ohne
   OPENAI_API_KEY gibt es den Knopf nicht, und der Abstand bleibt; ein Browser
   ohne Spracherkennung behaelt ihn (verborgen) – ein Selektor auf [hidden]
   widerspraeche HiddenAttributeTest. */
body:has(.voice-launcher) .main { padding-bottom: calc(var(--s-6) + var(--target-min) + var(--s-6)); }

/* Das Panel: eine schwebende Ebene, gebaut wie ein Kalk-Dialog (Weiss,
   rechteckig, Schatten 3). Es steht rechts unten, oberhalb des Inhalts, und
   nimmt ihm nichts weg -- die Seite bleibt bedienbar, waehrend gesprochen wird. */
.voice {
    position: fixed;
    right: var(--page-inset);
    bottom: var(--s-6);
    z-index: 55;
    display: flex;
    flex-direction: column;
    width: min(380px, calc(100vw - 2 * var(--page-inset)));
    max-height: min(70dvh, 640px);
    background: var(--surface);
    box-shadow: var(--schatten-3);
    color: var(--fg);
    overflow: hidden;
    animation: voice-arrive var(--dur) var(--ease);
}

.voice__head {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-2) 0 var(--s-4);
}

.voice__title { flex: 1; font: var(--fw-display) var(--fs-19)/var(--lh-text) var(--font-display); letter-spacing: -0.01em; margin: 0; }
.voice__title span { display: inline-block; margin-left: var(--s-2); font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

.voice__status {
    margin: 0;
    font: 500 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    overflow-wrap: anywhere;
}

.voice[data-state="error"] .voice__status { color: var(--fehler-text); }
.voice__head .iconbtn { width: var(--target-min); height: var(--target-min); }
.voice__companion { display: flex; align-items: center; gap: var(--s-4); padding: var(--s-3) var(--s-4) var(--s-4); }
.voice__presence { flex: 1; min-width: 0; }
.voice__hint { margin: var(--s-1) 0 0; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.voice__content { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.voice[data-minimized="true"] [data-action="voice-minimize"] .icon { transform: rotate(180deg); }
.voice__retry { margin: var(--s-3) var(--s-4); }

/* Die Welle zeigt, wer gerade spricht – als Zustand, nicht als Schleife: in Kalk
   bewegt sich nichts von selbst. Fuenf Striche in Kiesel; spricht der Assistent,
   stehen sie in Tanne und in voller Hoehe (ein Wechsel in kurzer Dauer).
   Waehrend der Verbindung und des Nachdenkens steht dort der Ladeindikator. */
.voice__wave { display: flex; align-items: center; justify-content: center; gap: 3px; width: 28px; height: 24px; color: var(--kiesel); transition: color var(--dur-fast) var(--ease); }
.voice__wave i { width: 3px; height: 16px; background: currentColor; transform: scaleY(.35); transition: transform var(--dur-fast) var(--ease); }
.voice__wave i:nth-child(2), .voice__wave i:nth-child(4) { transform: scaleY(.6); }
.voice__wave i:nth-child(3) { transform: scaleY(.85); }
.voice[data-mood="speaking"] .voice__wave { color: var(--tanne); }
.voice[data-mood="speaking"] .voice__wave i { transform: scaleY(1); }
.voice[data-mood="error"] .voice__wave { color: var(--fehler-mark); }
.voice__companion .loading-mark { display: none; }
.voice[data-mood="connecting"] .voice__companion .loading-mark,
.voice[data-mood="thinking"] .voice__companion .loading-mark { display: block; }
.voice[data-mood="connecting"] .voice__wave,
.voice[data-mood="thinking"] .voice__wave { display: none; }
@keyframes voice-arrive { from { opacity: 0; transform: translateY(var(--versatz)); } }

.voice__log {
    min-height: 0;
    margin: 0;
    padding: var(--s-3) var(--s-4);
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
}
.voice__log:empty { display: none; }

.voice__line { margin: 0; overflow-wrap: anywhere; }
.voice__line--user { color: var(--fg); font-weight: 500; }
.voice__line--user::before { content: "Du: "; color: var(--fg-muted); font-weight: 400; }
.voice__line--assistant { color: var(--fg); }
.voice__line--tool, .voice__line--note { color: var(--fg-muted); font-size: var(--fs-13); }

.voice__proposals { display: flex; flex-direction: column; gap: var(--s-2); padding: 0 var(--s-4); }
.voice__proposals:empty { display: none; }

/* Eine Karte je Vorschlag: ein Hinweis wie die Meldungen, auf der hellen
   Markenflaeche (Blatt) -- sie sagt, was gleich passiert, und traegt die
   Entscheidung. */
.voice__proposal {
    padding: var(--s-4);
    background: var(--accent-soft);
    color: var(--accent-ink);
}
.voice__proposal-text { margin: 0 0 var(--s-3); font: 400 var(--fs-15)/var(--lh-text) var(--font-text); }
.voice__proposal-actions { display: flex; gap: var(--s-2); flex-wrap: wrap; }
.voice__fields { margin: 0 0 var(--s-3); font-size: var(--fs-15); }
.voice__fields dt { margin-top: var(--s-2); font-weight: 600; }
.voice__fields dd { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }

.voice__note {
    margin: 0;
    padding: var(--s-2) var(--s-4) var(--s-3);
    font: 400 var(--fs-13)/var(--lh-text) var(--font-text);
    color: var(--fg-muted);
    max-width: none;
}

@media (max-width: 999px) {
    .btn--sm { min-height: var(--target-min); border-radius: var(--ecke-knopf); }
    .identity > .initials--lg { width: 40px; height: 40px; font-size: var(--fs-19); }
    .voice { right: var(--page-inset); left: var(--page-inset); width: auto; max-height: 60dvh; }
}

/* ---- Das Foto am Kontakt (Migration 0052) ---------------------------------- */

/* Ein Foto nimmt den Platz der Initialen ein: dasselbe Mass, rechteckig (Kalk:
   Fotos sind rechteckig). object-fit
   schneidet ein Hochformat auf das Quadrat, statt es zu stauchen; die Groesse
   kommt von .initials bzw. .initials--lg und steht deshalb nicht hier. */
.photo { object-fit: cover; background: var(--ground); }
/* ---- Die Massenbearbeitung (CLO-109) --------------------------------------- */

/* Das Kaestchen steht in der ersten Zelle der Zeile, nicht in einer eigenen
   Spalte: bei den Kontakten sind die Spalten die Einstellung des Benutzers, und
   ihre Mindestbreite rechnet das Backend aus genau diesen Spalten. Als Float
   sitzt es links vom Inhalt der Zelle; .cell-person (flex) und .ell (block)
   weichen ihm aus, statt darunter zu rutschen. Im Board dasselbe Kaestchen
   (.checkbox__input), dort ueber der Karte. */
.bulk__pick { margin: 0; vertical-align: middle; }
.table :is(td, th) > .bulk__pick { float: left; margin: 3px var(--s-3) 0 0; }

/* Im Board steht das Kaestchen als Geschwister ueber seiner Karte, nicht darin:
   die Karte ist ein Link. Eng an die Karte gerueckt (negativer Abstand gegen das
   gap der Spalte), damit es zu ihr gehoert und nicht zur Karte darueber. */
.board__pick { display: flex; align-items: center; min-height: 20px; margin-bottom: calc(-1 * var(--s-3)); padding: 0 var(--s-1); }

/* Die Leiste unter der Liste: eine Reihe aus eigenstaendigen Teilen, die
   umbrechen darf (390 px). Dieselbe Bauform wie .searchform, nur unter der
   Tabelle statt neben den Chips. Kein eigener Grund: sie gehoert zur Tabelle
   darueber. */
.bulkbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-3);
    margin-top: calc(-1 * var(--s-4));
}
.bulkbar__count { font: 500 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); white-space: nowrap; }
.bulkbar__action { width: auto; max-width: 100%; min-height: 32px; padding: var(--s-1) var(--s-3); font-size: var(--fs-13); }
.bulkbar__action.control--select { padding-right: var(--s-8); background-position: right var(--s-2) center; }

/* Die Bestaetigung: drei Spalten, und die dritte traegt Saetze -- die Begruendung
   einer Ablehnung bricht in ihrer Zelle um statt zu kuerzen; deshalb `normal`
   gegen das nowrap der Tabellenzelle. */
.table--bulk { min-width: 640px; }
.bulk__reason { display: block; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fehler-text); white-space: normal; }
.bulk__row--refuse td { background: var(--fehler-soft); }
.bulk__confirm { margin-top: var(--s-6); }
/* ---- Fassungen einer Datei (CLO-104) ---------------------------------------- */

/* Eine aeltere Fassung steht eingerueckt unter ihrer neuesten, mit einer Linie
   am Rand statt eines eigenen Kastens: sie gehoert zur Zeile darueber und ist
   keine eigene Datei. Aufgeklappt wird ueber die Adresse (?fassungen=), also
   ohne Zustand im Markup, den ein Skript pflegen muesste. */
.list__row--sub { padding-left: var(--s-4); border-left: 2px solid var(--line); }

/* Der Titel eines Dateitreffers in der Suche traegt die Klammer vor dem Namen:
   ein Treffer, der auf den Datensatz fuehrt und nicht auf die Datei, muss
   sagen, dass er einer in der Datei ist. Inline, in Textgroesse. */
.search__file { display: inline-flex; align-items: center; gap: var(--s-1); }
.search__file .icon { flex: 0 0 auto; color: var(--fg-muted); }

/* Die Schritte einer Folge (Migration 0058, pages/sequences.html.twig): je
   Zeile Nummer, Art, Titel und Abstand. Der Titel waechst, die Art und der
   Abstand stehen fest; unter 640 px (Kalk s) stapeln die Felder, die Nummer bleibt
   vorne -- eine Folge liest sich von oben nach unten. Kein Rahmen um die Liste:
   sie ist Teil des Formulars, nicht eine Tabelle darin. */
.seqsteps { list-style: none; margin: var(--s-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-3); }
.seqsteps__row {
    display: grid;
    grid-template-columns: 28px minmax(120px, 160px) minmax(0, 1fr) minmax(100px, 140px);
    gap: var(--s-3);
    align-items: start;
}
.seqsteps__row .field { margin: 0; }
.seqsteps__no { padding-top: 28px; font: 500 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); text-align: right; }
@media (max-width: 639px) {
    .seqsteps__row { grid-template-columns: 28px minmax(0, 1fr); }
    .seqsteps__row .field { grid-column: 2; }
}
/* Die Liste der Verwaltung: die Spalte der Schritte traegt Saetze und kuerzt
   (.ell); die Untergrenze haelt die erste Spalte positiv. */
.table--sequences { min-width: 640px; }

/* Die Prognose (CLO-111): drei Tabellen, jede mit ihrer Mindestbreite aus den
   Spaltenbreiten des colgroup -- dieselbe Regel wie .table--funnel: unter der
   Breite scrollt .scroll-x, nicht die Seite. Eigene Klassen und nicht
   .table--funnel, weil die Spalten andere sind. */
.table--forecast { min-width: 870px; }
.table--stagerates { min-width: 580px; }
.table--forecastdeals { min-width: 1020px; }

/* Die Panels der Uebersicht in eigener Auswahl (CLO-110): dasselbe Zwei-Spalten-
   Raster wie die Auswertung, jedes Panel so hoch wie sein Inhalt (`start` von
   .grid-2). Die beschlossene Anordnung der Voreinstellung behaelt .grid-2--even;
   hier gibt es kein "links" und "rechts", nur eine Reihe. */
.dashpanels > .panel { min-width: 0; }

/* Die Kohorte hinter einer gemessenen Quote: leiser als die Zahl, aber daneben.
   Ohne sie liest jemand 35 % aus acht Projekten als Aussage. */
.stagerate__n { color: var(--fg-muted); font-weight: 400; }

/* ---- Das Logo eines Unternehmens (Migration 0060) ---------------------------- */

/* Dasselbe Quadrat wie das Foto, nur kleiner: 40 px am Kopf der Seite, 24 px
   (.initials--sm) in der Zeile der Liste -- rechteckig, kein Kreis.
   `contain` statt `cover`: ein Logo ist eine Wortmarke oder ein Zeichen, und
   angeschnitten ist es keines mehr; ein Gesicht darf man beschneiden, ein
   Schriftzug nicht. Der helle Grund traegt ein Symbol mit Transparenz. */
.initials--logo { width: 40px; height: 40px; }
.logo { object-fit: contain; background: var(--bg); padding: 2px; }

/* ---- Vorschlaege: der Eingang und die Anreicherung (Migration 0061) --------- */

/* Eine Karte je Vorschlag, untereinander -- ein Formular mit bis zu vierzehn
   Feldern passt in keine Tabellenzeile. Der Abstand ist der der Panels. */
.suggestion + .suggestion { margin-top: var(--s-6); }

/* Der Beleg: der Text der Mail, die Zeilen der Signatur. <pre>, weil die
   Zeilenumbrueche die Aussage tragen -- und umbrechend, weil eine lange Zeile
   sonst die Seite waagerecht scrollt (390 px). Die Schrift bleibt die des Textes:
   eine Mail ist kein Quelltext. */
.suggestion__evidence { margin-top: var(--s-6); }
.suggestion__evidence > summary { cursor: pointer; font: 500 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.suggestion__text {
    margin: var(--s-3) 0 0;
    padding: var(--s-4);
    background: var(--ground);
    font: 400 var(--fs-15)/var(--lh-text) var(--font-text);
    color: var(--fg);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    max-height: 24rem;
    overflow-y: auto;
}
.suggestion__label { margin: var(--s-6) 0 var(--s-2); font: 500 var(--fs-13)/1.4 var(--font-text); color: var(--fg-muted); }
.suggestion__expiry { margin-left: auto; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }

/* Die Tabelle der Website-Funde: fuenf Spalten, die letzte traegt den Satz der
   Seite und bricht um statt zu kuerzen -- ein Beleg, der mitten im Wort endet,
   belegt nichts. Deshalb `normal` gegen das nowrap der Zelle, und die
   Mindestbreite haelt die vier festen Spalten lesbar. */
.table--suggestion { min-width: 720px; }
.table--suggestion td.suggestion__cell { white-space: normal; }
.suggestion__quote { display: block; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.suggestion__row--kept td { color: var(--fg-muted); }
.suggestion__extras { margin: 0; padding-left: var(--s-6); font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg); }
.suggestion__extras li { margin-top: var(--s-1); }

/* Die Herkunftsmarke hinter einem Wert der Stammdaten: ein Wort in der Schrift
   der Nebentexte, kein Symbol -- "aus Website" ist die Auskunft, ein Icon waere
   eine Frage. */
.datarow__mark { margin-left: var(--s-2); font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); white-space: nowrap; }

/* ---- Der Kalender hat zwei Seiten (Migration 0067, CLO-53) ------------------- */

/* Die Zeile ueber dem Terminformular: "In Outlook: synchronisiert 12:03". Ein
   Satz in der Schrift der Nebentexte, kein Panel und kein Abzeichen -- der
   Zustand ist eine Auskunft, keine Aufforderung. Nur der Fehler bekommt die
   Farbe, denn nur er verlangt etwas. */
.sync-state { margin: 0 0 var(--s-4); font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.sync-state--error { color: var(--fehler-text); }
/* Der Satz unter den zwei Knoepfen des Mailformulars (CLO-56): "Einzelmail --
   kein Serienversand" gehoert zu den Knoepfen und nicht zu einem Feld, deshalb
   kein field__hint -- der steht innerhalb eines Feldes. */
.formactions__note { margin: var(--s-2) 0 0; font: 400 var(--fs-13)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
/* Die Mailzuordnung (Migration 0068). Die Standardliste der Ausschluesse steht
   eingeklappt als Satz: sie ist Code, kein Bestand, und eine Tabelle ohne
   Knoepfe saehe aus, als fehlte etwas. Der Weg ins Postfach am Verlaufseintrag
   ist ein Link wie ein Bezug -- kein Knopf, denn er fuehrt aus dem CRM hinaus
   und aendert nichts. */
.mailx__defaults { margin-top: var(--s-4); font: 400 var(--fs-15)/var(--lh-text) var(--font-text); color: var(--fg-muted); }
.mailx__defaults summary { cursor: pointer; color: var(--fg); }
.mailx__defaults p { margin: var(--s-2) 0 0; overflow-wrap: anywhere; }
.timeline__mail { display: inline-flex; align-items: center; gap: var(--s-1); }
