/* ═══════════════════════════════════════════════════════════════════════════════
   junaspin PIM — App-Shell
   Neubau ab 2026-07-16. Ersetzt app.css beim Cutover ersatzlos (docs/REDESIGN-SHELL.md).
   Keine Zeile ist aus der alten Datei kopiert: jede hier ist aus den Tokens + den
   5 Archetypen ableitbar. Wer etwas nicht ableiten kann, hat einen Rest gefunden.

   Die 11 Regeln, gegen die jede Zeile hier prüfbar ist:
    1 Höchstens eine Primäraktion pro Seite.
    2 Farbe ist Bedeutung, nie Dekoration.
    3 Tabellen sind randlos und randfüllend.
    4 Das Dokument scrollt nie — Panes scrollen.
    5 Jede Zahl ist ein Link.
    6 Ziffern tabellarisch, rechtsbündig, Einheiten grau.
    7 Ein Zustand wird ein Ort, wenn (a) sein Zähler auf 0 soll, (b) er exklusiv ist,
      (c) Menschen ihn beim Namen nennen. Sonst bleibt er Zustand.
    8 Ein Badge, das zwei Werte vergleicht, ist eine verlorene Spalte (→ Δ-Spalte).
    9 Ein Marker, der eine Zahl qualifiziert, klebt an der Zahl (Einheit/Nachbarspalte).
   10 Die Zeile zeigt, die Rail alarmiert.
   11 Genau EINE zweizeilige Zelle pro Tabelle — die Identitätszelle in Spalte 1.
   ═══════════════════════════════════════════════════════════════════════════════ */

/* ── TOKENS ──────────────────────────────────────────────────────────────────
   Marke: das junaspin-Logo ist gedecktes Stahlblau (#5B7F9E). --primary ist daraus
   auf Kontrast gezogen (Weiß darauf 6,9:1). Die Marken-/Ampelfarben bleiben in
   app_settings admin-pflegbar (layout.html überschreibt :root) — „Konfiguration > Code".
   ─────────────────────────────────────────────────────────────────────────── */
:root {
  --rail-w: 236px;
  /* Zwei Zeilenhöhen, abgeleitet aus Regel 11 — niemand wählt sie.
     obj = Objekt-Tabelle (eine zweizeilige Identitätszelle), evt = alles einwertig. */
  --row-obj: 44px;
  --row-evt: 32px;
  /* Shell-Höhen: die einzige Quelle für jeden Anker. Im Canvas-Scroll ist top=0 —
     diese Werte tragen Chrome-Geometrie, keine sticky-Offsets mehr. */
  --h-topbar: 48px;
  --h-pagehead: 56px;
  --h-tabs: 36px;
  --h-actionbar: 40px;
  --h-footbar: 40px;

  --f-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --f-mono: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, "Liberation Mono", monospace;

  /* Typo-Skala. Jede Stufe um ~1 px angehoben (19.07.) — die App war nicht dicht, sie war KLEIN.
     Nachgemessen: die faktische Lesegröße lag bei 11–12 px (`--fs-micro` 125×, `--fs-dense` 105×
     verwendet), `--fs-body` war mit 8 Verwendungen praktisch tot. Eine Tabellenzelle mit 12 px Text
     in einer 44-px-Zeile ist kein dichtes Werkzeug, sondern verschenkte Lesbarkeit.

     ⚠️ Das kostet KEINE Tabellenzeile: `.grid td` ist auf `--row-obj` (44 px) festgenagelt, unabhängig
     von der Schriftgröße. Die zweizeilige Identitätszelle braucht bei 13 px/17 px Zeile 2×17+6 = 40 px
     und passt damit weiter in die 44er-Zeile (nachgerechnet, nicht angenommen). */
  --fs-micro: 11px; --fs-dense: 13px; --fs-ui: 14px;
  --fs-body: 15px;  --fs-sect: 17px;  --fs-title: 22px; --fs-kpi: 28px;
  /* Carets, Pfeile und Sortier-Indikatoren waren als heimliche Schriftstufen unterwegs (9 / 9,5 / 10 px).
     Sie sind Symbole, keine Schrift — deshalb eine eigene, ehrlich benannte Stufe. */
  --fs-icon: 10px;


  /* ⚠️ ZEILENMASS — die Breite, ab der ein Fließtext umbricht. EIN Wert, ueberall.
     Gemessen standen sieben verschiedene Maße nebeneinander (680 im Wiki, 720 im Beispielkasten, 620 im
     Konfigurator, 477 auf den Ausschluessen, 1302 im Export) — dieselbe Sache, fuenf Antworten, und die
     rechte Kante einer Seite franste dadurch ueber vier Stufen aus. Genau das liest sich als „unfertig",
     nicht die Breite selbst. 780 px sind bei 15 px rund 95 Zeichen: das obere Ende des Angenehmen,
     bewusst gewaehlt, weil ein schmales Band auf einem breiten Schirm verloren wirkt.
     ⛔ Gilt NUR fuer Fließtext. Tabellen, Kacheln, Raster und Formeln haben dieses Maß absichtlich nicht. */
  --measure: 780px;

  /* 4er-Raster */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
  --sp-5: 20px; --sp-6: 24px; --sp-8: 32px;

  /* Radien: 0 = liegt (Shell, Tabellen, Leisten) · 1 = Controls · 2 = schwebt */
  --r-1: 3px; --r-2: 6px; --r-3: 10px; --r-full: 999px;

  /* Schatten NUR für z > 0. Nichts, was liegen bleibt, wirft einen Schatten (Regel 2). */
  --sh-pop:   0 4px 12px rgba(16,22,30,.13), 0 0 0 1px rgba(16,22,30,.06);
  --sh-modal: 0 16px 48px rgba(16,22,30,.26);

  /* ── DIE SCHIENE ───────────────────────────────────────────────────────────
     Bis 19.07. war sie in BEIDEN Themes dunkel (#12161B): das trennte „Werkzeug-Chrome" von „Inhalt"
     und hielt diese Trennung in Hell und Dunkel identisch. Sascha (19.07.): „unsers wirkt sehr eng,
     klein, dunkel" — Entscheidung für Variante B, die Schiene wird im Hell-Theme hell.

     Die Trennung Werkzeug/Inhalt entsteht jetzt anders, über DREI Mittel statt über Dunkelheit:
       1. ein spürbar KÜHLERER Grundton als der weiße Canvas (nicht bloß „etwas grauer"),
       2. eine eigene Kante rechts (`--rail-border`) — der dunklen Schiene genügte ihr Schwarz,
       3. der aktive Punkt trägt die Markenfarbe als Fläche, nicht nur als Kante.

     Die Facetten-Spalte der Arbeitsfläche steht direkt daneben und ist ebenfalls hell — sie bleibt
     bewusst auf `--canvas` (weiß). Kühl getönt neben weiß ist unterscheidbar; zwei Grautöne wären es nicht.

     ⚠️ Alles, was auf der Schiene sitzt, MUSS über diese Tokens gehen. Vor dem Umbau standen an vier
     Stellen feste Hellwerte im Code (`#E4E8ED`, `#fff`, `#2A333F`) — auf heller Schiene wären das
     unsichtbare Texte gewesen. */
  --rail: #EDE8DF;
  --rail-hover: #E5DFD3;
  --rail-active: #FFFFFF;         /* Aktiv = Canvas-Farbe: der Punkt liest sich als Reiter IN den Inhalt. */
  --rail-divider: #DDD6CA;
  --rail-border: #D9D2C6;         /* Traegt die Trennung — bei 1,22:1 tut das kein Farbwert allein. */
  --rail-text: #55504A;
  --rail-text-strong: #23201C;    /* Hover — vormals hart #E4E8ED */
  --rail-text-active: #1C4E75;    /* Aktiv  — vormals hart #fff */
  --rail-sub: #6B655D;
  --rail-section: #756D63;        /* 4,4:1 — kleine Versalien tragen die IA und waren mit 2,8:1 zu blass. */
  --rail-off: #9A938A;
  --rail-accent: #2C5F85;         /* Kante am aktiven Punkt — vormals --chart-2 */
  --rail-scroll: #C9C2B6;         /* Scrollbalken — vormals hart #2A333F */
  /* Kein Chip hinter dem Marken-Logo. Der dunkle Chip davor war ein Fehlschluss: das Logo wurde fuer
     die frueher dunkle Schiene falsch als "helle Grafik" eingeschaetzt. Nachgesehen ist es das
     Gegenteil — schwarzer Schriftzug mit blauer Spirale auf transparentem Grund, also eine Grafik fuer
     HELLEN Grund. Auf der warmen hellen Schiene steht sie ohne alles. Nur im Dunkel-Theme braucht sie
     eine helle Flaeche unter sich (dort unten gesetzt). */
  --rail-logo-chip: transparent;

  /* Der Toast ist ein Overlay ueber allem und bleibt in BEIDEN Themes dunkel — er lieh sich bisher
     die Schienenfarbe, was ihn beim Umbau still hell gemacht haette (weisser Text auf hellem Grund). */
  --toast-bg: #12161B;
  --toast-fg: #E4E8ED;

  /* Neutrale mit WARMEM Bias (19.07.). Vorher zogen Neutrale und Markenblau in dieselbe kuehle Richtung —
     dann hebt sich nichts ab und alles wirkt gleich. Auf warmem Grund bekommt das gedeckte Stahlblau erst
     Praesenz, und die Flaeche liest sich freundlicher statt klinisch.
     Nachgemessen, nicht angenommen: die Ampel haelt auf warmem Grund (good 6,2:1 · warn 5,0:1 · bad 6,1:1)
     — die Sorge, --warn wuerde untergehen, hat die Rechnung nicht bestaetigt. --selected bleibt bewusst
     KUEHL: Auswahl ist markenfarbig, und das ist eine Bedeutung, keine Flaeche. */
  --canvas: #FFFFFF;
  --sunken: #F7F6F4;
  --table-head: #EFECE6;  /* war #F6F8FA = 1,06:1 gegen Canvas, also faktisch unsichtbar */
  --table-filter: #FAF9F7;
  --group-head: #EBE8E1;
  --hover: #F5F2ED;
  --selected: #E8EFF5;
  --border: #E5E2DC;
  --border-strong: #CFCAC1;
  --row-line: #EDEAE4;

  --text: #23201C;        /* 16,2:1 */
  --text-muted: #6B655D;  /*  5,8:1 — Labels, Sekundaerwerte */
  --text-faint: #9A938A;  /*  3,0:1 — NUR Platzhalter/Einheiten/disabled. Nie auf einem Wert. */

  --primary: #2C5F85;
  --primary-hover: #234E6E;
  --accent: #2C5F85;
  --focus: #2C5F85;

  /* Ampel — Entscheidung D: existiert im ganzen System an GENAU EINER Stelle,
     dem DB-%-Badge. Taucht sie woanders auf, ist eine Regel gebrochen. */
  --good: #12693F;
  --warn: #B54708;
  --bad: #B32318;
  --warn-mark: #E09A28;

  --badge-ok-bg: #E9F4EE;   --badge-ok-bd: #C4E2D2;   --badge-ok-fg: #12693F;
  --badge-warn-bg: #FCF5E9; --badge-warn-bd: #EFDDBE; --badge-warn-fg: #8A4B0A;
  --badge-err-bg: #FCF0EE;  --badge-err-bd: #F2D1CC;  --badge-err-fg: #92221A;
  --badge-neu-bg: #F0F3F5;  --badge-neu-bd: #DEE3E8;  --badge-neu-fg: #4A545F;

  --chart-1: #2C5F85;
  --chart-2: #7BA0BC;

  color-scheme: light;
}

/* ⛔ SIE STEHT HIER UND NICHT OBEN BEI DEN TOKENS, und das ist kein Ordnungsgeschmack:
   im ersten Anlauf lag dieser Block INNERHALB von `:root {…}`. Er rendert dort — als verschachtelte
   CSS-Nesting-Regel — und macht dabei zwei Dinge kaputt, die man nicht sieht:
   (1) die 49 Tokens DAHINTER (--measure, das ganze Raster, alle Radien, Schatten, die komplette
       Farbwelt) haengen dann hinter einer verschachtelten Regel. Eine Engine, die das nicht kann,
       wirft sie lautlos weg — die App verliert ihre Palette, ohne dass irgendetwas meldet.
   (2) die Spezifitaet wird zu `:root .grid th` (0,2,1 statt 0,1,1). Jede spaetere Kopf-Regel mit
       normaler Spezifitaet verliert ab dann still. Heute kollidiert nichts — das ist eine gestellte
       Falle, keine Funktion. Gefunden bei der UX-Abnahme am 19.08. */
/* ── DIE KOPFZEILE EINER TABELLE — EINMAL (18.08.) ────────────────────────────────────────────────
   Diese Erklaerung stand FUENFMAL woertlich im Stylesheet: .grid th, .ck-scroll thead th,
   .st-matrix thead th, .tx-tbl th, .tx-pv th. Wer die Kopfzeile aendern wollte, heilte eine Stelle
   und liess vier stehen — genau das Muster, gegen das die Hausregel „eine Regel gehoert an EINE
   Stelle" geschrieben ist, und derselbe Fehler wie bei der festgeklebten `left: 56px` im Juli.
   Was die fuenf Selektoren SONST unterscheidet (Hoehe, Polsterung, sticky-Position, Schatten,
   Zeiger), bleibt bei ihnen — nur ihre gemeinsame Typografie steht hier.

   ⛔ MIXED-CASE STATT VERSALIEN, und die Breite traegt es: Versalien in 11 px sind auf einer
      14-spaltigen Tabelle schlecht lesbar, und `letter-spacing` verbreitert sie zusaetzlich.
      Gemessen (Chromium, je Kopf das Budget aus deklarierter Spaltenbreite minus Polsterung minus
      Chevron): 11 px Versalien laufen bei drei Koepfen ueber (refek +1 px, variante +10, staged +3),
      13 px gemischt bei denselben dreien (refek +5, variante +8, staged +3). Netto null — Mixed-Case
      spart rund 9 % Breite, die zwei Punkt mehr kosten rund 7 %. Es ist also keine Platzfrage.
   ⛔ 13 px IST EINE STUFE DER LEITER (--fs-dense). Der urspruengliche Wunsch lautete „12 px" — die
      gibt es hier nicht, und ein neues Token dafuer waere ein doppelter Regelbruch (Hausregel
      „Schriftgroessen nur ueber Tokens" plus eine Stufe ausserhalb der Leiter).
   ⛔ Die Beschriftungen stehen in den Templates laengst gemischt („Nächste Aufgabe", „Δ EK"); die
      Versalien entstanden erst hier. Es aendert sich deshalb kein einziges Wort im Markup. */
.grid th, .ck-scroll thead th, .st-matrix thead th, .tx-tbl th, .tx-pv th {
  font: 600 var(--fs-dense)/16px var(--f-ui); letter-spacing: 0; text-transform: none;
}


/* Dark. EIN Block, ein Selektor.
   Der frueher zusaetzliche `@media (prefers-color-scheme: dark)`-Block war toter Code: layout.html,
   login.html und mobil.html setzen `data-theme` allesamt VOR dem ersten Paint (aufgeloest aus
   localStorage, sonst aus der OS-Praeferenz) — `:root[data-theme="dark"]` deckt damit jeden Fall ab.
   Er war nicht nur ueberfluessig, sondern gefaehrlich: 24 Token-Zeilen, die jemand von Hand synchron
   halten musste. Beim Palettenwechsel am 19.07. ist genau das passiert — der eine Block wurde warm,
   der andere blieb kuehl, und welcher gewinnt, haengt an der Reihenfolge im File.
   Preis, bewusst bezahlt: ohne JavaScript gibt es kein Dark-Theme mehr. Die App ist ohne JS ohnehin
   nicht bedienbar (Klappen, Scroll-Container, Filter) — ein Farbschema fuer einen Zustand
   vorzuhalten, in dem nichts funktioniert, ist kein Sicherheitsnetz. */
:root[data-theme="dark"] {
  --canvas: #22201E; --sunken: #1A1917; --table-head: #2B2926; --table-filter: #262421;
  --group-head: #302D29; --hover: #2E2B27; --selected: #24384A;
  --border: #3A3733; --border-strong: #4B4741; --row-line: #302D29;
  --text: #EBE7E1; --text-muted: #A8A199; --text-faint: #7A736B;
  --primary: #6BA3D0; --primary-hover: #82B5DD; --accent: #8CBEE2; --focus: #8CBEE2;
  --good: #45C08A; --warn: #E5A34C; --bad: #F0655A; --warn-mark: #E09A28;
  --badge-ok-bg: #14301F; --badge-ok-bd: #1F5637; --badge-ok-fg: #6EDCA6;
  --badge-warn-bg: #2E2415; --badge-warn-bd: #5A4520; --badge-warn-fg: #F0BE6B;
  --badge-err-bg: #331A18; --badge-err-bd: #63302B; --badge-err-fg: #F58B80;
  --badge-neu-bg: #232A33; --badge-neu-bd: #363F4A; --badge-neu-fg: #A7B1BD;
  --chart-1: #5793C2; --chart-2: #35516A;
  /* Schiene bleibt im Dunkel-Theme dunkel — dort trennt sie sich vom Canvas ohnehin nach unten,
     nicht nach oben. Die Werte sind die der frueheren Einheits-Schiene. */
  --rail: #12161B; --rail-hover: #1C222B; --rail-active: #23303D; --rail-divider: #232B35;
  --rail-border: #232B35; --rail-text: #A7B1BD; --rail-text-strong: #E4E8ED; --rail-text-active: #FFFFFF;
  --rail-sub: #8A95A2; --rail-section: #626D7A; --rail-off: #566069;
  --rail-accent: #7BA0BC; --rail-scroll: #2A333F; --rail-logo-chip: #FFFFFF;
  --sh-pop: 0 4px 12px rgba(0,0,0,.5), 0 0 0 1px rgba(255,255,255,.07);
  --sh-modal: 0 16px 48px rgba(0,0,0,.62);
  color-scheme: dark;
}

/* ── BASIS ─────────────────────────────────────────────────────────────────── */
* { box-sizing: border-box; }

/* Regel 4: Das Dokument scrollt NIE. Wer den Seitenkopf wegscrollt, hat eine Homepage gebaut. */
html, body { height: 100%; overflow: hidden; margin: 0; }

body {
  font-family: var(--f-ui);
  font-size: var(--fs-ui);
  line-height: 20px;
  color: var(--text);
  background: var(--canvas);
  -webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
button { font: inherit; color: inherit; }
/* Ein sichtbarer Fokusring sagt „hier arbeitet jemand acht Stunden am Tag". */
:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; border-radius: 2px; }

.num { font-variant-numeric: tabular-nums; }
.dim { color: var(--text-muted); }
.empty { color: var(--text-faint); }
/* Regel 6: Einheiten sind grau und kleben an der Zahl. Auch „/m" (Längenware). */
.unit { color: var(--text-faint); font-size: var(--fs-micro); }
/* Regel 3 + §3.4: Prosa wird gedeckelt, Tabellen nie. Breite je Inhaltstyp, nicht je Seite. */
.prose { max-width: 68ch; }

/* ── SHELL ─────────────────────────────────────────────────────────────────── */
.app {
  display: grid;
  grid-template-columns: var(--rail-w) minmax(0, 1fr);
  height: 100dvh;
  transition: grid-template-columns .16s ease;
}
/* Der Zustand haengt an :root, weil der Head-Loader ihn VOR dem ersten Paint setzen muss —
   sonst springt die Schiene beim Laden um 168 px. Gleiche Begruendung wie beim Dark-Mode. */
:root[data-rail="collapsed"] .app { --rail-w: 56px; }

/* Flex statt Grid mit festen Zeilen: nicht jede Seite hat Tabs/Aktionsleiste/Fuß. Mit
   `grid-template-rows: 48px auto auto auto 1fr auto` säße der Canvas auf Seite 2 in einer
   auto-Zeile und würde wachsen statt zu scrollen — Regel 4 gebrochen, und zwar unsichtbar,
   weil es auf der Seite MIT Tabs stimmt. Flex bindet die 1fr an das Element, nicht an den Index. */
.main {
  display: flex;
  flex-direction: column;
  /* Beide tragend: ohne min-width sprengt die Tabelle das Layout, ohne min-height
     wächst der Canvas statt zu scrollen. Der häufigste Fehler bei diesem Layout. */
  min-width: 0;
  min-height: 0;
  background: var(--canvas);
}
.main > * { flex: none; }
/* Der Body-Slot ist eine ROLLE, kein Klassenname: genau ein Kind von .main trägt `data-body`,
   bekommt die Resthöhe und reicht die Begrenzung an seine Kinder weiter. Ob es selbst scrollt
   (.canvas) oder eigene Panes aufspannt (.wbbody: Facetten | Tabelle), entscheidet die Seite.
   Vorher hing der Vertrag an `.canvas` — und die Workbench schob mit .wbbody eine Ebene
   dazwischen: die kassierte flex:none, wuchs mit der Tabelle, html{overflow:hidden} schnitt
   den Rest ab. Ergebnis: kein Scrollbalken UND kein sticky, auf der wichtigsten Seite. */
.main > [data-body] { flex: 1 1 auto; min-width: 0; min-height: 0; }

/* Nur dieser Bereich scrollt. Klebt der Scrollbalken am Fensterrand, ist Regel 4 gebrochen. */
.canvas { overflow: auto; overscroll-behavior: contain; min-width: 0; min-height: 0; }
.canvas::-webkit-scrollbar { width: 11px; height: 11px; }
.canvas::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-2); border: 3px solid var(--canvas); }
.canvas::-webkit-scrollbar-track { background: var(--canvas); }

/* ── NAV-RAIL (dunkel = Rahmen) ────────────────────────────────────────────── */
.rail { background: var(--rail); border-right: 1px solid var(--rail-border); display: grid; grid-template-rows: var(--h-topbar) minmax(0,1fr) auto; overflow: hidden; }
.rail-logo { display: flex; align-items: center; gap: var(--sp-2); padding: 0 14px; border-bottom: 1px solid var(--rail-divider); overflow: hidden; }
.rail-mark { flex: none; width: 22px; height: 22px; color: var(--rail-accent); }
.rail-word { font-size: var(--fs-body); font-weight: 600; color: var(--rail-text-strong); letter-spacing: -.01em; white-space: nowrap; }
.rail-word span { font-size: var(--fs-micro); font-weight: 600; color: var(--rail-section); letter-spacing: .1em; text-transform: uppercase; margin-left: 5px; }
:root[data-rail="collapsed"] .app .rail-word { display: none; }
/* Marken-Logo (#270): Chip nur, wo die Schiene dunkel ist — auf der hellen steht das Logo frei und
   `padding` waere nur Rand ohne Flaeche. Deshalb haengt beides am selben Schalter. */
.rail-logo-img { max-height: 32px; max-width: 158px; object-fit: contain; }
:root[data-theme="dark"] .rail-logo-img { background: var(--rail-logo-chip); padding: 4px 8px; border-radius: var(--r-1); }
:root[data-rail="collapsed"] .app .rail-logo-img { max-width: 34px; }

/* ── Kanban-Board (#289) ───────────────────────────────────────────────── */
/* Fünf gleich breite Spalten, alle offen (Sascha 19.07.). Grid statt Flex: `1fr` teilt den Platz exakt
   gleichmäßig auf, während `flex: 1 1 0` mit unterschiedlich hohen Inhalten optisch auseinanderlief.
   `minmax(212px, 1fr)` hält die Untergrenze — wird der Platz enger, scrollt das Board waagerecht, statt
   die Karten unlesbar zu quetschen.
   ⛔ DIESE REGEL WAR AM 03.08. FÜR EINE STUNDE GELÖSCHT und das Board fiel damit auf `display: block`
   zurück: fünf Spalten untereinander, jede mit eigenem Scrollbereich in einer scrollenden Seite.
   Ursache war ein Aufräum-Werkzeug, kein Urteil über die Regel — sein Prüfschritt für zusammengesetzte
   Klassennamen (`kb{% raw %}{%{% endraw %} if …`) lief erst ab drei Zeichen Länge, und `kb` hat zwei.
   Eine Klasse, die kürzer ist als die Schleife, die sie prüfen soll, gilt als unbenutzt.
   Lehre fürs nächste Aufräumen: ein Suchlauf muss an seinen eigenen RÄNDERN geprüft werden — der
   kürzeste Name, der längste, der mit Bindestrich. Nicht in der Mitte, dort stimmt er immer. */
.kb { display: grid; grid-template-columns: repeat(5, minmax(212px, 1fr)); gap: var(--sp-3);
      padding: var(--sp-4); overflow-x: auto; align-items: stretch; min-height: 60vh; }
/* Spalte = leicht versenkte Bahn, damit die Karten darauf liegen statt darin zu verschwinden. */
.kb-col { min-width: 0; display: flex; flex-direction: column;
          border: 1px solid var(--border);
          border-radius: var(--r-2); background: var(--sunken); transition: border-color .12s, background .12s; }
.kb-col.is-over { border-color: var(--primary); background: var(--selected); }
/* Abgeschlossene Spalten (Erledigt/Verworfen) treten zurück, ohne zu verschwinden: sie stehen offen und
   nehmen Karten an, ziehen aber den Blick nicht auf sich. */
.kb-col--closed { background: transparent; border-style: dashed; }
.kb-col--closed .kb-card { opacity: .78; }
.kb-col--closed .kb-card:hover { opacity: 1; }
.kb-col-head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-3);
               font: 600 var(--fs-ui)/1.3 var(--f-ui); color: var(--text); }
.kb-col-name { flex: 1; min-width: 0; letter-spacing: -.01em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kb-col-n { font-variant-numeric: tabular-nums; color: var(--text-muted); background: var(--canvas);
            border: 1px solid var(--border); border-radius: var(--r-full); padding: 0 8px;
            font-size: var(--fs-micro); font-weight: 700; }
/* Der Punkt trägt die Phase als Farbe — die Spaltenüberschrift bleibt dabei normale Schrift (Regel: Farbe
   markiert den Zustand, nicht den Text). */
.kb-col-dot { width: 8px; height: 8px; border-radius: var(--r-full); flex: 0 0 8px; background: var(--text-faint); }
.kb-dot--neu       { background: var(--primary); }
.kb-dot--geplant   { background: var(--text-muted); }
.kb-dot--in_arbeit { background: var(--warn); }
.kb-dot--erledigt  { background: var(--good); }
.kb-dot--verworfen { background: var(--text-faint); }
/* `flex: 1` lässt die Bahn die gestreckte Spaltenhöhe ausfüllen — sonst blieben Rahmen und Inhalt
   verschieden hoch und die Ablagefläche endete über dem Spaltenboden. */
.kb-col-body { flex: 1; overflow-y: auto; max-height: calc(100vh - 208px); padding: 0 var(--sp-2) var(--sp-2);
               display: flex; flex-direction: column; gap: var(--sp-2); min-height: 44px; }
.kb-empty { margin: 0; padding: var(--sp-2); font-size: var(--fs-micro); text-align: center; }

.kb-card { border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
           border-radius: var(--r-2); background: var(--canvas); padding: var(--sp-2) var(--sp-3);
           cursor: pointer; /* Kein Schatten: was liegt, wirft keinen (Regel 2). Vorher 0 1px 2px rgba(0,0,0,.06) — hart
     kodiert und damit auch am Dark-Theme vorbei. */
           transition: box-shadow .12s, transform .12s, border-color .12s; }
.kb--readonly .kb-card { cursor: pointer; }
.kb-card:hover { border-color: var(--border-strong); box-shadow: 0 3px 10px rgba(0,0,0,.10); transform: translateY(-1px); }
.kb-card.is-dragging { opacity: .35; box-shadow: none; transform: none; }
.kb-card.is-outpim { opacity: .55; }
.kb-card.prio-1 { border-left-color: var(--good); }
.kb-card.prio-2 { border-left-color: var(--warn-mark); }
.kb-card.prio-3 { border-left-color: var(--warn); }
.kb-card.prio-4 { border-left-color: var(--bad); }
/* ⛔ `min-width: 0` UND `flex-wrap` (15.08.). Ohne beides schnitt die Karte ihren eigenen Kopf ab:
   gemessen bei 950 und 1280 px war `.kb-card-top` bis zu 37 px breiter als die Karte, die Karte bis zu
   25 px breiter als ihre Spalte — und es entstand KEIN Scrollbalken, der Inhalt war schlicht weg.
   Ein Flex-Kind hat per Voreinstellung `min-width: auto` und schrumpft deshalb nicht unter seinen
   Inhalt; genau dieselbe Zeile steht 30 Zeilen weiter oben schon an `.kb-col-name`, nur eine Ebene
   höher. */
.kb-card-top { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; min-width: 0; }
.kb-card-top > * { min-width: 0; }
.kb-card { min-width: 0; }
/* Laufende Nummer der Karte. Ziffernbreite fix (tabular-nums), damit #7 und #297 in einer Spalte
   untereinander fluchten statt zu wackeln. Bewusst gedaempft: sie ist Adresse, nicht Inhalt. */
.kb-id { font: 600 var(--fs-micro)/1 var(--f-ui); font-variant-numeric: tabular-nums; color: var(--text-faint); }
.kb-kind { font-size: var(--fs-dense); line-height: 1; }
.kb-cat-chip { font-size: var(--fs-micro); font-weight: 600; color: var(--text-muted); background: var(--selected);
               border-radius: var(--r-full); padding: 1px 7px; white-space: nowrap; }
.kb-cx { font-variant-numeric: tabular-nums; font-size: var(--fs-micro); font-weight: 700;
         color: var(--text-muted); border: 1px solid var(--border); border-radius: var(--r-1); padding: 0 5px; }
.kb-tag-out { font-size: var(--fs-micro); color: var(--text-faint); border: 1px dashed var(--border-strong); border-radius: var(--r-1); padding: 0 4px; }
/* Deckel bei drei Zeilen. Ohne ihn reichten die gemessenen Titelhöhen von 20 bis 157 px — eine einzige
   lange Karte fraß ein Viertel der Spalte, und acht verschiedene Kartenhöhen lassen eine Bahn unruhig
   aussehen. Der ganze Titel steht im Detail, das ohnehin ein Klick entfernt ist. */
.kb-card-title { font-size: var(--fs-ui); line-height: 1.4; color: var(--text); overflow-wrap: anywhere;
                 display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.kb-card-meta { display: flex; align-items: center; gap: 6px; margin-top: 6px; font-size: var(--fs-micro);
                color: var(--text-faint); flex-wrap: wrap; }
/* Urheber + Herkunft (Migration 094): „von wem" und „von welcher Seite" sind die zwei Fragen, die man bei
   einer fremden Karte zuerst stellt. Die Seite ist ein echter Link — ein Klick, und man steht dort. */
.kb-who { color: var(--text-muted); }
/* In einer 212-px-Spalte ist die Herkunft das erste, was reißt. Sie kürzt sich selbst, statt die Zeile
   umzubrechen — der volle Pfad steht im title und im Detail. */
.kb-page { font-family: var(--f-mono, ui-monospace, monospace); color: var(--text-faint);
           max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.kb-page:hover { color: var(--primary); text-decoration: underline; }

/* Karten-Detail: Popup in der Seitenmitte (19.07.) — die Seitenleiste drängte den Inhalt an den Rand und
   ließ das Board dahinter weiterlaufen; ein Modal richtet den Blick auf die eine Karte. */
.kb-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 40; }
.kb-modal { position: fixed; inset: 0; z-index: 41; display: flex; align-items: center; justify-content: center;
            padding: var(--sp-4); pointer-events: none; }
.kb-modal-card { pointer-events: auto; width: min(820px, 100%); max-height: min(92vh, 980px);
                 display: flex; flex-direction: column; background: var(--canvas);
                 border: 1px solid var(--border); border-radius: var(--r-3); box-shadow: var(--sh-modal); }
.kb-modal-head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-3) var(--sp-2) var(--sp-4); }
.kb-modal-body { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4);
                 display: flex; flex-direction: column; gap: var(--sp-3); }
.kb-modal-foot { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-4);
                 border-top: 1px solid var(--border); background: var(--sunken);
                 border-radius: 0 0 var(--r-3) var(--r-3); }
.kb-d-kind { font-size: var(--fs-ui); font-weight: 600; white-space: nowrap; color: var(--text-muted); }
/* `rows="2"` mal `line-height: 1.3` ergab bei 17 px eine Kastenhöhe, die die UNTERLÄNGEN abschnitt —
   aus „Keine einzige Zuordnung" wurde sichtbar „Keine einzia̶e Zuordnuna". Ein Textfeld rechnet seine
   Höhe aus Zeilen mal Zeilenhöhe und rundet dabei nach unten; g, j, ß und p fallen als Erstes raus.
   1.45 plus etwas mehr Innenabstand gibt den Buchstaben ihren Raum zurück. */
.kb-d-title { width: 100%; border: 1px solid transparent; border-radius: var(--r-1); background: none;
              color: var(--text); font: 600 var(--fs-sect) var(--f-ui); padding: 6px 6px 8px; margin-left: -6px;
              resize: vertical; line-height: 1.45; min-height: 68px; }
.kb-d-title:hover:not([readonly]), .kb-d-title:focus { border-color: var(--border-strong); background: var(--sunken); outline: none; }
.kb-d-origin { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-micro); margin-top: -6px; }
/* Drei Textebenen: Beschreibung (allgemein) · interne Notiz (Arbeitsstand) · Notizen (Gesprächsfaden).
   Die Kästen waren zu klein — 96 px zeigten von einem Absatz drei Zeilen und man schrieb durch ein
   Guckloch. Beschreibung 220, Notiz 140, beide von Hand vergrößerbar. */
.kb-d-field { display: flex; flex-direction: column; gap: 5px; }
.kb-d-body { resize: vertical; min-height: 220px; line-height: 1.55; }
.kb-d-note { resize: vertical; min-height: 140px; line-height: 1.55; background: var(--sunken); }
.kb-d-hint { display: inline; margin-left: 8px; font-weight: 400; text-transform: none; letter-spacing: 0;
             font-size: var(--fs-micro); color: var(--text-faint); }
/* Steuer-Block als Raster: die vier Regler stehen zusammen, statt sich durch die ganze Karte zu ziehen. */
.kb-d-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2) var(--sp-4); border: 0; padding: 0; margin: 0;
             border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.kb-d-grid[disabled] { opacity: .55; }
.kb-d-grid .kb-d-check { grid-column: 1 / -1; }
.kb-d-grid .kb-d-row:has(#kbdPrio) { grid-column: 1 / -1; }
.kb-d-row { display: flex; align-items: center; gap: var(--sp-2); }
.kb-d-lbl { font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
            color: var(--text-faint); min-width: 74px; }
.kb-d-check { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-ui); color: var(--text-muted); }
.kb-d-ro { margin: 0; font-size: var(--fs-micro); }
.seg--sm button { height: 24px; padding: 0 7px; font-size: var(--fs-micro); }
.kb-d-comments { display: flex; flex-direction: column; gap: var(--sp-2); border-top: 1px solid var(--border); padding-top: var(--sp-3); }
.kb-cmt-list { display: flex; flex-direction: column; gap: 6px; }
.kb-cmt { font-size: var(--fs-ui); color: var(--text); background: var(--sunken); border-radius: var(--r-1); padding: 6px 9px; white-space: pre-wrap; }
.kb-cmt-fehler { display: block; font-size: var(--fs-dense); line-height: 1.45; }
.kb-cmt-meta { display: block; font-size: var(--fs-micro); margin-bottom: 2px; }
/* Notiz-Eingabe als Textfeld statt einer einzeiligen Zeile: eine Notiz ist selten ein Satz. Der Knopf
   sitzt unten bündig, damit er beim Aufziehen des Feldes nicht mitwandert. */
.kb-cmt-add { display: flex; gap: var(--sp-2); align-items: flex-end; }
.kb-cmt-input { flex: 1; resize: vertical; min-height: 52px; line-height: 1.5; }
/* Dringlichkeit als Wort auf der Karte — die Farbe des Streifens allein war eine Information, die man
   auswendig können musste. Farbe markiert, das Wort benennt. */
.kb-prio { font-size: var(--fs-micro); font-weight: 700; letter-spacing: .02em; border-radius: var(--r-full);
           padding: 1px 7px; color: var(--text-muted); background: var(--selected); white-space: nowrap; }
.prio-3 .kb-prio { color: var(--warn); }
.prio-4 .kb-prio { color: var(--bad); }
.kb-d-body-ro { margin: 0; font-size: var(--fs-ui); line-height: 1.5; color: var(--text-muted); white-space: pre-wrap; }

/* ── Aufgaben-Chips über der Tabelle (P2, 19.07.) ──────────────────────────
   Ersetzt die Aufgaben-Klappleiste in der Schiene. Drei Zeilen mit fester Bedeutung: Abteilung, Aufgabe,
   Werkzeuge. Die Beschriftung links macht die Zeile lesbar, ohne dass man sie erraten muss. */
.chipbar { display: flex; flex-direction: column; gap: 6px; padding: var(--sp-2) var(--sp-4);
           border-bottom: 1px solid var(--border); background: var(--sunken); }
.chipbar-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-height: 26px; }
.chipbar-lbl { font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
               color: var(--text-faint); min-width: 68px; }
.chipbar-lbl--soft { min-width: 0; margin-left: 2px; }
.chipbar-sep { width: 1px; align-self: stretch; margin: 2px 6px; background: var(--border); }
.chipbar-row--tools { padding-top: 2px; border-top: 1px dashed var(--border); }

.dep-tab-arrow { width: 11px; height: 11px; opacity: .7; }

.tchip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 10px;
        border: 1px solid var(--border); border-radius: var(--r-full); background: var(--canvas);
        font-size: var(--fs-ui); color: var(--text); white-space: nowrap; transition: border-color .12s, background .12s; }
.tchip:hover { border-color: var(--border-strong); text-decoration: none; }
.tchip.active { border-color: var(--primary); background: var(--selected); font-weight: 600; }
.tchip-n { font-variant-numeric: tabular-nums; font-size: var(--fs-micro); font-weight: 700; color: var(--text-muted); }
.tchip.active .tchip-n { color: var(--primary); }
/* Ein Topf mit 0 bleibt SICHTBAR, nur leise — er verschwinden zu lassen hieße, „hier ist nichts zu tun"
   von „hier gibt es das gar nicht" ununterscheidbar zu machen. */
.tchip.is-zero { opacity: .5; }
/* „0 hier, n woanders" (28.07.): weder erledigt noch leer — die Arbeit liegt in einem anderen Reiter.
   Bewusst NICHT gedimmt wie `is-zero` (er ist klickbar und hat einen Auftrag) und bewusst ohne
   Ampelfarbe (die gehört der Marge, Entscheidung D) — der Zusatztext trägt die Aussage. */
.tchip--elsewhere { border-style: dashed; }
.tchip-else { font-size: var(--fs-micro); color: var(--text-muted); white-space: nowrap; }
.tchip--plain { border-style: dashed; }
.tchip--soft { background: none; border-style: dotted; color: var(--text-muted); }
/* Werkzeuge sind KEINE Filter — sie führen auf eine eigene Seite. Der Unterschied stand vorher in der
   Abwesenheit von Rahmen und Grund: in einer Reihe aus Pillen liest sich ein rahmenloses Element aber
   als DEAKTIVIERT, nicht als „andere Sorte". Jetzt tragen sie dieselbe Pillenform (also gleicher Rang),
   sagen ihre Andersartigkeit über die gestrichelte Kante und über den Pfeil, der das Wegführen benennt. */
.tchip--tool { border-style: dashed; background: none; color: var(--accent); }
.tchip--tool::after { content: "↗"; font-size: var(--fs-micro); opacity: .75; }
.tchip--tool:hover { background: var(--hover); border-color: var(--accent); }
.tchip--tool.active { background: var(--selected); color: var(--text); border-style: solid; border-color: var(--border); }

.rail-nav { overflow-y: auto; overflow-x: hidden; padding: var(--sp-2) 0; }
.rail-nav::-webkit-scrollbar { width: 6px; }
.rail-nav::-webkit-scrollbar-thumb { background: var(--rail-scroll); border-radius: var(--r-1); }

/* Die drei Gruppen sind der eigentliche IA-Fix: Arbeiten ≠ Einrichten ≠ Verwalten.
   Kein Punkt ist versteckt — sie sind nur nicht mehr gleich laut. */
/* Flex, seit die Ueberschrift „Stammdaten" eine Zahl rechts traegt (IA-Umbau 20.07.). Vorher ein
   reiner Block — zwei Kinder haetten sich aneinandergereiht statt an die Kanten zu gehen. */
.rail-sect { display: flex; align-items: baseline; gap: var(--sp-2); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--rail-section); padding: 14px var(--sp-3) 5px; white-space: nowrap; }
.rail-sect > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Die Gruppen-Zahl traegt kein letter-spacing/uppercase — sie ist eine Zahl, keine Beschriftung. */
.rail-n--sect { letter-spacing: 0; }
:root[data-rail="collapsed"] .app .rail-sect { font-size: 0; padding: 0; height: 1px; background: var(--rail-divider); margin: var(--sp-2) var(--sp-3); overflow: hidden; }

.rail-item {
  display: flex; align-items: center; gap: 10px;
  height: var(--sp-8); padding: 0 var(--sp-3);
  color: var(--rail-text); font-size: var(--fs-ui); font-weight: 500;
  cursor: pointer; border: 0; background: none; width: 100%;
  text-align: left; white-space: nowrap;
}
.rail-item:hover { background: var(--rail-hover); color: var(--rail-text-strong); }
.rail-item.active { background: var(--rail-active); color: var(--rail-text-active); font-weight: 600; box-shadow: inset 3px 0 0 var(--rail-accent); }
.rail-item svg { flex: none; width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.5; }
.rail-item .lbl { flex: 1; overflow: hidden; text-overflow: ellipsis; }
.rail-n { font-size: var(--fs-micro); font-weight: 600; color: var(--rail-section); font-variant-numeric: tabular-nums; }
.rail-item.active .rail-n { color: var(--rail-text-active); }
/* ⛔ NUR DIE SCHIENEN-ZEILE SELBST, NICHT DAS FLYOUT (18.08., zweite Abnahme). Diese Regel blendete
   `.rail-n` pauschal aus, sobald die Schiene schmal ist — und traf damit seit heute auch die Zahlen im
   aufklappenden Fenster, wo die Beschriftung ja in voller Breite dasteht. Schmal geklappt ist das
   Fenster die EINZIGE Navigation; dort verschwanden Einkauf 2.496 · Verkauf 51 · Stammdaten 504 ·
   Shop-Anlage 392 und der Fehlerzähler. Also genau die Zahlen, wegen derer man hinsieht.
   `.rail-item .rail-n` statt `.rail-n`: die Zahl der Ebene 1 gehört weiterhin weg (dort ist kein Platz),
   die im `.pk` bleibt. */
:root[data-rail="collapsed"] .app .rail-item .lbl,
:root[data-rail="collapsed"] .app .rail-item .rail-n { display: none; }
:root[data-rail="collapsed"] .app .rail-item { justify-content: center; padding: 0; }

.rail-foot { border-top: 1px solid var(--rail-divider); padding: 6px 0; }
.rail-toggle { display: flex; align-items: center; justify-content: center; width: var(--sp-8); height: 28px; margin: 2px 0 0 var(--sp-3); border: 0; background: none; color: var(--rail-section); cursor: pointer; }
.rail-toggle:hover { color: var(--rail-text-strong); }
.rail-toggle svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.6; transition: transform .16s; }
:root[data-rail="collapsed"] .app .rail-toggle svg { transform: rotate(180deg); }

/* ── ARBEITSPAKETE (Rail-Ebene 2) ──────────────────────────────────────────
   Regel 7: Ein Auftrag mit Zähler-Ziel 0 IST ein Ort. Deshalb Navigation — und
   deshalb kostet die Filter-Rail keine Breite mehr. */
.pk-wrap { display: none; }
.rail-item.open + .pk-wrap { display: block; }
/* Gleiche Spezifitaet wie die Regel darueber -> die spaetere gewinnt, kein !important noetig.
   Kollabiert zeigt die Schiene nur Icons; die Pakete kommen dann per Flyout (s. unten). */
:root[data-rail="collapsed"] .app .rail-item.open + .pk-wrap { display: none; }

/* ⛔ UND KOLLABIERT DARF DIE SCHIENE NICHTS MEHR WEGSCHNEIDEN (19.08.). Das Flyout unten war einen
   Tag lang gebaut und trotzdem unsichtbar: `.rail { overflow: hidden }` und `.rail-nav { overflow-x:
   hidden }` schneiden alles ab, was rechts ueber die 56 px hinausragt — und genau dort liegt es
   (`left: 100%`). Gemessen bei der Abnahme: das Menue steht an der richtigen Stelle, sechs Punkte,
   186 px hoch, mit Hintergrund — man sieht es nur nie. Sichtbar war stattdessen der Rest, der
   hineinragte: nackte Zahlen ohne Beschriftung in der schmalen Leiste.
   Der Verzicht auf den senkrechten Rollbalken ist hier folgenlos: kollabiert stehen nur die
   Bereichs-Symbole da, das passt in jedes realistische Fenster. Ausgeklappt bleibt alles wie gehabt —
   dort ist `overflow: hidden` richtig, weil die Unterpunkte inline stehen und die Liste lang wird. */
:root[data-rail="collapsed"] .app .rail,
:root[data-rail="collapsed"] .app .rail-nav { overflow: visible; }

/* ── DAS FLYOUT (18.08.) ───────────────────────────────────────────────────
   ⛔ DIESE ZEILE STAND SEIT MONATEN ALS VERSPRECHEN IM KOMMENTAR DARUEBER — „die Pakete kommen dann
   per Flyout" —, und es gab keinen. Gemessen bei der UX-Abnahme: kollabiert waren 24 der 29 Ziele
   ueberhaupt nicht mehr erreichbar (vorher waren alle 15 Eintraege als Symbole direkt klickbar).
   Ein Kommentar, der eine Loesung beschreibt, die es nicht gibt, ist schlimmer als eine offene
   Baustelle: er beendet die Suche.

   Es loest ZWEI Befunde auf einmal. Der zweite kam aus der Inhaber-Abnahme: seit ein Bereich auf die
   Seite seines ersten Unterpunkts fuehrt, kostete der Weg zu „Verkauf" erst einen vollen Ladevorgang
   der Einkaufs-Tabelle (gemessen 2,7 s) und dann den eigentlichen Klick. Ueberfahren zeigt jetzt die
   Unterpunkte, ohne die Zwischenseite zu laden — der Bereichsklick bleibt fuer die, die ihn wollen.

   Reines CSS, kein Skript: `:hover` auf dem Rahmen. Deshalb braucht es `.rail-slot` im Markup — ohne
   positionierten Vorfahren landen alle Flyouts auf derselben Hoehe. */
.rail-slot { position: relative; }
/* Der Hinweis auf das Flyout. Leise (halbe Deckkraft), aber vorhanden — er beantwortet „hier steckt
   noch etwas dahinter", ohne mit den Zahlen um Aufmerksamkeit zu konkurrieren. Beim Überfahren wird er
   deutlich, weil dann auch das Fenster erscheint. */
.rail-more { width: 11px; height: 11px; stroke-width: 2; flex: none; opacity: .38; margin-left: 2px; }
.rail-item:hover .rail-more { opacity: .85; }
:root[data-rail="collapsed"] .app .rail-more { display: none; }
/* Breit: nur fuer die NICHT offenen Bereiche — der aktive zeigt seine Unterpunkte ohnehin inline,
   und ein Flyout darueber waere dieselbe Liste zweimal. */
.rail-slot:hover .rail-item:not(.open) + .pk-wrap,
/* Kollabiert: fuer JEDEN Bereich, auch den aktiven — dort ist inline nichts zu sehen. */
:root[data-rail="collapsed"] .app .rail-slot:hover .rail-item + .pk-wrap {
  display: block;
  position: absolute; left: 100%; top: 0; z-index: 60;
  min-width: 210px; padding: var(--sp-2) 0;
  background: var(--rail);
  border: 1px solid var(--rail-border, rgba(0,0,0,.12));
  border-radius: 10px;
  box-shadow: 0 8px 28px rgba(0,0,0,.18);
}
/* Im Flyout gibt es keine Einrueckung zu respektieren — die 34 px links waren fuer die Hierarchie
   INNERHALB der Schiene gedacht und sehen freistehend wie ein Fehler aus. */
.rail-slot:hover .rail-item:not(.open) + .pk-wrap .pk,
:root[data-rail="collapsed"] .app .rail-slot:hover .rail-item + .pk-wrap .pk { padding-left: var(--sp-3); }

/* Klapp-Gruppe (P3, 24.07.): reine Werkzeug-Sammlungen ohne eigene Zielseite (Daten & System,
   Einrichten) — natives <details>, Header sieht aus wie ein normaler .rail-item (gleiche Klasse),
   navigiert aber nicht.
   (Der Verweis auf `.pk-group` stand hier bis zum 02.08.; das Abteilungs-Akkordeon ist am 19.07. in
   die Chip-Leiste gezogen, seine Regeln sind mit dem Aufraeumen entfallen.) */
.rail-group > summary { list-style: none; cursor: pointer; }
.rail-group > summary::-webkit-details-marker { display: none; }
.rail-group[open] > .pk-wrap { display: block; }
/* Ohne diese Zeile (ux-designer-Fund, 24.07.): kollabiert zerlegt der Browser die Sub-Labels der
   offenen Gruppe in einzelne Buchstaben neben dem Icon — dieselbe Kollabiert-Ausnahme wie Zeile 495,
   die dort nur fuer die alte .rail-item.open + .pk-wrap-Form galt, nicht fuer natives <details>. */
:root[data-rail="collapsed"] .app .rail-group[open] > .pk-wrap { display: none; }
.rail-group-caret { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 2; transition: transform .13s; flex: none; }
.rail-group[open] .rail-group-caret { transform: rotate(90deg); }

.pk-caret { width: 9px; height: 9px; stroke: currentColor; fill: none; stroke-width: 2.4; transition: transform .13s; flex: none; }

.pk {
  display: flex; align-items: center; height: 28px; padding: 0 var(--sp-3) 0 34px;
  border: 0; background: none; cursor: pointer; width: 100%; text-align: left;
  color: var(--rail-text); font-size: var(--fs-dense);
}
.pk:hover { background: var(--rail-hover); color: var(--rail-text-strong); }
/* ⛔ EBENE 2 AKTIV OHNE FLÄCHE (18.08., UX-Abnahme). Bis hierher teilten `.rail-item.active` und
   `.pk.active` Hintergrund UND Textfarbe; der ganze Unterschied war ein 3-px-Balken und ein
   Gewichtsschritt. Seit die Unterpunkte nur noch beim aktiven Bereich stehen, leuchten die zwei
   Zeilen unmittelbar übereinander — und sahen dann fast gleich aus, obwohl die eine der Ort und die
   andere der Ausschnitt darin ist. Farbe und Gewicht markieren weiterhin; die Fläche bleibt der
   Ebene 1 vorbehalten, und damit ist die Hierarchie ohne Nachdenken lesbar. */
.pk.active { color: var(--rail-text-active); font-weight: 600; box-shadow: inset 2px 0 0 var(--rail-accent, currentColor); }
.pk.active .rail-n { color: var(--rail-text-active); }
.pk .lbl { flex: 1; overflow: hidden; text-overflow: ellipsis; }
/* Zähler 0: ausgegraut, ABER klickbar. „Sichtbar, nicht versteckt" heißt nicht „tot" —
   ein toter Eintrag lässt offen, ob man nicht darf oder ob nichts da ist. */
.pk.is-zero, .pk.is-zero .rail-n { color: var(--rail-off); }
.pk.is-zero:hover { color: var(--rail-sub); }

/* ── ABTEILUNGEN (C/3) — server-getriebenes Akkordeon in der Rail ─────────────
   4 benannte Linsen aus EINER Quelle (departments.js), identisch auf /sortiment
   und /preise. Der Header ist ein LINK (?dep=…): der Server rendert nur die
   offene Abteilung aus (single-open, kein Client-Toggle). KEINE Zahl am Header
   (überlappende To-dos → eine Summe läse sich als Aufteilung). Verkauf ist
   extern (→ /preise): der Pfeil sagt, dass der Klick die Seite wechselt. */
/* Werkzeuge (Editor-Seiten) — eigener Unterblock innerhalb der offenen Abteilung, keine Tabellenzeilen. */

/* ── TOPBAR ────────────────────────────────────────────────────────────────── */
.topbar {
  display: grid; grid-template-columns: 1fr minmax(240px, 460px) 1fr;
  align-items: center; gap: var(--sp-3); padding: 0 var(--sp-3);
  height: var(--h-topbar);
  border-bottom: 1px solid var(--border); background: var(--canvas);
}
/* Statt eines „+ Neu": ein PIM erstellt nichts — Daten kommen aus Importen. Das Verb
   dieses Systems ist „aufbauen/ausspielen", die Dauerfrage „sind die Zahlen aktuell?". */
.stand {
  display: inline-flex; align-items: center; gap: 7px; height: 28px; padding: 0 10px;
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: none; cursor: pointer; font-size: var(--fs-dense);
  color: var(--text-muted); justify-self: start; white-space: nowrap;
}
.stand:hover { background: var(--hover); }
.stand b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }

.dot { width: 6px; height: 6px; border-radius: var(--r-full); flex: none; }
.dot--ok { background: var(--good); }
.dot--warn { background: var(--warn-mark); }
.dot--bad { background: var(--bad); }

.search { position: relative; display: flex; align-items: center; }
.search svg { position: absolute; left: 9px; width: 15px; height: 15px; stroke: var(--text-faint); fill: none; stroke-width: 1.6; pointer-events: none; }
.search input { width: 100%; height: 30px; padding: 0 10px 0 30px; border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: inherit; font-size: var(--fs-dense); }
.search input::placeholder { color: var(--text-faint); }
.search kbd { position: absolute; right: var(--sp-2); font: 500 var(--fs-micro) var(--f-ui); color: var(--text-faint); border: 1px solid var(--border); border-radius: var(--r-1); padding: 1px 4px; background: var(--sunken); pointer-events: none; }

.topbar-right { display: flex; align-items: center; gap: var(--sp-1); justify-self: end; }
.avatar { width: 28px; height: 28px; border-radius: var(--r-full); background: var(--primary); color: #fff; display: grid; place-items: center; font: 600 var(--fs-micro) var(--f-ui); border: 0; cursor: pointer; margin-left: var(--sp-1); }

/* ── SEITENKOPF ────────────────────────────────────────────────────────────── */
.pagehead { display: flex; align-items: center; gap: 10px; padding: 0 var(--sp-4); height: var(--h-pagehead); border-bottom: 1px solid var(--border); }
/* ⛔ AUF 900 px LAGEN ZWEI KOPF-KNÖPFE AUSSERHALB DES FENSTERS (gemessen 29.07., Arbeitsfläche).
   Der Kopf ist 664 px breit, sein Inhalt brauchte 841 — die Werkzeuge rechts wurden auf x=1015 und
   x=1049 geschoben und waren schlicht nicht erreichbar. Weil das Dokument selbst nicht scrollt (Regel 4:
   der Canvas ist der einzige Scroll-Container), fiel es auch nicht als Balken auf: die Knöpfe waren
   einfach weg.
   Ursache war ein 463 px breites Auswahlfeld im Kopf, das nicht schrumpfen KONNTE — ein Flex-Kind hat
   `min-width: auto` und gibt unter seiner Inhaltsbreite nicht nach.
   Die Reihenfolge, in der nachgegeben wird, ist jetzt festgelegt: erst der Titel (er kürzt mit …),
   dann das Auswahlfeld, und die Werkzeug-Knöpfe NIE — sie sind die Handlung der Seite. */
.pagehead h1 { margin: 0; font-size: var(--fs-title); line-height: 28px; font-weight: 600; letter-spacing: -.01em;
  white-space: nowrap; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Der Zähler ist Teil des Titels, keine KPI-Kachel. */
.count-pill { font: 500 var(--fs-micro)/16px var(--f-ui); color: var(--text-muted); background: var(--sunken); border-radius: var(--r-1); padding: 1px 6px; font-variant-numeric: tabular-nums; }
.pagehead .spacer { flex: 1; }
.pagehead-tools { display: flex; align-items: center; gap: 6px; min-width: 0; }
/* Ein Auswahlfeld im Kopf darf mitschrumpfen (der gewählte Wert bleibt lesbar, der Rest kürzt) … */
.pagehead-tools .select { min-width: 0; }
/* … die Knöpfe NICHT. Sie sind 28 px breit und tragen die Handlung; ein geschrumpfter Knopf ist kein
   kleinerer Knopf, sondern ein unauffindbarer. */
.pagehead-tools .icon-btn, .pagehead-tools .btn, .pagehead-tools .export-cue { flex: none; }
/* D6 — „Vorgemerkt → Export"-Anker im Seitenkopf. Nur sichtbar wenn etwas wartet; macht den unsichtbaren
   Staging-Korb sichtbar und verlinkt ihn. Primär-Ton = Handlungsaufruf, nicht Warnung. */
.export-cue { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 9px; border-radius: var(--r-1); font: 600 var(--fs-dense)/1 var(--f-ui); color: var(--primary); background: var(--selected); white-space: nowrap; }
.export-cue:hover { background: var(--primary); color: #fff; }
.export-cue svg { width: 14px; height: 14px; }

/* ── TABS (Spaltensätze) ───────────────────────────────────────────────────── */
.views { display: flex; align-items: stretch; height: var(--h-tabs); padding: 0 var(--sp-4); border-bottom: 1px solid var(--border); overflow-x: auto; overflow-y: hidden; }
.views::-webkit-scrollbar { height: 0; }
.view-tab { display: inline-flex; align-items: center; gap: 6px; padding: 0 11px; border: 0; background: none; color: var(--text-muted); font-size: var(--fs-dense); font-weight: 500; cursor: pointer; white-space: nowrap; }
.view-tab:hover { color: var(--text); }
.view-tab.active { color: var(--text); font-weight: 600; box-shadow: inset 0 -2px 0 var(--primary); }
.view-hint { margin-left: auto; align-self: center; font-size: var(--fs-micro); color: var(--text-faint); white-space: nowrap; padding-left: var(--sp-4); }
/* Absprung-Reiter (Ausgeschlossen): kein Peer-Filter, sondern Link auf eine eigene Seite. Trennlinie +
   Pfeil setzen ihn vom Reiter-Satz ab, damit er nicht als vierte Ansicht verwechselt wird (A7). */
.view-tab--out { color: var(--text-faint); margin-left: 4px; padding-left: 15px; border-left: 1px solid var(--border); }
.view-tab--out:hover { color: var(--primary); }
.view-tab--out.active { box-shadow: none; }
.view-tab-arrow { width: 12px; height: 12px; opacity: .75; }

/* ── AKTIONSLEISTE (zwei Zustände, ein Rahmen → kein Layout-Sprung) ────────── */
.actionbar { display: flex; align-items: center; gap: 6px; height: var(--h-actionbar); padding: 0 var(--sp-4); border-bottom: 1px solid var(--border); background: var(--canvas); }
/* Auswahl-Zustand: gleicher Rahmen, andere Füllung. Eine Leiste, die beim Auswählen erscheint, schöbe
   die Tabelle 40 px nach unten — genau in dem Moment, in dem der Blick auf einer Zeile liegt. */
.actionbar.is-selecting { background: var(--selected); }
/* „Stand HH:MM" der Startseite — rechts, leise, aber lesbar. Sie sagt, dass die Zahlen aus einem
   Schnappschuss kommen; ohne sie gäben sie sich als live aus.
   ⛔ Kein Ampelgelb im Fehlerfall (Entscheidung D — die Skala gehört der Marge): der Warnzustand trägt
   die Fehlerfarbe der Shell, dieselbe wie der rpc-fail-Kasten darunter. */
.ab-stand { margin-left: auto; font-size: var(--fs-micro); color: var(--text-faint); white-space: nowrap; }
.ab-stand--alt { color: var(--bad); font-weight: 600; }
/* ⛔ Schmales Fenster: die Leiste darf umbrechen. Gemessen 04.08. auf /preise bei 950 px — die
   Filterzeile beansprucht 524 px und gibt als `flex: none` nichts ab, der Knopf „Alle Warengruppen
   aufklappen" braucht 202 px, verfuegbar sind 714. Der Knopf lief 34 px ueber den Rand hinaus und war
   abgeschnitten; anklicken liess er sich, lesen nicht mehr.

   Warum GENAU so und nicht anders — jede naheliegende Loesung bricht eine bewusste Entscheidung:
   · Knopftext kuerzen? `table.js` begruendet das Wort „Warengruppen" ausdruecklich — es trennt diese
     Aktion von den Varianten-Expandern („zwei verschiedene Aktionen teilten ein Wort").
   · Die Filterzeile schrumpfen lassen? Sie hat intern `flex-wrap: wrap` — bei fester Leistenhoehe
     braeche sie dann vertikal ab, also abgeschnitten statt ueberstehend.
   · Immer umbrechen lassen? Die feste Hoehe ist Absicht (Kommentar oben): eine Leiste, die beim
     Auswaehlen waechst, schoebe die Tabelle unter dem Blick weg.
   Der Umbruch greift deshalb NUR unterhalb der Breite, ab der es eng wird. Er ist dort stabil — die
   Leiste waechst einmal und bleibt, sie springt nicht beim Auswaehlen. Ab 1100 px aendert sich nichts.
   Gefunden hat das der Layout-Test in seiten.test.js, nicht das Auge. */
@media (max-width: 1100px) {
  .actionbar { height: auto; min-height: var(--h-actionbar); flex-wrap: wrap; row-gap: 4px; padding-top: 4px; padding-bottom: 4px; }
  /* ⛔ DAS KIND MUSS MIT (18.08., UX-Abnahme). Der Umbruch oben galt nur fuer die Leiste selbst; ihre
     zwei Flex-Kinder `.ab-idle`/`.ab-sel` trugen ihn nicht und quetschten ihren Inhalt weiter in EINE
     Zeile. Gemessen auf /sortiment bei 950 px: „Tabelle · Produktkarten · Warengruppe · flach" wurde
     hart abgeschnitten, „Produktkarten" und „flach" ueberlappten und waren nicht sauber klickbar.
     Ein Elternteil, das umbrechen darf, hilft nichts, solange das Kind es nicht auch tut. */
  .ab-idle, .ab-sel { flex-wrap: wrap; row-gap: 4px; }
}
.ab-idle, .ab-sel { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
.actionbar.is-selecting .ab-idle { display: none; }
.ab-search { display: flex; }
.selinfo { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-dense); font-weight: 600; color: var(--text); padding-right: 10px; margin-right: var(--sp-1); border-right: 1px solid var(--border-strong); font-variant-numeric: tabular-nums; }
/* `nowrap` verhindert den Umbruch — aber ohne `min-width:0` + `ellipsis` schiebt sich ein langer
   Hinweis stattdessen aus der Leiste heraus und malt über den Spaltenkopf darunter (auf
   /preise?tab=wettbewerb gemessen: +43 px bei 1600, +543 px bei 1100). Ein Flex-Item schrumpft nicht
   unter seine Inhaltsbreite, solange man ihm das nicht erlaubt. Der volle Text bleibt als `title`. */
.ab-hint { font-size: var(--fs-dense); color: var(--text-faint); margin-left: auto;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
/* Steht der Hinweis allein in der Leiste, ist das `margin-left:auto` sinnlos: er klebte rechts an
   der Fensterkante, meterweit von der Überschrift entfernt, auf die er sich bezieht. Ohne Nachbar
   links gibt es nichts wegzuschieben — dann liest er sich als das, was er ist: eine Unterzeile. */
.actionbar > .ab-hint:only-child { margin-left: 0; white-space: normal; overflow: visible; }
.bulk-ack { display: flex; align-items: center; gap: var(--sp-1); }
.input--sm { height: 26px; font-size: var(--fs-dense); }

/* ── FUSSLEISTE ────────────────────────────────────────────────────────────── */
.footbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: var(--h-footbar); padding: 0 var(--sp-4); border-top: 1px solid var(--border); font-size: var(--fs-dense); color: var(--text-muted); background: var(--canvas); }
.footbar .left { font-variant-numeric: tabular-nums; }
.pager { display: flex; align-items: center; gap: var(--sp-1); justify-self: center; }
.pager button { width: var(--sp-6); height: var(--sp-6); border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas); color: var(--text-muted); cursor: pointer; display: grid; place-items: center; }
.pager button:hover:not(:disabled) { background: var(--hover); color: var(--text); }
.pager button:disabled { opacity: .4; cursor: not-allowed; }
.pager svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 1.8; }
.pager input { width: 40px; height: var(--sp-6); text-align: center; border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: 500 var(--fs-dense) var(--f-ui); font-variant-numeric: tabular-nums; }

/* ── CONTROLS ──────────────────────────────────────────────────────────────── */
/* Entscheidung D: Aktionen sind NICHT farbcodiert. Hierarchie statt Farbe —
   gefüllt = primär, Outline = sekundär, Ghost = tertiär. Rot nur destruktiv, nur im Dialog. */
.btn {
  display: inline-flex; align-items: center; gap: 6px; height: var(--sp-8); padding: 0 var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--canvas); color: var(--text);
  font-size: var(--fs-ui); font-weight: 500; cursor: pointer; white-space: nowrap;
}
.btn:hover { background: var(--hover); }
.btn:disabled, button:disabled { opacity: .5; cursor: not-allowed; }
.btn svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.6; }
.btn--primary { background: var(--primary); border-color: var(--primary); color: #fff; font-weight: 600; }
.btn--primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
.btn--ghost { border-color: transparent; background: none; color: var(--accent); }
.btn--ghost:hover { background: var(--hover); }
.btn--danger { color: var(--bad); border-color: var(--border-strong); }
/* Kein vierter Hierarchie-Rang, sondern eine WARNUNG auf einer tertiaeren Aktion: „trotzdem angleichen,
   ohne Marge-Schutz". Stand als `style="color:var(--warn);font-weight:600"` im Template — dieselbe Aussage
   gehoert in den Bausatz, sonst steht die naechste Warnung wieder etwas anders da. */
.btn--warn { color: var(--warn); font-weight: 600; }
.btn--sm { height: 26px; padding: 0 var(--sp-2); font-size: var(--fs-dense); }
.btn .badge-n { background: var(--accent); color: #fff; border-radius: var(--r-full); font: 600 var(--fs-micro)/14px var(--f-ui); padding: 0 var(--sp-1); min-width: 14px; text-align: center; }

.icon-btn { position: relative; display: grid; place-items: center; width: var(--sp-8); height: var(--sp-8); border: 0; border-radius: var(--r-1); background: none; color: var(--text-muted); cursor: pointer; }
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.5; }
.icon-btn--sm { width: 28px; height: 28px; }
.icon-btn--sm svg { width: 15px; height: 15px; }
.icon-btn--row { width: var(--sp-6); height: var(--sp-6); color: var(--text-faint); }
.icon-btn--row svg { width: 13px; height: 13px; stroke-width: 1.7; }
.icon-btn .count { position: absolute; top: 3px; right: 2px; min-width: 15px; height: 15px; padding: 0 3px; border-radius: var(--r-full); background: var(--bad); color: #fff; font: 600 var(--fs-micro)/15px var(--f-ui); text-align: center; font-variant-numeric: tabular-nums; }

/* `.seg` traegt Buttons UND Links: der Ansichts-Umschalter (Tabelle/Karten) schaltet serverseitig, ist
   also ein <a>. Beide Varianten teilen sich einen Selektor statt zweier Regelsaetze — sonst driften
   Hoehe und Farbe des einen Segments vom anderen weg, und sie stehen nebeneinander. */
.seg { display: inline-flex; border: 1px solid var(--border-strong); border-radius: var(--r-1); overflow: hidden; }
.seg button, .seg a { display: inline-flex; align-items: center; height: 26px; padding: 0 9px; border: 0; background: var(--canvas); color: var(--text-muted); font-size: var(--fs-dense); cursor: pointer; text-decoration: none; }
.seg button + button, .seg a + a { border-left: 1px solid var(--border-strong); }
.seg button.on, .seg a.on { background: var(--selected); color: var(--accent); font-weight: 600; }

.input, .select {
  height: var(--sp-8); padding: 0 var(--sp-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--canvas); color: var(--text); font: inherit; font-size: var(--fs-ui);
}
.input:focus, .select:focus, .search input:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.select--sm { height: 28px; font-size: var(--fs-dense); color: var(--text-muted); padding: 0 var(--sp-1); }

/* ── BADGES & CHIPS ────────────────────────────────────────────────────────── */
.badge { display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: var(--r-1); font: 600 var(--fs-micro)/18px var(--f-ui); border: 1px solid; white-space: nowrap; }
.badge--ok   { background: var(--badge-ok-bg);   border-color: var(--badge-ok-bd);   color: var(--badge-ok-fg); }
.badge--warn { background: var(--badge-warn-bg); border-color: var(--badge-warn-bd); color: var(--badge-warn-fg); }
.badge--err  { background: var(--badge-err-bg);  border-color: var(--badge-err-bd);  color: var(--badge-err-fg); }
.badge--neu  { background: var(--badge-neu-bg);  border-color: var(--badge-neu-bd);  color: var(--badge-neu-fg); }

/* Herkunft des Referenz-EK — 4-wertig: WaWi · Liste · WaWi ≈ · Liste ≈.
   `≈` = von einer anderen Länge geerbt (bei ~81 % EK-Lücke der Normalfall).
   Immer grau: den Normalfall zu färben ist der schnellste Weg, Farbe wertlos zu machen.
   Die Spalte kodiert margin_vs_soll vollständig (= Herkunft ≠ WaWi) → Badge entfällt. */
.herk { display: inline-flex; align-items: center; gap: 3px; height: 18px; padding: 0 5px; border: 1px solid var(--badge-neu-bd); border-radius: var(--r-1); background: var(--badge-neu-bg); font: 600 var(--fs-micro)/18px var(--f-ui); color: var(--badge-neu-fg); cursor: pointer; }
.herk:hover { border-color: var(--border-strong); }
.herk i { font-style: normal; opacity: .7; }

/* Der EINZIGE Ort im System mit Grün/Gelb/Rot (Entscheidung D) — seit 23.07. stimmt der Satz auch:
   die Preistabelle färbte ihre Margen bis dahin über `badge--ok/warn/err`, also über eine ZWEITE
   Klassenfamilie mit exakt denselben Tokens. Beide Leser hängen jetzt am Makro in `_ampel.html`.
   `white-space` stand nur bei `.badge` — ohne diese Zeile hätte der Umzug die Preistabelle umbrechen
   lassen können; die Regel ist wieder eine, die Darstellung muss es dann auch sein. */
.db { display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: var(--r-1); font: 600 var(--fs-micro)/18px var(--f-ui); border: 1px solid; white-space: nowrap; }
.db--g { background: var(--badge-ok-bg);   border-color: var(--badge-ok-bd);   color: var(--badge-ok-fg); }
.db--w { background: var(--badge-warn-bg); border-color: var(--badge-warn-bd); color: var(--badge-warn-fg); }
.db--b { background: var(--badge-err-bg);  border-color: var(--badge-err-bd);  color: var(--badge-err-fg); }

/* ── KOPIEREN (`[data-copy]`) ─────────────────────────────────────────────────
   Eine SKU steht im PIM, gebraucht wird sie in WooCommerce, in WeClapp, in einer Mail an den
   Lieferanten. Bis 04.08. gab es dafuer genau EINEN Knopf im ganzen System — auf der Artikel-Detailseite
   (variante_detail.html), mit eigenem Skript. Ueberall sonst markiert man die Nummer mit der Maus, und
   bei einem `<a>` mit `text-overflow: ellipsis` in einer 44-px-Zeile ist das eine Fummelei, die
   regelmaessig im Detail-Link endet statt in der Zwischenablage.
   Der Knopf haengt an `[data-copy]` und der Handler liegt in shell.js — dieselbe Bauart wie
   `[data-nav-select]` (der Kommentar dort erklaert, warum ein Handler in einer Seiten-Datei genau die
   Sorte toter Bedienelemente erzeugt, die im HTML wie funktionierende aussehen).
   In der TABELLE erscheint er erst auf Hover: 25 Zeilen mit Dauer-Icon waeren wieder die Badge-Wand
   (dieselbe Begruendung wie bei `.row-acts .icon-btn--row`). Ausserhalb einer Tabelle steht er fest. */
.copy-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: 3px; padding: 0; vertical-align: -3px;
  border: 0; border-radius: var(--r-1); background: none; color: var(--text-faint); cursor: pointer;
}
.copy-btn svg { width: 11px; height: 11px; stroke: currentColor; fill: none; stroke-width: 1.8; }
.copy-btn:hover { color: var(--accent); background: var(--hover); }
.copy-btn.is-done { color: var(--good); }
.grid .copy-btn { opacity: 0; transition: opacity .1s; }
.grid tbody tr:hover .copy-btn, .grid .copy-btn:focus-visible { opacity: 1; }

/* ── LIVE-PULS (`.pulse[data-ts]`) ────────────────────────────────────────────
   „Wann lief das zuletzt?" ist auf jeder Seite mit einem Hintergrund-Job die erste Frage — und ein
   absoluter Zeitstempel („02.08.2026, 14:12") beantwortet sie nicht, er verlangt Kopfrechnen. Der
   Text kommt aus shell.js und rechnet mit; das Markup traegt nur die ISO-Zeit, damit sie nicht in zwei
   Formaten im Template steht.
   Der Punkt pulsiert NUR, solange etwas frisch ist (`is-live`, < 15 min) — sonst waere es Dekoration,
   und Bewegung ohne Bedeutung ist der teuerste Pixel im System (Regel 2).
   ⛔ Kein Ampelgruen: der Punkt sagt „aktuell", nicht „gut". Die Skala gehoert der Marge (Entscheidung D). */
.pulse { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-micro); color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.pulse::before { content: ""; width: 6px; height: 6px; border-radius: var(--r-full); background: var(--text-faint); flex: none; }
.pulse.is-live::before { background: var(--accent); animation: pulse-ring 2.4s ease-out infinite; }
.pulse.is-stale { color: var(--warn); }
.pulse.is-stale::before { background: var(--warn-mark); }
@keyframes pulse-ring {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); }
  70%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent); }
}
/* Wer Bewegung abbestellt hat, bekommt keine. Der Punkt bleibt, nur der Ring hoert auf. */
@media (prefers-reduced-motion: reduce) { .pulse.is-live::before { animation: none; } }

/* ── GEAENDERTER WERT (`.flash-set`) ──────────────────────────────────────────
   Nach einem Inline-Speichern stand die neue Zahl einfach da — in einer Tabelle mit 14 Zahlenspalten
   ist „hat er es genommen?" dann eine Frage, die man mit Neuladen beantwortet. Der kurze Aufleuchter
   sagt WELCHE Zelle sich geaendert hat, und er verschwindet von selbst: ein Dauer-Marker waere eine
   Spalte, die man spaeter wegraeumen muss. */
@keyframes flash-set {
  from { background: color-mix(in srgb, var(--accent) 26%, transparent); }
  to   { background: transparent; }
}
/* Beide Ebenen: eine einzelne Zelle ODER die ganze Zeile. Auf `<tr>` wirkt kein Hintergrund — die
   Flaeche gehoert den Zellen (derselbe Grund, aus dem die Hover-Regel oben auf `> td` zeigt). */
.flash-set, tr.flash-set > td { animation: flash-set 1.1s ease-out; }
@media (prefers-reduced-motion: reduce) { .flash-set, tr.flash-set > td { animation: none; } }

/* ── FACETTEN-RAIL (hell = Daten) ──────────────────────────────────────────
   Der Schlüsselzug ist visuell: Zwei vertikale Streifen verwirren nur, wenn sie
   dieselbe Spezies sind. Die Nav ist dunkel und klebt am Fensterrand; diese Rail
   ist hell und ist die erste Spalte des Canvas. Danach ist die Verwechslung aus. */
/* ⚠️ `[hidden]` schlagen: Die UA-Regel ist nur `[hidden] { display: none }` — JEDE eigene display-Regel
   ueberstimmt sie, weil sie spezifischer ist. `.ab-sel { display: flex }` liess die Massenaktions-Leiste
   dauerhaft sichtbar stehen, OBWOHL `hidden` gesetzt war: beide Leisten-Haelften teilten sich per `flex:1`
   den Platz, `#segGroup` („Warengruppe | flach") wurde auf 31 px gequetscht und war nicht mehr bedienbar,
   und „VK → Shop" war bei 0 ausgewaehlten Zeilen voll klickbar — genau das „Werkzeug ohne Objekt", das der
   Kommentar an der Leiste fuer abgeschafft erklaert.
   Die alte app.css hat diese Regel an DREI Stellen, mit Kommentaren, die exakt diese Falle benennen. Beim
   Neubau ist sie nicht mitgekommen — und der Fehler faellt beim Code-Lesen nicht auf: ein synthetisches
   .click() funktioniert, nur ein echter Mausklick trifft das falsche Element. Deshalb einmal global. */
[hidden] { display: none !important; }

/* Die Workbench: EINE Spalte — Filterzeile oben, Tabelle darunter (Saschas Entscheidung 2).
   Vorher ein Grid mit 232px Rail links. Der Unabhängige: „Sidebar = Was arbeite ich ab? Kopfzeile = Was
   schaue ich mir an? 40 px Höhe statt 232 px Breite — Breite ist euer knappes Gut." */
.wbbody { display: flex; flex-direction: column; min-height: 0; min-width: 0; }

/* FILTERZEILE — Dropdowns + aktive Filter in EINER Zeile. Sie klebt NICHT (sticky): sie steht über dem
   Canvas, nicht darin, und der Canvas scrollt für sich. Damit klebt im Canvas nichts über dem Spaltenkopf
   klebt oben am Canvas-Rand — genau der Anker-Vertrag von heute Morgen: der Offset kommt aus derselben
   Quelle wie das, woran er ankert. Eine Leiste, die manchmal da ist, darf kein konstanter Offset sein. */
.fbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1) var(--sp-2);
  min-height: var(--sp-8); padding: 5px var(--sp-3);
  background: var(--sunken); border-bottom: 1px solid var(--border); flex: none;
}
.fbar-lbl { font: 600 var(--fs-micro) var(--f-ui); letter-spacing: .06em; text-transform: uppercase; color: var(--text-faint); }
.fbar-sep { width: 1px; height: 16px; background: var(--border); margin: 0 2px; }
.fbar-reset { font: 400 var(--fs-micro) var(--f-ui); color: var(--accent); text-decoration: none; margin-left: auto; padding: 0 var(--sp-1); }
.fbar-reset:hover { text-decoration: underline; }

/* Ein Facetten-Dropdown. <details> statt JS-Popover: Auf/Zu gehört dem Browser. */
.fdrop { position: relative; }
.fdrop > summary {
  display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas);
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text); cursor: pointer; list-style: none; white-space: nowrap;
}
.fdrop > summary::-webkit-details-marker { display: none; }
.fdrop > summary:hover { border-color: var(--border-strong); }
.fdrop > summary svg { width: 9px; height: 9px; stroke-width: 2.4; opacity: .5; transition: transform .13s; }
.fdrop[open] > summary svg { transform: rotate(180deg); }
.fdrop[open] > summary { border-color: var(--border-strong); }
.fdrop[data-on] > summary { border-color: var(--accent); color: var(--accent); }
.fdrop-on { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; padding: 0 3px; border-radius: var(--r-full); background: var(--accent); color: #fff; font: 600 var(--fs-micro)/14px var(--f-ui); }

/* Das Panel bringt SEINEN EIGENEN Scroll mit — das ist der Punkt gegenüber der Rail: dort stapelten sich
   19 Kategorien in einer Spalte, und Sascha sah „nicht einmal alle Kategorien, selbst wenn ich keine
   einzelne aufklappe". Hier ist jede Facette für sich begrenzt. */
.fdrop-panel {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
  /* ⛔ `max-width` als Rückfalllinie (15.08.). Das Kippen nach rechts (`.fdrop.is-right`) wird in JS
     NACH dem Öffnen gemessen — greift also nicht, wenn das Panel in einem Container sitzt, dessen
     rechte Kante schon außerhalb des Fensters liegt. Gemessen bei 950 px: das Quellen-Panel endete bei
     x = 987, also 37 px außerhalb, in keinem Scroll-Container — die rechte Kante war nicht erreichbar.
     Eine feste Breite braucht eine Obergrenze, die das Fenster kennt; dann hält die Regel auch dort,
     wo die Messung zu spät kommt. */
  width: 340px; max-width: calc(100vw - 2 * var(--sp-3)); max-height: min(62vh, 520px); overflow-y: auto;
  padding: var(--sp-2) var(--sp-3) var(--sp-3);
  background: var(--canvas); border: 1px solid var(--border-strong); border-radius: var(--r-2);
  box-shadow: 0 8px 24px rgba(0,0,0,.16);
}
.fdrop--wide .fdrop-panel { width: 520px; }
.fdrop-panel::-webkit-scrollbar { width: var(--sp-2); }
.fdrop-panel::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-1); }
/* Nach rechts kippen, wenn das Panel sonst aus dem Fenster liefe. */
.fdrop.is-right .fdrop-panel { left: auto; right: 0; }

/* Zwischenüberschrift im Shop-Eigenschaften-Panel („Durchmesser", „Steigung" …). Sie bleibt: 19 Facetten
   in einem Panel brauchen eine Gliederung, sonst sind sie ein Chip-Teppich. */
.fattr { font: 600 var(--fs-micro)/20px var(--f-ui); color: var(--text-muted); padding-left: 2px; margin-top: 8px; }
.fattr:first-child { margin-top: 0; }
.fchips { display: flex; flex-wrap: wrap; gap: var(--sp-1); }

/* Der Chip ist ein Mini-Balkendiagramm der Verteilung, das nebenbei klickbar ist —
   deshalb kann eine Filterzeile ihn für Aufzählungen nicht ersetzen (sie kennt die
   Wertemenge nicht). Grenze: ≤ 30 Ausprägungen → Facette, sonst Filterzeile. */
/* white-space: nowrap ist hier kein Feinschliff, sondern Pflicht: die Höhe steht fest (22px), der Text darf
   also nicht umbrechen. Bei „Aufgabe: Marge zu niedrig ✕" tat er es über vier Zeilen und quoll aus dem
   Chip — bei kürzeren Filternamen fiel es nie auf (#359). Wer eine Höhe festnagelt, muss den Umbruch verbieten. */
.chip { display: inline-flex; align-items: center; gap: var(--sp-1); height: 22px; padding: 0 var(--sp-2); border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas); font: 400 var(--fs-micro) var(--f-ui); color: var(--text); cursor: pointer; white-space: nowrap; }
.chip:hover { border-color: var(--border-strong); }
.chip b { font-weight: 400; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.chip.is-on { background: var(--selected); border-color: var(--accent); color: var(--accent); font-weight: 500; }
.chip.is-on b { color: var(--accent); }
.chip.is-zero { color: var(--text-faint); pointer-events: none; }
.chip.is-zero b { color: var(--text-faint); }

/* „+ N weitere": NIE still kappen. Ein Filter, der hochkardinale Werte verschweigt, die das Detail sehr
   wohl kennt, macht die Facette unzuverlässig — und eine unzuverlässige Facette benutzt niemand zweimal. */
.fsub { margin-top: 5px; }
.fsub > summary { font: 400 var(--fs-micro) var(--f-ui); color: var(--accent); cursor: pointer; list-style: none; padding: 2px 0; }
.fsub > summary::-webkit-details-marker { display: none; }
.fsub > summary:hover { text-decoration: underline; }
.fsub .fchips { margin-top: 5px; }

/* ── ARCHETYP A: TABELLE ───────────────────────────────────────────────────── */
.grid {
  /* separate ist Pflicht: collapse killt Borders auf sticky th. */
  border-collapse: separate; border-spacing: 0;
  table-layout: fixed;
  width: max-content; min-width: 100%;
  font-variant-numeric: tabular-nums;
}
.grid th {
 position: sticky; z-index: 3; height: var(--sp-8); padding: 0 var(--sp-2);
 background: var(--table-head);
 color: var(--text-muted); text-align: left;
 box-shadow: inset 0 -1px 0 var(--border-strong);
 white-space: nowrap; cursor: pointer; user-select: none;
}
.grid th:hover { color: var(--text); }
.grid th.num { text-align: right; font-variant-numeric: tabular-nums; }
.grid th.is-static { cursor: default; }
.grid th.is-static:hover { color: var(--text-muted); }
/* Die Kopf-Offsets kommen aus EINER Quelle: der Höhe dessen, was über ihnen IM CANVAS klebt.
   Seit die Filter in die Kopfzeile über dem Canvas gewandert sind, klebt dort nichts mehr — der
   Spaltenkopf sitzt am oberen Canvas-Rand, der Gruppenkopf genau eine Kopfhöhe darunter. Das ist der
   Grund, warum die frühere `--h-stuck`-Rechnung entfallen konnte: sie musste einen BEDINGTEN Aufbau
   ausmessen (die Filterleiste war mal da, mal nicht), und ein konstanter Offset ist bei einem bedingten
   Aufbau garantiert in einem der Zustände falsch. Vorher stand hier 94 = 32+32+30 für DREI Ebenen, von
   denen die dritte nie gebaut wurde — der erste Gruppenkopf landete 62px tief in seinen eigenen Zeilen.
   ⚠️ Käme je wieder etwas Klebendes IN den Canvas, muss dieser Offset wieder aus dessen Höhe kommen
   und darf keine Konstante sein. Die Regel gilt weiter, nur ihr Anlass ist entfallen. */
.grid thead tr.grid-head th { top: 0; }
/* SORTIER-INDIKATOR — EINE Quelle für JEDE sortierbare Spalte (#373).
   Der Pfeil entsteht hier, nicht im Template. Gemessen trugen 34 von 90 `th[data-sort]` ein
   `<span class="sort-arrow">`; die anderen 56 zeigten auch als AKTIV sortierte Spalte nichts —
   „wonach ist sortiert?" war dort unbeantwortbar. `::after` bindet an genau das Attribut, an dem
   table.js den Klick festmacht: Sortierbarkeit und ihr Zeichen können nicht mehr auseinanderlaufen.
   Ruhend gedimmt = „hier lässt sich sortieren", aktiv voll + Akzent = „hiernach IST sortiert".
   Kein `.grid`-Präfix: die Regel gilt für alle table.js-Tabellen, auch die ungestylten (Archiv,
   Änderungslog, Geprüft). `maxlaenge` hat eine eigene Sortierung an `data-mlsort` — nicht betroffen. */
th[data-sort]::after {
  content: "▾"; display: inline-block; margin-left: 3px;
  font-size: var(--fs-icon); opacity: .25; transition: opacity .12s, color .12s;
}
th[data-sort]:hover::after { opacity: .55; }
th[data-sort].sorted-asc::after, th[data-sort].sorted-desc::after { opacity: 1; color: var(--accent); }
th[data-sort].sorted-asc::after { transform: rotate(180deg); }

.grid td {
  height: var(--row-obj); padding: 0 var(--sp-2);
  font: 400 var(--fs-dense)/16px var(--f-ui);
  border-bottom: 1px solid var(--row-line); background: var(--canvas);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* `font-variant-numeric` MUSS hier stehen, obwohl `.num` (Zeile 165) es global setzt: `.grid td` weiter
   oben benutzt die **font-Shorthand** — und eine Shorthand setzt alle nicht genannten Teile zurück, also
   auch font-variant-numeric auf `normal`. Mit Spezifität 0-1-1 schlägt sie `.num` (0-1-0). Ergebnis war:
   **0 von 220 gemessenen Zellen** hatten tabellarische Ziffern, Regel 6 war auf JEDER Tabelle gebrochen,
   seit dem Neubau am 16.07. Beträge in einer Spalte fluchteten nicht. Gefunden über den QA-Befund zur
   Massenaktions-Vorschau (17.07.) — dort fiel es auf, weil dort Preise verglichen werden. */
.grid td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* ⛔ HOVER GEHOERT JEDER DATENZEILE, NICHT NUR `.wg-row` (04.08.).
   Der Kommentar drei Zeilen tiefer sagt seit dem Neubau, Zebra sei entbehrlich, WEIL es :hover gibt.
   Das stimmte nur fuer die Arbeitsflaechen-Tabellen: die Regel hing an `.wg-row`, und diese Klasse
   vergibt ausschliesslich `_worktable_macros.html`. Jede andere Tabelle im System — Protokoll,
   Aenderungs-Archiv, Geprueft, Nutzung, Import-Spalten, die sechs Markt-Tabellen, die Rollen-Matrix —
   hatte damit WEDER Zebra NOCH Hover. In einer 32-px-Zeile ueber zwoelf Spalten ist das genau die
   Sorte Tabelle, in der man beim Lesen die Zeile verliert. Der Begruendungssatz war richtig, seine
   Voraussetzung war auf den meisten Seiten nicht erfuellt.
   `:not(.wg-head)` haelt den Gruppenkopf heraus (er hat seine eigene Flaeche und ist ein Schalter,
   keine Datenzeile); die zwei Hinweis-Zeilen ebenso — eine Fussnote leuchtet nicht auf.
   ⛔ Und die vier frueheren Sonderregeln fuer die klebenden Spalten (`td.col-cb/col-gut/col-ident/
   col-act`) sind ERSATZLOS entfallen: sie setzten denselben Wert wie die allgemeine Regel und
   gewannen nur gegen `.grid td`, was die allgemeine Regel ohnehin tut. Vier Kopien einer Aussage. */
.grid tbody tr:not(.wg-head):not(.is-sel):hover > td:not(.cap-note):not(.empty-state) { background: var(--hover); }
.grid tbody tr.is-sel > td { background: var(--selected); }
/* Zebra ist aus: Hairlines + tabellarische Ziffern lösen das Zeilen-Tracking besser.
   Zebra ist ein Krückstock aus einer Zeit ohne :hover. */

/* TASTATUR-CURSOR (Phase 1.2, 16.08.) — die Zeile, auf der die Tastatur gerade steht.
   ⛔ Bewusst ein BALKEN, kein Hintergrund. `is-sel` färbt die Fläche bereits, und beide treffen
   regelmäßig dieselbe Zeile: wer mit ↑↓ durch die Liste geht und mit der Leertaste auswählt, hat
   Cursor und Auswahl übereinander. Zwei Aussagen brauchen zwei Mittel — Fläche heißt „ist
   ausgewählt", Balken heißt „hier stehe ich". Mit derselben Farbe wären sie beim Durchgehen nicht
   auseinanderzuhalten, und die Auswahl ist die folgenreichere der beiden.
   Der Balken sitzt auf der ersten Zelle, weil die klebt (col-cb) — er bleibt damit beim
   Horizontal-Scrollen sichtbar, anders als ein Rahmen um die ganze Zeile.
   ⛔ UND BEIDE ZEILENARTEN BENUTZEN box-shadow, nicht eine davon `outline`. Hier stand für die Karte
   `outline: 2px solid` — und die Regel eine Zeile darunter (`:focus { outline: none }`, gegen den
   doppelten Rahmen) nahm ihn sofort wieder weg. In der Tabelle fiel das nicht auf, weil der Balken
   dort ein box-shadow ist; auf der Karte war der Cursor damit UNSICHTBAR, während Fokus, Auswahl und
   Massenleiste einwandfrei liefen. Gefunden beim Durchklicken, nicht im Test: eine Karte ohne Ring
   sieht aus wie eine Karte, auf der man eben nicht steht. Ein Mittel für eine Sache — dann kann die
   Fokus-Regel es nicht für die eine Hälfte aushebeln und für die andere nicht.
   Die Karte trägt `.wg-row` mit, damit die drei Klassen `.pcard:hover` schlagen (gleiche Spezifität,
   aber der Hover steht weiter unten in der Datei und gewänne sonst den Ring weg). */
.grid tbody tr.wg-row.is-cursor > td:first-child { box-shadow: inset 3px 0 0 var(--focus); }
.wg-row.pcard.is-cursor { box-shadow: 0 0 0 2px var(--focus); }
/* Der Balken IST der Fokus-Ausdruck. Der Browser-Ring zusätzlich wäre ein zweiter Rahmen um
   dieselbe Zeile — und auf einem <tr> zeichnet er in vielen Engines ohnehin nur die halbe Kante. */
.wg-row.is-cursor:focus { outline: none; }

/* Legende (`?`). Bewusst mittig und modal-nah statt an einer Zeile: sie beschreibt die ganze Liste,
   nicht die Zeile unter dem Cursor. */
.kb-help { left: 50%; top: 50%; transform: translate(-50%, -50%); width: 300px; padding: var(--sp-3) 0; }
.kb-help .kb-help-h { padding: 0 14px var(--sp-2); font-size: var(--fs-micro); font-weight: 600;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; }
.kb-help .kb-help-r { display: flex; align-items: center; gap: var(--sp-2); padding: 3px 14px; font-size: var(--fs-dense); }
.kb-help .kb-help-r kbd { flex: 0 0 74px; font: 500 var(--fs-micro) var(--f-ui); color: var(--text-muted);
  border: 1px solid var(--border); border-radius: var(--r-1); padding: 1px 4px; background: var(--sunken); text-align: center; }

/* Eingefrorene Kanten. Der Hintergrund MUSS auf td — `inherit` von tr greift nicht.
   Links bleiben Auswahl, Gerüst und IDENTITÄT stehen: beim Scrollen nach rechts ist eine Zahl ohne ihre
   Zeile wertlos. Rechts bleibt die Aktion — man entscheidet dort, wo man gerade liest. */
.grid .col-cb    { position: sticky; left: 0; z-index: 1; width: var(--sp-6); padding: 0 0 0 var(--sp-2); }
.grid .col-gut   { position: sticky; left: var(--sp-6); z-index: 1; width: var(--sp-8); padding: 0; }
/* ⛔ Der Anschlag der Identitätsspalte ist so breit wie das, was VOR ihr klebt — und das entscheidet die
   Tabelle selbst, nicht eine Ausnahmeliste. Fest eingetragene 56 px (Auswahl + Rinne) waren dreimal
   dieselbe Verletzung: eine Tabelle ohne Auswahlspalte bekam ihre erste Spalte 56 px nach rechts
   geschoben, wo sie mit ihrem deckenden Hintergrund die Nachbarspalte übermalte — die Nachbarzelle sah
   dann vorne abgeschnitten aus („nur Preisliste" → „liste", „Ergebnis" → „NIS"). Zweimal wurde das je
   Tabelle nachgebessert (#wbtable 28.07., #excl-table 20.07.), beim dritten Mal (Markt-Quellen 31.07.)
   stand dieselbe Diagnose zum dritten Mal da. Seitdem liest die Regel die Tabellenform ab: nur wo eine
   Auswahlspalte wirklich vorhanden ist, gibt es den Anschlag dafür. */
.grid .col-ident { position: sticky; left: 0; z-index: 1; width: 250px; box-shadow: 1px 0 0 var(--border); }
/* ⛔ 14.08.: die Regel liest jetzt BEIDE klebenden Vorspalten ab, nicht nur die Auswahl. Anlass ist der
   vierte Fall derselben Sorte — die Ausschluss-Tabelle hat seit heute eine Auswahlspalte, aber KEINE
   Rinne. Mit dem festen 56er-Anschlag (Auswahl + Rinne) wäre beim Scrollen eine Lücke zwischen Haken und
   Produktname entstanden, durch die der Inhalt darunter durchgelaufen wäre. Der Kommentar darüber sagt,
   dass beim dritten Mal umzubauen ist statt nachzubessern; beim vierten gilt dasselbe eine Ebene tiefer.
   Jede Bedingung nennt genau die Spalten, deren Breite sie summiert. */
.grid:has(.col-cb) .col-ident               { left: var(--sp-6); }                    /* nur Auswahl */
.grid:has(.col-cb):has(.col-gut) .col-ident { left: calc(var(--sp-6) + var(--sp-8)); } /* Auswahl + Rinne */
/* Die angeheftete Aktionsspalte liegt ÜBER dem Inhalt, der unter ihr durchscrollt. Mit nur einer
   Haarlinie las sich das nicht als Überlagerung, sondern als Fehler: vom Kopf „Führende Variante" blieb
   sichtbar „FÜ" stehen, und es sah aus, als überdecke eine Spalte ihre Nachbarin kaputt. Ein echter
   Schlagschatten sagt, was gilt — hier endet die Fläche, darunter geht es weiter (nach rechts scrollen).
   ⛔ #368: Das Kleben gilt erst ab 1240 px. Darunter kostet es mehr, als es bringt — im eigenen CSS
   steht an #wbtable und #excl-table ausdrücklich das Gegenteil („bei 1280 px verdeckte sie Marge, EK
   und Abstand vollständig"), und auf einem 1366er-Laptop ist die Arbeitsfläche derselbe Fall. Das
   Kleben war mit #359 selbst gewünscht; der Widerspruch löst sich nicht durch eine Seite, sondern
   durch die Breite. 1240 px ist bewusst der BESTEHENDE Breakpoint der Seite (Wiki-Zweispalter) statt
   einer neuen Stufe daneben — die Leiter 560/780/900/1100/1240 bleibt geschlossen.
   Der Schlagschatten sitzt in derselben Query: ohne Überlagerung behauptet er eine Kante, die es
   nicht gibt. Breite und Innenabstand gelten immer — sie beschreiben die Spalte, nicht ihr Kleben. */
.grid .col-act   { width: 168px; padding: 0 var(--sp-2); }
@media (min-width: 1240px) {
  .grid .col-act { position: sticky; right: 0; z-index: 1;
                   box-shadow: -1px 0 0 var(--border), -8px 0 12px -6px rgba(0,0,0,.16); }
}

/* Spaltengeometrie — EINE Quelle, und zwar hier statt 16x inline im Template.
   Klappbare Spalten und <colgroup> schließen sich aus: <col> bindet an den Spalten-INDEX, ein per
   display:none entfallenes th verschiebt alle folgenden. Bei table-layout:fixed nimmt der Browser die
   Breite aus der ersten Zeile — ein ausgeblendetes th ist dort einfach nicht vorhanden, und nichts rutscht. */
.grid [data-c="status"]   { width: 104px; }
.grid [data-c="laenge"]   { width: 78px; }
.grid [data-c="refek"]    { width: 104px; }
.grid [data-c="herk"]     { width: 86px; }
.grid [data-c="wawiek"]   { width: 94px; }
.grid [data-c="listek"]   { width: 94px; }
.grid [data-c="dek"]      { width: 92px; }
.grid [data-c="vkshop"]   { width: 100px; }
.grid [data-c="vkwawi"]   { width: 94px; }
.grid [data-c="dvk"]      { width: 88px; }
.grid [data-c="dbabs"]    { width: 96px; }
.grid [data-c="dbpct"]    { width: 74px; }
.grid [data-c="variante"] { width: 140px; }
.grid [data-c="man"]      { width: 118px; }
.grid [data-c="staged"]   { width: 88px; }

/* Klapp-Chevron auf der Kopfspalte einer Detail-Gruppe (Referenz-EK / VK Shop / Führende Variante).
   Er sitzt IM sortierbaren th — deshalb fängt table.js den Klick vor dem Sort ab. */
.colgrp-head { position: sticky; }
.colgrp-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; margin-right: 2px; padding: 0; vertical-align: -2px;
  border: 0; background: none; color: var(--text-muted); cursor: pointer; opacity: .55;
}
.colgrp-btn svg { width: 9px; height: 9px; stroke-width: 3; transition: transform .12s ease; }
.colgrp-btn:hover { opacity: 1; color: var(--text); }
.colgrp-btn.is-open svg { transform: rotate(90deg); }
.colgrp-btn.is-open { opacity: 1; }

/* ── TREE-GRID: die WaWi-Varianten als Zeilen DERSELBEN Tabelle ─────────────────────────────────────
   Der Höhenunterschied (32 statt 44) und der eingesenkte Grund signalisieren die Ebene ohne ein Wort —
   dieselbe Absicht wie beim alten Expander, nur ohne dessen zweiten Spaltenkopf und ohne width:auto.
   Die Einrückung sitzt NUR in der Identitäts-Zelle: rückte man die ganze Zeile ein, bräche genau die
   Spaltenflucht, deren Fehlen Sascha „verhoben" genannt hat. */
tr.wg-row.is-child > td { height: var(--row-evt); background: var(--sunken); }
/* Die eigene Hover-Zeile für Kind-Zeilen ist entfallen — die allgemeine Hover-Regel oben deckt sie mit. */
tr.wg-row.is-child .ident-sku { padding-left: var(--sp-4); color: var(--text-muted); }
tr.wg-row.is-child.is-locked .ident-sku { text-decoration: line-through; }
tr.wg-row.is-child .col-ident::before {
  content: ""; position: absolute; left: 8px; top: 0; bottom: 0;
  border-left: 1px solid var(--border); /* Baumkante: sagt „gehört zu der Zeile darüber" */
}
tr.wg-row.is-child .col-ident { position: sticky; }
tr.wg-row.is-child .col-gut { text-align: center; }
tr.wg-row.is-child input[type="radio"] { accent-color: var(--primary); margin: 0; cursor: pointer; }
.gut-lead { color: var(--primary); font-size: var(--fs-micro); }
.grid th.col-cb, .grid th.col-gut, .grid th.col-ident, .grid th.col-act { z-index: 4; }
.grid .col-cb input { margin: 0; cursor: pointer; accent-color: var(--primary); }
.mono { font-family: var(--f-mono); font-size: var(--fs-micro); }

/* Marker, die eine Zahl qualifizieren (Regel 9) — sie kleben an der Zahl, statt eine ganze Zelle zu färben.
   Bernstein ist bewusst NICHT das Ampel-Gelb: die Ampel bewertet die Marge, ein Marker meldet einen Zweifel
   an der Datenlage. Zwei verschiedene Aussagen dürfen nicht dieselbe Farbe tragen. */
.mark-warn { color: var(--warn-mark); font-size: var(--fs-micro); cursor: help; }
.mark-warn-t { color: var(--text); }
/* „Zuordnung geprüft" (#249). Grün ist hier KEINE Ampel-Aussage über den Preis, sondern über die HERKUNFT
   der Aussage — ein bestätigtes Fundament. Deshalb bewusst klein und neben dem Wettbewerber-Namen, nicht
   in der Zahlenspalte: Entscheidung D (Farbe nie auf Zahlen) bleibt unangetastet. */
.mark-ok { color: var(--good); font-size: var(--fs-micro); cursor: help; }
.comp-verdict { display: block; margin-top: 2px; }
/* Δ EK: eine Einsparchance ist eine gute Nachricht — aber sie steht in einer Zahlenspalte, deshalb
   trägt nur das Vorzeichen die Bedeutung, nicht die Ziffer (Regel 2). */
.delta-good { color: var(--badge-ok-fg); }

/* Inline-VK: die Fast-Lane. Enter IST „setzen" — ein eigener Knopf daneben wäre ein zweiter Weg für
   dieselbe Sache und damit die Frage „was ist der Unterschied?". */
.vk-inline-in { width: 72px; height: 24px; padding: 0 5px; border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: 400 var(--fs-dense) var(--f-ui); font-variant-numeric: tabular-nums; }
.vk-inline-in:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 16%, transparent); }
.act-msg { font-size: var(--fs-micro); white-space: nowrap; }

/* GUTTER — die 1:n-Transparenz (Shop-Produkt → n WaWi-Varianten) ist GERÜST, nicht
   Zelle: ein Spaltensatz kann eine Zelle abschalten, das Gerüst nicht. Damit ist sie
   in jeder Ansicht garantiert. */
.gut-btn { display: flex; align-items: center; justify-content: center; gap: 1px; width: var(--sp-8); height: var(--row-obj); border: 0; background: none; cursor: pointer; color: var(--text-muted); font: 500 var(--fs-micro) var(--f-ui); font-variant-numeric: tabular-nums; }
.gut-btn:hover { color: var(--accent); }
.gut-btn svg { width: 9px; height: 9px; stroke: currentColor; fill: none; stroke-width: 2.4; transition: transform .13s; }
tr.wg-row.is-open .gut-btn svg { transform: rotate(90deg); }
/* Die 1 steht auch da: ein leerer Gutter hieße „ich weiß es nicht" — die 1 heißt
   „ich habe nachgesehen, es ist eins". Das ist der Unterschied zu einer Auslassung. */
.gut-one { display: grid; place-items: center; width: var(--sp-8); height: var(--row-obj); color: var(--text-faint); font: 400 var(--fs-micro) var(--f-ui); font-variant-numeric: tabular-nums; }
/* Die 0 ist der wertvollste Wert der Spalte: hinter dieser Zeile steht KEINE WaWi-Variante. Sie sagt
   dasselbe wie die Aufgabe „nicht in WaWi" daneben — und darf deshalb weder alarmieren (das tut die
   Aufgabe) noch verschwinden (dann wäre sie ein Loch, das man für „egal" hält). */
.gut-zero { display: grid; place-items: center; width: var(--sp-8); height: var(--row-obj); color: var(--warn-mark); font: 600 var(--fs-micro) var(--f-ui); font-variant-numeric: tabular-nums; cursor: help; }

/* IDENTITÄTSZELLE — die EINZIGE zweizeilige Zelle (Regel 11). Name und SKU sind EINE
   Identität; in zwei Spalten liegen sie ~240px auseinander und das Auge muss sie je
   Zeile neu zusammenbinden. Alle anderen Zellen bleiben einwertig — nur die kann man
   von oben nach unten vergleichen, und das ist der Zweck einer Tabelle. */
.ident { line-height: 15px; padding-top: 6px; }
.ident-name { font-size: var(--fs-dense); font-weight: 500; color: var(--text); display: block; overflow: hidden; text-overflow: ellipsis; }
.ident-sku { font-size: var(--fs-micro); color: var(--accent); font-family: var(--f-mono); cursor: pointer; }
.ident-sku:hover { text-decoration: underline; }
.ident-meta { font-size: var(--fs-micro); color: var(--text-muted); }

/* Aktions-Spalte: genau EINE Paket-Aktion. Das Arbeitspaket macht die Zeilenmenge
   homogen — deshalb reicht eine, wo vorher zwölf Badges standen (Regel 10). */
.row-acts { display: flex; align-items: center; gap: 3px; }
/* Die Paket-Aktion ist IMMER sichtbar — sie ist der Grund, warum die Zeile in der Liste steht. Nur die
   Nebenwege (⋯, Detail) erscheinen auf Hover: 25 Zeilen à drei Dauer-Icons wären wieder die Badge-Wand,
   die wir gerade abgerissen haben. */
.row-acts .icon-btn--row { opacity: 0; transition: opacity .1s; }
tr.wg-row:hover .row-acts .icon-btn--row,
tr.wg-row.is-sel .row-acts .icon-btn--row,
.row-acts .icon-btn--row:focus-visible { opacity: 1; }
.next-act { font-size: var(--fs-micro); color: var(--accent); cursor: pointer; }
.next-act:hover { text-decoration: underline; }

/* WARENGRUPPEN-AKKORDION (Entscheidung A): Warengruppe bleibt die Gliederung — sie ist
   die Steuerungsebene (Konfigurator, Cockpit-Entscheidungen, Ausschlüsse hängen daran).
   Sticky, damit der Kontext beim Scrollen nie verloren geht. */
tr.wg-head th { position: sticky; top: var(--sp-8); z-index: 2; height: var(--h-tabs); padding: 0; background: var(--group-head); box-shadow: inset 0 -1px 0 var(--border); text-align: left; text-transform: none; letter-spacing: 0; cursor: pointer; }
/* Sticky gehört auf den INHALT, nicht auf die colspan-Zelle — sonst scrollt der
   Gruppenkopf waagerecht aus dem Bild. */
/* ⛔ FESTE TEXTSPALTEN, damit die zwei Gruppen-Aktionen rechts nicht wandern (#485, 03.08.).
   Vorher war das ein `inline-flex`: Name, Mengen und „übliche Marge" sind unterschiedlich lang, also
   begann die Knopfspalte in jeder Gruppenzeile woanders — gemessen schwankte ihre linke Kante über
   dreizehn Zeilen um 302 px. Bei zwei Aktionen, die eine ganze Warengruppe aus dem Shop nehmen, trifft
   man beim schnellen Durchgehen sonst den falschen. Jetzt liegen die drei Textfelder in festen Spuren
   (leer bleibt leer — die Spur bleibt), und die Knöpfe stehen in jeder Zeile auf derselben Kante.
   Zu langer Text wird gekürzt statt zu schieben; die volle Fassung steht im `title`. */
.wg-inner { position: sticky; left: 0; display: grid; grid-template-columns: 10px 17rem 14rem 14rem max-content;
  align-items: center; gap: 0 var(--sp-2); height: var(--h-tabs); padding: 0 var(--sp-3); }
.wg-caret { width: 10px; height: 10px; stroke: currentColor; fill: none; stroke-width: 2.4; transition: transform .13s; flex: none; }
.wg-title, .wg-inner .wg-n { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Ohne Tag-Praefix: derselbe Gruppenkopf traegt <tr> (Tabelle) und <header> (Karten). */
.wg-head:not(.collapsed) .wg-caret { transform: rotate(90deg); }
.wg-title { font: 600 var(--fs-dense) var(--f-ui); text-transform: uppercase; letter-spacing: .03em; color: var(--text); }
.wg-n { font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* Die WG-Aktionen (Löschliste/Ausschließen) treffen die GANZE Warengruppe — sie erscheinen erst auf Hover.
   Eine Aktion mit dieser Reichweite darf nicht dauerhaft neben dem Namen stehen wie eine Beschriftung. */
/* Die zwei Gruppen-Aktionen liegen IN der Zeile, die man zum Aufklappen anklickt — und beide treffen die
   ganze Warengruppe (bei „Trapezgewindemutter" sind das 1.670 Artikel). Randlos sahen sie aus wie Text
   und damit wie Teil der Klickfläche: „da will ich nicht danebengreifen". Beide fragen zwar vorher nach,
   aber ein Ziel muss als Ziel erkennbar sein, bevor man es trifft — deshalb eigener Rahmen, eigener Grund
   und ein Abstand, der sie von der Überschrift trennt. */
/* ⛔ DAUERHAFT SICHTBAR (29.07.). Sie standen auf `opacity: 0` und erschienen erst beim Überfahren —
   „wer die Rechte hat, weiß nicht, dass es diese Knöpfe gibt, bis er zufällig mit der Maus dranstößt.
   Die wichtigste Massenaktion der Seite darf sich nicht verstecken."
   Eine Aktion, die man nicht findet, ist keine Aktion. Zurückgenommen wird stattdessen ihre LAUTSTÄRKE:
   sie stehen leiser da (`--text-muted`, dünner Rahmen) und werden beim Überfahren voll kontrastiert —
   also sichtbar genug, um sie zu kennen, und zurückhaltend genug, um die Überschrift nicht zu
   überstimmen. Das war die eigentliche Absicht hinter dem `opacity: 0`. */
.wg-acts { display: flex; align-items: center; gap: var(--sp-2); margin-left: var(--sp-4);
           padding-left: var(--sp-3); border-left: 1px solid var(--border-strong);
           color: var(--text-muted); transition: color .1s; }
.wg-head:hover .wg-acts, .wg-acts:focus-within { color: var(--text); }
.wg-acts .btn--ghost { border-color: var(--border-strong); background: var(--canvas); }
.wg-acts .btn--ghost:hover { border-color: var(--bad); color: var(--bad); background: var(--canvas); }
.wg-acts form { display: inline-flex; margin: 0; }

/* ══ REIFEGRAD-BALKEN (Phase 1.2, 15.08.) ════════════════════════════════════════════════════════════
   Vier Bausteine, gelesen aus `worktable_base.reife_*`. Ein Makro (`reife` in _worktable_macros.html)
   bedient Tabellenzelle und Karte — hier stehen nur die zwei Größen.

   ⛔ KEIN GRÜN/GELB/ROT. Entscheidung D: die einzige Ampel im System ist die Marge. Ein zweiter farbiger
      Fortschritt daneben macht aus einer Aussage zwei konkurrierende Urteile — und dann weiß man bei
      keiner mehr, was sie bedeutet. Der Balken spricht über LÄNGE und die Zahl daneben, nicht über Farbe.
      `--chart-1` ist die neutrale Diagrammfarbe, dieselbe wie bei `.segbar-seg`.
   ⛔ Die Breite kommt als `--w` aus dem Template (Datenpunkt), nicht als `width:` — gleiche Figur wie
      `.segbar-seg`, damit das Aussehen vollständig im Stylesheet bleibt. */
.reife { display: inline-flex; align-items: center; gap: var(--sp-1); width: 100%; }
.reife-track { position: relative; display: flex; flex: 1; min-width: 34px; height: 6px;
  border-radius: var(--r-full); background: var(--sunken); overflow: hidden; }
.reife-fill { width: var(--w); background: var(--chart-1); }
/* „Nie geprüft" ist weder erfüllt noch offen — es bekommt deshalb weder die Füllfarbe noch den leeren
   Grund, sondern eine eigene, erkennbar unentschiedene Schraffur. */
.reife-unk { width: var(--w); background: repeating-linear-gradient(45deg,
  var(--border-strong) 0 2px, transparent 2px 4px); }
.reife-n { font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Karten-Größe: mehr Platz, also darf der Balken kräftiger sein und über die volle Kartenbreite laufen. */
.reife--breit .reife-track { height: 8px; }
.pcard-reife { padding-top: var(--sp-1); }
/* Die Wegweiser stehen UNTER dem Balken, nicht daneben: der Balken ist eine Messung, die Chips sind
   Ausgaenge. Nebeneinander laesen sie sich als Beschriftung des Balkens. */
.reife-wege { display: flex; flex-wrap: wrap; gap: 4px; margin-top: var(--sp-1); }

/* ══ PRODUKTKARTEN (Phase 1.1, 15.08.) ═══════════════════════════════════════════════════════════════
   Die zweite Ansicht auf dieselbe Zeilenmenge. Sie teilt sich mit der Tabelle die Gruppen-Gliederung
   (.wg-group/.wg-head/.wg-row) — deshalb steht sie hier und nicht in einem eigenen Abschnitt.

   ⛔ DER GRUPPENKOPF IST EINE RASTERZEILE UEBER DIE VOLLE BREITE (`grid-column: 1/-1`), kein Wrapper um
      die Karten. Grund ist die Mechanik, nicht die Optik: `loadGroup` haengt nachgeladene Karten per
      `insertAdjacentHTML('afterend')` DIREKT hinter den Kopf. Steckten die Karten in einem eigenen
      Container, landeten die nachgeladenen daneben statt darin — und zwar nur beim Aufklappen, also
      genau da, wo es niemand testet. */
.cards { display: block; }
/* ⛔ KEIN `align-items: start`. Damit bekommt jede Karte ihre Eigenhöhe — und weil ein zweizeiliger Name
   oder eine zweite Blocker-Zeile 22 px kostet, steht dann in einer Reihe jede Fußleiste woanders
   (gemessen 383 px neben 405 px). Die Preiszeile ist der Grund, warum man das Raster überhaupt liest;
   sie muss über die Reihe hinweg auf einer Linie liegen. Gleiche Höhe ist hier keine Kosmetik. */
.cards .wg-group { display: grid; grid-template-columns: repeat(auto-fill, minmax(224px, 1fr));
  gap: var(--sp-3); padding: 0 0 var(--sp-5); }
/* ⛔ `top: 0`, NICHT `var(--sp-8)` wie in der Tabelle. Dort klebt der Gruppenkopf unter dem 32 px hohen
   Spaltenkopf — den gibt es hier nicht. Mit dem geerbten Wert klebte er bei ungescrollter Seite bereits
   20 px zu tief und deckte die Oberkante der ersten Kartenreihe zu (gemessen: Kopf-Unterkante 358,
   Karten-Oberkante 338). Ein Kopf, der etwas verdeckt, bevor überhaupt gescrollt wurde, ist kein
   Sticky-Verhalten, sondern ein falscher Bezugspunkt. */
.cards .wg-head { grid-column: 1 / -1; position: sticky; top: 0; z-index: 2;
  height: var(--h-tabs); background: var(--group-head); box-shadow: inset 0 -1px 0 var(--border); cursor: pointer; }
/* Der Kopf braucht die festen Textspuren der Tabelle nicht — hier schiebt nichts eine Nachbarspalte.
   Die zwei Warengruppen-Aktionen bleiben trotzdem rechts, damit sie an derselben Stelle stehen wie dort. */
.cards .wg-inner { position: static; display: flex; align-items: center; gap: var(--sp-2); }
.cards .wg-acts { margin-left: auto; }
/* ⛔ MUSS SEIN: `[hidden]` setzt `display:none`, aber `.pcard` setzt `display:flex` — bei gleicher
   Spezifitaet gewinnt die spaetere Regel. Ohne diese Zeile bliebe eine zugeklappte Warengruppe sichtbar,
   und „Alle Warengruppen zuklappen" waere wirkungslos. */
.cards .wg-row[hidden] { display: none; }
.cards .empty-state { height: auto; padding: var(--sp-8) 0; }

.pcard { display: flex; flex-direction: column; background: var(--canvas); border: 1px solid var(--border);
  border-radius: var(--r-2); overflow: hidden; transition: border-color .1s, box-shadow .1s; }
.pcard:hover { border-color: var(--border-strong); box-shadow: 0 1px 3px rgb(0 0 0 / .07); }
.pcard.is-sel { border-color: var(--accent); background: var(--selected); }
/* Das Bild bekommt ein FESTES Seitenverhaeltnis. Produktfotos aus dem Shop sind unterschiedlich hoch;
   ohne feste Box springt das Raster beim Nachladen (`loading=lazy`) genau dann, wenn man liest. */
.pcard-img { position: relative; aspect-ratio: 4 / 3; background: var(--sunken);
  border-bottom: 1px solid var(--border); }
/* `contain`, nicht `cover`: bei einer Gewindestange schneidet `cover` die Enden ab — und die Enden sind
   das, woran man sie erkennt. Lieber Rand als ein Bild, das etwas anderes behauptet. */
.pcard-img img { width: 100%; height: 100%; object-fit: contain; padding: var(--sp-2); }
.pcard-noimg { display: flex; align-items: center; justify-content: center; height: 100%;
  padding: var(--sp-3); text-align: center; font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.pcard-mark { position: absolute; left: var(--sp-2); bottom: var(--sp-2); padding: 1px 5px;
  border-radius: var(--r-1); background: var(--canvas); border: 1px solid var(--border);
  font: 600 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
/* „Gruppenbild" ist die haeufigste und die folgenreichste Herkunft (2.494 von 3.977) — sie bekommt
   deshalb die Warnfarbe, nicht nur ein anderes Wort. */
.pcard-mark--warn { border-color: color-mix(in srgb, var(--warn) 50%, var(--border)); color: var(--warn); }
/* Der Auswahl-Haken sitzt AUF dem Bild und nicht ueber dem Text: in der Tabelle ist er die erste Spalte,
   also die obere linke Ecke der Zeile — auf der Karte ist das dieselbe Stelle. */
.pcard-pick { position: absolute; top: var(--sp-2); left: var(--sp-2); display: flex; padding: 3px;
  border-radius: var(--r-1); background: var(--canvas); border: 1px solid var(--border); cursor: pointer; }
.pcard-body { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-2) var(--sp-3); flex: 1; }
/* ⛔ HIER MUSS DER BADGE UMBRECHEN DÜRFEN. `.badge` steht global auf `white-space: nowrap` — in einer
   Tabellenzelle richtig, in einer 286 px breiten Karte nicht: der Ausschlussgrund „Warengruppe nicht im
   Shop geführt — Sortimentsentscheidung" stand 61 px über den Kartenrand hinaus, gemessen an 40 Karten
   bei 950 px Fensterbreite. Umbrechen statt kürzen, weil dieser Text die Antwort auf „warum ist das
   draußen?" IST — eine Ellipse würde genau die Hälfte davon schlucken. */
.pcard-top { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.pcard-top .badge { white-space: normal; text-align: left; line-height: 1.35; }
/* Zwei Zeilen Name, dann Ellipse: ein dreizeiliger Name schoebe den Preis aus der Kartenflucht, und die
   Preise nebeneinander lesen zu koennen ist der halbe Zweck des Rasters. Voller Name im `title`. */
.pcard-name { font: 600 var(--fs-dense) var(--f-ui); color: var(--text); text-decoration: none;
  display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.pcard-name:hover { color: var(--accent); text-decoration: underline; }
/* Umbrechen ja, aber nicht MITTEN in der Längenangabe: bei einer langen SKU (S-M-10x1.5-LH-4.6-ZN-1000-A)
   schrumpfte das flex-Kind „· 1000 mm" so weit, dass „1000" und „mm" untereinander standen. Eine Zahl und
   ihre Einheit gehören auf eine Zeile — getrennt liest man erst die Zahl und sucht dann die Einheit. */
.pcard-sku { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-1); font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.pcard-sku .mono { overflow-wrap: anywhere; }
.pcard-sku .dim { white-space: nowrap; }
/* Der Preis-Dreisatz in EINER Zeile mit Tabellenziffern — so fluchten die Zahlen wenigstens innerhalb
   einer Rasterspalte, auch wenn es keine Tabellenspalte mehr ist. */
.pcard-preis { display: flex; align-items: baseline; gap: var(--sp-1); margin-top: auto;
  padding-top: var(--sp-2); border-top: 1px solid var(--border); font-variant-numeric: tabular-nums; }
.pcard-preis .num { font-size: var(--fs-dense); }
.pcard-preis-pfeil { color: var(--text-muted); font-size: var(--fs-micro); }
.pcard-preis-db { margin-left: auto; }
.pcard-blocker { display: flex; flex-wrap: wrap; gap: var(--sp-1); padding-top: var(--sp-1); }
.pcard-acts { display: flex; padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--border); background: var(--sunken); }
.pcard-acts .row-acts { display: flex; align-items: center; gap: var(--sp-1); width: 100%; }
.pcard-acts .act-msg { margin-left: auto; font-size: var(--fs-micro); }
/* Bei schmalem Fenster wird die Karte breiter statt kleiner — unter ~200 px ist ein Produktfoto keine
   Information mehr, sondern ein Icon. */
@media (max-width: 640px) { .cards .wg-group { grid-template-columns: 1fr; } }
.empty-state { height: 88px; text-align: center; color: var(--text-muted); white-space: normal; }
/* Deckel-Hinweis als LETZTE Zeile einer Gruppe (#369): sagt, dass unter dieser Liste noch etwas liegt.
   Er sitzt bewusst IN der Tabelle statt darüber — die Aussage gilt dieser einen Gruppe, und am Listenende
   ist genau die Stelle, an der man sonst „fertig" denkt. */
.cap-note { height: var(--row-evt); text-align: center; color: var(--text-muted);
            font-size: var(--fs-micro); white-space: normal; background: var(--sunken); }

/* Ereignis-Tabelle (Protokoll, Verlauf, Nutzung): kein Identitätsobjekt → alles einwertig. */
.grid--evt td { height: var(--row-evt); }

/* ── ARCHETYP C/E: FORMULAR & WIZARD ──────────────────────────────────────── */
/* Links ausgerichtet, nicht zentriert: ein zentrierter Formularblock in einer breiten
   Shell ist eines der stärksten Homepage-Signale. */
.fset { margin-bottom: 26px; }
.fset-legend { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: var(--sp-3); font: 600 var(--fs-ui)/20px var(--f-ui); margin-bottom: var(--sp-3); }
.fset-legend::after { content: ""; height: 1px; background: var(--border); }
/* Hilfe steht neben dem Feld, permanent. Ein Tooltip ist versteckte Dokumentation. */

/* Read-only für fehlende Rechte: deaktivieren, nie verstecken (Projektgrundsatz).
   Kein display:none — der Nutzer sieht, dass es die Funktion gibt, und warum sie sperrt. */
.ro-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
/* ══ EIN AUSSEHEN FÜR „GESPERRT" (29.07.) ═══════════════════════════════════════════════════════
   Abnahme: „Gesperrt sieht an sechs Stellen sechs Mal anders aus." Gemessen an einem Konto ohne
   Schreibrechte: Zahlenfelder grau (im Dunkelmodus kaum erkennbar) · Hex-Farbfelder weiß wie normal ·
   ein Schalter blassblau · der nächste kräftig türkis · Farbschema-Knöpfe völlig normal · die Häkchen
   der Rechte-Matrix normal, „und der Mauszeiger wird zur Hand. Ich klicke, nichts passiert."

   ⛔ Die Ursache war, dass die Regeln am CONTAINER hingen (`.ro-fieldset:disabled …`) statt am ZUSTAND.
   Alles, was außerhalb eines `.ro-fieldset` gesperrt wurde — Matrix-Häkchen, Farbfelder, Schalter —
   bekam nichts. Deshalb hängt es jetzt an `:disabled` selbst und gilt überall, auch an Stellen, die es
   noch gar nicht gibt.

   Der Zustand wird über die FORM getragen, nicht über die Farbe: gestrichelter Rahmen plus
   `not-allowed`. Farbe allein hat im Dunkelmodus nicht getragen (der graue Grund verschwindet dort),
   eine Strichelung trägt in beiden. Grün/Gelb/Rot bleiben ohnehin der Marge vorbehalten (Entscheidung D). */
.input:disabled,
.select:disabled,
textarea:disabled,
input[type="color"]:disabled { background: var(--sunken); color: var(--text-muted);
  border-style: dashed; cursor: not-allowed; }
/* Häkchen, Radios und Schiebeschalter: sie haben keine Fläche für einen Rahmen, also trägt die
   Deckkraft den Zustand — kräftig genug, um den Wert noch abzulesen. */
input[type="checkbox"]:disabled,
input[type="radio"]:disabled,
/* Diese drei stehen hier, weil sie eigene `cursor`-Regeln tragen, die sonst gewinnen — gemessen am
   Matrix-Haekchen, das trotz Sperre die Hand zeigte. Wer eine Zeiger-Regel setzt, muss ihren
   Gesperrt-Fall mitschreiben; die Liste ist die Erinnerung daran. */
.st-matrix-cell input[type="checkbox"]:disabled,
.st-switch > input:disabled,
.kb-d-grid input:disabled { opacity: .45; cursor: not-allowed; }
/* Der Zeiger MUSS mitziehen, sonst verspricht die Hand eine Wirkung, die es nicht gibt. Er sitzt auf
   dem Label, weil der Zeiger über der Beschriftung steht, nicht über der 13-px-Box. */
label:has(> :disabled),
label:has(> * > :disabled) { cursor: not-allowed; }
.st-ig:has(.input:disabled) .st-ig-suffix { color: var(--text-faint); }
.ro-fieldset:disabled .st-ig-suffix { color: var(--text-faint); }
.ro-fieldset:disabled label { cursor: not-allowed; }
.ro-hint { display: flex; align-items: center; gap: var(--sp-2); height: var(--sp-8); padding: 0 var(--sp-3); margin-bottom: var(--sp-3); background: var(--sunken); border-left: 3px solid var(--border-strong); font-size: var(--fs-dense); color: var(--text-muted); }
.ro-hint svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.6; flex: none; }
/* Fehlerfall derselben Leiste (z. B. „Zeilen konnten nicht geladen werden") — war ein Inline-Style. */
.ro-hint--bad { border-left-color: var(--bad); }

/* ── ARCHETYP D: COCKPIT ──────────────────────────────────────────────────── */
/* KPI-Zellen ohne Karte: kein Rahmen, kein Radius, kein Schatten — getrennt durch
   eine Hairline. Regel 5: jede Zahl führt in die Ansicht, die sie erzeugt hat. */
.kpirow { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; padding: 0 var(--sp-4); height: 84px; align-items: center; border-bottom: 1px solid var(--border); }
.kpi-lbl { font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); }
/* Die Farbe sitzt auf dem Pfeil, nie auf der Zahl (Regel 2). */

/* Panel: Rahmen, KEIN Schatten — es liegt, es schwebt nicht. */
.panel { border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); display: flex; flex-direction: column; min-width: 0; }
.panel-head { display: flex; align-items: center; gap: var(--sp-2); height: var(--h-actionbar); padding: 0 14px; border-bottom: 1px solid var(--border); }
.panel-head h2 { margin: 0; font-size: var(--fs-ui); font-weight: 600; }
.panel-head .more { margin-left: auto; font-size: var(--fs-dense); color: var(--accent); background: none; border: 0; cursor: pointer; }
.panel-head .more:hover { text-decoration: underline; }
.panel-body { padding: var(--sp-3) 14px; }

.bars { display: grid; gap: 7px; }
.bar-row { display: grid; grid-template-columns: 130px 1fr 46px; align-items: center; gap: 10px; font-size: var(--fs-micro); }
.bar-row .nm { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { height: var(--sp-3); background: var(--sunken); border-radius: 2px; overflow: hidden; }
.bar-fill { height: 100%; background: var(--chart-1); border-radius: 2px; }
.bar-row .v { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); font-weight: 500; }

/* ── OVERLAYS (das Einzige, was Schatten tragen darf) ─────────────────────── */

/* Drawer: klebt am Rand → --r-0. Zeilenklick öffnet ihn, die Tabelle bleibt sichtbar
   (bei 8.839 Zeilen ist Kontextverlust der teuerste Vorgang des Systems). */
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(720px, 96vw); background: var(--canvas); box-shadow: var(--sh-modal); z-index: 45; display: grid; grid-template-rows: 64px var(--h-tabs) minmax(0,1fr);
  transform: translateX(100%); transition: transform .2s ease; }
/* P2 (24.07.): erste Fassung ist eine EINZIGE lesende Ansicht, keine Reiter -> die mittlere Zeile entfaellt. */
.drawer--flat { grid-template-rows: 64px minmax(0,1fr); }
.drawer.is-open { transform: translateX(0); }
.drawer-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); padding: 0 var(--sp-4); border-bottom: 1px solid var(--border); }
.drawer-crumb { font-size: var(--fs-micro); color: var(--text-muted); }
.drawer-body { overflow: auto; padding: var(--sp-5) var(--sp-4); }
.drawer-backdrop { position: fixed; inset: 0; z-index: 44; background: rgba(0,0,0,.35); opacity: 0; pointer-events: none; transition: opacity .2s ease; }
.drawer-backdrop.is-open { opacity: 1; pointer-events: auto; }
/* Drawer-Inhalt ist dieselbe Kalkulationsleiste/Eigenschaften-Liste wie variante_detail.html — nur
   schmaler (720px statt Vollbreite) und ohne die Editier-Felder (der Drawer ist lesend, s. sortiment.js). */
.drawer-body .vd-calc { flex-wrap: wrap; }
.drawer-body .vd-link { display: inline-flex; align-items: center; gap: 4px; font-weight: 600; }
.drawer-mt { margin-top: var(--sp-4); }

/* ══ DRAWER: PFLEGEN OHNE SEITENWECHSEL (Phase 1.3, 15.08.) ══════════════════════════════════════════
   Ein Block je Thema, in jedem Feld links die Beschriftung und rechts das Eingabefeld. Die Beschriftung
   bekommt eine feste Spur, damit die Felder untereinander fluchten — im schmalen Panel ist eine
   ausgefranste linke Kante der Unterschied zwischen „Formular" und „Zettelsammlung".
   ⛔ Deaktivierte Felder bleiben SICHTBAR (Hausregel „alle sehen alles"): wer das Recht nicht hat, soll
      erkennen, dass es die Funktion gibt, und im Tooltip lesen, welches Recht fehlt. */
.drawer-block { margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--border); }
.drawer-block-t { font: 600 var(--fs-micro) var(--f-ui); text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted); margin-bottom: var(--sp-3); }
.drawer-feld { display: grid; grid-template-columns: 8.5rem 1fr; align-items: center; gap: var(--sp-2);
  margin-bottom: var(--sp-2); }
.drawer-feld-k { font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); line-height: 1.3; }
.drawer-feld-in { display: flex; align-items: center; gap: var(--sp-1); }
.drawer-feld-in .input { flex: 1; min-width: 0; }
.drawer-feld--knopf .btn { justify-self: start; }
.drawer-feld input:disabled { background: var(--sunken); cursor: not-allowed; }
/* Die Quittung steht unter den Feldern und ist im Normalfall leise — ein Fehler bekommt Farbe, weil er
   eine Handlung verlangt. Das ist keine Ampel, sondern eine Meldung. */
.drawer-msg { min-height: 1.2em; margin: var(--sp-2) 0 0; font-size: var(--fs-micro); }
.drawer-msg.is-bad { color: var(--bad); }
@media (max-width: 520px) { .drawer-feld { grid-template-columns: 1fr; } }
.drawer-actions { margin-top: var(--sp-5); display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* FEEDBACK GEBEN — ein Kärtchen in der Seitenmitte, auf jeder Seite gleich (19.07.).
   Vorher stand hier eine Sidebar, die die vorhandenen Meldungen ein zweites Mal auflistete. Die Liste lebt
   jetzt ausschließlich auf dem Board (/roadmap); melden ist ein Zehn-Sekunden-Vorgang und braucht kein
   Werkzeug. Optik bewusst wie eine Board-Karte — man sieht, wohin die Meldung wandert. */
.fbm-back { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 46; }
.fbm { position: fixed; inset: 0; z-index: 47; display: flex; flex-direction: column; align-items: center;
       justify-content: center; gap: var(--sp-2); padding: var(--sp-4); pointer-events: none; }
.fbm-card { pointer-events: auto; width: min(560px, 100%); max-height: min(88vh, 780px);
            display: flex; flex-direction: column; background: var(--canvas);
            border: 1px solid var(--border); border-radius: var(--r-3); box-shadow: var(--sh-modal); }
.fbm-head { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-3) var(--sp-3) 0 var(--sp-4); }
.fbm-head h2 { flex: 1; margin: 0; font-size: var(--fs-sect, 16px); font-weight: 600; }
.fbm-body { flex: 1; overflow-y: auto; padding: var(--sp-3) var(--sp-4);
            display: flex; flex-direction: column; gap: var(--sp-3); }
.fbm-lbl { display: block; font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase;
           letter-spacing: .06em; color: var(--text-faint); margin-bottom: 5px; }
/* Die Eingabefelder waren auf ein Drittel zusammengequetscht (Dominik): `.input` bringt keine Breite mit,
   und ein `display:block`-Label gibt seinem Kind keine. Hier explizit — es ist ein Formular, kein Raster. */
.fbm-field { display: block; }
.fbm-field .input { width: 100%; }
/* Kategorie als Kacheln statt Radio-Liste: sechs Ziele, eines davon ist immer das richtige — man soll es
   sehen, nicht lesen müssen. */
.fbm-cats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--sp-2); }
.fbm-cat { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: var(--sp-2) 4px;
           border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken);
           cursor: pointer; text-align: center; transition: border-color .12s, background .12s; }
.fbm-cat:hover { border-color: var(--border-strong); }
.fbm-cat input { position: absolute; opacity: 0; width: 0; height: 0; }
.fbm-cat-ic { font-size: var(--fs-sect); line-height: 1; }
.fbm-cat-t { font-size: var(--fs-micro); color: var(--text-muted); line-height: 1.25; }
.fbm-cat:has(input:checked) { border-color: var(--primary); background: var(--selected); }
.fbm-cat:has(input:checked) .fbm-cat-t { color: var(--text); font-weight: 600; }
.fbm-cat:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 1px; }
.fbm-card textarea.input { resize: vertical; padding: 7px var(--sp-2); line-height: 1.45; height: auto; }
.fbm-foot { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3) var(--sp-4);
            border-top: 1px solid var(--border); background: var(--sunken);
            border-radius: 0 0 var(--r-3) var(--r-3); }
.fbm-ctx { flex: 1; min-width: 0; font-size: var(--fs-micro); }
.fbm-msg { font-size: var(--fs-micro); }
.fbm-msg.is-ok { color: var(--good); }
.fbm-msg.is-err { color: var(--bad); }
/* Der Hinweis „wohin wandert das?" steht IM Kasten. Draußen auf dem abgedunkelten Grund rutschte er durch —
   und es ist die Antwort auf die erste Frage, die man beim Absenden hat (Dominik). */
.fbm-hint { margin: 0; font-size: var(--fs-micro); }
@media (max-width: 560px) { .fbm-cats { grid-template-columns: repeat(2, 1fr); } }

.popover { position: fixed; z-index: 55; min-width: 200px; background: var(--canvas); border-radius: var(--r-2); box-shadow: var(--sh-pop); padding: var(--sp-2) 0; }
.popover button, .popover a { display: flex; align-items: center; gap: var(--sp-2); width: 100%; height: 30px; padding: 0 var(--sp-3); border: 0; background: none; text-align: left; font-size: var(--fs-ui); color: var(--text); cursor: pointer; white-space: nowrap; }
.popover button:hover, .popover a:hover { background: var(--hover); text-decoration: none; }

/* Benachrichtigungen-Popover (Glocke, C/2-Rework): der Digest „seit letztem Besuch", von notifications.js gefüllt.
   Breiter als das reine Menü-Popover und mit Textblöcken statt Klick-Zeilen — deshalb eigene Innen-Abstände. */
.notif-menu { width: 320px; max-width: 92vw; max-height: 70vh; overflow-y: auto; padding: 0; }
.notif-menu .nm-head { padding: var(--sp-3) 14px var(--sp-2); font-size: var(--fs-micro); font-weight: 600;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: .04em; }
.notif-menu .nm-lines { margin: 0; padding: 0 14px var(--sp-2) 31px; display: grid; gap: 5px;
  font-size: var(--fs-dense); line-height: 1.5; }
.notif-menu .nm-grp { padding: var(--sp-2) 14px; border-top: 1px solid var(--border); font-size: var(--fs-dense); }
.notif-menu .nm-grp strong { font-weight: 600; }
.notif-menu .nm-grp ul { margin: var(--sp-1) 0 0; padding-left: 17px; display: grid; gap: 3px; color: var(--text-muted); }
.notif-menu .nm-more { display: block; margin-top: var(--sp-1); font-size: var(--fs-micro); }
.notif-menu .nm-empty { padding: var(--sp-2) 14px var(--sp-3); font-size: var(--fs-dense); }
/* „Alle Neuerungen →": normale Popover-Zeile (erbt Flex/Höhe/Hover von `.popover a`), nur farblich betont. */
.notif-menu .nm-all { border-top: 1px solid var(--border); color: var(--primary); font-weight: 600; }

/* Befehlspalette (P1, 24.07.): Strg K / Klick auf die Suche. Vier Trefferarten in Gruppen (Artikel/Orte/
   Aufgaben/Werkzeuge) — dieselbe Popover-Basis wie die Glocke, eigene Zeile mit Kind-Tag statt Icon. */
.palette-menu { width: 420px; max-width: 94vw; max-height: 70vh; overflow-y: auto; padding: var(--sp-2) 0; }
.palette-menu .pm-grp { font-size: var(--fs-micro); font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em; padding: var(--sp-2) 14px 4px; }
.palette-menu .pm-grp:not(:first-child) { border-top: 1px solid var(--border); margin-top: var(--sp-1); }
.palette-menu a.pm-item { height: 34px; }
/* ⛔ Bei knappem Platz kürzt die UNTERZEILE, nicht die Nummer (Abnahme 28.07.). Vorher war es umgekehrt:
   auf ~900 px standen zwei Treffer da, die beide sichtbar `S-TR-20x4-RH-V4A…` hießen — abgeschnitten wurde
   genau der Teil, der sie unterscheidet (`-500-A` gegen `-500-A-RI`), während „Variante von …" ungekürzt
   danebenstand. Man musste raten, welche Zeile die gesuchte ist. Die Nummer ist die Identität des Treffers
   und bricht deshalb um; der Kontext daneben darf weichen. */
.palette-menu .pm-item span.pm-label { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.palette-menu .pm-item span.pm-sub { flex: 0 1 auto; min-width: 0;
  font-size: var(--fs-micro); color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Die feste Höhe war auf einzeilige Labels gerechnet — mit dem Umbruch muss die Zeile mitwachsen. */
.palette-menu a.pm-item { height: auto; min-height: 34px; align-items: baseline; }
.palette-menu a.pm-item.is-active { background: var(--hover); }
.palette-menu .pm-empty { padding: var(--sp-2) 14px var(--sp-3); font-size: var(--fs-dense); color: var(--text-muted); }

/* Toast: die einzige Stelle, an der das System ungefragt spricht — deshalb nur für Dinge, die man
   rückgängig machen können muss. Eine Bestätigung ohne Undo wäre eine Selbstbeweihräucherung. */
.toast { position: fixed; left: 50%; bottom: 24px; transform: translate(-50%, 12px); z-index: 60; display: flex; align-items: center; gap: var(--sp-3); padding: 0 14px; height: var(--h-actionbar); background: var(--toast-bg); color: var(--toast-fg); border-radius: var(--r-2); box-shadow: var(--sh-modal); font-size: var(--fs-ui); opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; }
.toast.show { opacity: 1; transform: translate(-50%, 0); pointer-events: auto; }
.toast-undo { background: none; border: 0; color: var(--chart-2); font-weight: 600; font-size: var(--fs-ui); cursor: pointer; }
.toast-undo:hover { text-decoration: underline; }

/* Flach (Arnes Querschnitt): die Warengruppen-Köpfe verschwinden, die Sortierung läuft über alles.
   Das Akkordion BLEIBT der Standard (Entscheidung A) — flach steht daneben, nicht statt seiner. */
.grid.is-flat tr.wg-head { display: none; }

/* ── ANSICHT „WETTBEWERB" (/preise?tab=wettbewerb) ─────────────────────────── */
/* Die Vergleichsbasis steht über der Tabelle, nicht darunter: sie qualifiziert JEDE Zahl darunter. */
.wb-lead { font-weight: 600; font-size: var(--fs-dense); white-space: nowrap; }
.wb-basis { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-6); padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border); font-size: var(--fs-dense); }
.wb-basis-z { display: flex; align-items: baseline; gap: 6px; }
.wb-basis-k { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .04em; color: var(--text-faint); }

/* Verglichen-mit: der Klartext-Artikel trägt die Zeile. Zwei Zeilen wie .col-ident — Name oben, Beleg unten.
   ⛔ Stand bis 20.07. auf `max-width: 0` — der übliche Trick, um `text-overflow: ellipsis` in einer
   Tabellenzelle überhaupt greifen zu lassen. Er funktionierte nur, SOLANGE die Spalte an vierter Stelle
   stand und die Nachbarn schmal waren: Beim Umsortieren (Spalte 2) zerquetschte `auto`-Layout sie auf
   Minimalbreite, der Artikelname wurde vorne abgeschnitten und der Rest brach in die Nachbarzeile.
   Eine Breitenregel, die von der Spaltenposition abhängt, ist keine Breitenregel — jetzt eine echte
   Zielbreite mit Unter-/Obergrenze, unabhängig davon, wo die Spalte steht. */
.col-comp { width: 26%; min-width: 200px; max-width: 340px; }
.comp-art { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Die Verdikt-Knöpfe brechen sonst untereinander, sobald die Spalte schmal wird. */
.comp-verdict { display: inline-flex; flex-wrap: wrap; gap: 4px; }
/* Zielbreiten der übrigen Spalten — ohne sie verteilt `auto` den Platz nach Textlänge, und die
   Aktionsspalte lief rechts aus dem Bild. */
/* Den Anschlag setzt seit 31.07. das Gerüst selbst (`.grid:has(.col-cb)`); hier bleibt nur die Breite. */
#wbtable .col-ident { width: 18%; min-width: 170px; }
/* Die Aktionsspalte bewusst NICHT sticky: sie ist die letzte Spalte, angeheftet bringt sie nichts, kostet
   aber bei schmalem Fenster die Nachbarspalten — bei 1280 px verdeckte sie Marge, EK und Abstand
   vollständig, und im Kopf sprang es von „VK Wettbewerb" direkt auf „Aktion". */
#wbtable .col-act { position: static; width: 1%; white-space: nowrap; box-shadow: none; }
.wb-act { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.comp-meta { display: flex; align-items: center; gap: 6px; font-size: var(--fs-micro); color: var(--text-muted);
  overflow: hidden; white-space: nowrap; }
/* Wettbewerber = eine QUELLE, und Quellen tragen im neuen System einen Text-Chip, nie eine Farbe
   (Entscheidung D). Das Cockpit tönt dafür heute noch ganze Spalten mit brand_color — das ist die alte Welt. */
.comp-chip { padding: 0 5px; border: 1px solid var(--border); border-radius: var(--r-1);
  color: var(--text-muted); background: var(--sunken); }
/* „2 Quellen" ist die AUSNAHME (75 % der Zeilen haben eine) und damit die gute Nachricht — markiert wird
   sie, nicht die Regel. Bewusst kein Ampel-Grün: das ist eine Konfidenz-Aussage über die DATENLAGE, kein
   Urteil über den Preis (Entscheidung D). Text-Chip wie die Quelle daneben. */
.comp-2q { padding: 0 5px; border: 1px solid var(--border); border-radius: var(--r-1);
  color: var(--text); background: transparent; cursor: help; }

/* Abstand: Richtung statt Farbe. Die Mittellinie IST der Wettbewerbspreis; der Balken wächst nach links
   (wir günstiger) oder rechts (wir teurer). Bewusst KEINE Ampel: es gibt kein Urteil zu färben —
   competition_gap_pct sagt „ab hier nennen wir es teurer", nicht „ab hier ist es schlecht". Eine Skala
   darf keine Kategorie-Farbe tragen (Saschas Grün-Ausnahme vom 17.07. gilt dem Shop-Status, einer Kategorie). */
.col-abstand { white-space: nowrap; }
.wb-bar { position: relative; display: inline-block; width: 96px; height: 9px; margin-right: 8px;
  background: var(--sunken); border-radius: 2px; vertical-align: middle; }
/* Die Mittellinie sitzt auf dem Track selbst, nicht als eigenes Element: ein <span> weniger je Zeile. */
.wb-bar::before { content: ""; position: absolute; left: 50%; top: -1px; bottom: -1px; width: 1px;
  background: var(--border-strong, var(--text-faint)); }
/* min-width: ein Abstand von 0,4 % ist trotzdem ein Abstand — er darf klein sein, aber nicht verschwinden
   (sonst läse man die leere Spur als „kein Vergleich", und das ist eine andere Aussage). */
.wb-bar-fill { position: absolute; top: 0; bottom: 0; right: 50%; min-width: 2px;
  background: var(--chart-1); border-radius: 2px 0 0 2px; }
.wb-bar-fill.is-teurer { right: auto; left: 50%; border-radius: 0 2px 2px 0; }
.wb-pct { font-variant-numeric: tabular-nums; }
/* Die Spalte ist so breit wie ihr Inhalt: 96 px Spur + 8 px Luft + „−66,1 %". Ohne feste Breite zieht die
   Tabelle sie auf und Balken und Zahl driften auseinander — dann vergleicht man zwei Dinge, die nicht
   nebeneinander stehen. */
.grid [data-c="abstand"] { width: 184px; }

/* „Was ich nicht weiß": die zwei Lücken NEBENEINANDER, weil sie verschiedene Dinge sind — die eine ist ein
   Crawl-Auftrag, die andere eine Wettbewerber-Entscheidung. Untereinander läsen sie sich als eine Liste. */
.wb-blind { margin: var(--sp-6) var(--sp-4) var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--border); }
.wb-blind h2 { font-size: var(--fs-dense); font-weight: 600; margin: 0 0 var(--sp-3); }
.wb-blind h2 em { font-style: normal; text-decoration: underline; text-underline-offset: 2px; }
.wb-blind-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--sp-4); }
.wb-blind-k { padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); }
/* `>` ist Pflicht, nicht Kosmetik: ohne den Kind-Selektor erbte JEDES <strong> im Kasten die 22 px — auch
   die Betonungen im Erklärtext („Das ist ein Crawl-Auftrag" stand dann so groß wie die Kennzahl daneben und
   sprengte die Karte). Nur die Zahl selbst ist die Kennzahl. */
.wb-blind-k > strong { display: block; font-size: var(--fs-title); font-variant-numeric: tabular-nums; line-height: 1.2; }
.wb-blind-k > span { font-size: var(--fs-dense); font-weight: 500; }
.wb-blind-k p { margin: var(--sp-2) 0 var(--sp-3); font-size: var(--fs-dense); line-height: 1.5; }
.wb-blind-fuss { margin: var(--sp-4) 0 0; font-size: var(--fs-dense); line-height: 1.5; color: var(--text-muted); }

/* ── PORTIER-BAUSTEINE (Cutover 2026-07-17) ──────────────────────────────────
   Gefunden beim Portieren der 24 Alt-Seiten: Figuren, die app.css hatte und die neue
   Datei noch nicht. Jede ist aus den Tokens abgeleitet, keine Zeile ist kopiert.
   Was hier NICHT steht, ist Absicht — es hat schon einen Nachfolger:
     table→.grid · card→.panel · btn-primary→.btn--primary · btn-sm→.btn--sm
     badge-status good/warn/bad→.badge--ok/warn/err · muted→.dim · right→.num
     filterbar→.fbar · page-header→{% block pagehead %} · btn-x→.icon-btn--sm
   ─────────────────────────────────────────────────────────────────────────── */

/* Serverseitige Meldung nach einem Redirect (?fehler=…). Bewusst kein .toast: ein Toast ist
   flüchtig und JS-getrieben, das hier steht schon im ersten Paint und bleibt bis zum Reload.
   Gleiche Figur wie .ro-hint (Kante links + gesenkter Grund) — beide sagen „lies mich, dann
   arbeite weiter", nur mit anderer Dringlichkeit. */
/* ⛔ Stand bis 20.07. auf `display: flex` — und damit wurde JEDES betonte Wort zu einem eigenen
   Flex-Kasten: „9 Artikel verkaufen unter Einkaufspreis | — sie | stehen | nicht | in dieser Liste."
   Jeder Hinweistext mit <strong>/<em>/<a> war davon betroffen, also die Texte, die am meisten erklären
   sollen. `flow-root` behält den Kasten (eigener Block-Kontext, keine kollabierenden Margen), lässt
   Inline-Elemente aber normal im Satz fließen. Icons stehen dadurch in der Zeile statt zentriert —
   das ist der Preis, und er ist deutlich kleiner als ein unlesbarer Satz. */
.note { display: flow-root; min-height: var(--sp-8); line-height: 1.5;
  padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3); background: var(--sunken);
  border-left: 3px solid var(--border-strong); font-size: var(--fs-dense); color: var(--text); }
.note > svg { vertical-align: -.15em; margin-right: var(--sp-1); }
.note--err { border-left-color: var(--bad); }
/* RPC-Ausfall-Banner (29.07.): sitzt zwischen Chips und Aktionsleiste, also AUSSERHALB des
   Scroll-Containers — es soll ohne Scrollen sichtbar sein. Eigener Rand, damit es nicht an der
   Aktionsleiste klebt und wie deren Beschriftung wirkt. */
.rpc-fail { margin: var(--sp-2) var(--sp-4); }
.rpc-fail .btn { margin-left: var(--sp-2); vertical-align: baseline; }
.rpc-fail-det { margin-top: var(--sp-2); font-size: var(--fs-micro); }
.rpc-fail-det summary { cursor: pointer; color: var(--text-muted); }
.rpc-fail-det ul { margin: var(--sp-1) 0 0; padding-left: var(--sp-4); color: var(--text-muted); }
.note--ok  { border-left-color: var(--good); }

/* Label über einer Überschrift — dieselbe Figur wie .kpi-lbl, nur ohne die Kennzahl darunter.
   Trägt NIE die Markenfarbe (der alte .eyebrow tat das): Regel 2 — Farbe ist Bedeutung. Ein
   Rubrik-Label bedeutet nichts, es ordnet ein. */
.eyebrow { font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-muted); }

/* Einleitungssatz unter einer Überschrift. Regel: Prosa wird gedeckelt (§3.4). */
.lead { max-width: 68ch; margin: 0 0 var(--sp-4); font-size: var(--fs-dense); line-height: 1.55;
  color: var(--text-muted); }

/* Überschrift + Beiwerk (Zähler, Chip, Link) auf einer Grundlinie. */
.head-row { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }

/* Bearbeiten-Affordanz. `is-disabled` ist keine Kosmetik, sondern der Grundsatz „alle sehen
   alles, nur Berechtigte schreiben" (CLAUDE.md): wer nicht darf, sieht den Stift ausgegraut —
   nicht gar nicht. pointer-events:none, damit der Server-Guard nie der erste Lehrer ist. */
.edit-pencil { display: inline-flex; align-items: center; gap: var(--sp-1); height: 20px;
  padding: 0 5px; border: 0; border-radius: var(--r-1); background: none; color: var(--accent);
  font: 600 var(--fs-micro)/20px var(--f-ui); cursor: pointer; }
.edit-pencil:hover { background: var(--hover); text-decoration: none; }
.edit-pencil svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 1.7; }
.edit-pencil.is-danger { color: var(--bad); }
.edit-pencil.is-disabled { color: var(--text-faint); cursor: not-allowed; pointer-events: none; }

/* Innenabstand für Seiten, deren Inhalt KEINE randfüllende Tabelle ist. Regel 3 gilt weiter:
   .grid bekommt diesen Wrapper nie — sie geht bis an den Rand. */
.page { padding: var(--sp-5) var(--sp-4); }
.page > h2 { font-size: var(--fs-sect); font-weight: 600; margin: 0 0 var(--sp-3); }
.page > h2:not(:first-child) { margin-top: var(--sp-6); }
.nowrap { white-space: nowrap; }

/* ── SCHMALE FENSTER ─────────────────────────────────────────────────────────
   Die Shell ist für den Schreibtisch gebaut, und echte Handys landen auf /mobil — die Weiche in
   layout.html schickt sie dorthin, BEVOR etwas gemalt wird. Drei Seiten sind davon aber bewusst
   ausgenommen: /changelog, /info und /login. Sie sollen auf dem Handy lesbar sein (der Kunde liest
   „Was ist neu" nicht am Schreibtisch). Ohne diesen Breakpoint fräst die 224-px-Rail den Text dort
   auf Briefmarkenbreite — ausgerechnet auf den drei Seiten, die für Fremde gedacht sind.
   Die Shell wird hier zur Lese-Hülle: Rail weg (die Seiten sind Ziele, keine Ausgangspunkte),
   Datenstand und Suche weg (Arbeits-Werkzeuge, kein Lese-Werkzeug). Die Icons bleiben — über sie
   kommt man von /changelog nach /info und zurück.
   ─────────────────────────────────────────────────────────────────────────── */
/* Mobile-Navigation: Burger im Kopf + Overlay-Backdrop. Auf dem Desktop display:none — kostet nichts,
   stört das 3-Spalten-Topbar-Grid nicht (ein display:none-Grid-Item wird nicht platziert). */
.nav-burger { display: none; place-items: center; width: var(--sp-8); height: var(--sp-8);
  border: 0; border-radius: var(--r-1); background: none; color: var(--text-muted); cursor: pointer; justify-self: start; }
.nav-burger:hover { background: var(--hover); color: var(--text); }
.nav-burger svg { width: 20px; height: 20px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; }
.nav-backdrop { display: none; }
.search-toggle { display: none; }   /* nur < 780 px — Desktop hat die volle Suchzeile */

@media (max-width: 780px) {
  /* EINE Spalte, nicht `0 minmax(0,1fr)`. Grund ist derselbe wie bei .topbar zwei Zeilen tiefer, und
     er ist hier nur teurer: `display:none` nimmt die Schiene aus dem Grid-FLUSS, die Spalten bleiben —
     .main rutschte in Spalte 1, und die war 0 breit. Die ganze App war unter 780 px 0 px breit.
     Der eigene Test hat es nicht gefunden, weil er „ist die Rail weg?" prüfte statt „ist der Inhalt da?". */
  .app { grid-template-columns: minmax(0, 1fr); }
  /* Schiene = Slide-in-Overlay statt versteckt (dominik: darunter war KEINE Navigation erreichbar).
     Fixiert = aus dem Grid-Fluss, deshalb bleibt .app oben einspaltig. data-rail=collapsed ist hier per
     shell.js entfernt → immer volle Breite + Labels. */
  .rail { position: fixed; top: 0; left: 0; bottom: 0; width: 224px; z-index: 60;
          transform: translateX(-100%); transition: transform .2s ease; box-shadow: 2px 0 24px rgba(0,0,0,.5); }
  .app.nav-open .rail { transform: translateX(0); }
  .rail-foot { display: none; }   /* schmal/breit-Toggle im Overlay sinnlos */
  .nav-burger { display: grid; }
  .nav-backdrop { display: block; position: fixed; inset: 0; z-index: 55; background: rgba(0,0,0,.45);
                  opacity: 0; pointer-events: none; transition: opacity .2s ease; }
  .app.nav-open .nav-backdrop { opacity: 1; pointer-events: auto; }
  /* Datenstand bleibt als Dot — der „sind die Zahlen aktuell?"-Alarm (rot > 72 h) darf schmal nicht
     verschwinden. Nur Label + Alter fallen weg, der farbige Punkt (dieselbe #standDot) trägt die Bedeutung. */
  .stand { padding: 0; width: var(--sp-8); justify-content: center; }
  .stand span:not(.dot), .stand b { display: none; }   /* Label + Alter weg, der Dot-Span bleibt */
  .stand .dot { width: 8px; height: 8px; }
  /* Suche ausgeblendet, bis der Lupe-Button sie als Overlay-Zeile unter den Kopf klappt (dieselbe
     #globalSearch → gleiche Enter-/Strg-K-Logik, keine zweite Sucheingabe). */
  .search { display: none; }
  .search kbd { display: none; }   /* kein Strg K auf schmalen/Touch-Geräten */
  .search-toggle { display: grid; }
  .topbar.search-open .search { display: flex; position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
    padding: var(--sp-2) var(--sp-3); background: var(--canvas); border-bottom: 1px solid var(--border);
    box-shadow: 0 8px 16px rgba(0,0,0,.12); }
  .topbar.search-open .search input { width: 100%; }
  /* Kopf: Burger | Datenstand-Dot | Rest rechts. position:relative verankert die Such-Overlay-Zeile. */
  .topbar { position: relative; grid-template-columns: auto auto minmax(0, 1fr); }
  .pagehead { padding: 0 var(--sp-3); }
  .pagehead h1 { font-size: var(--fs-sect); }
  .page { padding: var(--sp-4) var(--sp-3); }
}

/* ── /changelog — „Was ist neu" (Kunden-Seite) ─────────────────────────────── */
/* ⛔ EINE LISTE MIT HALTEPUNKTEN, KEIN ENDLOSTEXT (03.08.). Vorher war das eine Zeitleiste, in der
   jeder der 363 Einträge vollständig ausgeschrieben stand: zwei Einträge füllten einen Bildschirm,
   und über die ganze Seite gab es keine einzige Zwischenüberschrift. Ein Jahr Arbeit las sich als
   eine Textwand, in der man weder etwas findet noch überblickt, wann wie viel passiert ist.
   Jetzt: Monatsüberschrift als Halt, je Eintrag eine Zeile, der Text kommt auf Klick.
   ⛔ `68ch` ist ersatzlos weg. Das war ein SIEBTES Zeilenmaß neben `--measure` — genau die Sorte
   Insellösung, die die Hausregeln seit dem 19.07. verbieten. Der Fließtext trägt jetzt `--measure`,
   die Titelzeile darf breiter laufen (sie ist eine Zeile, kein Absatz). */
.cl { display: grid; gap: var(--sp-5); }
.cl-monat { display: grid; gap: 0; }
.cl-monat-kopf { position: sticky; top: 0; z-index: 1; margin: 0 0 var(--sp-2);
  padding: var(--sp-2) 0; background: var(--canvas);
  font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-muted); border-bottom: 1px solid var(--border); }
.cl-monat-kopf .dim { font-variant-numeric: tabular-nums; }
.cl-item { border-bottom: 1px solid var(--border); }
.cl-item:last-child { border-bottom: 0; }
.cl-sum { display: grid; grid-template-columns: 74px minmax(0, 1fr) auto 44px; align-items: baseline;
  gap: var(--sp-3); padding: var(--sp-2) 0; cursor: pointer; list-style: none; }
.cl-sum::-webkit-details-marker { display: none; }
.cl-sum:hover .cl-title { color: var(--accent); }
.cl-art { justify-self: start; }
.cl-title { font-size: var(--fs-ui); font-weight: 600; line-height: 1.4; }
.cl-tag { text-align: right; font-variant-numeric: tabular-nums; font-size: var(--fs-micro); }
.cl-body { max-width: var(--measure); margin: 0 0 var(--sp-3) calc(74px + var(--sp-3));
  font-size: var(--fs-dense); line-height: 1.6; color: var(--text-muted); }
.cl-foot { max-width: var(--measure); margin: var(--sp-5) 0 0; padding-top: var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--fs-micro); line-height: 1.6; color: var(--text-muted); }
.cl-foot code { font-family: var(--f-mono); background: var(--sunken); padding: 0 3px; border-radius: 2px; }
/* Schmal: Nummer und Datum unter den Titel, der Text rückt an den Rand zurück. */
@media (max-width: 700px) {
  .cl-sum { grid-template-columns: 74px minmax(0, 1fr); row-gap: 2px; }
  .cl-sum .kb-id, .cl-tag { grid-column: 2; text-align: left; }
  .cl-body { margin-left: 0; }
}

/* ── /aenderungslog — Protokoll ────────────────────────────────────────────── */
/* Eine Massenaktion ist EIN Vorgang: die betroffenen Artikel stehen aufklappbar in ihrer Zeile,
   statt als neunzig wortgleiche Zeilen die Tabelle zu füllen (03.08.). */
.pr-mehr > summary { cursor: pointer; list-style: none; color: var(--text-muted); }
.pr-mehr > summary::-webkit-details-marker { display: none; }
.pr-mehr > summary::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-icon); margin-right: 4px; }
.pr-mehr[open] > summary::before { content: "▾"; }
.pr-mehr > summary:hover { color: var(--text); }
.pr-mehr > summary strong { color: var(--text); font-variant-numeric: tabular-nums; }
.pr-liste { display: flex; flex-wrap: wrap; gap: 2px var(--sp-3); margin-top: var(--sp-2);
  max-height: 220px; overflow-y: auto; }
.pr-ziel { font-family: var(--f-mono); font-size: var(--fs-micro); white-space: nowrap; }
.pr-detail { font-size: var(--fs-micro); color: var(--text-faint); }

/* ── /activity — Nutzung ───────────────────────────────────────────────────── */
.ac-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: var(--sp-3); }
.ac-card { padding: var(--sp-3); gap: var(--sp-1); color: var(--text); }
.ac-card:hover { border-color: var(--border-strong); background: var(--hover); text-decoration: none; }
.ac-card.is-sel { border-color: var(--primary); box-shadow: inset 0 0 0 1px var(--primary); }
.ac-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
.ac-role { font-size: var(--fs-micro); }
.ac-last { font-size: var(--fs-dense); color: var(--text-muted); }
.ac-stats { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3); margin-top: var(--sp-2);
  padding-top: var(--sp-2); border-top: 1px solid var(--border); font-size: var(--fs-micro); color: var(--text-muted); }
.ac-stats strong { color: var(--text); font-variant-numeric: tabular-nums; }
.ac-detail-h { margin: var(--sp-6) 0 var(--sp-3); }
.ac-detail-h h2 { margin: 0; font-size: var(--fs-sect); font-weight: 600; }
.ac-detail-h .dim { font-size: var(--fs-dense); }
.ac-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--sp-4); align-items: start; }
.ac-cols .panel, .ac-trail { overflow: hidden; }
.ac-trail { margin-top: var(--sp-4); }
/* Tabellen in Panels: der Panel-Rahmen ist schon da, die Tabelle darf ihn nicht doppeln (Regel 3 —
   randlos und randfüllend gilt auch INNERHALB eines Panels). */
.panel > .grid th:first-child, .panel > .grid td:first-child { padding-left: 14px; }
.panel > .grid th:last-child, .panel > .grid td:last-child { padding-right: 14px; }

/* ── /export/archiv ────────────────────────────────────────────────────────── */
.ar-name { font-size: var(--fs-micro); }

/* ═══════════════════════════════════════════════════════════════════════════════
   STARTSEITE (Archetyp D — Cockpit). Cutover 2026-07-17.
   Neu geschrieben, nicht portiert: app.css setzte hier 10-px-Radien, Schatten auf
   liegenden Flächen und knalliges #0b5fff. Beides bricht Regel 2 (Farbe ist Bedeutung)
   und die Radien-Skala (0 liegt · 3 Controls · 6 schwebt).
   ═══════════════════════════════════════════════════════════════════════════════ */

/* Mehr Luft zwischen den Sektionen als innerhalb einer Sektion: die Startseite trägt sechs
   gleichrangige Blöcke: bei gleichem Abstand innen wie außen liest sie sich als eine lange Liste. */
.ck { padding: var(--sp-5) var(--sp-4) var(--sp-8); display: flex; flex-direction: column; gap: var(--sp-6); }

/* ⚠️ Kicker ÜBER den Titel, nicht daneben. Nebeneinander begann jede Überschrift dort, wo ihr Kicker
   endete — gemessen drei gleichrangige Abschnitte mit drei verschiedenen linken Kanten (350 / 358 / 434 px).
   Fluchtung ist die stärkste Gruppierung, die eine Seite hat; eine wandernde Titelkante zerlegt sie in
   Einzelteile. `.ansteht-head` war dieselbe Figur unter zweitem Namen — eine Figur, eine Regel. */
/* Ein Block = Überschrift + was zu ihr gehört. `.ck` setzt den GROSSEN Abstand zwischen den Sektionen;
   innerhalb einer Sektion gilt der kleine. Ohne diese Klammer standen Überschrift, Trichter und
   Verteilungs-Leiste im selben Abstand zueinander wie zu den Nachbar-Sektionen — dann trägt der
   Abstand keine Gruppierung mehr, und die Seite liest sich als eine lange Liste gleichrangiger Kästen.
   `.pm-block` und `.ansteht` waren bereits genau diese Figur unter zwei weiteren Namen. */
.ck-block { display: grid; gap: var(--sp-3); }
.ck-sect { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--sp-2); }
.ck-sect .eyebrow { flex: 0 0 100%; margin-bottom: 2px; }
.ck-sect h2, .ck-sect h3 { margin: 0; font-size: var(--fs-sect); font-weight: 600; letter-spacing: -.01em; }
.ck-sect .edit-pencil { margin-left: auto; }

/* ── Fokus-Kasten „Was heute am meisten kostet" (#480) ─────────────────────────
   Der einzige Kasten der Startseite, der NICHT aufklappbar ist und keine Rubrik trägt: er beantwortet
   die Frage vor allen Karten („wo fange ich an"), also darf er selbst keine Entscheidung verlangen.
   Die drei Zeilen sind bewusst gleich gebaut wie die Kartenköpfe darunter — Zahl in eigener Spur,
   Text links davon, Aktion rechts —, damit der Blick beim Weiterlesen nicht umlernen muss.
   Die Tönung sitzt auf dem RANG-Punkt und der linken Kante, nicht auf der ganzen Fläche: drei
   vollflächig getönte Zeilen wären drei Alarme, und dann ist keiner mehr der erste. */
.fokus { border: 1px solid color-mix(in srgb, var(--bad) 28%, var(--border)); border-radius: var(--r-2);
  background: var(--canvas); overflow: hidden; }
.fokus-head { padding: var(--sp-3) 14px; border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--bad) 5%, var(--canvas)); }
.fokus-title { display: flex; align-items: center; gap: var(--sp-2); margin: 0;
  font-size: var(--fs-sect); font-weight: 600; letter-spacing: -.01em; }
.fokus-flame { font-size: var(--fs-body); line-height: 1; }
.fokus-sub { margin: 4px 0 0; max-width: var(--measure); font-size: var(--fs-micro); color: var(--text-muted); }
.fokus-list { list-style: none; margin: 0; padding: 0; }
.fokus-item { display: grid; grid-template-columns: 22px 92px minmax(0, 1fr) auto;
  align-items: center; gap: 0 var(--sp-4); padding: var(--sp-3) 14px;
  border-top: 1px solid var(--border); border-left: 3px solid transparent; }
.fokus-list > .fokus-item:first-child { border-top: 0; }
.fokus-item--verlust { border-left-color: var(--bad); }
.fokus-item--marge   { border-left-color: var(--warn); }
.fokus-item--umsatz  { border-left-color: var(--accent); }
.fokus-rang { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px;
  border-radius: var(--r-full); background: var(--sunken); color: var(--text-muted);
  font: 600 var(--fs-micro)/1 var(--f-ui); }
.fokus-num { display: grid; gap: 1px; text-align: right; }
.fokus-num b { font: 600 var(--fs-kpi)/1 var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.fokus-item--verlust .fokus-num b { color: var(--bad); }
.fokus-body { display: grid; gap: 2px; min-width: 0; }
.fokus-label { font-size: var(--fs-ui); font-weight: 600; }
.fokus-einheit { font-size: var(--fs-micro); color: var(--text-muted); }
/* Der Grund ist Fließtext und hält deshalb das EINE Zeilenmaß (`--measure`, CLAUDE.md). Ohne die
   Grenze läuft er über die volle Containerbreite und liest sich als Bandwurm — die Zahl links
   verliert dabei ihren Bezug. */
.fokus-why { max-width: var(--measure); font-size: var(--fs-micro); line-height: 1.45; color: var(--text-muted); }
.fokus-go { white-space: nowrap; }
.fokus-clear { margin: 0; padding: var(--sp-4) 14px; color: var(--text-muted); }
/* Schmal: Zahl und Rang rutschen über den Text, der Knopf auf eine eigene Zeile — sonst quetscht die
   feste 92-px-Zahlenspur den Grund auf zwei Wörter je Zeile. */
@media (max-width: 900px) {
  .fokus-item { grid-template-columns: 22px auto 1fr; row-gap: var(--sp-2); }
  .fokus-num { grid-auto-flow: column; align-items: baseline; gap: var(--sp-2); text-align: left; }
  .fokus-body { grid-column: 1 / -1; }
  .fokus-go { grid-column: 1 / -1; justify-self: start; }
}

/* ── Karten-Gruppen ────────────────────────────────────────────────────────────
   Die Rubrik (Entscheiden / Erweitern / Prüfen …) stand bis 27.07. als `.eyebrow` in JEDEM Kartenkopf —
   neun Versal-Zeilen für fünf Rubriken, und die Wiederholung trug keine Information: zwei Karten mit
   demselben Wort standen ohnehin untereinander. Sie steht jetzt EINMAL über der Gruppe, und die Gruppe
   trägt EINEN Rahmen statt neun. Das ist derselbe Schnitt wie bei einer Tabelle: der Spaltenkopf steht
   über den Zeilen, nicht in jeder. */
.ck-group { display: flex; flex-direction: column; gap: var(--sp-2); }
.ck-group-head { display: flex; align-items: center; gap: var(--sp-3); }
.ck-group-head::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.ck-group-lbl { margin: 0; font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-muted); white-space: nowrap; }
.ck-group-list { border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); }
/* Kein `overflow: hidden` für den Radius: die nachgeladenen Tabellen (.ck-scroll) sind eigene
   Scroll-Container, und ein klemmender Vorfahr kappt ihren klebenden Kopf. Der Radius greift auf der
   Gruppe, die Kinder liegen quadratisch darin — sichtbar ist das nur an den vier Ecken. */
.ck-group-list > .ck-card { border: 0; border-top: 1px solid var(--border); border-radius: 0; }
.ck-group-list > .ck-card:first-child { border-top: 0; }

/* ── Entscheidungs-Karten ──────────────────────────────────────────────────────
   EIN Kopf-Raster für alle Karten: [Caret] [Titel] [Fakten] [Metrik]. Die Metrik-Spalte ist FEST breit
   und die Zahl steht in ihrer eigenen 92-px-Spur — damit fluchten die neun Kopfzahlen auf einer Kante.
   Vorher war der Kopf ein Flexbox mit rechtsbündigem Fließtext: die Zahl stand LINKS von einem ein- bis
   dreizeiligen Satz, also je Karte an einer anderen Stelle, und die Satzhöhe bestimmte die Kartenhöhe.
   Beides zusammen ist der Grund, warum der Stapel „verzerrt" wirkte — nicht die Karten selbst. */
.ck-card { padding: 0; }
.ck-card > summary { list-style: none; cursor: pointer; }
.ck-card > summary::-webkit-details-marker { display: none; }
/* `minmax(14rem, …)` auf der Titelspalte: die Fakten-Spur ist `auto` und wuechse sonst mit der Zahl der
   Pillen so weit, dass sie den Titel zusammendrueckt — „Fehlt noch im Shop" traegt vier davon. */
.ck-head { display: grid; grid-template-columns: 14px minmax(14rem, 1fr) auto 300px;
  align-items: center; gap: 0 var(--sp-4); min-height: 68px; padding: var(--sp-3) 14px; }
.ck-head::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-icon);
  justify-self: center; transition: transform .15s ease; }
.ck-card[open] > .ck-head::before { transform: rotate(90deg); }
.ck-head:hover { background: var(--hover); }
/* Offen = getönt + Trennkante. `box-shadow: inset` statt `border-bottom`, weil eine echte Kante die
   Kopfhöhe um 1 px ändert und die Karte beim Aufklappen springt. */
.ck-card[open] > .ck-head { background: var(--sunken); box-shadow: inset 0 -1px 0 var(--border); }
.ck-id { min-width: 0; }
.ck-title { margin: 0; font-size: var(--fs-sect); font-weight: 600; letter-spacing: -.01em; }
.ck-head-num { display: grid; grid-template-columns: minmax(0, 1fr) 92px; align-items: center;
  gap: var(--sp-3); text-align: right; }
.ck-metric-lbl { font-size: var(--fs-micro); line-height: 1.4; color: var(--text-muted); }
.ck-big { font: 600 var(--fs-kpi)/1 var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }

/* Nebenzahlen als Pillen, nicht als Nebensätze. „24 sofort angleichbar" hing vorher als dritte Zeile
   an der Kopfzahl und machte die Karte höher als ihre Nachbarn; als Pille steht sie in ihrer eigenen
   Spur und die Kopfhöhe bleibt konstant. Farbe nur auf der ZAHL (Regel 2) — der Text daneben bleibt
   gedämpft, sonst schreit die halbe Pille. */
.ck-facts { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-1) var(--sp-2); max-width: 34ch; }
.ck-stat { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-full); background: var(--canvas);
  font: 400 var(--fs-micro)/18px var(--f-ui); color: var(--text-muted); white-space: nowrap; }
.ck-stat b { font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
.ck-stat--good b { color: var(--good); }
.ck-stat--warn b { color: var(--warn); }
.ck-stat--brand b { color: var(--accent); }

/* Enger Live+0-Alarm „Shop verkauft Luft" (P7, 24.07.): steht als EINZIGE Karte außerhalb der Gruppen,
   weil ein Alarm keine Rubrik hat — er unterbricht. Deshalb eigener Rahmen, Tönung und Flag. */
.ck-alarm { border: 1px solid color-mix(in srgb, var(--warn) 50%, var(--border)); border-radius: var(--r-2);
  background: color-mix(in srgb, var(--warn) 5%, var(--canvas)); }
.ck-alarm > .ck-head:hover { background: color-mix(in srgb, var(--warn) 10%, var(--canvas)); }
.ck-alarm[open] > .ck-head { background: color-mix(in srgb, var(--warn) 9%, var(--canvas)); }
.ck-alarm .ck-big { color: var(--bad); }
.ck-flag { display: inline-flex; align-items: center; height: 18px; padding: 0 var(--sp-2); margin-bottom: 3px;
  border: 1px solid var(--badge-warn-bd); border-radius: var(--r-full); background: var(--badge-warn-bg);
  color: var(--badge-warn-fg); font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .06em;
  text-transform: uppercase; }

/* ── Karten-Inneres ───────────────────────────────────────────────────────────
   EIN Innen-Padding für jede Karte. Bis 27.07. hatten sechs Karten überhaupt keines: ihr Fließtext war
   direktes Kind der Karte, und `.ck-card { padding: 0 }` ließ ihn am Rahmen kleben — genau das liest
   sich als „unfertig". Die Kanban-Karten pflastern das seit jeher über `.ck-cols`; jetzt gilt eine Regel
   für beide Fälle. Der Abstand zwischen den Blöcken kommt aus `gap`, nicht aus Rand-Margins. */
.ck-body { display: grid; gap: var(--sp-3); padding: var(--sp-4) 14px; }
.ck-body > p { margin: 0; }
/* Kanban: links entscheiden, rechts das schon Vorgemerkte (rückholbar). */
.ck-cols { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: var(--sp-4); padding: var(--sp-4) 14px; }
.ck-col-main { min-width: 0; display: grid; gap: var(--sp-3); align-content: start; }
/* Regel 3: Fließtext wird auf EIN Zeilenmaß gedeckelt (--measure), Tabellen und Raster nie. */
.ck-lead { font-size: var(--fs-dense); line-height: 1.6; margin: 0; max-width: var(--measure); }
.ck-hint { font-size: var(--fs-micro); line-height: 1.5; margin: 0; max-width: var(--measure); }
.ck-info { display: grid; gap: var(--sp-2); justify-items: start; }
.ck-empty { margin: 0; }

/* Karte „Steuerflächen füllen" (P5, 24.07.): drei Zeilen Bereich + Kurzstatus + Werkzeug-Link. */
.ck-fuellstand-rows { display: grid; gap: var(--sp-2); }
.ck-fuellstand-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); }
.ck-fuellstand-hint { font-size: var(--fs-dense); line-height: 1.5; margin: 3px 0 0; max-width: var(--measure); }

/* Karte „Unbekannte SKU-Bausteine": seit 01.08. nur noch Hinweis + Verweis, das Formular liegt auf
   /einrichten/attribute. Die sechs Regeln des ausgebauten Formulars (.ck-sku-rows/-row/-token/-msg)
   sind am 03.08. mitgegangen — sie standen noch da, ohne dass es dazu noch Markup gab. */
.ck-sku-foot { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* ── Arbeitspakete in der Karte ────────────────────────────────────────────── */
.ck-packs { display: grid; gap: var(--sp-2); }
.ck-pack { border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); overflow: hidden; }
/* Dasselbe Raster wie der Kartenkopf, eine Ebene kleiner: Caret · Name · Kennzahl · Zusatz rechts.
   `margin-left: auto` auf dem Zusatz (vorher) hat ihn je nach Textlänge des Nachbarn wandern lassen —
   im Raster steht er in seiner eigenen Spur und flucht über alle Pakete hinweg. */
.ck-pack > summary { display: grid; grid-template-columns: 14px minmax(0, 1fr) auto auto;
  align-items: center; gap: 0 var(--sp-3); min-height: 42px; padding: var(--sp-2) var(--sp-3);
  cursor: pointer; list-style: none; }
.ck-pack > summary::-webkit-details-marker { display: none; }
.ck-pack > summary::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-icon);
  justify-self: center; transition: transform .15s ease; }
.ck-pack[open] > summary::before { transform: rotate(90deg); }
.ck-pack > summary:hover { background: var(--hover); }
.ck-pack[open] > summary { background: var(--sunken); box-shadow: inset 0 -1px 0 var(--border); }
.ck-pack-name { font-weight: 600; font-size: var(--fs-ui); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ck-pack-stat { font-size: var(--fs-dense); color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ck-pack-sub { font-size: var(--fs-micro); color: var(--text-faint); text-align: right; }
.ck-pack-actions { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Nur IM Paket ist die Aktionszeile eine Fußleiste. Steht dieselbe Zeile direkt in einer Karte
   (`.ck-body`), ist sie eine gewöhnliche Knopfreihe und bekommt keinen zweiten Rahmen. */
.ck-pack > .ck-pack-actions { padding: var(--sp-3); border-top: 1px solid var(--border); background: var(--sunken); }
/* Regel 1: die Primäraktion je Paket steht rechts und ist die einzige gefüllte. */
.ck-pack-actions .btn--primary { margin-left: auto; }
.ck-msg { padding: var(--sp-2) var(--sp-3); font-size: var(--fs-dense); }
/* Fußnote INNERHALB eines Pakets: `.ck-hint` allein hat kein Padding (es steht sonst in `.ck-body`,
   das seines mitbringt) — im Paket säße der Satz sonst an der Kante. */
.ck-pack-note { padding: var(--sp-2) var(--sp-3) 0; }
.ck-pack > .ck-pack-note:last-child { padding-bottom: var(--sp-3); }
.ck-comp-noek { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; padding: var(--sp-3) var(--sp-3) 0; }
.ck-comp-laenge { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); align-items: baseline;
  padding: var(--sp-3) var(--sp-3) 0; }
.ck-comp-laenge > .dim { flex: 1; min-width: 18rem; }

/* ── Nachgeladene Listen: eine Scroll-Fläche, ein Tabellenbild ────────────────
   Der teuerste Befund des Umbaus: die per JS nachgeladenen Tabellen (Preis-Vorschau, Wettbewerbs-Liste,
   Sortimentslücken) hatten ÜBERHAUPT KEIN CSS — `renderCompRows()` & Co. schreiben nacktes `<table>` in
   einen Container, der nur `padding` trug. Sieben Spalten in Browser-Standard, ohne Kopfabsetzung, ohne
   Zeilenlinie, in beliebiger Höhe mitten in der Karte. Die Klassen `is-suspect`/`is-stop`, die das JS
   seit Monaten setzt und in seinen Kommentaren als „getönt" beschreibt, waren nirgends definiert.
   Deshalb hier: EINE Fläche (gedeckelte Höhe, klebender Kopf), EIN Tabellenbild, und die zwei Tönungen,
   die das JS ohnehin ansagt. Kein `border-collapse: collapse` — das killt Rahmen auf klebenden `th`. */
.ck-scroll { max-height: 460px; overflow: auto; padding: var(--sp-3);
  border-top: 1px solid var(--border); background: var(--sunken); }
.ck-scroll > p { margin: 0 0 var(--sp-2); font-size: var(--fs-dense); line-height: 1.5; max-width: var(--measure); }
.ck-scroll table { width: 100%; border-collapse: separate; border-spacing: 0;
  border: 1px solid var(--border); background: var(--canvas);
  font-size: var(--fs-dense); font-variant-numeric: tabular-nums; }
/* `top: 0` heißt: Oberkante des SCROLLPORTS (= Padding-Box), nicht des Inhalts — der Kopf klebt also am
   sichtbaren Rand der Fläche und der Innenabstand scrollt hinter ihm weg. Ein negativer Offset würde ihn
   um genau diesen Abstand nach oben aus dem Sichtbaren ziehen. */
.ck-scroll thead th { position: sticky; top: 0; z-index: 2;
 padding: 7px var(--sp-2); background: var(--table-head); text-align: left; white-space: nowrap;
 color: var(--text-muted); box-shadow: inset 0 -1px 0 var(--border-strong); }
.ck-scroll th.num, .ck-scroll td.num { text-align: right; }
.ck-scroll td { padding: 7px var(--sp-2); border-top: 1px solid var(--row-line); vertical-align: top; line-height: 1.45; }
.ck-scroll tbody tr:first-child td { border-top: 0; }
.ck-scroll tbody tr:hover td { background: var(--hover); }
/* Die zwei Tönungen, die das JS ansagt: „prüfen" (Marge unbekannt / außerhalb des Bands) und
   „bleibt" (Angleich stoppt an der Mindestmarge). Nur die Fläche, nie die Schrift einfärben. */
.ck-scroll tr.is-suspect td { background: color-mix(in srgb, var(--warn) 9%, var(--canvas)); }
.ck-scroll tr.is-stop td { color: var(--text-muted); }

/* ── Szenarien (Marge retten) ──────────────────────────────────────────────── */
.ck-scenarios { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: var(--sp-2); padding: var(--sp-3); }
/* Zwei Spalten, nicht eine: der Radio ist ein eigenes Grid-Kind und bekäme sonst seine eigene Zeile —
   er stand dann ÜBER dem Namen statt davor, und die Kachel las sich wie eine Überschrift mit Punkt. */
.ck-sc { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px var(--sp-2);
  align-items: start; align-content: start; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border);
  border-radius: var(--r-2); background: var(--sunken); cursor: pointer; }
.ck-sc:hover { border-color: var(--border-strong); }
.ck-sc:has(input:checked) { border-color: var(--primary); background: var(--selected);
  box-shadow: inset 0 0 0 1px var(--primary); }
.ck-sc input[type=radio] { grid-column: 1; grid-row: 1; accent-color: var(--primary); margin: 3px 0 0; }
.ck-sc-name { grid-column: 2; font-size: var(--fs-dense); font-weight: 600; }
/* Ergebnis und Fußzeile stehen unter dem NAMEN, nicht unter dem Radio — sonst bricht die Einrückung. */
.ck-sc-result { grid-column: 2; font-size: var(--fs-dense); }
.ck-sc-meta { grid-column: 2; font-size: var(--fs-micro); color: var(--text-muted); line-height: 1.45; font-variant-numeric: tabular-nums; }
.ck-scv-input { width: 54px; height: 22px; padding: 0 var(--sp-1); border: 1px solid var(--border-strong);
  border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: 600 var(--fs-dense) var(--f-ui);
  text-align: right; font-variant-numeric: tabular-nums; }

/* ── „Vorgemerkt"-Rail (rechte Kanban-Spalte) ──────────────────────────────── */
.ck-rail { border-left: 1px solid var(--border); padding-left: var(--sp-4); min-width: 0; }
.ck-rail-head { font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-muted); margin-bottom: var(--sp-2); }
.ck-rail-head .dim { text-transform: none; letter-spacing: 0; font-weight: 400; }
.ck-rail-items { display: grid; gap: var(--sp-2); }
.ck-rail-card { padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); }
.ck-rail-label { font-size: var(--fs-dense); font-weight: 500; line-height: 1.4; }
.ck-rail-meta { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap;
  margin: var(--sp-1) 0; font-size: var(--fs-micro); color: var(--text-muted); }
.ck-rail-empty { font-size: var(--fs-micro); line-height: 1.5; margin: 0; }

/* ── „passt so"-Quittungen ─────────────────────────────────────────────────── */
.ck-acked { padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.ck-acked-head { font-size: var(--fs-dense); font-weight: 600; }
.ck-acked-items { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.ck-acked-item { display: inline-flex; align-items: center; gap: var(--sp-1); padding: 2px var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-full); background: var(--sunken); font-size: var(--fs-micro); }

/* ── Wettbewerbs-Steuerung in der Karte ────────────────────────────────────── */
.ck-comp-ctrl { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3); background: var(--sunken); border-bottom: 1px solid var(--border); }
.ck-comp-targets { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.ck-comp-ctrl-lbl { font-size: var(--fs-micro); color: var(--text-muted); text-transform: uppercase; letter-spacing: .07em; font-weight: 600; }
.ck-comp-btn { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 var(--sp-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas);
  font-size: var(--fs-dense); cursor: pointer; }
.ck-comp-btn:hover { background: var(--hover); }
/* Die Marke des Wettbewerbers färbt nur die Kante des AKTIVEN Knopfes — sie sagt „gegen den vergleiche
   ich gerade", das ist Bedeutung. Auf den inaktiven wäre sie Dekoration (Regel 2). */
.ck-comp-btn.is-active { border-color: var(--cc, var(--primary)); box-shadow: inset 0 -2px 0 var(--cc, var(--primary)); font-weight: 600; }
.ck-comp-logo { height: 12px; width: auto; max-width: 44px; object-fit: contain; }
.ck-comp-slider-lbl, .ck-comp-scope-lbl { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-dense); }
.ck-comp-slider { width: 120px; accent-color: var(--primary); }
.ck-comp-pct-out { font-weight: 600; font-variant-numeric: tabular-nums; }
.ck-comp-scope { accent-color: var(--primary); margin: 0; }

/* ── Verlauf & Status ──────────────────────────────────────────────────────── */
.ck-decisions-wrap { max-height: 420px; overflow: auto; border: 1px solid var(--border); border-radius: var(--r-2); }

/* ── Trichter Sortiment → Shop ─────────────────────────────────────────────── */
.ck-steps { padding: var(--sp-3) 14px; }
.ck-steps > summary { cursor: pointer; font-size: var(--fs-dense); font-weight: 600; list-style: none; }
.ck-steps > summary::-webkit-details-marker { display: none; }
.ck-steps > summary::before { content: "▸ "; color: var(--text-faint); }
.ck-steps[open] > summary::before { content: "▾ "; }
.ck-steps ol { margin: var(--sp-2) 0 0; padding-left: 20px; display: grid; gap: 4px;
  font-size: var(--fs-dense); line-height: 1.6; max-width: var(--measure); }
/* Die vier Stufen sind eine Bilanz: Gesamt = Shop + Potenzial + Ausgeschlossen. Deshalb gleich breit
   und mit Pfeilen — sie sind eine Rechnung, keine vier Kacheln. `1fr 0fr` je Paar hält die Stufen
   exakt gleich breit; als reines Flexbox-Wrap sind sie es nur, solange keine Unterzeile umbricht. */
.funnel { display: grid; grid-template-columns: repeat(3, 1fr auto) 1fr; align-items: stretch; gap: var(--sp-2); }
.fstage { display: flex; flex-direction: column; gap: 2px; min-width: 0; min-height: 92px;
  padding: var(--sp-3) 14px; border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
  border-radius: var(--r-2); background: var(--canvas); color: var(--text); }
.fstage:hover { text-decoration: none; background: var(--hover); border-color: var(--border-strong); border-left-color: var(--border-strong); }
.fstage.good { border-left-color: var(--good); }
.fstage.good:hover { border-left-color: var(--good); }
.fstage.warn { border-left-color: var(--warn); }
.fstage.warn:hover { border-left-color: var(--warn); }
.fstage.muted { border-left-color: var(--border-strong); }
.flabel { display: flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-micro); color: var(--text-muted); font-weight: 600; }
.fval { font: 600 var(--fs-kpi)/1.15 var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
/* Die Unterzeile wird nach unten gedrückt: so stehen die vier Zahlen auf EINER Grundlinie, auch wenn
   eine Unterzeile zweizeilig umbricht. Ohne das wandert jede Zahl mit der Länge ihres eigenen Textes. */
.fsub { margin-top: auto; padding-top: var(--sp-1); font-size: var(--fs-micro); color: var(--text-faint); line-height: 1.4; }
.farrow { align-self: center; color: var(--text-faint); }
@media (max-width: 1080px) {
  .funnel { grid-template-columns: 1fr 1fr; }
  .farrow { display: none; }
}

/* ── Verteilungs-Leiste (Ausschluss-Aufschlüsselung) ──────────────────────────
   War eine Reihe aus fünf gleich aussehenden grauen Text-Pillen. Fünf Zahlen in gleicher Größe, gleicher
   Farbe, gleichem Rahmen sind fünfmal dasselbe Gewicht — dass 2.732 die mit Abstand größte Gruppe ist und
   13 eine Randnotiz, stand nur in den Ziffern. Eine proportionale Leiste sagt genau das, bevor jemand
   liest. Die Rampe ist aus --chart-1 gemischt (nicht aus der Ampel): ein Ausschlussgrund ist eine
   Kategorie, kein Urteil — und über --chart-1 bleibt sie whitelabel-fähig. */
.segbar { display: grid; gap: var(--sp-2); }
.segbar-head { font-size: var(--fs-micro); color: var(--text-muted); line-height: 1.45; }
.segbar-track { display: flex; height: 10px; border-radius: var(--r-full); overflow: hidden; background: var(--sunken); }
.segbar-seg { width: var(--w); min-width: 3px; background: color-mix(in srgb, var(--chart-1) var(--t), var(--sunken)); }
.segbar-seg + .segbar-seg { border-left: 1px solid var(--canvas); }
.segbar-seg:hover { background: var(--primary); }
.segbar-legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--sp-1) var(--sp-4); }
.segbar-item { display: grid; grid-template-columns: 8px auto minmax(0, 1fr); align-items: baseline;
  gap: var(--sp-2); padding: 3px 0; color: var(--text); font-size: var(--fs-dense); }
.segbar-item:hover { text-decoration: none; color: var(--accent); }
.segbar-dot { width: 8px; height: 8px; border-radius: 2px; align-self: center;
  background: color-mix(in srgb, var(--chart-1) var(--t), var(--sunken)); }
.segbar-item b { font-weight: 600; font-variant-numeric: tabular-nums; }
.segbar-item span { color: var(--text-muted); font-size: var(--fs-micro); line-height: 1.4; }

/* ── Kennzahlen ────────────────────────────────────────────────────────────── */
.ck-onboard { padding: var(--sp-4); }
.ck-onboard .workflow-grid { margin-top: var(--sp-3); }
/* Nicht .kpirow: die steht als 84-px-Leiste unter dem Seitenkopf. Hier sind es zwei Kacheln MITTEN auf
   der Seite, mit Unterzeile und Balken — eine andere Figur, deshalb ein eigener Name.
   `grid-auto-rows: 1fr` zwingt beide Kacheln auf dieselbe Höhe, auch wenn nur eine einen Balken trägt. */
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--sp-3); grid-auto-rows: 1fr; }
.kpi-card { display: flex; flex-direction: column; padding: var(--sp-4) 14px; border: 1px solid var(--border);
  border-left: 3px solid var(--border-strong); border-radius: var(--r-2); background: var(--canvas); color: var(--text); }
.kpi-card:hover { text-decoration: none; background: var(--hover); }
.kpi-num { font: 600 var(--fs-kpi)/1.05 var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.kpi-label { font-size: var(--fs-dense); font-weight: 600; margin-top: 2px; }
.kpi-sub { font-size: var(--fs-micro); color: var(--text-muted); margin-top: 4px; line-height: 1.45; }
.kpi-sub strong { color: var(--text); }
/* Regel 2: die Ampel färbt die Kante und die Zahl — beide sagen dasselbe Urteil. */
.kpi-card.tone-good { border-left-color: var(--good); } .kpi-card.tone-good .kpi-num { color: var(--good); }
.kpi-card.tone-warn { border-left-color: var(--warn); } .kpi-card.tone-warn .kpi-num { color: var(--warn); }
.kpi-card.tone-bad  { border-left-color: var(--bad); }  .kpi-card.tone-bad  .kpi-num { color: var(--bad); }
.kpi-card.tone-none .kpi-num { color: var(--text-muted); }
.kpi-bar { height: 4px; border-radius: 2px; background: var(--border); margin-top: auto; overflow: hidden; }
.kpi-bar > span { display: block; height: 100%; background: var(--border-strong); }
.kpi-card.tone-good .kpi-bar > span { background: var(--good); }
.kpi-card.tone-warn .kpi-bar > span { background: var(--warn); }
.kpi-card.tone-bad  .kpi-bar > span { background: var(--bad); }

/* ── „Was ansteht" ─────────────────────────────────────────────────────────── */
.workflow-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: var(--sp-2); }
.wf { display: flex; flex-direction: column; gap: 2px; padding: var(--sp-3); border: 1px solid var(--border);
  border-radius: var(--r-2); background: var(--canvas); color: var(--text); }
.wf:hover { text-decoration: none; background: var(--hover); border-color: var(--border-strong); }
.wf-step { width: 22px; height: 22px; border-radius: var(--r-full); background: var(--primary); color: #fff;
  display: grid; place-items: center; font: 600 var(--fs-dense) var(--f-ui); }
.wf strong { font-size: var(--fs-ui); font-weight: 600; }
.wf > span:last-child { font-size: var(--fs-micro); color: var(--text-muted); line-height: 1.45; }
/* Aufgaben als halbe Zeilen statt Kacheln (C/2-Rework 18.07.): Zahl links, Label + Erklärzeile daneben, Pfeil
   rechts. auto-fill/minmax = zwei nebeneinander auf breitem Grund, eine je Zeile schmal.
   `align-items: start` ist Pflicht: sonst streckt das Raster JEDE Zeile auf die Höhe der höchsten — und
   sobald die Verlust-Zeile darunter aufklappt, standen ihre drei Nachbarn als 400 px hohe leere Kästen da. */
.wfrows { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: var(--sp-2); align-items: start; }
.wfrow { display: flex; align-items: center; gap: var(--sp-3); padding: 10px var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); color: var(--text); }
.wfrow:hover { text-decoration: none; background: var(--hover); border-color: var(--border-strong); }
.wfrow-num { flex: 0 0 auto; min-width: 2.4ch; text-align: right; font: 600 var(--fs-title)/1.1 var(--f-ui);
  font-variant-numeric: tabular-nums; }
.wfrow-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.wfrow-label { font-size: var(--fs-ui); font-weight: 600; }
.wfrow-sub { font-size: var(--fs-micro); color: var(--text-muted); line-height: 1.4; }
.wfrow-go { flex: 0 0 auto; color: var(--text-muted); font-size: var(--fs-ui); }
/* „Preis & Marge"-Block (Cockpit, #4): die zwei Health-KPIs als Kopf, darunter die Signal-Zeilen (wfrow), Verlust
   zuoberst. Absorbiert die frühere „Kennzahlen"-Sektion — ein Blickfang pro Sektion (ux-designer 18.07.). */
/* Die Verlust-Zeile ist eine Alert-Card, keine Zeile unter Gleichen: sie spannt IMMER die volle Breite.
   Vorher lag sie als gewöhnliches Rasterkind in einer 340-px-Spalte — beim Aufklappen wurde die
   siebenspaltige Tabelle in diese Spalte gepresst und musste waagerecht gescrollt werden, während
   rechts daneben leerer Raum stand. Ein Aufklapper darf das Raster nicht sprengen; also gehört er
   über die ganze Zeile. */
.pm-rows > .pl-details { grid-column: 1 / -1; }
.pm-alarm { border-color: color-mix(in srgb, var(--bad) 45%, var(--border));
  background: color-mix(in srgb, var(--bad) 6%, var(--canvas)); }
.pm-alarm:hover { background: color-mix(in srgb, var(--bad) 11%, var(--canvas)); border-color: var(--bad); }
.pm-alarm .wfrow-num { color: var(--bad); }
/* Verweis-Zeile „Marge zu niedrig" zeigt mit ↑ nach oben auf Card A — dezent, liest als „woanders lösen". */
.pm-ref .wfrow-go { color: var(--primary); font-weight: 700; }
/* Verlust-Liste: die „unter EK"-Zeile klappt inline auf und zeigt genau die N Verlustbringer (Item 2, dominik).
   Das <summary> trägt die .wfrow/.pm-alarm-Optik; nur den nativen Marker abschalten + Caret drehen. */
.pl-details > summary { list-style: none; cursor: pointer; }
.pl-details > summary::-webkit-details-marker { display: none; }
.pl-caret { display: inline-block; transition: transform .13s; }
.pl-details[open] .pl-caret { transform: rotate(90deg); }
/* Eigene Scroll-Fläche mit klebendem Kopf statt „so lang wie die Liste": zehn Verlustbringer sind der
   Regelfall, aber die Zahl ist nicht gedeckelt — ohne Deckel schiebt die Tabelle alles darunter weg. */
.pl-body { margin-top: var(--sp-2); max-height: 340px; overflow: auto;
  border: 1px solid var(--border); border-radius: var(--r-2); }
.pl-tbl { width: 100%; }
/* ⛔ Die Vergleichs-Artikelnummer kuerzt MIT Auslassungspunkten (19.08.). Bei 950 px stand in der
   Spalte "EK anderer Bauform" `M-TR-20x4-RH-PC` — mittendrin gekappt, ohne jedes Zeichen dafuer, und
   damit von einer echten anderen Nummer nicht zu unterscheiden. Die Nachbarspalte kuerzte laengst
   mit "…"; zwei Abschneide-Regeln in einer Tabelle sind eine zu viel. */
.pl-sub { font-size: var(--fs-micro); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ansteht-more > summary { list-style: none; cursor: pointer; font-size: var(--fs-dense); font-weight: 600;
  color: var(--text-muted); display: inline-flex; align-items: center; gap: var(--sp-1); }
.ansteht-more > summary::-webkit-details-marker { display: none; }
.ansteht-more > summary::before { content: "▸"; }
.ansteht-more[open] > summary::before { content: "▾"; }
.ansteht-more[open] > summary { color: var(--text); }
.ansteht-more .wfrows { margin-top: var(--sp-2); }

/* ── Schmale Fenster: der Kartenkopf gibt die feste Metrik-Spalte auf ─────────
   Unter ~1000 px bleibt für Titel + Fakten + 300-px-Metrik zu wenig übrig; die Fakten-Pillen würden den
   Titel abschneiden. Dann stapelt der Kopf: Titel oben, Metrik darunter rechts, Pillen dazwischen. */
@media (max-width: 1000px) {
  .ck-head { grid-template-columns: 14px minmax(0, 1fr) auto; row-gap: var(--sp-2); }
  .ck-head-num { grid-column: 2 / -1; grid-template-columns: minmax(0, 1fr) auto; }
  .ck-facts { grid-column: 2 / -1; justify-content: flex-start; max-width: none; }
  .ck-cols { grid-template-columns: minmax(0, 1fr); }
  .ck-rail { border-left: 0; border-top: 1px solid var(--border); padding: var(--sp-3) 0 0; }
}

/* ── Die zwei Analyse-Blöcke nebeneinander ─────────────────────────────────── */
/* `stretch`, nicht `start` — „Analyse-Blöcke gleich hoch" ist Saschas Anforderung (Commit 528d047).
   Damit die rechte Karte das auch füllt, wächst das Chart mit, statt auf 320 px stehen zu bleiben. */
.ov-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); align-items: stretch; }
.ov-cols > .chart-card { display: flex; flex-direction: column; }
.ov-cols > .chart-card > .chart-wrap { flex: 1 1 auto; height: auto; min-height: 400px; }
@media (max-width: 1100px) { .ov-cols { grid-template-columns: 1fr; } }
.ov-card-head { padding: var(--sp-3) 14px; border-bottom: 1px solid var(--border); }
.ov-card-head h3 { margin: 0 0 2px; font-size: var(--fs-ui); font-weight: 600; }
.ov-card-head .dim { font-size: var(--fs-micro); line-height: 1.45; display: block; }
.chart-wrap { position: relative; height: 320px; padding: var(--sp-3); }

/* ── Shop-Abdeckung je Warengruppe ─────────────────────────────────────────── */
.catbars { padding-bottom: var(--sp-2); }
/* Die Marge-Spalte trägt seit #262 ihre Basis mit und braucht dafür Platz. Die Basis steht UNTER der
   Marge (zweizeilig), nicht daneben: „72.9% aus 323 von 1670" in einer Zeile wäre breiter als das
   Warengruppen-Label daneben — der Ausschnitt schluckte das Wichtigste. */
.catbar { display: grid; grid-template-columns: minmax(0, 1fr) 110px 84px 96px 40px; align-items: center;
  gap: var(--sp-2); padding: 5px 14px; color: var(--text); font-size: var(--fs-dense); }
.catbar:hover { text-decoration: none; background: var(--hover); }
.catbar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.catbar-gap-tag { font: 600 var(--fs-micro)/14px var(--f-ui); color: var(--badge-warn-fg); background: var(--badge-warn-bg);
  border: 1px solid var(--badge-warn-bd); border-radius: var(--r-1); padding: 0 4px; margin-left: var(--sp-1); }
/* --chart-1, nicht --chart-2: im Dark-Theme fällt chart-2 auf #35516A und der gefüllte Teil war vom
   leeren Track kaum zu unterscheiden — der Balken sagte dann nichts mehr. Die Spur bekommt eine Kante,
   sonst verschwindet sie auf der Panel-Fläche. Und NICHT die Shop-Quellenfarbe (Entscheidung D: die
   Quelle trägt nur noch den Text-Chip, nie eine Fläche). */
.catbar-track { height: 8px; border: 1px solid var(--border); border-radius: 2px;
  background: var(--sunken); overflow: hidden; }
.catbar-fill { display: block; height: 100%; min-width: 2px; background: var(--chart-1); }
.catbar-num { font-size: var(--fs-micro); font-variant-numeric: tabular-nums; text-align: right; }
.catbar-db { font-size: var(--fs-dense); font-weight: 600; font-variant-numeric: tabular-nums; text-align: right;
  white-space: nowrap; }
/* Die Basis gehört zur Zahl, aber nicht auf ihre Lautstärke: eigene Zeile, gedämpft, normales Gewicht.
   Farbe explizit — sonst erbt sie das tone-* der Marge und färbt sich rot. */
.catbar-db-basis { display: block; color: var(--text-muted); font-weight: 400; font-size: var(--fs-micro);
  line-height: 1.2; }
.catbar-comp { font-size: var(--fs-micro); color: var(--warn); text-align: right; font-variant-numeric: tabular-nums; }
.catbar-rest { padding: var(--sp-2) 14px 0; font-size: var(--fs-micro); }
.catbar-potential { margin-top: var(--sp-3); padding: var(--sp-3) 14px 0; border-top: 1px solid var(--border); }
.catbar-potential-head { font-size: var(--fs-dense); font-weight: 600; }
.catbar-potential-sub { font-size: var(--fs-micro); line-height: 1.45; margin: 3px 0 var(--sp-2); }
.catbar-pot { display: flex; justify-content: space-between; gap: var(--sp-2); padding: 3px 0;
  font-size: var(--fs-dense); color: var(--text); }
.catbar-pot:hover { text-decoration: none; color: var(--accent); }
.tone-good { color: var(--good); } .tone-warn { color: var(--warn); } .tone-bad { color: var(--bad); }
/* ⚠️ Die drei oben sind Text-Utilities und faerben ALLES, worauf sie sitzen. Auf einer KARTE ist das
   falsch: `.kpi-card` traegt `tone-*` (fuer Kante + Zahl), hat zwar selbst `color: var(--text)`, aber
   bei gleicher Spezifitaet gewinnt die spaetere Regel — also die Utility. Ergebnis war, dass
   „EK-Abdeckung im Shop" rot dastand, obwohl Rot der ZAHL gilt und nicht ihrem Namen.
   Farbe ist Bedeutung: ein rotes Label neben einer roten Zahl sagt zweimal dasselbe, einmal davon falsch.
   Dieser Block holt die Kartenschrift zurueck; die Ampel behaelt Kante und Zahl (Regeln darueber).
   Der Bug war lokal schon einmal gepflastert worden (.catbar-db-basis) — das hier ist die Wurzel. */
.kpi-card.tone-good, .kpi-card.tone-warn, .kpi-card.tone-bad, .kpi-card.tone-none { color: var(--text); }
.tone-brand { color: var(--accent); } .tone-none { color: var(--text-muted); }

/* ── Roadmap ───────────────────────────────────────────────────────────────── */

/* ── IMPORT-BANNER ───────────────────────────────────────────────────────────
   import-banner.js läuft auf JEDER Seite (layout.html lädt es), erzeugt diese Klassen aber im JS —
   sie stehen in keinem Template. Beim Shell-Neubau ist das übersehen worden: auf /sortiment und
   /preise war der Banner seit dem 16.07. ungestylt (unsichtbar geblieben, weil er nur während eines
   Imports erscheint). Gefunden beim Cutover, 17.07.
   Er schwebt (position:fixed, z>0) — Schatten und Radius --r-2 sind hier richtig. Der Anker ist die
   Topbar-Höhe: --shell-header-h gibt es in der neuen Shell nicht mehr, sie heißt --h-topbar. */
.import-banner {
  position: fixed; top: calc(var(--h-topbar) + var(--sp-3)); right: var(--sp-4); z-index: 9999;
  display: flex; align-items: center; gap: var(--sp-3);
  max-width: min(90vw, 380px); padding: var(--sp-3);
  background: var(--canvas); color: var(--text);
  border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
  border-radius: var(--r-2); box-shadow: var(--sh-pop);
  transform: translateX(130%); opacity: 0; pointer-events: none;
  transition: transform .35s cubic-bezier(.22,1,.36,1), opacity .35s ease;
}
.import-banner.show { transform: translateX(0); opacity: 1; pointer-events: auto; }
/* Regel 2: die Kante trägt den Zustand. „läuft" ist kein Fehler — deshalb neutral-blau und nicht rot
   (app.css färbte den laufenden Import rot wie einen Absturz). */
.import-banner.running { border-left-color: var(--chart-1); }
.import-banner.done    { border-left-color: var(--good); }
.import-banner.error   { border-left-color: var(--bad); }
.import-banner .ib-body { min-width: 0; }
.import-banner .ib-title { font-weight: 600; font-size: var(--fs-ui); line-height: 1.3; }
.import-banner .ib-sub { color: var(--text-muted); font-size: var(--fs-micro); margin-top: 2px; }
.import-banner .ib-sub b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.import-banner .ib-spin {
  flex: none; width: 16px; height: 16px; border-radius: var(--r-full);
  border: 2px solid color-mix(in srgb, var(--chart-1) 25%, transparent);
  border-top-color: var(--chart-1); animation: ib-rot .7s linear infinite;
}
.import-banner .ib-mark {
  flex: none; width: 18px; height: 18px; border-radius: var(--r-full); display: grid; place-items: center;
  font-size: var(--fs-micro); font-weight: 600; color: #fff;
}
.import-banner.done  .ib-mark { background: var(--good); }
.import-banner.error .ib-mark { background: var(--bad); }
@keyframes ib-rot { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .import-banner .ib-spin { animation-duration: 2s; }
  .import-banner { transition: opacity .2s ease; transform: none; }
}

/* ── HANDY-ANSICHT (/mobil) ──────────────────────────────────────────────────
   Eigenes schlankes Layout ohne Shell — reine Ansicht, einspaltig, große Tap-Targets. Die Weiche in
   layout.html schickt echte Handys hierher. Die EINE erlaubte Eingabe ist die Roadmap-Schnellnotiz.
   Die Prefix-Verwirrung aus app.css ist hier nicht mitgekommen: `.m-prod-*`, `.m-cap`, `.m-side`
   gehören zu /export (dort heißt m- „Matrix"), nicht hierher — sie stehen beim Export. Und
   `.m-opt-*` hatte null Nutzer und stirbt ersatzlos mit app.css. */
body.mobil { max-width: 640px; margin: 0 auto; background: var(--canvas); }
.m-top { display: flex; align-items: center; justify-content: space-between;
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border);
  background: var(--canvas); position: sticky; top: 0; z-index: 10; }
.m-brand { display: inline-flex; align-items: center; gap: var(--sp-2); color: var(--text); font-weight: 600; }
.m-brand img { height: 20px; width: auto; }
.m-brand small { color: var(--text-muted); font-size: var(--fs-micro); letter-spacing: .08em; text-transform: uppercase; }
.m-menu-btn { border: 1px solid var(--border-strong); background: var(--sunken); color: var(--text);
  cursor: pointer; width: 40px; height: 40px; border-radius: var(--r-1); font-size: var(--fs-sect); line-height: 1; }
.m-sheet { display: flex; flex-direction: column; padding: var(--sp-1); gap: 1px;
  background: var(--canvas); border-bottom: 1px solid var(--border); }
.m-sheet a, .m-sheet button { display: block; width: 100%; text-align: left; box-sizing: border-box;
  padding: 13px var(--sp-3); border: 0; background: none; color: var(--text); font: inherit;
  font-size: var(--fs-body); font-weight: 500; border-radius: var(--r-1); cursor: pointer; }
.m-sheet a:hover, .m-sheet button:hover { background: var(--hover); text-decoration: none; }
.m-sheet-logout { color: var(--text-muted); }
.m-main { padding: var(--sp-4) var(--sp-3) var(--sp-8); }
.m-main h1 { font-size: var(--fs-title); margin: 2px 0 var(--sp-3); font-weight: 600; }
.m-main .panel { margin-bottom: var(--sp-3); padding: var(--sp-3); }
.m-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-bottom: var(--sp-2); }
.m-card-head strong { font-size: var(--fs-body); }
.m-hero { text-align: center; }
.m-hero strong { font-size: var(--fs-sect); display: block; }
.m-hero .dim { font-size: var(--fs-dense); }
/* Die zwei Kennzahlen am Handy (#262/#263): nebeneinander, weil sie zusammen gelesen werden — die Marge
   ist nur so viel wert wie die EK-Abdeckung darunter. `min-width:0` ist Pflicht, sonst schiebt der lange
   Basis-Text („aus 1176 von 1184 mit EK") das Grid über den 390-px-Rand. */
.m-kpis { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-2); }
.m-kpi { min-width: 0; display: flex; flex-direction: column; gap: 2px; color: var(--text);
  border-left: 3px solid var(--border-strong); padding-left: var(--sp-2); }
.m-kpi:hover { text-decoration: none; }
.m-kpi b { font-size: var(--fs-sect); font-variant-numeric: tabular-nums; }
.m-kpi-label { font-size: var(--fs-dense); font-weight: 600; }
.m-kpi .dim { font-size: var(--fs-micro); line-height: 1.3; }
/* Regel 2 wie bei .kpi-card oben: die Ampel färbt Kante und Zahl. Die Selektoren brauchen die
   `.m-kpi`-Klasse davor — `.tone-good` allein (0-1-0) steht weiter oben in der Datei und verliert gegen
   das `color` von `.m-kpi`; am Handy standen beide Zahlen deshalb in Textfarbe statt in der Ampel. */
.m-kpi.tone-good { border-left-color: var(--good); } .m-kpi.tone-good b { color: var(--good); }
.m-kpi.tone-warn { border-left-color: var(--warn); } .m-kpi.tone-warn b { color: var(--warn); }
.m-kpi.tone-bad  { border-left-color: var(--bad); }  .m-kpi.tone-bad  b { color: var(--bad); }
.m-kpi.tone-none b { color: var(--text-muted); }
.m-funnel { display: flex; flex-direction: column; gap: var(--sp-1); }
.m-fstage { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 1px var(--sp-3);
  padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
  border-radius: var(--r-2); background: var(--sunken); }
.m-fstage b { grid-row: 1 / 3; grid-column: 2; font-size: var(--fs-title); font-variant-numeric: tabular-nums; font-weight: 600; }
.m-fstage .m-flabel { font-weight: 600; font-size: var(--fs-ui); }
.m-fstage .dim { font-size: var(--fs-micro); }
.m-fstage.good { border-left-color: var(--good); }
.m-fstage.warn { border-left-color: var(--warn); }
.m-fstage.muted { border-left-color: var(--border-strong); }
.m-farrow { text-align: center; color: var(--text-faint); line-height: 1; }
.m-todo-total b { font-size: var(--fs-kpi); font-weight: 600; font-variant-numeric: tabular-nums; }
.m-todo-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.m-todo-list li { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-ui); }
.m-todo-n { flex: none; min-width: 30px; text-align: center; font-weight: 600; font-size: var(--fs-dense);
  padding: 1px var(--sp-1); border-radius: var(--r-1); background: var(--badge-neu-bg);
  color: var(--badge-neu-fg); font-variant-numeric: tabular-nums; }
.m-todo-n.bad { background: var(--badge-err-bg); color: var(--badge-err-fg); }
.m-ampel { display: flex; align-items: flex-start; gap: var(--sp-2); }
.m-ampel .m-dot { flex: none; width: 10px; height: 10px; border-radius: var(--r-full); margin-top: 4px; }
.m-ampel div { display: flex; flex-direction: column; }
.m-ampel div b { font-size: var(--fs-ui); }
.m-ampel div .dim { font-size: var(--fs-micro); }
.m-ampel.good .m-dot { background: var(--good); }
.m-ampel.warn .m-dot { background: var(--warn); }
.m-ampel.info .m-dot { background: var(--chart-1); animation: ib-rot 1.4s linear infinite; }
.m-bar { margin: var(--sp-2) 0; }
.m-bar-top { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-dense); margin-bottom: 3px; }
.m-bar-label { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.m-bar-val { flex: none; white-space: nowrap; font-variant-numeric: tabular-nums; }
.m-bar-track { height: 8px; border: 1px solid var(--border); border-radius: 2px; background: var(--sunken); overflow: hidden; }
.m-bar-fill { display: block; height: 100%; min-width: 2px; background: var(--chart-1); }
.m-bar-fill.tone-good { background: var(--good); }
.m-bar-fill.tone-warn { background: var(--warn); }
.m-bar-fill.tone-bad { background: var(--bad); }
.m-db { font-weight: 600; }
.m-roadmap > summary { list-style: none; cursor: pointer; }
.m-roadmap > summary::-webkit-details-marker { display: none; }
.m-roadmap > summary::before { content: "▸ "; color: var(--text-faint); }
.m-roadmap[open] > summary::before { content: "▾ "; }
.m-rm-add { display: flex; gap: var(--sp-1); align-items: center; margin: var(--sp-3) 0; flex-wrap: wrap; }
.m-rm-add input { flex: 1 1 8rem; height: 34px; padding: 0 var(--sp-2); border: 1px solid var(--border-strong);
  border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: inherit; font-size: var(--fs-ui); }
.m-rm-msg { flex-basis: 100%; font-size: var(--fs-micro); }
.m-rm-chip { padding: var(--sp-2); margin-bottom: var(--sp-1); background: var(--sunken);
  border: 1px solid var(--border); border-left: 3px solid var(--cc, var(--border));
  border-radius: var(--r-1); font-size: var(--fs-dense); }
.m-foot { text-align: center; font-size: var(--fs-micro); margin-top: var(--sp-6); }
.m-empty { margin: var(--sp-1) 0 0; font-size: var(--fs-dense); }
.m-rest { font-size: var(--fs-micro); margin: var(--sp-2) 0 0; }

/* ── /sortiment/ausschluesse — Regelformular + Wirkungs-Liste ────────────────
   Eigener Archetyp: die Seite ist kein Workbench-View. Oben die REGELN (überlappende Reichweiten),
   unten ihre WIRKUNG (disjunkte Grund-Zerlegung). Zwei Tabellen, beide randfüllend (Regel 3), die
   Prosa dazwischen in `.page`.
   ─────────────────────────────────────────────────────────────────────────── */

.au-head > .head-row { margin-bottom: var(--sp-2); }
.au-head h2 { margin: 0; font-size: var(--fs-sect); font-weight: 600; }
/* Die Fehler-Note steht direkt über der Überschrift, die sie betrifft — kein doppelter Abstand. */
.au-pb0 { padding-bottom: 0; }
/* Der zweite Abschnitt ist ein neuer Gedanke, kein neuer Absatz. */
.au-eff { padding-top: var(--sp-6); }
.au-eff h2 { scroll-margin-top: var(--sp-3); }

/* ── REGELFORMULAR ───────────────────────────────────────────────────────────
   Kein `.form-layout`/`.frow`: das hier ist kein Einstellungs-Formular mit 160-px-Labels, sondern ein
   Bedingungs-Baukasten. 12 Felder untereinander wären zwei Bildschirmhöhen für einen Ausdruck, den
   man in einem Blick lesen will. Deshalb: Fieldsets als Gliederung, Felder als umbrechende Zeile.
   ─────────────────────────────────────────────────────────────────────────── */
.au-form { margin: var(--sp-3) 0 0; padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); }
.au-fset { margin-bottom: var(--sp-4); }
.au-fset:last-of-type { margin-bottom: var(--sp-3); }
.au-fields { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-2) var(--sp-3); }

/* Label über dem Feld: bei umbrechenden Feldern ist ein Label DANEBEN der schnellste Weg zu einer
   Zeile, in der Label und Feld nicht mehr zusammengehören. */
.au-f { display: grid; gap: 3px; min-width: 0; flex: 0 1 200px;
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.au-f--xs { flex: 0 0 118px; }
/* Die Notiz darf länger sein als ein Select, aber nicht die ganze Zeile: ein 1100 px breites Feld für
   „China-Schrott" verspricht einen Aufsatz. */
.au-f--wide { flex: 0 1 360px; }
.au-f .select, .au-f .input { width: 100%; }
.au-check { display: inline-flex; align-items: center; gap: 6px; height: 28px; font-size: var(--fs-ui); cursor: pointer; }
.au-check input { accent-color: var(--primary); margin: 0; }
/* attr-picker.js klont dieses Paar für „+ Eigenschaft" — es muss als Einheit umbrechen. */
.au-pair { display: flex; align-items: flex-end; gap: var(--sp-2); flex: 0 1 420px; min-width: 0; }
.au-pair .au-f { flex: 1 1 0; }

/* Die UND-Regel stand vorher in einem „?"-Tooltip über der Seite. Sie gehört ins Formular: dort gilt
   sie, und dort liest man sie, bevor man baut — „ein Tooltip ist versteckte Dokumentation". */
.au-hints { max-width: var(--measure); margin: 0 0 var(--sp-3);
  font: 400 var(--fs-dense)/18px var(--f-ui); color: var(--text-muted); }
.au-hints strong { color: var(--text); font-weight: 600; }

.au-actions { display: flex; align-items: center; gap: var(--sp-2);
  padding-top: var(--sp-3); border-top: 1px solid var(--border); }
/* Die Vorschau-Meldung ist das Ergebnis, die Knöpfe sind die Ursache — Ergebnis links, Knöpfe rechts. */
.au-msg { flex: 1; margin: 0; min-height: 16px; font-size: var(--fs-dense); }
.au-msg strong { color: var(--text); }

/* ── REGEL-TABELLE ───────────────────────────────────────────────────────────
   Einwertige Zeilen -> `grid--evt` (32 px). Die Spaltengeometrie steht hier statt 7× inline im
   Template; `table-layout: fixed` nimmt die Breiten aus der ersten Zeile.
   ─────────────────────────────────────────────────────────────────────────── */
/* ⚠️ Die Breiten waren nur ein VORSCHLAG: die Tabelle läuft mit `table-layout: auto`, und dort verteilt
   der Browser den Rest auf die Textspalten. Gemessen bekam die meist LEERE Notiz-Spalte 902 px, während
   „trifft (draußen)" bei x=1654 anfing — auf einem 1600 breiten Schirm also außerhalb. Die drei Zahlen,
   wegen derer man auf diese Seite geht, waren zu 36 Regeln unsichtbar, und weil „Aktion" rechts angeheftet
   klebt, sah die Tabelle aus, als wäre sie zu Ende.
   Jetzt trägt die BEDINGUNG den Dehnungsdruck (sie darf lang sein), alle anderen sind gedeckelt. */
.grid [data-c="au-typ"]  { width: 104px; }
.grid [data-c="au-cond"] { width: auto; min-width: 260px; }
.grid [data-c="au-note"] { width: 200px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grid [data-c="au-out"]  { width: 108px; min-width: 108px; white-space: nowrap; }
.grid [data-c="au-live"] { width: 108px; min-width: 108px; white-space: nowrap; }
.grid [data-c="au-var"]  { width: 88px; min-width: 88px; white-space: nowrap; }
/* Die Regel-Tabelle hat nur EINE Aktion („Entfernen") — 168 px wie die Workbench wären zwei Drittel Luft. */
.au-rules .col-act { width: 108px; }
.au-rules .row-acts form { display: inline-flex; margin: 0; }

/* Eine Bedingung ist ein UND-Ausdruck, keine Kategorie — deshalb eine neutrale Ausdrucks-Marke und
   kein Badge. Ein Badge behauptet „so einer bin ich"; hier steht „so lautet die Formel". */
.au-cond { display: inline-flex; align-items: center; gap: 4px; height: 18px; padding: 0 5px;
  border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas);
  font: 400 var(--fs-micro)/18px var(--f-ui); color: var(--text-muted); white-space: nowrap; }
.au-cond + .au-cond { margin-left: 3px; }
.au-cond b { font-weight: 600; color: var(--text); }

/* ── FILTERZEILE DER WIRKUNGS-LISTE ──────────────────────────────────────────
   `.fbar` steht sonst über dem Canvas; hier steht sie IM Canvas direkt über ihrer Tabelle — sie
   filtert diese Tabelle, nicht die Regeln darüber. Deshalb bekommt sie auch eine Oberkante.
   ─────────────────────────────────────────────────────────────────────────── */
.au-fbar { border-top: 1px solid var(--border); }
.au-search { display: flex; align-items: center; gap: var(--sp-1); margin: 0; }
.au-search .input { width: 220px; }

/* ── WIRKUNGS-LISTE ──────────────────────────────────────────────────────────
   EK/VK/Länge/Grund erben ihre Breiten aus app-new.css (`[data-c="wawiek"]` usw.) — dieselbe Spalte
   bedeutet dasselbe und ist deshalb gleich breit wie in der Arbeitsfläche.
   ─────────────────────────────────────────────────────────────────────────── */
.grid [data-c="au-wg"]    { width: 190px; }
.grid [data-c="au-grund"] { width: 210px; }
/* Nur „↩ aufnehmen" + Detail — dieselbe Rechnung wie oben. */
/* ⛔ NICHT sticky (20.07.). `.grid .col-act` heftet die Aktionsspalte rechts an und legt einen deckenden
   Schatten nach links (`-8px 0 12px -6px`). In dieser Tabelle liegt daneben die GRUND-Spalte — also
   ausgerechnet die Antwort, für die es die Seite gibt. In der Gruppenansicht stand dort nur noch
   „Warengruppe au…", „Artikelnummer-M…", während dieselbe Angabe in der Einzelsuche vollständig
   erschien: zwei Ansichten, eine funktionierte. Angeheftet bringt sie hier ohnehin nichts — sie ist
   die letzte Spalte und wird beim Querscrollen nicht verdeckt. */
#excl-table .col-act { position: static; width: 128px; box-shadow: none; }
/* Der Grund braucht Platz zum Ausschreiben, statt hinter der Aktionsspalte zu enden. */
#excl-table [data-c="status"] { min-width: 190px; }
/* Das Detail-Icon der Workbench erscheint auf Hover; hier ist es der einzige Weg in die Zeile und
   darf nicht erst gesucht werden. */
#excl-table .row-acts .icon-btn--row { opacity: 1; }

/* „+ Preise zeigen": EK/VK/DB gemeinsam ein- und ausblenden. Eine Regel im Stylesheet statt Klassen an
   Zellen — sie gilt auch für Gruppen, die erst später nachgeladen werden. Bei `table-layout: fixed`
   nimmt der Browser die Breiten aus der ersten Zeile: ein ausgeblendetes th ist dort schlicht nicht
   vorhanden, und nichts rutscht. */
#excl-table.prices-hidden .col-price { display: none; }

/* ── /crossselling — Cross-Selling (Cutover 2026-07-17) ─────────────────────────
   Der Präfix .cs- war schon der richtige — die Figuren .cs-grid/.cs-form/.cs-check/.cs-preview
   bleiben unter ihrem Namen, ihre Werte sind aus den Tokens neu abgeleitet (die alten standen in rem).
   Was ersatzlos entfiel, weil es einen Nachfolger hat:
     .info-head → {% block pagehead %} · .wiki-hint → .icon-btn im pagehead-tools
     .src-head  → .panel-head · .card → .panel · .table → .grid
     .ampel.bad → .note--err (dieselbe Aussage, ohne den Ampelpunkt: der Kasten IST schon die Warnung)
     .chips/.chip-tog/.chip-tog.on → .fchips/.chip/.chip.is-on (die Facetten-Chips der Shell)
     .prop-row → .cs-prop · .btn.is-danger → .btn--danger · badge-status → .badge--*
   ─────────────────────────────────────────────────────────────────────────── */

/* Editor und Live-Vorschau nebeneinander: die Vorschau ist der Beleg zum Editor, keine zweite Seite.
   Sie klebt beim Scrollen — man ändert oben und liest unten, das darf nicht auseinanderlaufen. */
.cs-grid { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: var(--sp-4);
  align-items: start; margin-top: var(--sp-4); }
.cs-preview { position: sticky; top: 0; }
/* Editor zu (Normalfall beim Laden) = die Vorschau steht allein. Ohne diese Regel nähme sie die
   Editor-Spalte ein (1,5fr) und liesse daneben ein 1fr grosses Loch — das liest sich als Fehler,
   nicht als leere Spalte. Der Editor ist `hidden`, also aus dem Grid-Fluss; nur die Spaltenzahl
   weiss davon nichts. */
.cs-grid:has(> #editorCard[hidden]) { grid-template-columns: minmax(0, 520px); }
@media (max-width: 1100px) { .cs-grid { grid-template-columns: minmax(0, 1fr); } .cs-preview { position: static; } }

/* Formularfelder mit Beschriftung darüber, umbrechend — kein .frow-Raster (das ist für ein
   Seitenformular mit 160-px-Labelspalte gebaut, hier stehen die Felder nebeneinander). */
.cs-form { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-3); }
.cs-form > label { display: flex; flex-direction: column; gap: var(--sp-1); flex: 1 1 200px; min-width: 0;
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.cs-form > label.cs-wide { flex: 2 1 260px; }
.cs-check { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-ui); color: var(--text); cursor: pointer; }
.cs-check input { accent-color: var(--primary); margin: 0; }
.cs-checks { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); margin: var(--sp-3) 0; }

/* Eine Eigenschaft im Matching: Name · Modus · Zielwerte. Die Zielwerte sind Chips, weil man sie
   ein-/ausschaltet — eine Mehrfachauswahl aus ~5 Werten ist kein <select multiple>. */
.cs-prop { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-2) 0; border-bottom: 1px solid var(--row-line); }
.cs-prop > strong { min-width: 92px; font-size: var(--fs-dense); }
.cs-prop-hint { font-size: var(--fs-micro); color: var(--text-muted); }

/* Größen-Zuordnung: je Quellwert eine Zeile mit den erlaubten Zielwerten. Der Quellwert steht in einer
   festen Spalte, damit 26 Zeilen untereinander lesbar bleiben; die Chips brechen daneben um. Die Zeile
   „alle Quellwerte" steht oben und gilt zusätzlich zu den Einzelzeilen. */
.cs-map { width: 100%; margin-top: var(--sp-2); border-top: 1px solid var(--row-line); }
.cs-map-row { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: var(--sp-2);
  align-items: start; padding: var(--sp-1) 0; border-bottom: 1px solid var(--row-line); }
.cs-map-key { font: 500 var(--fs-dense) var(--f-ui); color: var(--text);
  font-variant-numeric: tabular-nums; padding-top: 3px; }
.cs-map-row--any .cs-map-key { color: var(--text-muted); font-weight: 400; }
.cs-map-head { font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); text-transform: uppercase;
  letter-spacing: .04em; }
.cs-map-head .cs-map-key { font: inherit; color: inherit; }
.cs-map-n { color: var(--text-faint); font-weight: 400; }

.cs-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin-top: var(--sp-3); }
.cs-row .cs-spacer { margin-left: auto; }
.cs-msg { font-size: var(--fs-micro); }
.cs-add { margin-top: var(--sp-2); }

/* Live-Vorschau: je Gruppe eine Überschrift + die SKUs als Chips. */
.cs-pv-group { margin-bottom: var(--sp-3); }
.cs-pv-group:last-child { margin-bottom: 0; }
.cs-pv-head { display: flex; align-items: baseline; gap: var(--sp-2); margin-bottom: var(--sp-1); font-size: var(--fs-dense); }
.cs-pv-head strong { font-weight: 600; }

/* Spaltengeometrie der Regel-Tabelle — EINE Quelle statt text-align:left an jedem th. */
.grid [data-c="cs-wg"]    { width: 230px; }
.grid [data-c="cs-grp"]   { width: 150px; }
.grid [data-c="cs-fit"]   { width: 100px; }
.grid [data-c="cs-props"] { width: 230px; }
.grid [data-c="cs-dir"]   { width: 130px; }
.grid [data-c="cs-act"]   { width: 70px; }
.grid [data-c="cs-edit"]  { width: 110px; }

.grid [data-c="cs-albez"] { width: 220px; }
.grid [data-c="cs-alwo"]  { width: 150px; }
.grid [data-c="cs-alwg"]  { width: 150px; }
.grid [data-c="cs-alsku"] { width: 300px; }
.grid [data-c="cs-alakt"] { width: 70px; }
.grid [data-c="cs-aldel"] { width: 60px; }

/* Die Eigenschafts-Badges einer Regel sind mehrere je Zelle — sie brauchen Luft, dürfen die
   32-px-Zeile aber nicht sprengen. */
.cs-props { display: flex; align-items: center; gap: 3px; overflow: hidden; }

/* ── /export — Wertevorrat & Export (Cutover 2026-07-17) ────────────────────────
   Präfix `.ex-`. Der Ausgang des ganzen Systems: hier verlässt eine Zahl das PIM.

   Was BEWUSST bleibt wie es war: das Kachel-Dashboard mit den fünf Vormerk-Töpfen und den
   aufklappbaren Sektionen. Es ist keine Alt-Last, sondern die Identität der Seite — „Nichts davon
   ist im Shop, bis du die CSV herunterlädst". Die Kacheln sind Navigation IN der Seite (data-target),
   der offene Zustand lebt in localStorage (`pim.export.open`). Beides ist unangetastet; nur die
   Farben/Maße kommen jetzt aus den Tokens statt aus rem-Werten.
   ─────────────────────────────────────────────────────────────────────────── */

/* Klappbares Panel — dieselben vier Zeilen wie in shop_texte.css/shop_crawler.css. Figur der Shell,
   kein Seiten-Stil: gehört beim Einsammeln EINMAL nach app-new.css. */

/* Ein <a class="btn"> kann kein `disabled` tragen — die Statusabfrage schaltet „Neu in den Shop" aber ab,
   wenn nichts ableitbar ist. Spiegelt `.btn:disabled`. Gehört eigentlich neben jenes in app-new.css. */
.btn.is-disabled { opacity: .5; pointer-events: none; }

/* ── Zwei Datenquellen-Spalten: links Shop, rechts WaWi ──────────────────────
   Die Quellenfarbe trägt NUR Kopf/Chip/Logo, nie eine Zahl (Entscheidung D). */
.ex-frame { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--sp-4); }
.ex-col { border: 1px solid var(--border); border-radius: var(--r-2); overflow: hidden; min-width: 0; }
.ex-col-head { display: flex; align-items: center; gap: var(--sp-2); height: var(--h-actionbar); padding: 0 14px;
  border-bottom: 1px solid var(--border); font-size: var(--fs-ui); font-weight: 600; background: var(--sunken); }
.ex-col--shop .ex-col-head { box-shadow: inset 3px 0 0 var(--src-shop); }
.ex-col--wawi .ex-col-head { box-shadow: inset 3px 0 0 var(--src-wawi); }
.ex-col-sub { margin-left: auto; font-size: var(--fs-micro); font-weight: 400; color: var(--text-muted); }
.ex-col-body { display: grid; gap: var(--sp-2); padding: var(--sp-3); }

/* Kopf eines Topfes: links was drin ist, rechts was man damit tut. */
.ex-p { margin: var(--sp-1) 0 0; font-size: var(--fs-dense); line-height: 1.55; }
.ex-sub { font-size: var(--fs-micro); color: var(--text-muted); margin-top: 1px; }

/* Massenaktions-/Einzel-Block (Accordion, kein Tabellen-Gruppen-Muster). */
.ex-act { margin-top: var(--sp-2); border: 1px solid var(--border); border-radius: var(--r-1); overflow: hidden; }
.ex-act > summary { display: flex; align-items: center; gap: var(--sp-3); min-height: var(--h-actionbar);
  padding: 6px var(--sp-3); background: var(--sunken); cursor: pointer; list-style: none; flex-wrap: wrap; }
.ex-act > summary::-webkit-details-marker { display: none; }
.ex-act > summary:hover { background: var(--hover); }
.ex-act[open] > summary { border-bottom: 1px solid var(--border); }
.ex-act-title { flex: 1; min-width: 0; font-size: var(--fs-dense); }
.ex-act-meta { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; font-size: var(--fs-micro); color: var(--text-muted); }
.ex-act-meta b { color: var(--text); font-variant-numeric: tabular-nums; }
.ex-act-caret { color: var(--text-faint); font-size: var(--fs-micro); transition: transform .13s; }
.ex-act[open] .ex-act-caret { transform: rotate(90deg); }
.ex-act-body { padding: var(--sp-3); }
.ex-act-bar { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); }

/* Batch-Block der EK-/VK-/Löschlisten-Töpfe (kein <details>: export.js klappt ihn per JS auf). */
.ex-batch { border: 1px solid var(--border); border-radius: var(--r-1); margin-top: var(--sp-2); overflow: hidden; }
.ex-batch-head { display: flex; align-items: center; gap: var(--sp-2); padding: 5px var(--sp-2); background: var(--sunken); cursor: pointer; }
.ex-batch-head:hover { background: var(--hover); }
.ex-toggle { border: 0; background: none; color: var(--text-faint); font-size: var(--fs-micro); cursor: pointer; padding: 0 2px; }
.ex-batch-title { flex: 1; min-width: 0; font-size: var(--fs-dense); }
.ex-batch-body { padding: 0; }
.ex-batch-body > .grid { border-top: 1px solid var(--border); }
.ex-singles { margin-top: var(--sp-4); }
.ex-singles > .eyebrow { display: block; margin-bottom: var(--sp-1); }

/* Breite Tabellen scrollen IN SICH (Regel 4) — `.grid` ist `width: max-content` und will breiter
   werden als ihr Kasten; ohne Fänger schneidet html{overflow:hidden} die letzten Spalten lautlos ab. */
/* ── EINE BREITE TABELLE SCROLLT IN SICH, NICHT DIE SEITE (15.08.) ─────────────────────────────────
   Gemessen bei 950 / 1280 / 1920 px: acht Seiten schoben ihren KOMPLETTEN Seitenkörper nach links —
   Seitentitel, Reiterleiste und Filterzeile wanderten mit weg. Genau die Orientierung, die man beim
   seitlichen Suchen braucht, verschwand als Erstes. `/einrichten/attribute` lief selbst bei 1920 px
   noch 310 px über.
   ⛔ `html, body { overflow: hidden }` (weiter oben) macht „kein Scrollbalken am BODY" in dieser App
   zu einer nichtssagenden Aussage — der echte Träger der Hausregel ist der `[data-body]`-Slot.
   ⛔ UND: Diese Regel steht hier zum NEUNTEN Mal im Haus (.ex-tbl · .im-log-scroll · .st-tblwrap ·
   .st-matrix-wrap · .cr-tbl · .tz-scroll · .wm-parse-scroll · .mk-scroll). Neue Stellen benutzen
   `.tblwrap`; die acht Zwillinge gehören hierher zusammengelegt — als eigener Schritt, nicht nebenbei
   (jeder trägt zusätzlich Rahmen oder Radius, das ist kein Suchen-und-Ersetzen). */
.tblwrap { overflow-x: auto; min-width: 0; }
.ex-tbl { overflow-x: auto; min-width: 0; }
.ex-tbl::-webkit-scrollbar { height: 8px; }
.ex-tbl::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-1); }

/* ── Shop ⟷ WaWi Matrix ─────────────────────────────────────────────────────
   Links das Shop-Produkt, rechts seine WaWi-Längen mit Soll-VK. Die Seiten tragen die Quellenfarbe
   als KANTE (nie als Fläche über Zahlen) — der rote Faden, den `uiConfig.sources.*.color` pflegt. */
.ex-mx { border: 1px solid var(--border); border-radius: var(--r-1); overflow: hidden; }
.ex-mx + .ex-mx { margin-top: var(--sp-2); }
.ex-mx-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); }
.ex-mx-side { padding: var(--sp-3); min-width: 0; }
.ex-mx-side--shop { border-right: 1px solid var(--border); box-shadow: inset 3px 0 0 var(--src-shop); }
.ex-mx-side--wawi { box-shadow: inset 3px 0 0 var(--src-wawi); }
.ex-mx-cap { display: flex; align-items: center; gap: 6px; margin-bottom: var(--sp-2); padding-bottom: var(--sp-2);
  border-bottom: 1px solid var(--border); font-size: var(--fs-micro); color: var(--text-muted); }
.ex-mx-sku { font: 500 var(--fs-dense) var(--f-ui); }
.ex-mx-name { font-size: var(--fs-micro); }
.ex-mx-pr { margin-top: var(--sp-1); font-size: var(--fs-body); font-variant-numeric: tabular-nums; }
.ex-mx-base { margin-top: var(--sp-1); font-size: var(--fs-micro); line-height: 1.5; }
.ex-mx-foot { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); flex-wrap: wrap; }
/* Der alte Preis ist der VERLASSENE Wert — durchgestrichen, nicht rot. Rot wäre ein Urteil. */
.ex-old { color: var(--text-muted); text-decoration: line-through; }
.ex-arw { color: var(--text-faint); }
/* Ausreißer-Zeile: Haken bewusst raus. Gedämpft, nicht alarmiert — es ist eine Vermutung, kein Fehler. */
.ex-off > td { background: var(--sunken); }
.ex-keep { color: var(--warn-mark); font-size: var(--fs-micro); cursor: help; }
.ex-mx .grid { border-top: 1px solid var(--border); }
.ex-mx input[type="checkbox"] { accent-color: var(--primary); margin: 0; cursor: pointer; }

/* ── Weitere Exporte (sekundär, kein Vormerk-Topf) ───────────────────────── */
.ex-more { margin-top: var(--sp-6); border-top: 1px solid var(--border); padding-top: var(--sp-4); }
.ex-more > summary { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.ex-more-title { font-size: var(--fs-sect); font-weight: 600; }
.ex-more-sub { font-size: var(--fs-dense); }
.ex-more-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--sp-3); margin-top: var(--sp-3); align-items: start; }
.ex-card { display: grid; gap: 6px; align-content: start; padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2); min-width: 0; }
.ex-card > .btn, .ex-card > a.btn { justify-self: start; }
.ex-num { font: 600 var(--fs-kpi)/32px var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.ex-num-lbl { margin-left: 6px; font: 400 var(--fs-dense) var(--f-ui); color: var(--text-muted); letter-spacing: 0; }
.ex-card p { margin: 0; font-size: var(--fs-dense); line-height: 1.5; }
.ex-note { margin-top: var(--sp-1); }
.ex-note > summary { cursor: pointer; list-style: none; font-size: var(--fs-micro); color: var(--accent); }
.ex-note > summary::-webkit-details-marker { display: none; }
.ex-note > summary::before { content: "▸ "; }
.ex-note[open] > summary::before { content: "▾ "; }
.ex-note > summary:hover { text-decoration: underline; }
.ex-note p { margin: var(--sp-1) 0 0; font-size: var(--fs-micro); line-height: 1.55; }

/* ── C/4 Export-Neubau (2026-07-18, ux-designer): Manifest · Export-Zeilen mit Download rechts · Detailzone
   · Reconcile-Fußzeile. Baut auf .ex-col/.ex-frame/.wfrow auf. ─────────────────────────────────────────── */
.ex-manifest { margin: 0; font-size: var(--fs-dense); color: var(--text-muted); line-height: 1.5; }
.ex-manifest strong { color: var(--text); font-variant-numeric: tabular-nums; }
/* Export-Zeilen: immer eine pro Zeile (nie 2 nebeneinander wie im auto-fill-Standard von .wfrows). */
.ex-wfrows { grid-template-columns: 1fr; }
.ex-exrow-acts { flex: 0 0 auto; display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.ex-detail-btn.is-open { background: var(--selected); border-color: var(--border-strong); }
/* Woran die wartenden Neuanlagen hängen (17.08.). Eingerückt unter ihrer Export-Zeile, weil es eine
   Aufschlüsselung DIESER Zeile ist und keine eigene Zeile — sonst läse sich der Block als weiterer Topf,
   aus dem etwas herausgeht. Kein Kasten, keine Farbe: es ist eine Erklärung, keine Warnung. */
.ex-blocker { margin: calc(var(--sp-1) * -1) 0 var(--sp-3) var(--sp-5); padding-left: var(--sp-3); border-left: 2px solid var(--border); }
.ex-blocker-head { margin: 0 0 var(--sp-1); font-size: var(--fs-micro); color: var(--text-muted); }
.ex-blocker-list { margin: 0; padding: 0; list-style: none; display: grid; gap: 2px; }
.ex-blocker-list li { display: flex; gap: var(--sp-2); align-items: baseline; font-size: var(--fs-dense); }
/* Rechtsbündige Zahlenspalte: die Gründe sind zum Vergleichen da, nicht zum Lesen — deshalb tabellarisch
   und mit fester Breite, damit die Mengen untereinander stehen. */
.ex-blocker-n { flex: 0 0 3.2em; text-align: right; font-variant-numeric: tabular-nums; color: var(--text); font-weight: 600; }
.ex-blocker-txt { flex: 1 1 auto; }
.ex-blocker-txt strong { font-weight: 600; }
.ex-blocker-foot { margin: var(--sp-1) 0 0; font-size: var(--fs-micro); }

/* Leere Zeile (Dominik #3): dünn, gestrichelt, grau, kein Button — „hier wäre was, ist aber leer". */
.wfrow.is-empty { background: transparent; border-style: dashed; padding-top: 5px; padding-bottom: 5px; cursor: default; }
.wfrow.is-empty:hover { background: transparent; border-color: var(--border); }
.wfrow.is-empty .wfrow-num { color: var(--text-faint); font-weight: 500; font-size: var(--fs-sect); }
.wfrow.is-empty .wfrow-label { color: var(--text-muted); font-weight: 500; }
/* Ehrlichkeits-Hinweise ans Panel geheftet (Dominik #5), nicht als Prosa vergraben. */
.ex-honesty { margin-top: var(--sp-2); padding: var(--sp-2) var(--sp-3); border-radius: var(--r-1); font-size: var(--fs-micro); line-height: 1.5;
  border: 1px solid color-mix(in srgb, var(--warn) 35%, var(--border)); background: color-mix(in srgb, var(--warn) 7%, var(--canvas)); }
.ex-honesty--err { border-color: color-mix(in srgb, var(--bad) 40%, var(--border)); background: color-mix(in srgb, var(--bad) 7%, var(--canvas)); }
.ex-honesty + .ex-honesty { margin-top: var(--sp-1); }
/* Detailzone: EINE Vollbreite-Fläche, immer nur ein Detail offen (kein zweiter Zählerbalken, Dominik #1). */
.ex-detailzone:empty { display: none; }
.ex-detail[hidden] { display: none; }
.ex-detail { margin-top: var(--sp-4); border: 1px solid var(--border-strong); border-radius: var(--r-2); padding: var(--sp-3) 14px 14px; background: var(--canvas); }
.ex-detail-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-2); flex-wrap: wrap; }
.ex-detail-head h3 { margin: 0; font-size: var(--fs-sect); font-weight: 600; }
.ex-detail-head .spacer { flex: 1; }
/* Reconcile-Fußzeile (arne): Shop + Potenzial + Ausgeschlossen = Gesamt. */
.ex-reconcile { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-5);
  padding: var(--sp-3) 14px; border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); font-size: var(--fs-dense); }
.ex-rec-eq { font-variant-numeric: tabular-nums; }
.ex-rec-op { color: var(--text-faint); margin: 0 2px; }
.ex-rec-note { font-size: var(--fs-micro); }
.ex-reconcile .spacer { flex: 1; }
/* Ein Hinweis, der eine ANNAHME korrigiert („zuerst die Parent-Option anlegen, sonst kippt die
   Variante") — Bernstein wie `.mark-warn`, nicht das Ampel-Gelb der Marge. */
.ex-note-warn { color: var(--warn-mark); }
.ex-foot { margin: var(--sp-4) 0 0; font-size: var(--fs-micro); line-height: 1.6; color: var(--text-muted); }
/* Sammel-Download (Phase 2.3): Knopf und seine Grenze auf einer Zeile — was er NICHT umfasst, gehört
   neben ihn und nicht in einen Tooltip, den man erst nach dem Klicken liest. */
.ex-zip { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin: var(--sp-3) 0 0; font-size: var(--fs-micro); }
/* Aufzählung im async geladenen „Neu in den Shop"-Status (neue Attribut-Optionen / Review-Gründe). */
.ex-pvlist { margin: var(--sp-1) 0 0; padding-left: 16px; font-size: var(--fs-micro); line-height: 1.6; }

@media (max-width: 900px) {
  .ex-mx-grid { grid-template-columns: minmax(0, 1fr); }
  .ex-mx-side--shop { border-right: 0; border-bottom: 1px solid var(--border); }
}

/* ── /hersteller — Herstellerlisten (Cutover 2026-07-17) ────────────────────────
   Nachfolger der app.css-Figuren .up-inline · .cfg-detail · .rule-form · .pp-section*
   · .table.sub. Aus den Tokens abgeleitet, keine Zeile kopiert (die alten waren in rem).
   Was ersatzlos entfiel, weil es einen Nachfolger hat:
     table→.grid · card→.panel · btn-primary→.btn--primary · badge-status→.badge/.mark-warn
     right→.num · muted→.dim · eyebrow→.eyebrow (jetzt ohne Markenfarbe)
   ─────────────────────────────────────────────────────────────────────────── */

/* Die drei Zähler-Begriffe (Zuordnungen · greifen · ohne Treffer) stehen ÜBER der Tabelle,
   nicht als Tooltip an ihr: sie qualifizieren drei Spalten gleichzeitig, und ein Tooltip ist
   versteckte Dokumentation. Gleiche Figur wie .wb-basis auf /preise?tab=wettbewerb. */
.hl-defs { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-6); padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border); font-size: var(--fs-dense); }
.hl-def { flex: 1 1 260px; max-width: 46ch; }
.hl-def dt { font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-faint); }
.hl-def dd { margin: 0; color: var(--text-muted); line-height: 1.45; }

/* Spaltengeometrie — EINE Quelle statt inline am th (.grid ist table-layout:fixed). */
.grid [data-c="hl-kat"]   { width: 200px; }
.grid [data-c="hl-lief"]  { width: 150px; }
.grid [data-c="hl-art"]   { width: 104px; }
.grid [data-c="hl-zu"]    { width: 110px; }
.grid [data-c="hl-greif"] { width: 88px; }
.grid [data-c="hl-ohne"]  { width: 104px; }
.grid [data-c="hl-up"]    { width: 300px; }
.grid [data-c="hl-cfg"]   { width: 104px; }

/* Datei-Upload je Zeile: Datei + Knopf + Ergebnis auf einer Grundlinie. Der native
   Datei-Knopf ist nicht stylbar — er bekommt nur eine Deckelung, damit er die 44-px-Zeile
   nicht sprengt. */
.hl-up { display: flex; align-items: center; gap: var(--sp-2); }
.hl-up input[type="file"] { min-width: 0; max-width: 150px; font-size: var(--fs-micro); color: var(--text-muted); }
.hl-up-msg { font-size: var(--fs-micro); min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* AUSKLAPP-ZEILE — sie hängt die Regeln der .grid aus: dort ist jede Zelle einzeilig,
   44 px hoch und beschnitten (nowrap/ellipsis). Ein Formular darin wäre unsichtbar
   abgeschnitten, ohne dass es beim Code-Lesen auffällt. */
.hl-cfg-row > td { height: auto; padding: 0; white-space: normal; overflow: visible; background: var(--sunken); }
.hl-cfg { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: var(--sp-6);
  padding: var(--sp-4); }
@media (max-width: 1100px) { .hl-cfg { grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); } }
.hl-cfg-note { margin: var(--sp-1) 0 var(--sp-3); font-size: var(--fs-dense); line-height: 1.5; color: var(--text-muted); }

/* Formularzeile: Felder mit ihrer Beschriftung darüber, umbrechend. Kein .frow-Raster —
   das ist für ein Seitenformular (160px Label | Feld) gebaut, hier stehen 8 Felder nebeneinander. */
.hl-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-2) var(--sp-3);
  margin: var(--sp-2) 0 var(--sp-3); }
.hl-form label { display: flex; flex-direction: column; gap: var(--sp-1);
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.hl-form input[type="file"] { max-width: 190px; font-size: var(--fs-micro); color: var(--text-muted); }
.hl-form .input, .hl-form .select { min-width: 0; }
.hl-msg { font-size: var(--fs-micro); }

/* Unterabschnitt in der Ausklapp-Zeile (Divisor in der Masse). */
.hl-sect { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.hl-sect-t { font: 600 var(--fs-dense)/18px var(--f-ui); color: var(--text); }
.hl-sect-n { margin: 2px 0 0; font-size: var(--fs-micro); line-height: 1.5; color: var(--text-muted); }

/* Import-Historie: eine Tabelle IN einer Tabellenzelle. `position:sticky` am th würde sich
   am Canvas ausrichten statt an dieser Zelle — der Kopf klebte dann quer über der Seite. */
.hl-sub th { position: static; cursor: default; }
.hl-sub th:hover { color: var(--text-muted); }

/* Feldbreiten — sie tragen Bedeutung (Kopfzeile = 2-stellig, Divisor = 4-stellig), deshalb
   hier und nicht als style="" am Element (der IDE-Linter meckert Inline-Styles an). */
.hl-w-hr { width: 72px; }
.hl-w-div { width: 96px; }

/* ── Listen einlesen (/import) ────────────────────────────────────────────────
   Zwei Quell-Panels (Shop | WaWi) mit je zwei Upload-Zeilen, ein <details> mit den
   Spalten-Specs, ein Log-Panel. Alles aus den Tokens; nichts aus app.css kopiert.
   Der frühere `.srcframe`-Aussenrahmen ist ersatzlos weg: er lag um zwei Karten mit
   eigenem Rahmen — genau der verbotene Doppelrahmen. Jetzt trägt das Panel den Rahmen,
   die Upload-Zeilen darin nur eine Trennlinie. */

.im-cols { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); margin-bottom: var(--sp-5); }
@media (max-width: 900px) { .im-cols { grid-template-columns: 1fr; } }

/* „CSV-Datei" rechts im Panel-Kopf — sagt, was erwartet wird, ohne eine zweite Zeile zu kosten. */
.im-col-sub { margin-left: auto; font-size: var(--fs-dense); }

/* Upload-Zeile: Titel/Erklärung, dann Datei + Knopf, dann die Rückmeldung.
   Kein eigener Rahmen (Regel: kein card-in-panel) — nur eine Trennlinie zwischen den beiden. */
.im-up { display: grid; gap: var(--sp-2); padding: var(--sp-3) 0; }
.im-up + .im-up { border-top: 1px solid var(--border); }
.im-up-t { display: grid; gap: 2px; }
.im-up-t strong { font-size: var(--fs-ui); font-weight: 600; }
.im-up-t .dim { font-size: var(--fs-dense); }
.im-up-a { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.im-up .up-result { font-size: var(--fs-dense); min-height: 16px; }

/* Datei-Feld: der Browser rendert den Knopf selbst — nur Rahmen/Höhe angleichen. */
.im-file { flex: 1; min-width: 180px; height: var(--sp-8); padding: 4px var(--sp-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--canvas); color: var(--text-muted); font: 400 var(--fs-dense) var(--f-ui); }
.im-file::file-selector-button { height: 22px; margin-right: var(--sp-2); padding: 0 var(--sp-2);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--sunken); color: var(--text); font: 500 var(--fs-micro) var(--f-ui); cursor: pointer; }
.im-file::file-selector-button:hover { background: var(--hover); }
.ro-fieldset:disabled .im-file { opacity: .5; }

/* Spalten-Transparenz: aufklappbare Referenz. <details> statt JS — Auf/Zu gehört dem Browser. */
.im-det { border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas);
  margin-bottom: var(--sp-5); overflow: hidden; }
.im-det > summary { display: flex; align-items: center; gap: var(--sp-2); height: var(--h-actionbar);
  padding: 0 14px; font-size: var(--fs-ui); color: var(--text); cursor: pointer; list-style: none; }
.im-det > summary::-webkit-details-marker { display: none; }
.im-det > summary::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-micro); }
.im-det[open] > summary::before { content: "▾"; }
.im-det > summary:hover { background: var(--hover); }
.im-det[open] > summary { border-bottom: 1px solid var(--border); }
.im-det-body { padding-bottom: var(--sp-3); }
.im-det-body > h3 { margin: var(--sp-4) 0 var(--sp-2); padding: 0 14px;
  font: 600 var(--fs-sect)/22px var(--f-ui); color: var(--text); }
.im-det-lead { margin: 0 0 var(--sp-2); padding: 0 14px; }
.im-det code { background: var(--sunken); padding: 0 3px; border-radius: 2px; }

/* Referenz-Tabelle: hier ist der Hinweistext der Inhalt, nicht eine Zahl — deshalb umbrechen
   statt abschneiden (das `nowrap`/`ellipsis` der Objekt-Tabelle würde ihn unlesbar machen). */
.im-cols-tbl { table-layout: auto; width: 100%; }
.im-cols-tbl td { white-space: normal; overflow: visible; text-overflow: clip;
  padding-top: 5px; padding-bottom: 5px; line-height: 15px; }
.im-cols-tbl th:first-child, .im-cols-tbl td:first-child { padding-left: 14px; }
.im-cols-tbl th:last-child, .im-cols-tbl td:last-child { padding-right: 14px; }
.im-c-excel { width: 62px; }
.im-c-status { width: 88px; }
.im-c-note { width: 38%; }

/* Das Log ist breiter als das Panel, sobald das Fenster kleiner als ~1.300 px ist. Es scrollt darum
   IN sich — nicht der Canvas, und schon gar nicht das Dokument (Regel 4). `overflow:hidden` am Panel
   hält die abgerundete Ecke; der Scroll gehört dem inneren Container. */
.im-log { overflow: hidden; }
.im-log-scroll { overflow-x: auto; }
.im-log-lead { margin: 0; }

/* ── /info — Wiki & Hilfe (Archetyp F: Dokument) ─────────────────────────────
   Inhaltsverzeichnis links (eigener Scroll), Dokument rechts (eigener Scroll), Prosa gedeckelt.

   Zwei Sorten Inhalt, und sie gehören verschiedenen Leuten:
   · Das GERÜST (`.wk-*`) ist views/info.html + views/_wiki_keys.html — es gehört uns.
   · Der TEXT kommt aus `wiki_topics.intro` / `wiki_entries.answer`, ist im UI editierbar und bringt
     sein eigenes HTML mit. Er lebt unter `.wk-rt` und ist unten separat behandelt.
   ─────────────────────────────────────────────────────────────────────────── */

.wk-body { display: grid; grid-template-columns: 264px minmax(0, 1fr); min-width: 0; min-height: 0; }

/* INHALTSVERZEICHNIS — hell, erste Spalte des Canvas, mit eigenem Scroll. Kein zweiter dunkler
   Streifen neben der Schiene: die Nav ist Werkzeug-Chrome, das hier ist Inhalt (§ Facetten-Rail). */
/* ⚠️ FORMREGEL — Schiene vs. Navigation IM Inhalt.
   Seit die Haupt-Schiene hell ist (19.07.), stehen auf /info zwei helle Navigationsspalten nebeneinander:
   links die Schiene, direkt daneben dieses Wiki-Menü. Über die HELLIGKEIT sind sie nicht mehr sicher zu
   trennen — also über die FORM, und die überlebt jeden künftigen Palettenwechsel:

     Die Schiene ist randbündig und eckig.  ·  Jede Navigation im Inhalt ist eingerückt und rund.

   Konkret hier: Abstand ringsum, eigener Radius, versenkter Grund. Sie liest sich damit als Objekt AUF
   der Seite, nicht als zweite Schiene NEBEN der Schiene. */
.wk-nav {
  display: flex; flex-direction: column; gap: 1px;
  overflow-y: auto; overflow-x: hidden; min-height: 0;
  margin: var(--sp-2); padding: var(--sp-2) 0;
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken);
}
.wk-nav::-webkit-scrollbar { width: 6px; }
.wk-nav::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-1); }

.wk-topic {
  display: flex; align-items: center; gap: var(--sp-2); flex: none;
  height: var(--sp-8); padding: 0 var(--sp-3); width: 100%;
  border: 0; background: none; color: var(--text-muted);
  font: 500 var(--fs-ui)/16px var(--f-ui); text-align: left; cursor: pointer;
}
.wk-topic:hover { background: var(--hover); color: var(--text); }
/* Der aktive Punkt trägt die Markenfarbe als KANTE, nicht als Fläche: er ist ein Ort, kein Urteil. */
.wk-topic.is-active { background: var(--selected); color: var(--text); font-weight: 600; box-shadow: inset 2px 0 0 var(--primary); }
.wk-ico { flex: none; width: 16px; text-align: center; font-size: var(--fs-dense); line-height: 1; }
.wk-lbl { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wk-topic-add { color: var(--accent); }

/* SUCHE — sitzt in der Navigationsspalte, nicht in der Topbar: die Topbar-Suche ist die Produktsuche
   (shell.js), und zwei Suchen im selben Feld mit verschiedenem Ziel ist genau der Musterbruch, den man
   nicht baut. Dominik tippte „Löschliste" oben ein und landete in einer leeren Artikelliste. */
.wk-search { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--border); }
.wk-search .input { width: 100%; }

.wk-tree { display: flex; flex-direction: column; gap: 1px; padding: var(--sp-2) 0; }
.wk-cnt {
  flex: none; min-width: 20px; padding: 0 5px; border-radius: var(--r-1);
  background: var(--sunken); color: var(--text-faint);
  font: 600 var(--fs-micro)/16px var(--f-ui); font-variant-numeric: tabular-nums; text-align: center;
}
.wk-topic.is-active .wk-cnt { background: var(--canvas); color: var(--text-muted); }
.wk-home-btn { font-weight: 600; }

/* ZWEITE EBENE — die Fragen. Nur unter dem OFFENEN Thema: sonst stünden 61 Zeilen in 264 px und der Baum
   wäre wieder das, was er ersetzen soll. Sichtbar ist, was gerade zur Sache gehört. */
.wk-sub { display: none; padding: 2px 0 var(--sp-2); }
.wk-sub.is-open { display: block; }
/* EINZEILIG mit Ellipse — die Fragen sind ganze Sätze. Umgebrochen ergaben sie 26/42/59 px hohe Zeilen
   ohne Rhythmus, und beim größten Thema (11 Einträge) war der Block 476 px hoch: drei der elf Themen
   fielen aus dem Bild, sobald man es öffnete (gemessen, ux-designer 19.07.). Der volle Text steht im
   `title` — die Liste ist zum Scannen da, nicht zum Lesen. */
.wk-subitem {
  display: block; width: 100%; height: 26px; padding: 0 var(--sp-3) 0 var(--sp-6);
  border: 0; background: none; color: var(--text-muted);
  font: 400 var(--fs-dense)/26px var(--f-ui); text-align: left; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wk-subitem:hover { background: var(--hover); color: var(--text); }

/* TREFFERLISTE — ersetzt den Baum, statt ihn zu überlagern: wer sucht, sucht eine Navigation. */
.wk-results { padding: var(--sp-2) 0; }
.wk-res-head { margin: 0 0 var(--sp-1); padding: 0 var(--sp-3); font-size: var(--fs-micro); color: var(--text-faint); }
.wk-res-empty { padding: var(--sp-3); font-size: var(--fs-dense); line-height: 1.5; }
.wk-res {
  display: block; width: 100%; padding: var(--sp-2) var(--sp-3);
  border: 0; border-bottom: 1px solid var(--row-line); background: none;
  text-align: left; cursor: pointer;
}
.wk-res:hover { background: var(--hover); }
.wk-res-top { display: block; font-size: var(--fs-micro); color: var(--text-faint); }
.wk-res-q { display: block; margin: 1px 0 2px; font: 600 var(--fs-dense)/1.35 var(--f-ui); color: var(--text); }
.wk-res-sn { display: block; font-size: var(--fs-micro); line-height: 1.45; color: var(--text-muted); }
.wk-res-sn mark { background: var(--selected); color: var(--text); border-radius: 2px; padding: 0 1px; }

/* DOKUMENT — `.wk-panels` trägt zusätzlich `.page` (Innenabstand aus app-new.css). */
/* Der Einleitungssatz steht IM Dokument, nicht in einer Aktionsleiste: /info hat keine Aktion, und
   `.ab-hint:only-child` bricht bei 390 px um, während die Leiste auf 40 px festgenagelt bleibt — der
   Satz malte sich quer über Seitenkopf und Text (gemessen, 17.07.). */
.wk-lead { max-width: var(--measure); }
/* Der Deckel sitzt auf der PROSA (`.wk-rt p/ul/ol`, weiter unten), nicht auf dem Panel: Zeilenlänge ist
   Lesbarkeit — Badge-Raster, Rechenbeispiele und Live-Kacheln sind es nicht.
   Der Panel-Deckel stand auf 1120 px und ließ auf einem breiten Schirm ein Drittel der Seite leer
   („warum nur auf 1/2 der Seitenbreite?", Sascha 19.07.). Jetzt füllt das Panel die Spalte; nur die
   Fließtext-Elemente behalten ihr Maß. Die Kacheln, Formeln und Tabellen wachsen also mit, die Absätze
   nicht — genau das ist der Unterschied zwischen „breit" und „unlesbar". */
.wk-panel { display: none; }
.wk-panel.is-active { display: block; }
.wk-h2-ico { margin-right: var(--sp-2); }

/* SPICKZETTEL — die drei Formeln ohne Klick. Bewusst system-gerendert und nicht editierbar: hier stehen
   Schwellen aus den Einstellungen, und eine zweite, von Hand gepflegte Kopie davon wäre genau die
   Falschaussage, gegen die die Platzhalter gebaut wurden. */
.wk-cheat { margin: 0 0 var(--sp-5); }
.wk-cheat-h { margin: 0 0 var(--sp-3); font-size: var(--fs-sect); font-weight: 600; letter-spacing: -.01em; }
.wk-cheat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(276px, 1fr)); gap: var(--sp-3); }
.wk-formula {
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2);
  background: var(--sunken);
}
.wk-formula--wide { grid-column: 1 / -1; }
.wk-formula-t { margin-bottom: var(--sp-2); font: 600 var(--fs-dense) var(--f-ui); color: var(--text-muted); }
.wk-formula code {
  display: block; padding: var(--sp-2); border-radius: var(--r-1);
  background: var(--canvas); border: 1px solid var(--border);
  font-family: var(--f-mono); font-size: var(--fs-dense); line-height: 1.5; overflow-wrap: anywhere;
}
.wk-formula p { margin: var(--sp-2) 0 0; font-size: var(--fs-dense); line-height: 1.5; }

/* KACHEL-EINSTIEG — kein eigener Ort, sondern der Zustand „noch kein Thema gewählt". Bei 11 Themen
   lohnt ein dauerhafter Zwischenhalt nicht: Orientierung ist ein Zustand, kein Ort. */
.wk-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: var(--sp-3); }
.wk-tile {
  display: grid; grid-template-rows: auto auto auto auto; gap: 2px; align-content: start;
  padding: var(--sp-3); min-height: 118px;
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas);
  text-align: left; cursor: pointer;
}
.wk-tile:hover { border-color: var(--border-strong); background: var(--hover); }
.wk-tile-ico { font-size: var(--fs-sect); line-height: 22px; }
.wk-tile-lbl { font: 600 var(--fs-body)/1.3 var(--f-ui); color: var(--text); }
.wk-tile-num { margin-top: var(--sp-2); font: 600 var(--fs-title)/24px var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--text); }
.wk-tile-sub { font-size: var(--fs-micro); line-height: 15px; color: var(--text-muted); }
/* Kachel ohne Fachzahl: der Zeilenabstand ersetzt die fehlende Zahl, damit die Figur gleich hoch bleibt. */
.wk-tile-sub--only { margin-top: var(--sp-2); }
/* Zurück-zum-Baum über der Trefferliste. */
.wk-res-back { display: block; width: 100%; padding: var(--sp-1) var(--sp-3) var(--sp-2);
  border: 0; background: none; color: var(--accent); font: 500 var(--fs-dense) var(--f-ui); text-align: left; cursor: pointer; }
.wk-res-back:hover { text-decoration: underline; }
.wk-panel-head { margin-bottom: var(--sp-3); }
.wk-panel-head h2 { margin: 0; font-size: var(--fs-title); font-weight: 600; letter-spacing: -.01em; }
.wk-tools, .wk-entry-tools { display: inline-flex; align-items: center; gap: var(--sp-1); }

/* LIVE-ZAHLEN — der einzige system-gerenderte Teil des Panels (nie editierbar, sonst zweite Wahrheit
   neben der Datenbank). Bewusst ohne farbige Kante: sie sind der Normalfall, und den Normalfall zu
   färben ist der schnellste Weg, Farbe wertlos zu machen (Regel 2). */
/* Grid statt `flex: 1 1 140px`: mit Flex wuchsen zwei Kacheln auf je 530 px und drei auf je 368 px —
   dieselbe Figur hatte je Thema eine andere Größe, und 530 px für eine dreistellige Zahl ist viel
   Fläche für wenig Inhalt. Jetzt ist die Kachelgröße von ihrer Anzahl unabhängig. */
/* ZWEI SPALTEN — Text links, Live-Zahlen rechts als Infokasten.
   Der Grund ist gemessen: Absätze enden bei 780 px, die Trennlinie unter jeder Frage lief bis zum rechten
   Rand — ein leerer Streifen neben jeder Antwort, unter einer Linie, die bis ganz nach rechts zeigt. Das
   liest sich als „nutzt nur die halbe Seite", obwohl das Textmaß richtig war. Der zweite Strang füllt die
   Fläche mit etwas, das man beim Lesen wirklich gebrauchen kann.
   `:has()` statt einer Template-Bedingung: die zweite Spalte entsteht nur, wenn es auch Kacheln gibt —
   sonst stünde bei eigenen Themen eine leere 300-px-Rinne. */
/* ⛔ CONTAINER-QUERY, KEIN @media (korrigiert 02.08.). Die Bedingung war `min-width: 1240px` auf das
   FENSTER — und das Fenster ist hier die falsche Größe. Vom Fenster gehen erst die Rail (236 px), dann
   der Wiki-Baum (264 px) und zweimal Seitenrand ab; bei 1280 px Fenster blieben dem Dokument rund
   730 px. Davon nahm die zweite Spalte 300 + 20, sodass der Fließtext bei ~410 px umbrach — bei einem
   Zeilenmaß von `--measure` (780 px). Die Regel griff also genau dort, wo sie nicht greifen durfte:
   die Bedingung war erfüllt, der Platz war es nie. Gemessen sah man es als „bricht auf halber Breite".
   Jetzt misst die Regel den PLATZ, den sie verteilt: 780 (Textmaß) + 20 (Rinne) + 300 (Kasten). Unter
   1100 px gibt es die zweite Spalte nicht — dann ist sie nicht bezahlbar, egal wie breit das Fenster ist. */
.wk-panels { container-type: inline-size; container-name: wkdoc; }
.wk-cols { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); }
.wk-main { min-width: 0; }
/* Auf schmalen Schirmen fällt die Spalte weg — dann gehören die Zahlen wieder NACH OBEN, nicht unter
   alle Antworten. Ohne das stünden sie am Seitenende, wo sie niemand mehr als Einstieg liest. */
.wk-aside { min-width: 0; order: -1; }
@container wkdoc (min-width: 1100px) {
  .wk-cols:has(.wk-key) { grid-template-columns: minmax(0, 1fr) 300px; }
  .wk-aside { position: sticky; top: var(--sp-3); align-self: start; order: 0; }
  /* Im Kasten stehen die Zahlen untereinander: eine Spalte liest sich als Liste, zwei als Raster —
     und ein Raster mit drei Feldern hätte in 300 px wieder eine Waise in der letzten Reihe. */
  .wk-aside .wk-keys { grid-template-columns: 1fr; margin: 0; }
}
/* Die Schmal-Fassung der Prozess-Bausteine steht weiter unten, direkt hinter ihren Grundregeln —
   ein @container erhöht die Spezifität NICHT, eine spätere gleichrangige Regel gewinnt. */
.wk-keys { display: grid; grid-template-columns: repeat(auto-fill, minmax(176px, 1fr)); gap: var(--sp-2); margin: 0 0 var(--sp-4); }
.wk-key { padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); }
.wk-kn { font: 600 var(--fs-title)/26px var(--f-ui); font-variant-numeric: tabular-nums; letter-spacing: -.02em; color: var(--text); }
.wk-kl { margin-top: 2px; font-size: var(--fs-micro); line-height: 15px; color: var(--text-muted); }
/* „Spedition" ist keine Zahl — dieselbe Kachel, aber ohne den Kennzahl-Grad. War ein Inline-Style. */
.wk-key--txt .wk-kn { font-size: var(--fs-sect); line-height: 26px; }

.wk-intro-wrap { margin-bottom: var(--sp-4); }
.wk-intro-wrap > .wiki-edit-intro { margin-top: var(--sp-1); }

/* AKKORDION — Hairlines statt Karten: die Fragen sind gleichrangig und werden am Stück gelesen.
   Ein Rahmen je Frage addierte 49 Rahmen und zerhackte genau den Lesefluss, der hier der Zweck ist. */
.wk-faq { border-bottom: 1px solid var(--row-line); }
.wk-faq:first-child { border-top: 1px solid var(--row-line); }
.wk-faq > summary {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: var(--h-actionbar); padding: var(--sp-2) 0;
  cursor: pointer; list-style: none;
}
.wk-faq > summary::-webkit-details-marker { display: none; }
.wk-faq > summary:hover .wk-q { color: var(--accent); }
.wk-q { flex: 1; min-width: 0; font-size: var(--fs-body); font-weight: 500; line-height: 20px; color: var(--text); }
.wk-faq > summary::after { content: "＋"; flex: none; width: 16px; text-align: center; color: var(--text-faint); font-size: var(--fs-dense); }
.wk-faq[open] > summary::after { content: "－"; }
.wk-faq[open] > summary .wk-q { font-weight: 600; }
.wk-faq .faq-body { padding: 0 0 var(--sp-4); }

/* Werkzeuge je Antwort erscheinen bei Hover — sie sollen erreichbar sein, ohne dass 50 Icons dauerhaft
   im Text stehen und den Lesefluss zerhacken. Tastatur-Fokus zeigt sie ebenfalls. */
.wk-entry-tools { opacity: 0; transition: opacity .12s; }
.wk-faq > summary:hover .wk-entry-tools,
.wk-entry-tools:focus-within { opacity: 1; }

/* Sprungziel kurz markieren: wer aus der Suche oder aus der App hierher kommt, soll sehen, WO er gelandet
   ist — ohne die Markierung sucht man die Antwort auf der Seite, auf der man schon steht. */
.wk-faq.is-flash { animation: wk-flash 1.6s ease-out; }
@keyframes wk-flash {
  0%, 40% { background: var(--selected); }
  100%    { background: transparent; }
}

/* EDITOR (cap wiki_edit) — der Rahmen in Akzentfarbe ist Bedeutung, nicht Deko: „hier wird gerade
   geschrieben, nicht gelesen". */
.wk-editor { display: grid; gap: var(--sp-2); margin: var(--sp-2) 0 var(--sp-4);
  padding: var(--sp-3); border: 1px solid var(--accent); border-radius: var(--r-2); background: var(--canvas); }
.wk-elbl { display: grid; gap: var(--sp-1);
  font: 600 var(--fs-micro) var(--f-ui); letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
.wk-eq { width: 100%; }
.wk-ea { width: 100%; height: auto; padding: var(--sp-2); font-family: var(--f-mono); font-size: var(--fs-micro); line-height: 1.6; resize: vertical; text-transform: none; letter-spacing: 0; }
.wk-eact { display: flex; align-items: center; gap: var(--sp-2); }
.wk-emsg { font-size: var(--fs-dense); }

/* Entwickler-Credit in der Fußleiste — er ist eine Fußnote, also steht er in einer Fußleiste. */
.wk-madeby { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.wk-madeby img { flex: none; height: 20px; width: auto; border-radius: 2px; }

/* ── WIKI-INHALT (`.wk-rt`) — das HTML kommt aus der DATENBANK ────────────────
   ⚠️ Diese Klassennamen sind NICHT frei wählbar: sie stehen in `wiki_topics.intro` und
   `wiki_entries.answer` (11 Themen / 49 Einträge, aus der alten info.html geseedet) und wurden
   gegen `app.css` geschrieben. Mit dem Cutover fällt app.css weg — ohne diesen Block wären die
   Wiki-Texte ab dann unformatiert.
   Deshalb steht das Vokabular hier, SCOPED auf `.wk-rt`: keine dieser Regeln wirkt außerhalb des
   Wiki-Textes, und keine ist aus app.css kopiert — alle sind aus den Tokens abgeleitet.
   Der saubere Endzustand wäre, die 49 DB-Einträge auf das neue Vokabular umzuschreiben
   (`badge-status good` → `badge badge--ok` usw.). Das ist eine INHALTS-Änderung an Texten, die
   Sascha selbst editiert — sie gehört nicht in einen Layout-Cutover. Siehe Rückmeldung.
   ─────────────────────────────────────────────────────────────────────────── */

/* `var(--brand)` kommt in Inline-Styles der DB vor (Legenden-Punkte im Architektur-Thema).
   Das Token ist mit app.css gestorben — hier lokal auf das neue gemappt, statt es global
   wiederzubeleben. */
.wk-rt { --brand: var(--primary); font-size: var(--fs-body); line-height: 1.6; color: var(--text); }
.wk-rt > :first-child { margin-top: 0; }
.wk-rt > :last-child { margin-bottom: 0; }
/* Zeilenmaß: 780 px sind bei 15 px rund 95 Zeichen — die Obergrenze für angenehmes Lesen, bewusst am
   oberen Rand, weil der Text sonst auf einem breiten Schirm als schmales Band im Nichts steht. Alles,
   was KEIN Fließtext ist (Kacheln, Formeln, Badge-Raster, Tabellen), hat diesen Deckel absichtlich nicht. */
.wk-rt p, .wk-rt ul, .wk-rt ol { max-width: var(--measure); margin: var(--sp-3) 0; }
.wk-rt ul, .wk-rt ol { padding-left: var(--sp-5); }
.wk-rt li { margin: var(--sp-1) 0; }
.wk-rt code { font-family: var(--f-mono); font-size: var(--fs-dense);
  background: var(--sunken); border: 1px solid var(--border); border-radius: 2px; padding: 0 4px; overflow-wrap: anywhere; }
.wk-rt strong { font-weight: 600; }

/* ⛔⛔ ALLES UNTER `.wk-rt` WIRD VON DER DATENBANK BENUTZT, NICHT VON EINEM TEMPLATE (03.08.).
   Die Wiki-Inhalte liegen seit Migration 070 als HTML in `wiki_topics.intro` und `wiki_entries.answer`.
   Wer tote Klassen sucht, indem er `views/`, `src/` und `public/js/` durchsucht, findet HIER die
   Klassen nie — und haelt sie fuer tot. Genau das ist beim Aufraeumen am 03.08. passiert: die zehn
   `arch-*`-Regeln des Architektur-Diagramms standen schon auf der Loeschliste, sie leben aber im
   `intro` des Themas „Architektur".
   REGEL: Vor dem Loeschen einer `.wk-rt`-Klasse gegen die DATENBANK pruefen, nicht gegen die Dateien:
     select 1 from wiki_topics  where intro  like '%KLASSE%'
     union all
     select 1 from wiki_entries where answer like '%KLASSE%';
   Betroffen sind aktuell: wiki-lede, wk-lede-sub, br-chip/br-do/br-text, st-t, hinweis--tip,
   arch-diagram/arch-tier/arch-node/arch-arrow/arch-legend/an-dot/an-name/an-role. */

/* Einleitungssatz eines Themas — dieselbe Figur wie `.lead`, nur mit dem DB-Namen. */
.wk-rt .wiki-lede { max-width: var(--measure); margin: 0 0 var(--sp-4); font-size: var(--fs-sect); line-height: 1.5; color: var(--text-muted); }
.wk-rt .wiki-lede strong { color: var(--text); }

/* Container um Diagramme (DB schreibt `card`). Panel-Figur: Rahmen, kein Schatten — es liegt. */
/* Der Kasten ist ein PROSA-Container, kein Raster: sein Text bricht bei 680 px, also endet auch sein
   Rahmen dort (+ Innenabstand). Ohne den Deckel dehnte er sich auf die vollen 1120 px und ließ bei
   1920 px rund 440 px leeren Rahmen rechts stehen — der Breiten-Gewinn landete im Nichts.
   Die Regel lautet damit sauber: Prosa und Prosa-Container 680/720, echte Raster 1120. */
.wk-rt .card { max-width: var(--measure); padding: var(--sp-3); margin: var(--sp-3) 0; border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); }

/* Badges im Fließtext. Sie ZITIEREN die Badges der Arbeitsfläche, deshalb tragen sie deren Figur
   (18 px, --r-1, 600/11) und deren Bedeutungs-Farben — ein Wiki, dessen Badge anders aussieht als
   der Badge, den es erklärt, erklärt nichts. */
.wk-rt .badge-status, .wk-rt .badge-action {
  display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: var(--r-1);
  font: 600 var(--fs-micro)/18px var(--f-ui); white-space: nowrap;
  border: 1px solid var(--badge-neu-bd); background: var(--badge-neu-bg); color: var(--badge-neu-fg);
}
.wk-rt .badge-action { text-decoration: none; }
.wk-rt .badge-status.good,  .wk-rt .badge-action.good  { background: var(--badge-ok-bg);   border-color: var(--badge-ok-bd);   color: var(--badge-ok-fg); }
.wk-rt .badge-status.warn,  .wk-rt .badge-action.warn  { background: var(--badge-warn-bg); border-color: var(--badge-warn-bd); color: var(--badge-warn-fg); }
.wk-rt .badge-status.bad,   .wk-rt .badge-action.bad   { background: var(--badge-err-bg);  border-color: var(--badge-err-bd);  color: var(--badge-err-fg); }
.wk-rt .badge-status.brand, .wk-rt .badge-action.brand { background: var(--selected); border-color: var(--border-strong); color: var(--accent); }
.wk-rt .badge-status.soft,  .wk-rt .badge-action.soft,
.wk-rt .badge-status.muted, .wk-rt .badge-action.muted { background: var(--sunken); border-color: var(--border); color: var(--text-muted); }

/* Badge-Erklärtabelle: Chip links, Bedeutung rechts, „→ was tun" darunter. */
.wk-rt .badge-cat { max-width: var(--measure); margin: var(--sp-3) 0; }
.wk-rt .badge-row { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: var(--sp-1) var(--sp-3);
  align-items: start; padding: var(--sp-2) 0; border-top: 1px solid var(--row-line); }
.wk-rt .badge-row:first-child { border-top: 0; }
.wk-rt .br-chip { display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.wk-rt .br-text { font-size: var(--fs-dense); line-height: 1.55; color: var(--text-muted); }
.wk-rt .br-text strong { color: var(--text); }
.wk-rt .br-do { margin-top: var(--sp-1); font-size: var(--fs-micro); line-height: 1.5; color: var(--text-faint); }

/* Prozess-Kacheln („1 Listen einlesen → 2 Sortiment → …"). Klickbare Karten, deshalb Panel-Figur. */
.wk-rt .workflow-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--sp-2); margin: var(--sp-3) 0 var(--sp-4); }
.wk-rt .wf { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); color: var(--text); }
.wk-rt .wf:hover { border-color: var(--border-strong); background: var(--hover); text-decoration: none; }
.wk-rt .wf-step { display: grid; place-items: center; width: 20px; height: 20px; border-radius: var(--r-full);
  background: var(--primary); color: #fff; font: 600 var(--fs-micro) var(--f-ui); }
.wk-rt .wf strong { font-size: var(--fs-ui); font-weight: 600; }
.wk-rt .wf span { font-size: var(--fs-dense); line-height: 1.5; color: var(--text-muted); }

/* ── PROZESS-BAUSTEINE (Wiki-Relaunch 02.08.) ────────────────────────────────
   Vier Figuren, aus denen die Prozess-Anleitungen gebaut sind. Sie stehen HIER und nicht als
   Inline-Style im Text, obwohl der Text aus der Datenbank kommt — genau deshalb: eine im Text
   eingebackene Farbe wäre die eine Stelle, die beim nächsten Theme-Wechsel nicht mitzieht, und
   niemand fände sie wieder. Der Editor listet die vier unter dem Textfeld auf; ein Baustein, von
   dem niemand weiß, wird nicht benutzt (dieselbe Lehre wie bei den Platzhaltern).

   ⛔ Die SCHRITT-NUMMER kommt aus einem CSS-Zähler, nicht aus dem Text. Wer einen Schritt
      einschiebt, müsste sonst alle folgenden von Hand umnummerieren — und das wird beim dritten
      Mal vergessen, worauf die Anleitung zwei Schritte 3 hat und keinen 4.
   ─────────────────────────────────────────────────────────────────────────── */
.wk-rt ol.steps { list-style: none; counter-reset: schritt; max-width: var(--measure);
  margin: var(--sp-4) 0; padding: 0; }
.wk-rt ol.steps > li { counter-increment: schritt; display: grid;
  grid-template-columns: 76px minmax(0, 1fr); gap: var(--sp-3);
  margin: 0; padding: var(--sp-3) 0; border-top: 1px solid var(--row-line); }
.wk-rt ol.steps > li:first-child { border-top: 0; padding-top: 0; }
.wk-rt ol.steps > li::before {
  content: "Schritt " counter(schritt); grid-column: 1; grid-row: 1;
  align-self: start; justify-self: start;
  height: 20px; padding: 0 var(--sp-2); border-radius: var(--r-full);
  background: var(--selected); color: var(--accent);
  font: 600 var(--fs-micro)/20px var(--f-ui); white-space: nowrap; }
/* ⛔ ALLE Kinder in Spalte 2 — sonst füllt die Auto-Platzierung des Rasters die Zellen der Reihe nach:
   die Marke in Zeile 1/Spalte 1, die Überschrift in Zeile 1/Spalte 2, der ERSTE ABSATZ aber wieder in
   Spalte 1 — also in die 76 px, die für die Marke gedacht sind. Sichtbar wurde das als Textsäule von
   einem Wort Breite, während die Begriffstabelle daneben aus dem Schritt herauslief. */
.wk-rt ol.steps > li > * { grid-column: 2; min-width: 0; }
/* Im Schritt gilt das Zeilenmaß nicht noch einmal — es steckt schon in der Liste. Ein zweiter
   Deckel machte aus 780 px erst 690 und dann, mit der Kasten-Verschachtelung, 600. */
.wk-rt ol.steps p, .wk-rt ol.steps ul, .wk-rt ol.steps ol,
.wk-rt ol.steps dl.kv, .wk-rt ol.steps .hinweis { max-width: none; margin: var(--sp-2) 0; }
.wk-rt ol.steps > li > :first-child { margin-top: 0; }
.wk-rt ol.steps > li > :last-child { margin-bottom: 0; }
/* Der erste fette Satz eines Schritts ist seine Überschrift — er darf auf eigener Zeile stehen. */
.wk-rt ol.steps .st-t { display: block; font-size: var(--fs-ui); font-weight: 600; color: var(--text); }

/* Kasten für das, was man falsch machen kann (--warn) oder wissen sollte (--tip). Bewusst schmaler
   Farbstreifen links statt flächiger Tönung: ein farbiger Block im Fließtext liest sich als Warnung,
   auch wenn nur eine Randnotiz drinsteht. */
.wk-rt .hinweis { max-width: var(--measure); margin: var(--sp-3) 0; padding: var(--sp-3);
  border: 1px solid var(--border); border-left: 3px solid var(--border-strong);
  border-radius: var(--r-2); background: var(--sunken);
  font-size: var(--fs-dense); line-height: 1.6; color: var(--text-muted); }
.wk-rt .hinweis strong { color: var(--text); }
.wk-rt .hinweis p, .wk-rt .hinweis ul, .wk-rt .hinweis ol { max-width: none; margin: var(--sp-2) 0; }
.wk-rt .hinweis > :first-child { margin-top: 0; }
.wk-rt .hinweis > :last-child { margin-bottom: 0; }
.wk-rt .hinweis--warn { border-left-color: var(--warn); }
.wk-rt .hinweis--tip { border-left-color: var(--good); }

/* Der Absprung auf die echte Seite. Eine Anleitung ohne Tür zur Sache ist ein Aufsatz — man liest
   sie zu Ende und steht immer noch im Wiki. */
.wk-rt a.go { display: inline-flex; align-items: center; gap: var(--sp-2); height: 28px;
  margin: var(--sp-1) var(--sp-2) var(--sp-1) 0; padding: 0 var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas);
  color: var(--accent); font: 600 var(--fs-dense)/28px var(--f-ui);
  text-decoration: none; white-space: nowrap; }
.wk-rt a.go::after { content: "→"; color: var(--text-faint); }
.wk-rt a.go:hover { background: var(--hover); border-color: var(--accent); text-decoration: none; }
.wk-rt a.go:hover::after { color: var(--accent); }

/* Begriff/Bedeutung als Karte statt als Fließtext-Aufzählung. Absichtlich `dt`/`dd` im Wechsel und
   ohne Wrapper-<div>: die Liste wird von Hand im Editor getippt, und jede Verschachtelung mehr ist
   eine Stelle, an der ein `</div>` fehlt. */
.wk-rt dl.kv { display: grid; grid-template-columns: minmax(110px, max-content) minmax(0, 1fr);
  max-width: var(--measure); margin: var(--sp-3) 0;
  border: 1px solid var(--border); border-radius: var(--r-2); overflow: hidden; }
.wk-rt dl.kv dt { padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--border);
  background: var(--sunken); font: 600 var(--fs-dense)/1.55 var(--f-ui); color: var(--text); }
.wk-rt dl.kv dd { margin: 0; padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--border);
  font-size: var(--fs-dense); line-height: 1.55; color: var(--text-muted); }
.wk-rt dl.kv dt:first-of-type, .wk-rt dl.kv dt:first-of-type + dd { border-top: 0; }

/* SCHMALER CONTAINER — dieselbe Messung wie bei der zweiten Spalte, nur von der anderen Seite.
   Der Wiki-Baum ist 264 px breit und schrumpft nicht mit: Bei 950 px Fenster bleiben dem Dokument
   gemessene 407 px, und davon nahm die Schritt-Marke noch einmal 76. Übrig blieb eine Textsäule von
   319 px, in der die Begriffstabelle vierzeilig umbrach. Unter 620 px wandert die Marke deshalb ÜBER
   den Schritt, statt neben ihm Platz zu kosten.
   ⛔ Als @media wäre das erneut die falsche Größe: Derselbe Schritt steht im Suchergebnis und auf
      breiten Schirmen in 780 px — dort ist die zweispaltige Form richtig.
   ⛔ Und die Reihenfolge ist kein Zufall: Ein @container erhöht die Spezifität nicht. Stünde dieser
      Block VOR den Grundregeln, gewänne die gleichrangige spätere Regel und die Schmal-Fassung wäre
      wirkungslos — genau so ist sie beim ersten Anlauf durchgefallen. */
@container wkdoc (max-width: 620px) {
  .wk-rt ol.steps > li { grid-template-columns: minmax(0, 1fr); gap: var(--sp-2); }
  .wk-rt ol.steps > li::before, .wk-rt ol.steps > li > * { grid-column: 1; }
}
@container wkdoc (max-width: 430px) {
  .wk-rt dl.kv { grid-template-columns: minmax(0, 1fr); }
  .wk-rt dl.kv dd { border-top: 0; padding-top: 0; }
  .wk-rt dl.kv dt:first-of-type + dd { border-top: 0; }
}

/* Architektur-Diagramm (Thema „Architektur", seit 2026-07-12 ins Wiki aufgesogen). */
.wk-rt .arch-diagram { display: flex; flex-direction: column; gap: var(--sp-1); }
.wk-rt .arch-tier { display: grid; gap: var(--sp-2); }
.wk-rt .arch-tier.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.wk-rt .arch-tier.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.wk-rt .arch-tier.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.wk-rt .arch-node { padding: var(--sp-2) var(--sp-3); border: 1px solid var(--border); border-left: 2px solid var(--border-strong);
  border-radius: var(--r-1); background: var(--sunken); }
.wk-rt .arch-node .an-name { display: block; font-size: var(--fs-ui); font-weight: 600; }
.wk-rt .arch-node .an-role { display: block; margin-top: 1px; font-size: var(--fs-micro); line-height: 1.45; color: var(--text-muted); }
.wk-rt .arch-node.sm { padding: 6px var(--sp-2); }
.wk-rt .arch-node.sm .an-name { font-size: var(--fs-dense); }
.wk-rt .arch-node code { background: var(--canvas); }
/* Schreiben/Lesen/DB sind KATEGORIEN (welche Rolle hat der Knoten), keine Bewertung — deshalb dürfen
   sie Farbe tragen. Sie sind der einzige Ort im Wiki, an dem eine Kante etwas bedeutet. */
.wk-rt .arch-node.tone-write { border-left-color: var(--warn); }
.wk-rt .arch-node.tone-read { border-left-color: var(--good); }
.wk-rt .arch-node.tone-db { border-left-color: var(--primary); border-color: var(--primary); }
.wk-rt .arch-arrow { display: flex; align-items: center; gap: var(--sp-2); margin: 2px 0 2px var(--sp-3);
  font-size: var(--fs-micro); color: var(--text-muted); }
.wk-rt .arch-arrow::before { content: "↓"; font-weight: 600; color: var(--text-faint); }
.wk-rt .arch-arrow.tone-write::before { color: var(--warn); }
.wk-rt .arch-arrow.tone-read::before { color: var(--good); }
.wk-rt .arch-legend { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4);
  margin-top: var(--sp-3); padding-top: var(--sp-2); border-top: 1px solid var(--border);
  font-size: var(--fs-micro); color: var(--text-muted); }
.wk-rt .arch-legend .an-dot { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin-right: var(--sp-1); vertical-align: middle; }

/* ── SCHMALE FENSTER ─────────────────────────────────────────────────────────
   /info ist von der Handy-Weiche ausgenommen (layout.html) — dieser Zustand ist kein Sonderfall,
   sondern der Normalfall für jeden, der die Seite auf dem Handy aufmacht. app-new.css nimmt hier
   schon Rail/Datenstand/Suche weg; die Seite muss nur noch ihre zwei Spalten auflösen.
   ─────────────────────────────────────────────────────────────────────────── */
@media (max-width: 780px) {
  /* Aus „Spalte neben dem Text" wird „Reiterleiste über dem Text": senkrecht gestapelt wären 11
     Themen die ganze erste Bildschirmseite, und der Text begänne unter der Falz. */
  .wk-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  /* Die Chips leben in `.wk-tree`, NICHT direkt in `.wk-nav` — nur `.wk-nav` auf `row` zu stellen ließ
     die Leiste senkrecht und schob den Panel-Kopf bei 760 px auf y=826 von 900 px: wer ein Thema
     anklickte, sah den Baum statt der Antwort (gemessen, ux-designer 19.07.). Das Suchfeld bleibt
     darüber — auf dem Handy ist es der einzige brauchbare Einstieg. */
  .wk-nav { flex-direction: column; gap: 0; padding: 0;
    border-right: 0; border-bottom: 1px solid var(--border); }
  .wk-search { padding: var(--sp-2); border-bottom: 0; }
  .wk-tree, .wk-results { flex-direction: row; display: flex; gap: var(--sp-1);
    padding: 0 var(--sp-2) var(--sp-2); overflow-x: auto; overflow-y: hidden; }
  .wk-tree::-webkit-scrollbar, .wk-results::-webkit-scrollbar { height: 0; }
  .wk-topic { width: auto; flex: none; height: 28px; border-radius: var(--r-1); }
  /* Die zweite Ebene hat in einer waagerechten Leiste keinen Platz — und keinen Zweck: man sieht
     ohnehin nur ein Thema auf einmal. */
  .wk-sub { display: none !important; }
  /* Trefferliste waagerecht wäre unlesbar: sie bleibt gestapelt und bekommt die volle Breite. */
  .wk-results { flex-direction: column; overflow-x: hidden; }
  .wk-res { border-radius: var(--r-1); }
  /* Die Kante folgt der Achse: senkrechte Liste → links, waagerechte Leiste → unten. */
  .wk-topic.is-active { box-shadow: inset 0 -2px 0 var(--primary); }
  .wk-rt .arch-tier.cols-4, .wk-rt .arch-tier.cols-3, .wk-rt .arch-tier.cols-2 { grid-template-columns: minmax(0, 1fr); }
  .wk-rt .badge-row { grid-template-columns: minmax(0, 1fr); }
  /* Die Prozess-Bausteine brauchen hier NICHTS: Ihre Schmal-Fassung hängt am Container, nicht am
     Fenster, und auf dem Handy ist der Container ohnehin unter 620 px. Sie hier ein zweites Mal zu
     regeln wären zwei Stellen für eine Sache — die eine zieht man später nach, die andere nicht. */
  .wk-madeby span { overflow: hidden; text-overflow: ellipsis; }
}

/* ── /konfigurator — Shop-Konfigurator (Cutover 2026-07-17) ─────────────────────
   Nachfolger der app.css-Figuren .versand-* · .pp-* · .cfg-cell · .wt-toolbar · .settings-*.
   Aus den Tokens abgeleitet, keine Zeile kopiert (die alten standen in rem).
   Was ersatzlos entfiel, weil es einen Nachfolger hat:
     .card → .panel · .table → .grid · .btn-primary → .btn--primary · badge-status → .badge--*
     .wt-toolbar → {% block actionbar %} (die EINE Primäraktion der Seite sitzt dort)
     .settings-card/.settings-intro/.settings-save-bar → .panel/.panel-head/.lead/.kf-save
     .filterbar (Vorschau-Kopf) → .kf-form
   ─────────────────────────────────────────────────────────────────────────── */

/* Abschnittskopf über einer randfüllenden Tabelle: die Tabelle darf keinen .page-Wrapper bekommen
   (Regel 3), ihre Überschrift braucht aber den Seiten-Innenabstand. */
.kf-sect { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-2);
  padding: var(--sp-5) var(--sp-4) var(--sp-2); }
.kf-sect h2 { margin: 0; font-size: var(--fs-sect); font-weight: 600; }
.kf-note { max-width: 92ch; margin: 0 0 var(--sp-3); padding: 0 var(--sp-4); font-size: var(--fs-dense);
  line-height: 1.55; color: var(--text-muted); }
.kf-note--tight { margin-bottom: var(--sp-2); }
.kf-details { margin: 0 0 var(--sp-3); padding: 0 var(--sp-4); font-size: var(--fs-dense); color: var(--text-muted); }
.kf-details > summary { cursor: pointer; color: var(--accent); list-style: none; padding: var(--sp-1) 0; }
.kf-details > summary::-webkit-details-marker { display: none; }
.kf-details > summary:hover { text-decoration: underline; }
.kf-details p { max-width: 92ch; margin: var(--sp-2) 0 0; line-height: 1.55; }
.kf-details code { font-family: var(--f-mono); font-size: var(--fs-micro); background: var(--sunken); padding: 0 3px; border-radius: 2px; }

/* Formularzeile mit Beschriftung über dem Feld — kein .frow-Raster (das ist für ein Seitenformular
   mit 160-px-Labelspalte gebaut; hier stehen die Felder nebeneinander). */
.kf-form { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-2) var(--sp-3); }
.kf-form label { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0;
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.kf-form .kf-grow { flex: 1 1 240px; }
.kf-msg { font-size: var(--fs-micro); }
.kf-save { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); }
.kf-w-cap { width: 116px; }
.kf-w-mode { width: 176px; }
.kf-w-steps { width: 100%; }

/* ── VERSANDSTAFFEL ────────────────────────────────────────────────────────
   Die alten Stufen-Marken waren grün/bernstein/orange/rot durchgefärbt. Das ist Dekoration:
   eine Versandstufe ist eine SKALA (länger = teurer), kein Urteil — „Stufe 1" ist nicht gut und
   „Spedition" nicht schlecht (Regel 2, Entscheidung D: Grün/Gelb/Rot gehört exklusiv der
   Marge-Ampel). Die Reihenfolge trägt die Bedeutung schon; sie steht in der Zeile. */
.kf-vs { display: grid; gap: var(--sp-2); max-width: 620px; margin: var(--sp-2) 0 var(--sp-4); }
.kf-vs-head, .kf-vs-row { display: grid; grid-template-columns: 110px 1fr 1fr; gap: var(--sp-3); align-items: center; }
.kf-vs-head { font: 600 var(--fs-micro) var(--f-ui); letter-spacing: .04em; text-transform: uppercase;
  color: var(--text-faint); }
.kf-vs-badge { display: inline-flex; align-items: center; height: 24px; padding: 0 var(--sp-2);
  border-left: 2px solid var(--border-strong); background: var(--sunken);
  font: 500 var(--fs-dense) var(--f-ui); color: var(--text); }
.kf-vs-frei { font-size: var(--fs-dense); }
.kf-vs-notice { display: block; max-width: 620px; margin-bottom: var(--sp-2);
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.kf-vs-notice textarea { width: 100%; margin-top: var(--sp-1); padding: 6px var(--sp-2); resize: vertical;
  border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas);
  color: var(--text); font: inherit; font-size: var(--fs-ui); line-height: 18px; }

/* Häkchen + Standardwert in einer Tabellenzelle. */
.kf-cell { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); }
.kf-cell input[type="checkbox"] { accent-color: var(--primary); margin: 0; flex: none; cursor: pointer; }
.kf-w-deflen { width: 78px; }
.kf-w-defcut { width: 74px; }
.kf-w-defri { width: 106px; }
.grid td.kf-mid, .grid th.kf-mid { text-align: center; }

/* Spaltengeometrie — EINE Quelle statt style="" am th (.grid ist table-layout:fixed).
   ⚠️ `table-layout:fixed` allein hält die Breite nicht: die .grid trägt zugleich `width:max-content`,
   dessen Messung an nowrap-Inhalt wächst. Wo Inhalt breiter werden kann, steht `max-width:0` an der
   Zelle (.kf-clip) — sonst scrollt die Tabelle waagerecht, obwohl sie passt. */
.grid [data-c="kf-wg"]   { width: 230px; }
.grid [data-c="kf-ueber"]{ width: 160px; }
.grid [data-c="kf-len"]  { width: 150px; }
.grid [data-c="kf-cut"]  { width: 176px; }
.grid [data-c="kf-ri"]   { width: 180px; }
.grid [data-c="kf-prod"] { width: 100px; }
.grid [data-c="kf-var"]  { width: 100px; }

.grid [data-c="ml-wg2"]    { width: 240px; }
.grid [data-c="ml-prod"]   { width: 100px; }
.grid [data-c="ml-cap"]    { width: 130px; }
.grid [data-c="ml-mode"]   { width: 190px; }
.grid [data-c="ml-steps"]  { width: 260px; }
.grid [data-c="ml-save"]   { width: 160px; }
.kf-clip { max-width: 0; }
.kf-w-mlcap { width: 96px; }
.kf-w-mlmode { width: 172px; }
.kf-w-mlsteps { width: 100%; }

/* ── PREISPARAMETER je Warengruppe ─────────────────────────────────────────
   Karten, weil hier je Warengruppe ein eigener kleiner Datensatz gepflegt wird (kein Vergleich
   von oben nach unten -> keine Tabelle). */
.kf-pp { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: var(--sp-4);
  padding: 0 var(--sp-4) var(--sp-4); align-items: start; }
.kf-pp-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.kf-pp-head strong { font-size: var(--fs-sect); font-weight: 600; }
.kf-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-3);
  align-items: start; margin-top: var(--sp-3); }
.kf-field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0;
  font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.kf-field.kf-wide { grid-column: span 2; }
.kf-help { font-size: var(--fs-micro); line-height: 1.35; color: var(--text-faint); }
/* Die Mindestberechnung trägt den Preis jeder Kurzlänge — sie ist das Feld, das man zuerst sucht.
   Betonung über die Rahmenfarbe, nicht über eine Ampelfarbe (Regel 2). */
.kf-field.kf-primary input { border-color: var(--accent); font-weight: 600; }

.kf-sub { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.kf-sub-t { font: 600 var(--fs-dense)/18px var(--f-ui); color: var(--text); }
.kf-sub-n { margin: 2px 0 var(--sp-2); font-size: var(--fs-micro); line-height: 1.5; color: var(--text-muted); }

/* Staffelzeilen: display:contents -> die Spalten fluchten mit der Kopfzeile, ohne Wrapper-Element. */
.kf-tiers { display: grid; grid-template-columns: max-content max-content max-content;
  gap: var(--sp-2) var(--sp-3); align-items: center; margin: var(--sp-2) 0 var(--sp-3); }
.kf-tier, .kf-tier-head { display: contents; }
.kf-tier-head > span { font: 600 var(--fs-micro) var(--f-ui); letter-spacing: .03em; text-transform: uppercase;
  color: var(--text-faint); }
.kf-tier-qty, .kf-tier-eur { width: 104px; }

/* Konfigurator-Vorschau (Fremd-Widget aus dem Shopapi). */
.kf-pv { padding: 0 var(--sp-4) var(--sp-5); }
.kf-pv-mount { margin-top: var(--sp-3); }
.kf-pv-status { margin: var(--sp-3) 0 0; font-size: var(--fs-micro); color: var(--text-muted); }

/* MaxLaenge-Ampel: meldet Produkte ohne eigene Ist-Länge. Gleiche Figur wie .note (Kante links +
   gesenkter Grund) — die Kantenfarbe setzt die JS, weil sie den Befund erst zur Laufzeit kennt. */
.kf-ampel { margin: 0 var(--sp-4) var(--sp-3); }

/* ── Lieferzeiten (/sortiment/lieferzeiten) ───────────────────────────────────
   Präfix `.lz-`. Nachfolger von `.rule-form` / `.rule-actions` aus app.css — neu aus den
   Tokens, nicht portiert (die alten Werte waren rem und lagen neben dem 4er-Raster).
   `.subtabs` ist ersatzlos weg: die drei Reiter sind jetzt `.views`/`.view-tab` in der Shell. */

.lz-note { align-items: flex-start; margin-bottom: var(--sp-4); }
.lz-lead { margin: 0 0 var(--sp-3); }
/* Texte für Bestand/Zuschnitt: Label, Feld, Erklärung untereinander. Das Feld bekommt das Textmaß aus
   --measure statt der vollen Panelbreite — ein einzeiliger Satz über 1200 px liest sich nicht besser. */
.lz-text-row { margin-bottom: var(--sp-4); max-width: var(--measure); }
.lz-text-row label { display: block; margin-bottom: var(--sp-1); font-weight: 600; font-size: var(--fs-ui); }
.lz-text-row input { width: 100%; }
.lz-text-row p { margin: var(--sp-1) 0 0; font-size: var(--fs-micro); }
.lz-msg { font-size: var(--fs-dense); }

/* Zwei Panels nebeneinander, ab 760 px untereinander. `overflow:hidden` ist tragend: die Tabelle
   darin ist `width:max-content` und würde sonst über den Panel-Rahmen hinauslaufen. */
/* 460 px Mindestbreite, nicht 340: die Übersichts-Tabellen tragen fünf Spalten, und unter ~460 px
   schnitten die Köpfe „mit eigener Regel"/„Lieferzeit(en)" wortlos ab (gemessen bei 1.100 px Fenster).
   Lieber untereinander und lesbar als nebeneinander und abgeschnitten. */
.lz-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(460px, 1fr));
  gap: var(--sp-4); align-items: start; margin-bottom: var(--sp-4); }
.lz-cols .panel { overflow: hidden; }
/* Sicherheitsnetz: `.grid th` ist nowrap ohne Ellipse — ein zu enger Kopf bricht sonst mitten im Wort ab
   und liest sich als Rendering-Fehler statt als „hier fehlt Platz". */
.lz-cols .grid th { overflow: hidden; text-overflow: ellipsis; }
/* `.grid` ist auf `width:max-content` gebaut (Arbeitsfläche: die Tabelle IST die Seite und darf den
   Canvas breit schieben). Hier steckt sie in einem halb so breiten Panel — ohne diese zwei Zeilen
   läuft sie unter dem `overflow:hidden` des Panels weg und die letzte Spalte ist wortwörtlich
   abgeschnitten (gemessen: „Lieferzeit(e" statt „Lieferzeit(en)"). Mit `width:100%` greift wieder
   das `text-overflow: ellipsis` der Zelle — abgeschnitten wird der Wert, nicht die Spalte. */
.lz-cols .grid { width: 100%; min-width: 0; }

/* Waagerechtes Formular: Beschriftung über dem Feld, Aktionen am Ende der Zeile. Es ist bewusst
   kein `.form-layout` — das ist die Einstellungs-Figur (Label links, 160 px Spalte). Hier stehen
   drei kurze Felder, die zusammen EINEN Satz bilden („Lieferant X bekommt Y"). */
.lz-form { display: flex; align-items: flex-end; gap: var(--sp-3); flex-wrap: wrap; }
.lz-field { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.lz-field > span { font: 400 var(--fs-dense)/16px var(--f-ui); color: var(--text-muted); }
.lz-field .input, .lz-field .select { min-width: 210px; }
.lz-acts { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-height: 26px; }

.lz-newval { width: 300px; }
.lz-cov { margin-bottom: var(--sp-4); }
.lz-cov-p { margin: 0; font-size: var(--fs-body); line-height: 20px; }
.lz-cov-p strong { font-variant-numeric: tabular-nums; }

/* Farbwahl: der native Farbknopf trägt keinen eigenen Rahmen — er bekäme sonst überall einen anderen. */
.lz-color { display: flex; align-items: center; gap: var(--sp-2); }
.val-color { width: 28px; height: 26px; padding: 0; border: 1px solid var(--border-strong);
  border-radius: var(--r-1); background: var(--canvas); cursor: pointer; }
.val-color::-webkit-color-swatch-wrapper { padding: 2px; }
.val-color::-webkit-color-swatch { border: 0; border-radius: 2px; }
.val-color:disabled { cursor: not-allowed; opacity: .5; border-style: dashed; }

.lz-c-prio { width: 116px; }
.lz-c-scope { width: 104px; }
.lz-c-label { width: 172px; }
.lz-c-act { width: 72px; }
.lz-c-act2 { width: 118px; }
.lz-c-color { width: 172px; }
/* Zahlenspalten sind so breit wie ihr KOPF, nicht wie ihr Wert — „mit eigener Regel" ist das längste
   Wort der Spalte, die Zahl darunter ist dreistellig. */
.lz-c-n { width: 92px; }
.lz-c-n-wide { width: 130px; }

/* ── /sortiment/laengen — Max. Länge (Cutover 2026-07-17) ───────────────────────
   Nachfolger der app.css-Figuren .export-dash/.vtile* · .mlchip · .sort-ind · .vk-inline.
   Was ersatzlos entfiel, weil es einen Nachfolger hat:
     .export-dash/.vtile (KPI-Kacheln, die in Wahrheit View-Filter waren) → {% block tabs %} + .view-tab
       — dieselbe Streichung wie auf der Arbeitsfläche: „Als Tabs sagen sie, was sie sind."
     .mlchip (Längen-/Quellen-Chips) → .fdrop + .fchips + .chip aus der Facetten-Kopfzeile.
       Der alte Teppich aus ~20 Knöpfen stand ungekapselt in der Seite; als Facette bringt jede
       Liste ihren eigenen Scroll mit.
     #bulkBar (.card, position:sticky) → .actionbar.is-selecting + .ab-sel — eine Leiste, die beim
       Auswählen erscheint, schöbe die Tabelle 40 px nach unten, genau während der Blick auf einer Zeile liegt.
     th.sortable/.sort-ind → eigene Sortierung bleibt (die Seite lädt alles und sortiert client-seitig)
       und bringt ihre eigene Pfeil-Figur mit: der zentrale Indikator hängt an `data-sort`, das
       diese Seite bewusst nicht trägt.
     .alert/.alert-error → .note/.note--err · badge-status → .badge/.mark-warn · .right → .num
   ─────────────────────────────────────────────────────────────────────────── */

/* Kontext-Streifen über der Tabelle: was der Wert bedeutet + woher die globale Regel kommt.
   Er qualifiziert JEDE Zahl darunter, deshalb steht er darüber (gleiche Figur wie .wb-basis). */
.ml-basis { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-6); align-items: baseline;
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border); font-size: var(--fs-dense); }
.ml-basis-z { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.ml-basis-k { flex: none; font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-faint); }
.ml-basis-t { max-width: 78ch; color: var(--text-muted); line-height: 1.45; }

/* Spaltengeometrie — EINE Quelle statt style="" am th (.grid ist table-layout:fixed).
   Summe (inkl. .col-cb 24) = 1206 und damit knapp unter dem Canvas bei 1440er Fenster: die Tabelle
   passt, statt 54 px waagerecht zu scrollen. Verteilung nach Bedarf gemessen, nicht geschätzt:
   „Angewandte Regel" trägt höchstens „Produkt-Wert 3000", die Warengruppe dagegen „Metrische
   Gewindestangen" — bei 140 px war JEDE Warengruppe abgeschnitten. */
.grid [data-c="ml-ident"] { width: 230px; }
.grid [data-c="ml-wg"]    { width: 175px; }
.grid [data-c="ml-base"]  { width: 124px; }
.grid [data-c="ml-src"]   { width: 215px; }
.grid [data-c="ml-rule"]  { width: 130px; }
.grid [data-c="ml-final"] { width: 148px; }
.grid [data-c="ml-edit"]  { width: 160px; }

/* ⚠️ `table-layout: fixed` allein hält die Breite NICHT: die .grid trägt zugleich `width: max-content`,
   und dieser Messdurchgang wächst an nowrap-Inhalt — die deklarierte Breite ist faktisch nur ein Minimum.
   Gemessen: Warengruppe 140→153, Quelle 200→251. `max-width: 0` nimmt die Zelle aus der max-content-
   Rechnung; die Ellipse in .grid td greift dann wie vorgesehen. Gleiche Figur wie .col-comp (Wettbewerb). */
.ml-clip { max-width: 0; }

/* Quelle: eine QUELLE trägt einen Text-Chip, nie eine Farbe (Entscheidung D) — deshalb sind alle
   vier (Herstellerliste · WaWi · Shop-Wert · WG-Fallback) neutral. Den Beleg (Artikelnr.) daneben. */
.ml-src { display: flex; align-items: center; gap: 5px; min-width: 0; }
.ml-src-sku { overflow: hidden; text-overflow: ellipsis; font-family: var(--f-mono); font-size: var(--fs-micro);
  color: var(--text-muted); }

/* ⚠️ Diese Seite sortiert SELBST (`data-mlsort`), nicht über table.js (`data-sort`) — und das ist Absicht:
   table.js' units() schiebt jede `hidden` Zeile als „extra" an die vorhergehende sichtbare Zeile. Das ist
   für den Varianten-Baum richtig (zugeklappte Kinder wandern mit), hier aber falsch: `hidden` heisst auf
   dieser Seite „vom Client-Filter ausgeblendet". Ein Sort bei aktivem Filter hätte die ausgeblendeten
   Zeilen unter willkürliche Nachbarn geklebt. Zusätzlich wirft table.js die .empty-state-Zeile aus der
   Sortierung und lässt sie so mitten in der Liste stehen. Solange BEIDE `data-sort` lesen, sortieren zwei
   Implementierungen denselben Klick — deshalb der eigene Attributname. Der Preis: der zentrale
   Pfeil (th[data-sort]::after + .sorted-asc) gehört table.js, hier braucht es einen eigenen Indikator. */
.sort-ind { font-size: var(--fs-micro); color: var(--accent); margin-left: 3px; }

/* Inline-Bearbeitung des Produkt-Werts. Enter IST „speichern" (der Haken bleibt für die Maus). */
.ml-edit { display: flex; align-items: center; gap: var(--sp-1); }
.ml-w-cap { width: 76px; }

/* ── /sortiment/rohbestand — Rohbestand Längenware (RFC Phase 1, read-only) ─────────────────────
   Spaltengeometrie wie .ml-* (EINE Quelle statt style="" am th; .grid ist table-layout:fixed). Die
   Verteilungs-Spalte bekommt den meisten Platz — sie IST der Inhalt der Seite. */
.grid [data-c="rs-ident"]  { width: 210px; }
.grid [data-c="rs-wg"]     { width: 144px; }
.grid [data-c="rs-total"]  { width: 100px; }
.grid [data-c="rs-lc"]     { width: 78px; }
.grid [data-c="rs-dist"]   { width: 456px; }
.grid [data-c="rs-maxlen"] { width: 116px; }
/* Warengruppe-td clippt bereits über .ml-clip (max-width:0 nur am td, nicht am th) — kein Zusatz nötig. */

/* Bestandsverteilung: neutrale Fakten-Chips (Farbe nur der Marge-Ampel, Entscheidung D), längste
   Lagerlänge (= Rohstange) leicht hervorgehoben. Sie wrappen innerhalb der Zelle.
   ⚠️ Die Zelle braucht max-width:0 (NUR am td, nicht am th — gleiche Figur wie .ml-clip): ein flex-wrap-
   Container meldet als max-content ALLE Chips in EINER Reihe, und `.grid` ist `width: max-content` — ohne
   den Clip bläht diese eine Spalte die ganze Tabelle über den Canvas hinaus. Mit dem Clip fällt sie auf
   ihre deklarierte Spaltenbreite (th) zurück und die Chips wrappen darin; die Zeilenhöhe wächst mit. */
.rs-distcell { max-width: 0; white-space: normal; }
.rs-dist { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 0; }
.rs-len { display: inline-flex; align-items: baseline; gap: 1px; font-size: var(--fs-micro);
  font-variant-numeric: tabular-nums; color: var(--text-muted); background: var(--sunken);
  border: 1px solid var(--border); border-radius: var(--r-1); padding: 1px 5px; white-space: nowrap; }
.rs-len--max { color: var(--text); background: var(--selected); border-color: var(--border-strong); font-weight: 600; }
.rs-x { color: var(--text-faint); margin: 0 1px; }

/* Lieferzeit-Probe (RFC Phase 2): Zeile anklickbar, Probe als eingeschobene Sub-Zeile. */
.rs-probe-row { cursor: pointer; }
.rs-probe-row.is-active td { background: var(--selected); }
.rs-probe > td { background: var(--sunken); border-bottom: 1px solid var(--border); }
.rs-probe-box { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; padding: 8px 4px; font-size: var(--fs-dense); }
.rs-probe-box label { display: inline-flex; align-items: center; gap: 5px; color: var(--text-muted); white-space: nowrap; }
.rs-probe-box .input--sm { width: 78px; }
.rs-probe-box .rs-p-ri { gap: 4px; }
.rs-probe-lbl { font-weight: 600; color: var(--text); }
/* Neutraler Rahmen, KEINE Ampelfarbe (Entscheidung D: Grün/Gelb/Rot gehört exklusiv der Marge-Ampel).
   Die drei Zustände (ab Lager / gestaffelt / Nachbestellung) stehen ohnehin im Text. */
.rs-probe-out { display: inline-flex; flex-direction: column; gap: 1px; padding: 2px 10px; border-radius: var(--r-1);
  border-left: 3px solid var(--border-strong); }
.rs-probe-sub { font-size: var(--fs-micro); color: var(--text-faint); }

/* ── /settings — Einstellungen (Umbau 2026-07-28) ───────────────────────────────
   Präfix `.st-`. Drei Tabs (Global · Seite · Benutzer) über {% block tabs %} = .views/.view-tab;
   die Panels darunter sind reine JS-Hooks (settings.js schaltet [data-panel] per .hidden).

   Warum umgebaut: die Seite stand bis 28.07. in EINER 940-px-Spalte, links ausgerichtet. Auf einem
   1600er Schirm blieben damit rund 40 % der Fläche leer, während sich ~25 Felder mitsamt ihren
   Erklärtexten untereinander stapelten — jedes Feld eine eigene Zeile, jeder Hinweis ein Absatz
   dazwischen. Man scrollte an Fließtext vorbei, um das nächste Eingabefeld zu finden.

   Die Figur jetzt: eine Karte je Thema, zweispaltig. LINKS steht, worum es geht (Titel + Kontext),
   RECHTS stehen die Felder in einem eigenen Raster. Damit unterbricht der Erklärtext die Feldreihe
   nicht mehr, sondern steht neben ihr — und die Felder dürfen nebeneinander, weil sie nicht mehr
   gegen einen Textabsatz um dieselbe Zeile konkurrieren.
   ─────────────────────────────────────────────────────────────────────────── */

/* Der Stapel selbst ist ungedeckelt: die KARTE trägt die Breitenbegrenzung, nicht die Seite.
   Fließtext bleibt drinnen auf --measure gedeckelt (Spielregel „ein Zeilenmaß"). */
.st-form { display: grid; gap: var(--sp-4); }
/* Die Abstände zwischen den Stapeln kommen NICHT aus `gap`: dazwischen liegen `<fieldset>`-Hüllen
   (Grundsatz „alle sehen alles" — read-only heißt disabled, nicht weg), und die zerschneiden das Raster. */
.st-form + .st-form,
.ro-fieldset + .ro-fieldset,
.st-panel > .ro-fieldset + .ro-fieldset { margin-top: var(--sp-4); }
.st-panel > .lead { max-width: var(--measure); }

/* ── Die Karte ─────────────────────────────────────────────────────────────────
   Kontext links, Felder rechts. 320 px links sind ~45 Zeichen — genug für zwei, drei Sätze,
   zu wenig für einen Aufsatz. Das ist Absicht: was hier nicht hineinpasst, gehört ins Wiki. */
/* 280 px links, nicht 320. Die zwei Schwellen dieser Seite (Aside daneben vs. oben · Felder ein- vs.
   zweispurig) sind AUFEINANDER ausgerichtet, damit es den schlechtesten Zustand gar nicht gibt: eine
   leere Aside neben einem Einspalter. Nachgerechnet bei 1280 px (der häufigsten Laptop-Breite):
   1280 − 236 Schiene − 32 Seitenrand = 1012 Karte, − 280 Aside − 1 Kante − 32 Innenabstand = 699 px
   Rumpf, also über der 660er-Container-Schwelle → zwei Spuren. Mit den ersten 320 px waren es 659 —
   einen Pixel darunter, und die Karte kippte auf eine Spur. */
/* Die Aside WÄCHST mit der Karte, mit 280 px als Boden. Fest 280 zu lassen war der zweite Fehler nach
   den festen 320: auf 1920 px standen dann 35 Zeichen Kontext links neben 93 Zeichen Hinweis rechts —
   der Text, der den Zweck der Karte erklärt, im engsten Maß der Seite, die Fußnote im weitesten. Das
   dreht die Hierarchie um, und genau daran liest man „links gequetscht, rechts ausgefranst".
   Mit dem Anteil wird daraus 52 zu 85 Zeichen. Der Boden hält den 1280er-Fall (siehe unten). */
.st-card {
  display: grid; grid-template-columns: minmax(0, clamp(280px, 24%, 400px)) minmax(0, 1fr);
  border: 1px solid var(--border); border-radius: var(--r-2);
  background: var(--canvas); overflow: hidden;
}
/* ⛔ KEINE Tönung. Der erste Wurf füllte die Aside mit `--sunken` — und machte damit genau das sichtbar,
   was man nicht sehen soll: bei einer Karte mit drei Sätzen links und sechs Feldern rechts stand darunter
   ein halber Bildschirm grauer Fläche. Bei der Abnahme prompt gemeldet als „die alte leere rechte Hälfte,
   nur gespiegelt". Ungetönt ist derselbe leere Platz einfach Weißraum. Die Trennung trägt die Kante. */
.st-card-aside { padding: var(--sp-4); border-right: 1px solid var(--border); }
.st-card-title { margin: 0; font: 600 var(--fs-sect)/1.3 var(--f-ui); letter-spacing: -.01em; }
.st-card-desc { margin: var(--sp-2) 0 0; font: 400 var(--fs-dense)/1.55 var(--f-ui); color: var(--text-muted); }
.st-card-desc + .st-card-desc { margin-top: var(--sp-2); }
/* Der Rumpf misst sich selbst (`container-type`), damit das Feld-Raster unten nach der KARTENBREITE
   umbrechen kann und nicht nach der Fensterbreite. Der Unterschied ist nicht theoretisch: bei 1180 px
   Fenster ist der Rumpf nur ~590 px breit — eine Fenster-Regel hätte dort zwei Spalten zu je 270 px
   aufgemacht und den Hinweis unter dem Feld auf vier Zeilen gebrochen. */
.st-card-main { padding: var(--sp-4); min-width: 0; container-type: inline-size; }
/* Regel 3: Tabellen sind randlos und randFÜLLEND. In einer Karte mit 16 px Innenabstand stünde eine
   Tabelle in einem Rahmen im Rahmen — also nimmt der Rumpf für Tabellen sein Padding zurück und die
   Fußleiste bringt ihres selbst mit. */
.st-card-main--flush { padding: 0; }
.st-card-main--flush > .st-matrix-wrap { border: 0; border-radius: 0; }

/* Breite Inhalte (Rechte-Matrix, Tabellen, Listen) haben keine Aside-Spalte übrig — dort läuft der
   Kopf über die volle Breite und der Rumpf darunter. */
.st-card--full { grid-template-columns: minmax(0, 1fr); }
.st-card--full > .st-card-aside { border-right: 0; border-bottom: 1px solid var(--border); }
.st-card--full .st-card-desc { max-width: var(--measure); }

/* Ab hier klappt die Aside nach OBEN — und zwar früher, als sie „müsste". Absicht: unter 1260 px bliebe
   der Rumpf sonst unter der 660er-Container-Schwelle, die Felder fielen auf eine Spur, und daneben stünde
   eine 280 px breite Aside mit drei Zeilen Text und einem halben Bildschirm Leere. Oben liegend gibt sie
   dem Rumpf ihre Breite zurück, und die Felder bleiben zweispurig. Es gibt damit KEINE Fensterbreite mehr,
   in der eine leere Aside neben einem Einspalter steht. */
@media (max-width: 1259px) {
  .st-card { grid-template-columns: minmax(0, 1fr); }
  .st-card-aside { border-right: 0; border-bottom: 1px solid var(--border); }
}

/* ── Das Feld-Raster ───────────────────────────────────────────────────────────
   Zwei Spuren, nie drei. Mit `auto-fit` waren es auf einem 1600er Schirm drei und auf einem 2560er
   vier — und jede weitere Spur nimmt sie dem HINWEIS unter dem Feld, nicht dem Feld selbst (das ist
   92 px breit und bleibt es). Nachgemessen: bei drei Spuren brach „EK-Toleranz: Mindest-Unterschied"
   auf fünf Zeilen um, bei zwei auf zwei. Die Spalte ist für das Feld da, gelesen wird der Satz. */
.st-fields { display: grid; grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4) var(--sp-5); align-items: start; }
@container (min-width: 660px) { .st-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Eine Einstellung: Label · Steuerelement · Hinweis, gestapelt. Die frühere Geometrie (160-px-Label
   links, alles andere rechts) kostete auf JEDER Zeile 160 px Breite für ein Wort. */
.st-row { display: grid; gap: 5px; align-content: start; min-width: 0; }
.st-row > label { font: 500 var(--fs-ui)/18px var(--f-ui); color: var(--text); }
/* Zeilen, deren Inhalt selbst breit ist (Farb-Presets, lange Hinweise, Listen). */
.st-row--wide { grid-column: 1 / -1; }
.st-ctl { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-width: 0; }
.st-hint { margin: 0; font: 400 var(--fs-dense)/1.45 var(--f-ui); color: var(--text-muted); max-width: var(--measure); }
.st-hint code { font-family: var(--f-mono); font-size: var(--fs-micro); background: var(--sunken); padding: 0 3px; border-radius: 2px; }
.st-hint a { color: var(--accent); }
/* Ein Hinweis, der eine FOLGE beschreibt („was verliere ich, wenn ich das ausschalte") ist keine
   Fußnote — er bekommt eine Kante, damit man ihn vor dem Umlegen des Schalters sieht. */
.st-impact { margin: var(--sp-1) 0 0; padding: var(--sp-2) var(--sp-3);
  border-left: 3px solid var(--warn-mark); background: var(--sunken); border-radius: 0 var(--r-1) var(--r-1) 0;
  font: 400 var(--fs-dense)/1.5 var(--f-ui); color: var(--text); max-width: var(--measure); }

/* Feldbreiten. Textfelder füllen ihre Rasterzelle (max als Deckel), Zahlenfelder bleiben schmal —
   ein 90-px-Kasten für „40" sagt „hier steht eine kleine Zahl", ein 280er täuscht eine Freitext-Angabe vor. */
.st-w-num { width: 92px; }
.st-w-md  { width: 100%; max-width: 180px; }
.st-w-lg  { width: 100%; max-width: 280px; }
.st-w-xl  { width: 100%; max-width: 380px; }

/* ── Eingabe-Gruppe: Feld + angehängte Einheit ─────────────────────────────────
   Die Einheit stand vorher als loses graues Wort NEBEN dem Feld und wanderte beim Umbruch weg.
   Angehängt ist sie Teil des Feldes und kann nicht mehr zur Nachbarzahl rutschen (Regel 9). */
.st-ig { display: inline-flex; align-items: stretch; min-width: 0; }
.st-ig > .input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.st-ig > .input:focus { position: relative; z-index: 1; }
.st-ig-suffix { display: inline-flex; align-items: center; height: var(--sp-8); padding: 0 var(--sp-2);
  border: 1px solid var(--border-strong); border-left: 0; border-radius: 0 var(--r-1) var(--r-1) 0;
  background: var(--sunken); color: var(--text-muted);
  font: 400 var(--fs-dense)/1 var(--f-ui); white-space: nowrap; }

/* ── Schalter ──────────────────────────────────────────────────────────────────
   Ein Kontrollkästchen mit dem Wort „an" daneben beantwortet die Frage „ist das gerade an?" erst
   nach Hinsehen. Der Schiebeschalter beantwortet sie aus dem Augenwinkel. Farbe = Markenfarbe, wie
   schon beim `accent-color` der alten Kästchen — sie bedeutet „gesetzt", nicht „gut" (Regel 2). */
.st-switch { display: inline-flex; align-items: center; gap: var(--sp-2); margin: 0; cursor: pointer; }
.st-switch > input {
  appearance: none; -webkit-appearance: none; margin: 0; flex: none;
  width: 38px; height: 22px; border: 0; border-radius: var(--r-full);
  background: var(--border-strong); cursor: pointer; position: relative;
  transition: background .15s ease;
}
.st-switch > input::after {
  content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
  border-radius: var(--r-full); background: #fff; box-shadow: 0 1px 2px rgba(16,22,30,.35);
  transition: transform .15s ease;
}
.st-switch > input:checked { background: var(--primary); }
.st-switch > input:checked::after { transform: translateX(16px); }
.st-switch > input:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.st-switch > input:disabled { opacity: .45; cursor: not-allowed; }
.st-switch-txt { display: grid; gap: 1px; min-width: 0; }
.st-switch-txt strong { font: 500 var(--fs-ui)/18px var(--f-ui); }
.st-switch-txt .dim { font-size: var(--fs-dense); line-height: 1.4; }
/* Der Zustand in Worten neben dem Schalter — „an"/„aus" bleibt lesbar, wenn jemand Farben nicht
   unterscheidet, und ist die einzige Stelle, an der der Schalter sich selbst benennt. */
.st-switch-state { font-size: var(--fs-dense); color: var(--text-muted); }
.st-switch > input:checked ~ .st-switch-state { color: var(--text); }
/* Das Wort kommt aus dem Zustand, nicht aus dem Template: vorher stand fest „an" daneben, auch wenn
   der Schalter aus war — ein Etikett, das seinem eigenen Kasten widerspricht. */
.st-switch-state::after { content: "aus"; }
.st-switch > input:checked ~ .st-switch-state::after { content: "an"; }

/* Schalter-Liste (Handy-Ansicht): eine Zeile je Karte, Schalter links, Bedeutung rechts.
   ⛔ Gedeckelt auf --measure — dieselbe EINE Zahl wie überall, kein zweites Maß. Ohne Deckel liefen die
   Zeilen auf 1920 px über 1292 px, der Inhalt endete nach ~400, und die Trennlinien zogen 890 px ins
   Leere. Das ist Weißraum als Verschwendung, nicht als Ruhe: Nähe ist die stärkste Gruppierung, und
   eine Linie, die ins Nichts läuft, gruppiert nichts mehr. */
.st-switchlist { display: grid; gap: 0; max-width: var(--measure); }
.st-switchlist > .st-switch { align-items: flex-start; padding: var(--sp-2) var(--sp-1); border-bottom: 1px solid var(--row-line); }
.st-switchlist > .st-switch:last-child { border-bottom: 0; }
.st-switchlist > .st-switch:hover { background: var(--hover); }
.st-switchlist > .st-switch > input { margin-top: 1px; }

/* ── Speicherleisten ───────────────────────────────────────────────────────────
   Jede Karte, die für sich speichert, trägt ihre Leiste im Kartenfuß. */
.st-savebar { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
/* Dieselbe Knopfreihe OHNE Trennlinie — für Karten, die nichts speichern (Verweis, Neuberechnung).
   Die Linie der `.st-savebar` ist ein Formularfuß-Signal; über einem Ein-Satz-Rumpf behauptet sie
   ein Formular, das es nicht gibt. */
.st-actionrow { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; margin-top: var(--sp-3); }
.st-msg { font-size: var(--fs-dense); color: var(--text-muted); }

/* Regel 1: EINE Primäraktion je Reiter. Der globale Reiter besteht aus sechs Karten unter EINEM
   Formular — sein Speichern-Knopf lag am Ende und war nach dem Scrollen weg. Als klebende Leiste
   bleibt er erreichbar, ohne dass jede Karte einen zweiten Knopf bekommt. */
.st-stickybar { position: sticky; bottom: 0; z-index: 3;
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-2); padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--border); border-radius: var(--r-2);
  background: var(--canvas); box-shadow: var(--sh-pop); }
.st-stickybar-note { font-size: var(--fs-dense); color: var(--text-muted); }
/* Ungespeicherte Änderungen (29.07.): die Leiste sagt, WELCHE Karten offen sind — die Frage aus der
   Abnahme war „weiß nicht, ob ich einen vergessen habe". Kein Ampelton (die Skala gehört der Marge):
   die Kante und das kräftigere Wort tragen den Zustand. */
.st-stickybar.is-dirty { border-top-color: var(--accent); }
.st-stickybar.is-dirty .st-stickybar-note { color: var(--text); font-weight: 600; }
/* Auf kleinen Schirmen bricht die Notiz zweizeilig um und die Leiste frisst ~11 % der Fensterhöhe —
   dort zählt der Knopf, nicht seine Fußnote. */
@media (max-width: 1000px) { .st-stickybar-note { display: none; } }

/* „🔒 Nur ansehen" — `.ro-hint` mit fester 32px-Höhe würde bei diesem Satz umbrechen und überlaufen. */
.st-ro { height: auto; min-height: var(--sp-8); padding: var(--sp-2) var(--sp-3); line-height: 18px; max-width: var(--measure); }

/* „Erweitert · Wann entsteht ein To-do?" — sekundär, deshalb zugeklappt und unter dem Rest.
   Als eigene Karte mit klappbarem Kopf, damit sie im Kartenstapel nicht als loser Rest dasteht. */
.st-adv { border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas); overflow: hidden; }
.st-adv > summary { display: flex; align-items: center; gap: var(--sp-2); cursor: pointer; list-style: none;
  padding: var(--sp-3) var(--sp-4); font: 600 var(--fs-ui)/20px var(--f-ui); }
.st-adv > summary::-webkit-details-marker { display: none; }
.st-adv > summary::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-icon); transition: transform .15s ease; }
.st-adv[open] > summary::before { transform: rotate(90deg); }
.st-adv > summary:hover { background: var(--hover); }
.st-adv[open] > summary { background: var(--sunken); box-shadow: inset 0 -1px 0 var(--border); }
/* Der Rumpf trägt DIESELBE Spaltenteilung wie `.st-card` — er nutzt sogar dieselben Kind-Klassen
   (`.st-card-aside`/`.st-card-main`), damit Innenabstand, Trennkante und das Feld-Raster nicht
   nachgebaut werden müssen. Vorher hatte er ein eigenes, einspaltiges Layout: aufgeklappt begannen
   seine Felder bei x=269, die der neun Karten darüber bei x=646 (gemessen 29.07. auf 1920 px) — die
   senkrechte Kante der Feldspalte brach genau hier. Eigenes Layout an einer Stelle heißt: es hält
   nur so lange, bis sich das Vorbild ändert. */
.st-adv-body { display: grid; grid-template-columns: minmax(0, clamp(280px, 24%, 400px)) minmax(0, 1fr); }
@media (max-width: 1259px) {
  .st-adv-body { grid-template-columns: minmax(0, 1fr); }
  .st-adv-body > .st-card-aside { border-right: 0; border-bottom: 1px solid var(--border); }
}

/* ── Marke & Design ───────────────────────────────────────────────────────────
   Farbschema-Presets. Der Punkt trägt die Farbe, die er setzt — das ist keine Dekoration, sondern
   der Wert selbst (Regel 2 bleibt gewahrt: die Farbe IST hier die Bedeutung). */
.st-presets { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.st-presets button {
  display: inline-flex; align-items: center; gap: 6px; height: var(--sp-8); padding: 0 var(--sp-3);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--canvas); color: var(--text); font-size: var(--fs-dense); cursor: pointer;
}
.st-presets button::before { content: ""; width: 14px; height: 14px; border-radius: var(--r-full); background: var(--sw); flex: none; box-shadow: inset 0 0 0 1px rgba(0,0,0,.12); }
.st-presets button:hover { border-color: var(--sw); background: var(--hover); }
.st-presets button.active { border-color: var(--sw); box-shadow: 0 0 0 2px color-mix(in srgb, var(--sw) 32%, transparent); }

/* Farbfeld: Picker (Komfort) + Hex-Text (Quelle der Wahrheit beim Submit). */
.st-color-field { display: flex; align-items: center; gap: var(--sp-2); }
.st-color-picker { width: var(--sp-8); height: var(--sp-8); padding: 3px; border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); cursor: pointer; flex: none; }
.st-color-picker::-webkit-color-swatch-wrapper { padding: 0; }
.st-color-picker::-webkit-color-swatch { border: 0; border-radius: 2px; }
.st-color-hex { width: 104px; font-family: var(--f-mono); font-size: var(--fs-dense); text-transform: lowercase; }
/* Fehler ist eine Aussage über den Wert und klebt deshalb an ihm (Regel 9). */
.st-color-err { color: var(--bad); }
.st-invalid { border-color: var(--bad); }
.st-invalid:focus { border-color: var(--bad); box-shadow: 0 0 0 3px color-mix(in srgb, var(--bad) 18%, transparent); }

/* Vorschau im Kartenkopf: zeigt die gewählten Farben an den DREI Stellen, an denen sie im Betrieb
   auftauchen — Knopf, Ampel, Auswahl. Ohne sie beurteilt man Hex-Codes, nicht Farben. Die Werte
   werden lokal auf diesen Kasten geschrieben (settings.js), nie auf :root — ein Inline-Style auf
   dem Wurzelelement schlüge auch `:root[data-theme="dark"]` und würde das Dunkel-Theme aushebeln. */
.st-preview { margin-top: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--border);
  border-radius: var(--r-2); background: var(--canvas); display: grid; gap: var(--sp-2); }
.st-preview-lbl { font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); }
/* Fest zweizeilig: Knopf oben, die drei Ampelpunkte darunter. Als eine umbrechende Reihe stand sie je
   nach Breite 4 / 3+1 / 2+2 da — eine Vorschau, die je nach Fenster anders aussieht, ist als Vergleich
   wertlos. Gleiches Muster wäre bei den acht Farbschema-Knöpfen fällig; die sind aber eine Auswahl,
   keine Vorschau, und dürfen umbrechen. */
.st-preview-row { display: flex; align-items: center; gap: var(--sp-2) var(--sp-3); flex-wrap: wrap; }
.st-preview-row--dots { margin-top: 2px; }
.st-preview-btn { display: inline-flex; align-items: center; height: 26px; padding: 0 var(--sp-3);
  border-radius: var(--r-1); background: var(--primary); color: #fff; font: 600 var(--fs-dense)/1 var(--f-ui); }
.st-preview-dot { display: inline-flex; align-items: center; gap: 5px; font: 400 var(--fs-micro)/1 var(--f-ui); color: var(--text-muted); }
.st-preview-dot::before { content: ""; width: 10px; height: 10px; border-radius: var(--r-full); background: var(--dot); }

/* Datenquellen Shop ⟷ WaWi nebeneinander: sie sind ein Paar, untereinander läsen sie sich als Liste. */
.st-srcgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--sp-4) var(--sp-5); align-items: start; }
.st-srcbox { border: 1px solid var(--border); border-radius: var(--r-2); padding: var(--sp-3); min-width: 0; }
.st-srchead { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-3); padding-bottom: var(--sp-2); border-bottom: 1px solid var(--border); font-size: var(--fs-dense); }
.st-srcbox .st-fields { grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); }
/* In der Box füllen alle Textfelder ihre Spalte. Mit den vier Stufen (280/180/110/280) fransten vier
   Feldkanten übereinander aus, während dieselben Werte in der Nachbarkarte „Logos & Favicon" fluchteten.
   Das Hex-Feld bleibt schmal: sieben Zeichen brauchen keine 280 px, und es hat den Wähler daneben. */
.st-srcbox .st-fields .input:not(.st-color-hex) { width: 100%; max-width: none; }

/* ── Gewichte (#209) · Name/Wert-Listen in einer Einstellungs-Karte ────────
   Werkstoff-Dichten und Gewindefaktoren sind Listen, keine Einzelfelder — `st-fields`/`st-row` passt
   nicht (die trägt ein Label je Zeile, hier IST der Name das Eingabefeld). Bewusst ohne JavaScript:
   das Formular rendert vorhandene Zeilen plus drei leere. Zwei Spalten mit fester Kante, damit die
   Werte fluchten (die rechte Kante ist der eigentliche Grund, warum eine solche Liste ordentlich
   aussieht oder nicht — Spielregel Fluchtung, 29.07.). */
.st-sub { margin: var(--sp-5) 0 var(--sp-2); font: 600 var(--fs-ui)/1.3 var(--f-ui); color: var(--text); }
.st-sub:first-child { margin-top: 0; }
.st-pairs { display: grid; gap: var(--sp-2); margin: 0 0 var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.st-pair { display: grid; grid-template-columns: minmax(0, 1fr) 96px; gap: var(--sp-2); min-width: 0; }
/* `min-width: 0` auf dem Feld: ohne das sprengt ein langer Werkstoffname („Edelstahl V4A (1.4401)")
   die Rasterspalte, statt sich zu kürzen — dieselbe Falle wie bei der „Was ansteht"-Liste (29.07.). */
.st-pair .input { width: 100%; max-width: none; min-width: 0; }
/* Die Muster-Tabelle scrollt in ihrem EIGENEN Container — nie das Dokument (Regel: breite Inhalte
   scrollen für sich, `overflow-x: auto` am Kasten). Gleiche Bauart wie .st-matrix-wrap darüber. */
.st-tblwrap { overflow-x: auto; min-width: 0; margin: 0 0 var(--sp-3); }

/* ── Startseite · „Was ansteht" ───────────────────────────────────────────── */
.st-ansteht { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-1); max-width: var(--measure); }
.st-ae-item { display: flex; align-items: center; gap: var(--sp-3); padding: 5px var(--sp-2); border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas);
  /* Grid-Kind: ohne das gibt die Zeile unter ihrer Inhaltsbreite nicht nach (29.07.). */
  min-width: 0; }
.st-ae-move { display: flex; flex-direction: column; gap: 1px; flex: none; }
/* 14 statt 11 px hoch: der Pfeil stand vorher auf 7 px — unter dem Boden der Skala und damit eher Grafik
   als Zeichen. Auf Skalengroesse gehoben braucht er einen Kasten, der ihn nicht abschneidet. */
.st-ae-move button { width: 20px; height: 14px; display: grid; place-items: center; padding: 0; border: 1px solid var(--border); border-radius: 2px; background: var(--canvas); color: var(--text-muted); font-size: var(--fs-micro); line-height: 1; cursor: pointer; }
.st-ae-move button:hover { border-color: var(--accent); color: var(--accent); }
.st-ae-pin { display: inline-flex; align-items: center; gap: var(--sp-1); flex: none; margin: 0; font-size: var(--fs-micro); color: var(--text-muted); white-space: nowrap; cursor: pointer; }
.st-ae-pin input { accent-color: var(--primary); margin: 0; }
/* `min-width: 0` ist die Voraussetzung dafuer, dass `text-overflow: ellipsis` in einem Flex-Kind
   ueberhaupt greift — ohne es waechst das Kind einfach mit und die Kuerzung passiert nie. */
.st-ae-label { font-size: var(--fs-dense); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Benutzer & Rechte ────────────────────────────────────────────────────── */
.st-userform { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: flex-end; }
.st-userform label { display: grid; gap: 3px; font: 500 var(--fs-micro)/16px var(--f-ui); color: var(--text-muted); }
.st-inline { display: flex; gap: var(--sp-2); align-items: center; }
/* Gedeckelt: „z. B. Einkauf" ist eine Kurzeingabe wie der Benutzername zwei Karten höher — ungedeckelt
   war das Feld 670 px breit, das Benutzername-Feld daneben 175. Zwei Maße für dieselbe Art Eingabe. */
.st-inline input { flex: 1; min-width: 0; max-width: 280px; }
.st-mini { display: inline-flex; gap: var(--sp-1); align-items: center; margin: 0; }
.st-user-off { opacity: .55; }
.st-pw > summary { cursor: pointer; font-size: var(--fs-micro); color: var(--text-muted); list-style: none; }
.st-pw > summary::-webkit-details-marker { display: none; }
.st-pw > summary::before { content: "▸ "; }
.st-pw[open] > summary::before { content: "▾ "; }
.st-pw form { margin-top: var(--sp-1); }

/* Rechte-Matrix: waagerecht scrollbar in SICH, nicht im Dokument (Regel 4) — bei 5+ Rollen wird sie breit.
   Die Recht-Spalte klebt links: beim Scrollen nach rechts sah man sonst Häkchen ohne zu wissen, wozu. */
/* Nur WAAGERECHT scrollbar. Der erste Wurf deckelte auch die Höhe (`max-height: 70vh`) — das ergab eine
   zweite senkrechte Scrollfläche IN einer Seite, die selbst scrollt, und das Mausrad wusste nicht mehr,
   wen es meint. Bei ~20 Zeilen ist der Deckel ohnehin kein Gewinn; die Rubrikzeilen ordnen die Liste. */
.st-matrix-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r-2); }
.st-matrix { width: 100%; border-collapse: separate; border-spacing: 0; }
.st-matrix th, .st-matrix td { padding: 0 var(--sp-3); height: var(--row-evt); border-bottom: 1px solid var(--row-line); font-size: var(--fs-dense); text-align: left; }
.st-matrix thead th { position: sticky; top: 0; z-index: 2; height: 46px; background: var(--table-head); color: var(--text-muted); box-shadow: inset 0 -1px 0 var(--border-strong); vertical-align: middle; }
/* 340 px fest, NICHT „der Rest". Die Tabelle ist randfüllend (Regel 3) — ohne feste Breite zog die
   Recht-Spalte den ganzen Überschuss an sich, und der Rechtename stand über 1.000 px von seinem
   Häkchen entfernt. Den Überschuss nimmt jetzt `.st-matrix-fill` ganz rechts auf: die Tabelle bleibt
   randfüllend, die Häkchen rücken an ihre Zeile heran. */
.st-matrix-cap { position: sticky; left: 0; z-index: 1; background: var(--canvas); width: 340px; min-width: 280px; box-shadow: inset -1px 0 0 var(--border); }
/* Füllspalte, auch für die Benutzer-/Rollen-Tabellen daneben: dieselbe Ursache (wenige Spalten in einer
   sehr breiten Karte), dieselbe Lösung. Ohne sie lagen zwischen „zurücksetzen" und dem ✕ 340 px Nichts. */
/* `width: 100%` ist hier kein Tippfehler und auch keine echte Breite: im automatischen Tabellenlayout
   bekommt die Spalte mit der größten angegebenen Breite den Überschuss. `auto` täte gar nichts — der
   Überschuss würde weiter gleichmäßig auf alle Spalten verteilt, und genau das war das Problem. */
.st-fill { width: 100%; padding: 0; }
.st-matrix thead .st-matrix-cap { z-index: 3; background: var(--table-head); }
/* Feste, schmale Rollenspalten: gleichmäßig verteilt bekam jede Rolle ~230 px für EINE 16-px-Checkbox,
   und die Zuordnung „Zeile ↔ Häkchen" überbrückte bis zu 900 px. Die Spalte „Recht" nimmt den Rest. */
.st-matrix-role { white-space: nowrap; text-align: center; width: 150px; }
.st-matrix-rolehead { display: inline-grid; gap: 2px; justify-items: center; }
.st-matrix-rolename { display: flex; align-items: center; gap: 5px; font: 600 var(--fs-dense)/16px var(--f-ui); color: var(--text); text-transform: none; letter-spacing: 0; }
.st-matrix-key { font: 400 var(--fs-micro)/13px var(--f-mono); color: var(--text-faint); text-transform: none; letter-spacing: 0; }
.st-matrix-group td { position: sticky; left: 0; height: var(--h-tabs); background: var(--group-head); font: 600 var(--fs-micro) var(--f-ui); letter-spacing: .03em; text-transform: uppercase; color: var(--text); box-shadow: none; }
.st-matrix-cell { text-align: center; }
/* Ein gesetztes Häkchen ist AUSWAHL, kein Urteil — deshalb --selected und nicht Ampel-Grün.
   (Die alte Datei tönte hier grün; Grün gehört seit Entscheidung D exklusiv der Marge-Ampel.) */
.st-matrix-cell:has(input:checked) { background: var(--selected); }
.st-matrix-cell input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; margin: 0; vertical-align: middle; }
.st-matrix-always { color: var(--text-muted); font-weight: 600; }
.st-matrix tbody tr:hover td { background: var(--hover); }
.st-matrix tbody tr:hover .st-matrix-cell:has(input:checked) { background: var(--selected); }
.st-matrix tbody tr:last-child td { border-bottom: 0; }
/* Zähler je Rolle, live aus den gesetzten Häkchen (settings.js). Beantwortet „wer darf wie viel"
   ohne die Spalte von oben bis unten abzuzählen. */
.st-matrix-count { display: inline-flex; align-items: center; height: 17px; padding: 0 6px;
  border: 1px solid var(--border-strong); border-radius: var(--r-full); background: var(--canvas);
  font: 600 var(--fs-micro)/15px var(--f-ui); color: var(--text-muted); font-variant-numeric: tabular-nums; }

.st-admincols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--sp-5); align-items: start; }

@media (max-width: 780px) {
  .st-card-aside, .st-card-main { padding: var(--sp-3); }
}

/* ── /preise/crawler — Wettbewerbspreise holen (Cutover 2026-07-17) ─────────────
   Präfix `.cr-`. Die Crawler sind die Reiter der Shell: jeder ist ein Ort (Regel 7), man nennt sie beim
   Namen („Mädler", „HFB"), und man arbeitet in genau einem. Vorher war die Crawler-Auswahl eine
   Kachel-Wand („Crawler-Gesundheit") MITTEN auf der Seite — Navigation, die wie eine Kennzahl aussah.
   ─────────────────────────────────────────────────────────────────────────── */

/* Klappbares Panel — dieselben vier Zeilen wie in shop_texte.css/export.css. Figur der Shell, kein
   Seiten-Stil: gehört beim Einsammeln EINMAL nach app-new.css. */

/* Der Zustands-Punkt am Reiter: EIN Zeichen, drei Werte. `.dot` der Shell trägt schon ok/warn/bad —
   `idle` (nie gecrawlt) ist die vierte Aussage und bewusst grau: „nie gemessen" ist kein Alarm. */
.cr-dot--idle { background: var(--text-faint); }

/* ── Gesundheit: eine Zeile über dem Inhalt, keine Kachelwand ────────────────
   Die Zahlen des AKTIVEN Crawlers, dazu die Auffälligkeiten. Was die anderen Crawler machen, steht
   an ihrem Reiter — nicht als sechs Karten, die man jedes Mal überliest. */
.cr-health { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border); font-size: var(--fs-dense); }
.cr-h-z { display: flex; align-items: baseline; gap: 6px; }
.cr-h-k { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .04em; color: var(--text-faint); }
.cr-h-z strong { font-variant-numeric: tabular-nums; }
.cr-flags { display: flex; flex-wrap: wrap; gap: var(--sp-1); }

/* ── Einstellungs-Formular ────────────────────────────────────────────────── */
.cr-cfg { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--sp-3); align-items: end; }
/* Die Beschriftung darf umbrechen. Sie stand vorher einzeilig und lief bei den längeren Feldnamen
   („Vorgabe Material (nur wenn der Name es nicht sagt)") über den Rand ihres Rasterfeldes hinaus —
   sichtbar blieb sie, aber sie schob sich über die Nachbarspalte. */
.cr-cfg > label, .cr-linkform > label, .cr-addform > label {
  display: grid; gap: 3px; margin: 0; min-width: 0;
  font: 400 var(--fs-micro)/16px var(--f-ui); color: var(--text-muted);
  overflow-wrap: anywhere;
}
.cr-check { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-ui); color: var(--text); cursor: pointer; white-space: nowrap; }
.cr-check input { accent-color: var(--primary); margin: 0; }
/* Schalter rechts im Panel-Kopf. NICHT `.panel-head .more` — das ist ein Link/Knopf-Stil (Akzentfarbe,
   border:0) und macht aus einem Kontrollkästchen-Label optisch einen Link. */
.cr-headtool { margin-left: auto; font-size: var(--fs-dense); color: var(--text-muted); }

/* Spaltenmaße der Preistabelle — EINE Quelle, statt je Zelle inline. Die zwei SKU-Spalten bekommen den
   Rest: an ihnen erkennt man, ob überhaupt derselbe Artikel verglichen wird. */
.grid [data-c="crprice"]  { width: 100px; }
.grid [data-c="crdelta"]  { width: 96px; }
.grid [data-c="crstatus"] { width: 210px; }
.grid [data-c="crwann"]   { width: 130px; }
.cr-color { width: var(--sp-8); height: var(--sp-8); padding: 2px; border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); cursor: pointer; }
.cr-cfg-foot { grid-column: 1 / -1; display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }

/* ── Preise ziehen (Lesezeichen) ──────────────────────────────────────────── */
.cr-book { display: grid; gap: var(--sp-2); }
.cr-book-steps { margin: 0; padding-left: 16px; font-size: var(--fs-dense); line-height: 1.65; color: var(--text-muted); }
/* Die Schritt-Anleitung liegt seit 03.08. in einem Aufklapper (#445): man liest sie einmal, sie stand
   danach jeden Tag zwischen dem Knopf und der Frage, was der letzte Lauf gebracht hat. */
.cr-anleitung > summary { cursor: pointer; list-style: none; font: 500 var(--fs-micro)/1.4 var(--f-ui);
  color: var(--text-muted); }
.cr-anleitung > summary::-webkit-details-marker { display: none; }
.cr-anleitung > summary::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-icon); margin-right: 4px; }
.cr-anleitung[open] > summary::before { content: "▾"; }
.cr-anleitung > summary:hover { color: var(--text); }
.cr-anleitung[open] > summary { margin-bottom: var(--sp-2); }
.cr-book-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.cr-book-row form { margin: 0; }

/* ── Warengruppen-Auswahl (linke Spalte der „nach Warengruppe"-Sicht) ─────── */
.cr-wg { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: var(--sp-4); align-items: start; }
/* ⛔ `overflow-x: hidden` + `min-width: 0` auf dem Knopf (unten) sind hier zusammen LOAD-BEARING. Rasterfelder
   haben `min-width: auto`, der Knopf ist also so breit wie Name PLUS Zahl. Bei „Kardanadapter für
   Flanschmuttern" sprengte das die 220-px-Spur, die Leiste bekam einen waagerechten Balken — und die Zahl
   rechts scrollte aus dem Bild. Sichtbar blieb von 277 die „27", von 201 die „20": eine Zahl, die um eine
   Ziffer daneben liegt und trotzdem plausibel aussieht. Der Name kürzt sich ohnehin mit Auslassungspunkten;
   die Zahl darf das nie. */
.cr-wg-nav { display: grid; gap: 2px; max-height: 60vh; overflow-y: auto; overflow-x: hidden; }
.cr-wg-nav::-webkit-scrollbar { width: 6px; }
.cr-wg-nav::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-1); }
.cr-pick {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); min-width: 0;
  height: 26px; padding: 0 var(--sp-2); border: 1px solid transparent; border-radius: var(--r-1);
  background: none; color: var(--text); font-size: var(--fs-dense); text-align: left; cursor: pointer;
}
.cr-pick:hover { background: var(--hover); }
.cr-pick.is-active { background: var(--selected); border-color: var(--accent); color: var(--accent); font-weight: 500; }
.cr-pick-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-pick-n { flex: none; font-size: var(--fs-micro); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.cr-pick.is-active .cr-pick-n { color: var(--accent); }
/* Der Auffälligkeits-Zähler ist ein Marker an einer Zahl (Regel 9), kein Ampel-Urteil. */
.cr-pick-flag { color: var(--warn-mark); font-weight: 600; }
.cr-groups { display: grid; gap: var(--sp-5); min-width: 0; }
.cr-group-h { display: flex; align-items: baseline; gap: var(--sp-2); margin: 0 0 var(--sp-2); font-size: var(--fs-ui); font-weight: 600; }

/* Breite Tabellen scrollen IN SICH (Regel 4: nur der Canvas scrollt, das Dokument nie).
   `.grid` ist `width: max-content; min-width: 100%` — sie WILL breiter werden als ihr Kasten. Ohne
   diesen Fänger schiebt sie 8 Spalten über den Canvas-Rand, und `html { overflow: hidden }` schneidet
   sie einfach ab: kein Scrollbalken, keine Fehlermeldung, die letzten Spalten schlicht weg. Gemessen
   bei „Ohne Zuordnung" (HFB): 1727px in einem 1216px-Canvas. Das `min-width: 0` auf den Grid-Kindern
   ist die zweite Hälfte — ohne es wächst der Grid-Track selbst mit dem Inhalt und der Fänger fängt nichts. */
.cr-tbl { overflow-x: auto; min-width: 0; }
.cr-tbl::-webkit-scrollbar { height: 8px; }
.cr-tbl::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--r-1); }
.cr-groups > .wg-group { min-width: 0; }

/* ── Kategorie-Links ──────────────────────────────────────────────────────── */
.cr-help { margin-bottom: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); }
.cr-help > summary { padding: var(--sp-2) var(--sp-3); cursor: pointer; font-size: var(--fs-dense); font-weight: 600; }
.cr-help > :not(summary) { margin: 0 var(--sp-3) var(--sp-3); }
.cr-help table { width: auto; }
.cr-links { display: grid; gap: var(--sp-2); min-width: 0; }
.cr-link { min-width: 0; border: 1px solid var(--border); border-radius: var(--r-2); overflow: hidden; background: var(--canvas); }
/* Fünf Spuren seit 31.07.: die Warengruppe steht jetzt in der Zusammenfassung. Ein Startlink ohne sie
   crawlt zwar, ordnet aber nichts zu — das darf man nicht erst nach dem Aufklappen sehen.
   Sechs seit 03.08. (#445): der ZUSTAND des letzten Laufs steht daneben. Er lag bis dahin nur im
   aufgeklappten Bereich, während der Seitenkopf „3 Fehler" meldete — man musste einundzwanzig Links
   aufklappen, um die drei zu finden.
   ⛔ FESTE SPUREN FÜR DIE VIER RECHTEN SPALTEN (03.08.). Jede Zeile ist ein EIGENES Raster — `auto`
   bemisst sich deshalb je Zeile am eigenen Inhalt, und zwanzig Startlinks standen in vier Spalten an
   zwanzig verschiedenen Stellen. Dieselbe Krankheit wie bei den Warengruppen-Knöpfen (#485): eine
   Liste, die man von oben nach unten überfliegen soll, muss senkrechte Kanten haben. */
.cr-link > summary {
  display: grid; grid-template-columns: minmax(140px, 1.2fr) minmax(110px, 1fr) 11rem 7rem 8rem 8.5rem; gap: var(--sp-3);
  align-items: center; min-height: var(--h-actionbar); padding: 0 var(--sp-3);
  background: var(--sunken); cursor: pointer; list-style: none; font-size: var(--fs-dense);
}
.cr-link > summary::-webkit-details-marker { display: none; }
.cr-link > summary:hover { background: var(--hover); }
.cr-link[open] > summary { border-bottom: 1px solid var(--border); }
.cr-link-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-link > summary > .dim:last-child { justify-self: end; white-space: nowrap; }
/* ⛔ Feste Spuren waren hier nicht mehr haltbar: das Formular trägt je nach Quelle vier Felder (Template-
   Weg) oder sieben (Profil-Weg: Warengruppe, drei Merkmals-Vorgaben, Suchweg). Eine fünfspurige Vorgabe
   quetschte den Profil-Fall in zwei halbe Zeilen mit einem Feld, das allein umbrach. `auto-fit` legt so
   viele Felder nebeneinander, wie bei 190 px Mindestbreite hineinpassen — bei jeder Fensterbreite. */
.cr-linkform, .cr-addform {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: var(--sp-3); align-items: end; padding: var(--sp-3);
}
.cr-addform { border: 1px solid var(--border); border-radius: var(--r-2); background: var(--sunken); margin-top: var(--sp-3); }
/* Die Aktionen bekommen immer eine eigene Zeile über die ganze Breite — sonst rutscht „Löschen" je nach
   Feldzahl an eine andere Stelle, und ein Knopf, der wandert, wird versehentlich getroffen. */
.cr-link-acts { grid-column: 1 / -1; display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap;
                padding-top: var(--sp-2); border-top: 1px solid var(--border); }

/* ── Ignorier-Liste ───────────────────────────────────────────────────────────
   Eine Zeile je Grund: Anzahl — was es bedeutet — zurücknehmen. Dieselbe Anordnung wie die Abnahme-
   Gruppen auf der Markt-Seite, damit „ignorieren" und „doch wieder fragen" wie zwei Richtungen
   derselben Handlung aussehen und nicht wie zwei verschiedene Werkzeuge. */
.cr-excl { display: grid; gap: 0; margin-bottom: var(--sp-3); }
.cr-excl-row { display: grid; grid-template-columns: 64px 1fr auto; gap: var(--sp-3); align-items: center;
               padding: var(--sp-2) 0; border-bottom: 1px solid var(--border); }
.cr-excl-row:last-child { border-bottom: 0; }
.cr-excl-n { font: 700 var(--fs-sect)/1.1 var(--f-ui); text-align: right; font-variant-numeric: tabular-nums; }
.cr-link .grid { border-top: 1px solid var(--border); }

/* „nur Auffällige": rein clientseitig, kein Reload. Saubere Zeilen (data-sev=0) und saubere Links
   verschwinden — der Rest der Seite bleibt, wo er war. */
body.only-flagged .cr-link[data-flagged="0"] { display: none; }
body.only-flagged .grid tbody tr[data-sev="0"] { display: none; }
.cr-wg.wg-only-flagged .grid tbody tr[data-sev="0"] { display: none; }
.cr-wg.wg-only-flagged .cr-pick[data-flagged="0"] { opacity: .45; }

/* ── Abdeckung ────────────────────────────────────────────────────────────── */
/* Ein Balken, EINE Farbe: der Anteil ist eine SKALA, kein Urteil. Die alte Datei färbte ihn grün/gelb/rot —
   damit behauptet 40 % Abdeckung „schlecht", obwohl niemand weiß, ob die fehlenden 60 % überhaupt
   vergleichbar sind (Mädler/HFB verkaufen das metrische Sortiment gar nicht). Richtung statt Farbe. */
.cr-cov { position: relative; display: inline-block; width: 120px; height: 9px; vertical-align: middle;
  background: var(--sunken); border-radius: 2px; overflow: hidden; }
.cr-cov > span { display: block; height: 100%; background: var(--chart-1); border-radius: 2px; }
.cr-cov-pct { margin-left: var(--sp-2); font-variant-numeric: tabular-nums; color: var(--text-muted); font-size: var(--fs-micro); }

/* Δ-Preis: nur das Vorzeichen trägt die Bedeutung, nicht die Ziffer (Regel 2). Ein steigender
   Wettbewerbspreis ist für uns nicht „schlecht" — er ist eine Richtung. */
.cr-delta-up { color: var(--text); }
.cr-delta-up::before { content: "▲ "; color: var(--text-faint); font-size: var(--fs-micro); }
.cr-delta-dn { color: var(--text); }
.cr-delta-dn::before { content: "▼ "; color: var(--text-faint); font-size: var(--fs-micro); }

@media (max-width: 1100px) {
  .cr-wg { grid-template-columns: minmax(0, 1fr); }
  .cr-wg-nav { grid-auto-flow: column; grid-auto-columns: max-content; max-height: none; overflow-x: auto; }
  .cr-linkform, .cr-addform { grid-template-columns: minmax(0, 1fr); }
}

/* ── /sortiment/texte — Texte anpassen (Cutover 2026-07-17) ─────────────────────
   Präfix `.tx-`. Die 13 Warengruppen sind die Reiter der Shell ({% block tabs %}) — jede ist ein Ort,
   exklusiv und beim Namen genannt (Regel 7). Vorher waren sie eine Knopfreihe in einer Karte.

   DREI KATEGORIEN, ZWEI CHART-FARBEN: Die Blöcke ①②③ müssen im Vorschau-Text wiedererkennbar sein —
   sonst weiß niemand, welcher Absatz aus welchem Block kommt. Die alte Datei nahm dafür Teal (#14b8a6)
   und Violett (#a78bfa): zwei frei erfundene Farben. Neu tragen die Blöcke die ZIFFER als Identität,
   die Farbe stützt nur: --chart-1 · --chart-2 · --border-strong. Keine eigene Farbe, kein Ampel-Ton
   (Grün/Gelb/Rot gehören exklusiv der Marge-Ampel, Entscheidung D).
   ─────────────────────────────────────────────────────────────────────────── */

/* Klappbares Panel: der Kopf IST die Klappe. `.panel-head` ist ein Flex-Container und schluckt den
   UA-Marker nicht — er muss ausdrücklich weg, sonst sitzt das Dreieck neben dem Flex-Layout.
   ⚠️ Auch export.css hat diese vier Zeilen. Sie sind kein Seiten-Stil, sondern eine Figur der Shell
   („Panel, das man zuklappen kann") und gehören eigentlich EINMAL in app-new.css — dort darf beim
   parallelen Cutover aber niemand schreiben. Beim Einsammeln zusammenlegen. */

.tx-blk {
  display: inline-flex; align-items: center; height: 18px; padding: 0 6px;
  border: 1px solid var(--border-strong); border-left-width: 3px; border-radius: var(--r-1);
  background: var(--sunken); font: 600 var(--fs-micro)/18px var(--f-ui); color: var(--text); white-space: nowrap;
}
.tx-blk--intro { border-left-color: var(--chart-1); }
.tx-blk--eig   { border-left-color: var(--chart-2); }
.tx-blk--zus   { border-left-color: var(--border-strong); }

/* „N Texte zu prüfen" ist ein Zweifel an der DATENLAGE, kein Urteil — Bernstein wie `.mark-warn`,
   bewusst nicht das Ampel-Gelb (das bewertet die Marge). */
.tx-warn { border-left-color: var(--warn-mark); }

/* Erklärung „So entsteht ein Varianten-Text" — Prosa, also gedeckelt (§3.4). */
.tx-erkl { margin: 0; padding-left: 18px; display: grid; gap: var(--sp-1); font-size: var(--fs-dense); line-height: 1.6; }
.tx-erkl li { padding-left: 2px; }

/* ── Reihenfolge der Blöcke ───────────────────────────────────────────────── */
.tx-order { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-1); max-width: 420px; }
.tx-order-item { display: flex; align-items: center; gap: var(--sp-1); padding: 5px var(--sp-2); border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas); }
/* Der Block-Name links, die Pfeile rechts — ohne Füll-<span>, das nur Luft transportiert. */
.tx-order-item .tx-blk { margin-right: auto; }

/* ── Vorlage: die zwei Block-Sektionen ────────────────────────────────────── */
.tx-sect { padding: var(--sp-3); border: 1px solid var(--border); border-left-width: 3px; border-radius: var(--r-1); }
.tx-sect + .tx-sect { margin-top: var(--sp-4); }
.tx-sect--eig { border-left-color: var(--chart-2); }
.tx-sect--zus { border-left-color: var(--border-strong); }
.tx-sect-h { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); font-size: var(--fs-dense); }
.tx-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-1); }
.tx-tokens { margin-top: var(--sp-3); }
.tx-tokens > .eyebrow { display: block; margin-bottom: var(--sp-1); }

/* Feld-Editor: die Zeilen sind Formular, keine Daten — deshalb kein `.grid` (das kappt mit
   nowrap/ellipsis auf fester Zeilenhöhe, was hier Eingabefelder sind). */
.tx-tbl { width: 100%; border-collapse: separate; border-spacing: 0; }
.tx-tbl th { height: var(--sp-8); padding: 0 var(--sp-2); background: var(--table-head); color: var(--text-muted); text-align: left; box-shadow: inset 0 -1px 0 var(--border-strong); white-space: nowrap; }
.tx-tbl td { height: var(--row-evt); padding: 3px var(--sp-2); border-bottom: 1px solid var(--row-line); font-size: var(--fs-dense); }
.tx-tbl tbody tr:last-child td { border-bottom: 0; }
.tx-tbl .tx-c { text-align: center; }
.tx-tbl .tx-acts { white-space: nowrap; text-align: right; }
.tx-tbl input[type="text"] { height: 24px; padding: 0 5px; border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: 400 var(--fs-dense) var(--f-ui); }
.tx-tbl input[type="text"]:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 16%, transparent); }
.tx-tbl input[type="checkbox"] { accent-color: var(--primary); margin: 0; }
.tx-w-lbl { width: 130px; }
.tx-w-unit { width: 60px; }
.tx-w-hint { width: 130px; }

.tx-area { width: 100%; min-height: 108px; padding: var(--sp-2); border: 1px solid var(--border-strong); border-radius: var(--r-1); background: var(--canvas); color: var(--text); font: 400 var(--fs-dense)/1.55 var(--f-ui); resize: vertical; }
.tx-area:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }

.tx-foot { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin-top: var(--sp-3); }
.tx-msg { font-size: var(--fs-dense); color: var(--text-muted); }

/* ── Wörterbuch ───────────────────────────────────────────────────────────── */
.tx-dict-add { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; margin-top: var(--sp-3); }

/* ── Vorschau / 3-Wege-Vergleich ──────────────────────────────────────────── */
/* Die Zone zeigt, WOHER ein Absatz kommt. Die Ziffer im Tag trägt die Identität, die Kante stützt sie. */
.tx-zone { padding: 5px var(--sp-2); border-left: 3px solid var(--border); border-radius: 0 var(--r-1) var(--r-1) 0; background: var(--sunken); }
.tx-zone + .tx-zone { margin-top: var(--sp-1); }
.tx-zone--intro { border-left-color: var(--chart-1); }
.tx-zone--eig { border-left-color: var(--chart-2); }
.tx-zone--zus { border-left-color: var(--border-strong); }
.tx-legend { display: inline-flex; gap: var(--sp-2); }
.tx-legend > span { padding: 0 5px; border-left: 3px solid var(--border); font-size: var(--fs-micro); }

/* Vorschau-Tabelle: KEIN `.grid` — die Spalte „Text" trägt mehrzeilige Blöcke, `.grid` würde sie auf
   eine 44px-Zeile mit nowrap+ellipsis eindampfen. Eine Vorschau, die man nicht lesen kann, ist keine. */
.tx-pv { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; }
.tx-pv th { height: var(--sp-8); padding: 0 var(--sp-2); background: var(--table-head); color: var(--text-muted); text-align: left; box-shadow: inset 0 -1px 0 var(--border-strong); white-space: nowrap; }
.tx-pv td { padding: var(--sp-2); border-bottom: 1px solid var(--row-line); font-size: var(--fs-dense); vertical-align: top; }
.tx-pv .tx-pv-sku { width: 190px; }
.tx-pv .tx-pv-st { width: 130px; }
.tx-pv .tx-pv-hi { width: 210px; }
.tx-pv code { font-family: var(--f-mono); font-size: var(--fs-micro); }

.tx-cmp-row > td { background: var(--sunken); }
.tx-cmp { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); padding: var(--sp-1) 0; }
.tx-cmp .eyebrow { display: inline-block; margin-bottom: var(--sp-1); }

/* ── Sichtbare Unterschiede im 3-Wege-Vergleich (Phase 2.2, 15.08.) ──────────────────────────────
   ⛔ KEIN GRÜN/ROT. Entscheidung D gilt: die einzige Ampel im System ist die Marge. Die Aussage
      trägt hier die FORM — neu ist unterstrichen, weggefallen durchgestrichen; die Tönung verstärkt
      nur. Damit bleibt der Vergleich auch lesbar, wenn jemand die zwei Töne nicht unterscheidet, und
      er konkurriert nicht mit der Ampel, die zwei Zeilen weiter über Geld urteilt. */
.tx-body--diff { white-space: normal; }
ins.tx-neu { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 2px;
  background: color-mix(in srgb, var(--accent) 12%, transparent); border-radius: 2px; padding: 0 1px; }
del.tx-weg { text-decoration: line-through; color: var(--text-muted);
  background: color-mix(in srgb, var(--text-muted) 10%, transparent); border-radius: 2px; padding: 0 1px; }
/* Die Kennzahl im Kopf beantwortet „muss ich hier hinsehen?", bevor man liest. */
.tx-gleich, .tx-anders { font: 400 var(--fs-micro) var(--f-ui); padding: 1px 6px; border-radius: var(--r-full); }
.tx-gleich { color: var(--text-muted); background: var(--sunken); }
.tx-anders { color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.tx-body { white-space: pre-wrap; font-size: var(--fs-dense); line-height: 1.55; max-height: 220px; overflow: auto; padding: var(--sp-2); border: 1px solid var(--border); border-radius: var(--r-1); background: var(--canvas); }

.tx-pvlist { margin: var(--sp-1) 0 0; padding-left: 18px; font-size: var(--fs-dense); line-height: 1.6; }

/* ── Zusatz-Eigenschaften (/sortiment/texte/zusatz) ───────────────────────────
   Präfix `.tz-`. Die Warengruppen-Karte ist zu `.views`/`.view-tab` geworden, die drei
   Karten (Erklärung · Import · Pflege) zu einem Lead + Aktionsleiste + einem Panel.
   Übrig bleibt hier nur, was diese Seite wirklich eigen hat: das Zellen-Raster. */

.tz-lead { margin-bottom: var(--sp-4); }
.tz-lead code { background: var(--sunken); padding: 0 3px; border-radius: 2px; }

/* Der Warengruppen-Wähler trägt die längsten Namen des Systems („Kardanadapter für Flanschmuttern") —
   ohne Deckel schöbe er die Werkzeuge im Kopf aus der Zeile. */
.tz-wg { max-width: 240px; }

.tz-file { width: 210px; height: 26px; padding: 2px var(--sp-1);
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--canvas); color: var(--text-muted); font: 400 var(--fs-micro) var(--f-ui); }
.tz-file::file-selector-button { height: 18px; margin-right: var(--sp-1); padding: 0 5px;
  border: 1px solid var(--border-strong); border-radius: var(--r-1);
  background: var(--sunken); color: var(--text); font: 500 var(--fs-micro) var(--f-ui); cursor: pointer; }
.tz-filter { width: 150px; }

/* Werkzeuge im Panel-Kopf statt in einer zweiten Leiste: „+ Zeile"/„+ Spalte" gehören zu DIESER
   Tabelle, nicht zur Seite — eine zweite Aktionsleiste hätte das behauptet. */
.tz-tools { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
.tz-add { width: 150px; }

/* Der Rahmen gehört dem Panel; die Tabelle scrollt darin waagerecht, wenn viele Eigenschaften
   gepflegt sind. Ohne diesen eigenen Scroll-Container schöbe sie den ganzen Canvas breit (Regel 4). */
.tz-scroll { overflow-x: auto; border-radius: 0 0 var(--r-2) var(--r-2); }
.tz-tbl { table-layout: auto; width: 100%; }
.tz-c-sku { width: 220px; }
.tz-cell { padding: 0 var(--sp-1); }
.tz-cell .z-cell { width: 100%; min-width: 96px; height: 24px; }

.tz-msg { justify-self: end; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 900px) {
  .tz-tools { margin-left: 0; flex-wrap: wrap; }
}

/* ── Zuordnungen (/sortiment/warengruppe) ─────────────────────────────────────
   Präfix `.wm-`. Nachfolger von `#sku-debugger` + `.parse-info` + `.table.sub.dense`.
   Aus den Tokens abgeleitet; die alten rem-Werte sind nirgends übernommen. */

.wm-dbg { margin-bottom: var(--sp-5); }
.wm-lead { margin: 0 0 var(--sp-3); }
.wm-fehl { margin-bottom: var(--sp-5); border-left: 3px solid var(--warn); }
.wm-fehl .lead { margin: var(--sp-3) 0 var(--sp-2); }
.wm-fehl-list { margin: 0 0 var(--sp-2); padding-left: var(--sp-4); line-height: 1.9; }
.wm-fehl-list code { font-size: var(--fs-dense); }

/* Seite /roadmap (Roadmap · Bugs · Changes) — Cutover 2026-07-18 */
.wm-h { margin: 0 0 var(--sp-2); }
.wm-h h2 { margin: 0; font: 600 var(--fs-sect)/22px var(--f-ui); }

.wm-dbg-in { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.wm-dbg-in .input { flex: 1; min-width: 260px; max-width: 420px; }

/* Debugger-Ausgabe: eine Zeile je Befund. Die Emoji-Marker tragen die Bedeutung (sie kommen so aus dem
   RPC-Klartext), deshalb hier keine zusätzliche Farbe — sonst stünde die Ampel an einer zweiten Stelle. */
.wm-dbg-msg { margin: var(--sp-3) 0 0; font-size: var(--fs-dense); }
.wm-dbg-list { margin: var(--sp-3) 0 0; padding-left: var(--sp-4);
  font-size: var(--fs-dense); line-height: 20px; color: var(--text); }
.wm-dbg-list li { margin-bottom: var(--sp-1); }
.wm-raw { margin-top: var(--sp-2); }
.wm-raw > summary { font-size: var(--fs-micro); color: var(--accent); cursor: pointer; list-style: none; }
.wm-raw > summary::-webkit-details-marker { display: none; }
.wm-raw > summary:hover { text-decoration: underline; }
.wm-raw pre { margin: var(--sp-2) 0 0; max-height: 320px; overflow: auto;
  background: var(--sunken); border: 1px solid var(--border); border-radius: var(--r-1);
  padding: var(--sp-2); font: 400 var(--fs-micro)/16px var(--f-mono); color: var(--text); }

/* Zuordnungs-Tabelle: die Auswahl ist die Arbeit, sie bekommt die Breite. */
.wm-c-n { width: 84px; }
.wm-c-map { width: 260px; }
.wm-c-act { width: 108px; }
.wm-tbl .map-sel { width: 100%; color: var(--text); }
.wm-tbl .map-sel:disabled { color: var(--text-faint); background: var(--sunken); cursor: not-allowed; }

/* Aufklapp-Zeile: die Objekt-Zelle ist auf 44 px, nowrap und `overflow:hidden` getrimmt — eine ganze
   Analyse darin würde unsichtbar abgeschnitten. Diese eine Zeile ist kein Objekt, sondern ein Panel. */
.wm-exp > td { height: auto; padding: var(--sp-3) var(--sp-4); white-space: normal;
  overflow: visible; text-overflow: clip; background: var(--sunken); }
.wm-exp .parse-info { font-size: var(--fs-dense); }
.wm-parse-head { margin-bottom: var(--sp-2); display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.wm-parse-sub { margin-bottom: var(--sp-2); font-size: var(--fs-micro); }
.wm-parse-scroll { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--r-1);
  background: var(--canvas); }
.wm-parse-tbl { width: 100%; table-layout: auto; }
.wm-parse-tbl th:first-child, .wm-parse-tbl td:first-child { padding-left: var(--sp-3); }
.wm-parse-tbl th:last-child, .wm-parse-tbl td:last-child { padding-right: var(--sp-3); }
.wm-parse-tbl code { font-size: var(--fs-micro); }
/* ── /sortiment/variante/:id — Artikel-Detail ────────────────────────────────
   Archetyp: Datenblatt. Oben die Antwort (Kalkulation), darunter die Einwände (Prüfaufträge), darunter
   die Belege (Hersteller → WaWi → Shop).
   ─────────────────────────────────────────────────────────────────────────── */

/* ── SEITENKOPF ────────────────────────────────────────────────────────────── */
/* Der Name neben der SKU, nicht darunter: der Kopf ist EINE 56-px-Zeile. Beides zusammen ist die
   Identität — dieselbe Begründung wie bei der Identitätszelle der Tabelle (Regel 11). */
.vd-name { font-size: var(--fs-ui); color: var(--text-muted); white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; min-width: 0; max-width: 30ch; }
.vd-badges { display: flex; align-items: center; gap: var(--sp-1); flex: none; }
.vd-badges:empty, .vd-badges > span:empty { display: none; }
.vd-badge-link { text-decoration: none; }
.vd-badge-link:hover { border-color: var(--border-strong); text-decoration: none; }

/* Bild + Zeichnung im Seitenkopf (12.08.). Sie stehen VOR der Artikelnummer, weil sie schneller
   beantworten „bin ich beim richtigen Artikel" als eine Zeichenkette aus vierzehn Zeichen — und weil
   der Kopf die einzige Stelle ist, die auf jeder Unterseite dieses Artikels gleich aussieht.
   Feste 32 px: der Kopf ist EINE 56-px-Zeile (s. oben), ein wachsendes Bild würde sie sprengen. */
.vd-bild { display: flex; align-items: center; gap: 4px; flex: none; }
.vd-bild img { width: 32px; height: 32px; object-fit: contain; border-radius: var(--r-1);
  border: 1px solid var(--border); background: var(--sunken); display: block; }
.vd-bild a:hover img { border-color: var(--border-strong); }
/* Die Zeichnung ist bewusst kein zweites Foto: gestrichelter Rahmen wie überall dort, wo etwas
   „anders geartet" ist — sie zeigt Maße, nicht das Teil. */
.vd-bild .vd-bild-zchg { border-style: dashed; }
.vd-bild-leer { width: 32px; height: 32px; display: grid; place-items: center; border-radius: var(--r-1);
  border: 1px dashed var(--border); color: var(--text-faint); font-size: var(--fs-micro); }

/* ── 1 KALKULATIONS-BAND ─────────────────────────────────────────────────────
   Die Figur ist `.kpirow`: randfüllend, Hairline unten, keine Karte. Sie ist die Antwort der Seite und
   steht deshalb ganz oben, nicht in einem Kasten in der Mitte. Kein `.kpirow` selbst, weil dessen
   `grid-auto-columns: 1fr` weder die Pfeile noch das Eingabefeld verträgt.
   ─────────────────────────────────────────────────────────────────────────── */
.vd-calc { display: flex; align-items: center; gap: var(--sp-4);
  min-height: 76px; padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--border); background: var(--canvas); flex-wrap: wrap; }
.vd-calc .spacer { flex: 1; }
.vd-ci { display: grid; gap: 2px; align-content: center; min-width: 0; }
.vd-cv { display: flex; align-items: center; gap: 6px;
  font: 600 var(--fs-sect)/24px var(--f-ui); font-variant-numeric: tabular-nums; color: var(--text); white-space: nowrap; }
/* Der effektive VK ist die Zahl, wegen der man hier ist — er darf größer sein als seine Vorstufen. */
.vd-cv--main { font-size: var(--fs-title); line-height: 26px; }
.vd-dbabs { font-size: var(--fs-dense); font-weight: 400; }
.vd-sep { color: var(--text-faint); font-size: var(--fs-sect); align-self: center; margin-top: 12px; }

/* Eigener VK: die einzige Schreib-Aktion im Band. `is-manual` sagt „dieser Preis ist gesetzt, nicht
   gerechnet" — eine Zustands-Aussage, deshalb die Akzentkante. */
.vd-vk { display: flex; align-items: center; gap: var(--sp-1); }
.vd-vk input { width: 96px; }
.vd-vk.is-manual input { border-color: var(--accent); box-shadow: inset 2px 0 0 var(--accent); }
.vd-in-xs { width: 68px; }
.vd-edit { justify-self: end; }

/* ── DOKUMENT ──────────────────────────────────────────────────────────────── */
.vd-page { display: grid; gap: var(--sp-4); align-content: start; }
.vd-page > * { margin: 0; }
.vd-formel { max-width: var(--measure); font-size: var(--fs-dense); line-height: 18px; color: var(--text-muted); }
.vd-formel strong { color: var(--text); font-weight: 600; }
/* Hilfe steht neben der Sache, permanent — ein Tooltip ist versteckte Dokumentation. */
.vd-hint { margin: var(--sp-1) 0 0; max-width: var(--measure); font: 400 var(--fs-micro)/16px var(--f-ui); color: var(--text-faint); }
.vd-msg { margin: var(--sp-2) 0 0; min-height: 16px; font-size: var(--fs-dense); }

/* ── 2 EIGENSCHAFTEN ─────────────────────────────────────────────────────────
   Keine Badges: eine Eigenschaft ist ein Schlüssel-Wert-Paar, kein Urteil. Der Schlüssel steht klein
   und grau VOR dem Wert, damit man die Zeile ohne Legende liest.
   ─────────────────────────────────────────────────────────────────────────── */
/* Bestand & Lieferzeit-Block (P7, 24.07.) — geteilt Drawer + Detailseite. */
.vd-stock { margin-top: var(--sp-3); padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--r-2); display: flex; flex-direction: column; gap: var(--sp-1); background: var(--sunken); }
.vd-stock-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-1); }
.vd-stock-dist { margin-top: var(--sp-1); }
.vd-stock-note { margin: var(--sp-1) 0 0; font-size: var(--fs-micro); }
.vd-stock-delivery, .vd-stock-fresh { font-size: var(--fs-ui); }
.vd-bware { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin-top: var(--sp-2); }
.vd-bware-note { flex: 1; min-width: 12rem; }
.vd-props { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-2); }
.vd-prop { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-1); background: var(--sunken);
  font: 400 var(--fs-dense) var(--f-ui); color: var(--text); white-space: nowrap; }
.vd-prop-k { font-size: var(--fs-micro); color: var(--text-faint); }

/* ── 3 PRÜFAUFTRÄGE ──────────────────────────────────────────────────────────
   Figur wie `.note`/`.ro-hint`: gesenkter Grund + Kante links. Block statt Zeile, weil hier Text UND
   Aktion stehen.
   FARBE: Grün/Gelb/Rot als SKALA gehört exklusiv der Marge-Ampel (Entscheidung D). Ein Prüfauftrag ist
   keine Skala, sondern eine Kategorie — er darf Farbe tragen, aber nicht die der Ampel:
     `--warn-mark` (Bernstein) = Zweifel an der Datenlage (dieselbe Bedeutung wie `.mark-warn` in der
     Tabelle: „welcher Wert stimmt?"). Bewusst NICHT das Ampel-Gelb.
     `--bad` = die Daten sind nicht zweifelhaft, sondern unmöglich (EK ≥ VK). Kategorisch, wie
     `badge--err` „Gesperrt, live" in der Arbeitsfläche — keine Skala.
   Die zwei Stufen sind exakt die des Vorgängers (`.ampel bad` / `.ampel warn`), nur nicht mehr in der
   Ampel-Sprache. Ob die Unterscheidung bleiben soll, ist Saschas Entscheidung — sie ist hier bewusst
   ERHALTEN und nicht eingeebnet, weil Einebnen Information kostet.
   ─────────────────────────────────────────────────────────────────────────── */
.vd-alerts { display: grid; gap: var(--sp-2); }
.vd-alert { padding: var(--sp-2) var(--sp-3); background: var(--sunken);
  border-left: 3px solid var(--border-strong); border-radius: 0 var(--r-1) var(--r-1) 0; }
.vd-alert p { margin: 0; max-width: var(--measure); font-size: var(--fs-dense); line-height: 18px; color: var(--text-muted); }
.vd-alert p strong { color: var(--text); font-weight: 600; }
.vd-alert--bad { border-left-color: var(--bad); }
.vd-alert--warn { border-left-color: var(--warn-mark); }
.vd-alert--good { border-left-color: var(--good); }
.vd-alert-acts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1); margin-top: var(--sp-2); }
/* Blocker-Liste („Nicht im Shop, weil:"). Ein Eintrag = ein Grund + sein Reparatur-Weg; die Liste ist
   bewusst untereinander und nicht als Chip-Reihe: die Sätze sind verschieden lang und tragen je einen
   eigenen Knopf — nebeneinander wäre nicht ablesbar, welcher Knopf zu welchem Grund gehört. */
.vd-blocker { margin: var(--sp-2) 0 0; padding-left: var(--sp-3); display: grid; gap: var(--sp-2);
  max-width: var(--measure); font-size: var(--fs-dense); line-height: 18px; color: var(--text-muted); }
.vd-blocker > li > div { margin-top: var(--sp-1); }

/* ── 4 DIE DREI QUELLEN ──────────────────────────────────────────────────────
   Nebeneinander, weil sie DERSELBEN Frage drei Antworten geben (was kostet der Artikel im Einkauf, was
   sagt die WaWi, was sieht der Kunde). Untereinander läsen sie sich als Reihenfolge.
   ─────────────────────────────────────────────────────────────────────────── */
/* ⛔ LEERE KARTEN SCHRUMPFEN, VOLLE WACHSEN (29.07.).
   Vorher verteilte `repeat(auto-fit, minmax(320px, 1fr))` gleichmäßig — unabhängig davon, ob eine Karte
   drei Zeilen oder dreißig trägt. Gemessen bei der Abnahme: „Keine Herstellerliste", „Keine
   Wettbewerbspreise" und „kein eigener Shop-Eintrag" bekamen je eine volle Spalte, während die
   Bezugsquellen mit fünf Lieferantennamen in 370 px erstickten und auf drei Zeilen umbrachen.
   Flex statt Grid, weil nur Flex „nimm, was du brauchst" kennt: volle Karten `flex: 3 1 380px`,
   leere `flex: 0 1 240px`. Die Karte sagt selbst, was sie ist (`vd-src--leer` aus dem Template) — das
   CSS rät nicht am Inhalt herum. Umbruch bleibt automatisch, also auch auf schmalen Fenstern sauber. */
.vd-sources { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: flex-start; }
.vd-src { flex: 3 1 380px; }
.vd-src--leer { flex: 0 1 240px; }
/* `min-width: 0` MUSS bleiben: ein Flex-Kind hat `min-width: auto`, und damit kann es von seinem
   längsten unumbrechbaren Inhalt (hier: Lieferantennamen) über seine Basis hinausgedrückt werden. */
.vd-src { min-width: 0; }
.vd-src .panel-head { gap: var(--sp-2); }
.vd-src .panel-head h2 { margin-right: auto; }

/* Schlüssel-Wert-Liste. `<dl>` statt `<table class="grid">`: eine kv-Liste hat keinen Spaltenkopf, und
   `.grid` würde ihre Werte per `nowrap`+ellipsis abschneiden — genau die stille Auslassung, die hier
   nicht passieren darf. */
/* Gewicht (#209): Eingabezeile aus mehreren Feldern + Knopf. Bricht um, statt die Kartenkante zu
   sprengen — bei ~900 px stand die Zeile sonst zur Hälfte außerhalb (derselbe Bruchpunkt, der am
   29.07. zwei Kopf-Knöpfe unerreichbar machte). `min-width: 0` auf dem Textfeld, weil ein
   Flex-Element sonst nicht unter seine Inhaltsbreite schrumpft. */
.vd-wform { margin: var(--sp-3) 0 0; }
.vd-wform > label { display: block; margin-bottom: var(--sp-1); font: 500 var(--fs-ui)/18px var(--f-ui); color: var(--text); }
.vd-wrow { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); min-width: 0; }
.vd-wrow > .input:not(.st-w-num) { flex: 1 1 160px; min-width: 0; }
.vd-kv { display: grid; grid-template-columns: minmax(0, 140px) minmax(0, 1fr); gap: 0 var(--sp-3); margin: 0; }
.vd-kv dt { padding: 5px 0; border-top: 1px solid var(--row-line);
  font-size: var(--fs-dense); line-height: 16px; color: var(--text-muted); min-width: 0; overflow-wrap: anywhere; }
.vd-kv dd { padding: 5px 0; margin: 0; border-top: 1px solid var(--row-line);
  font-size: var(--fs-dense); line-height: 16px; color: var(--text); font-variant-numeric: tabular-nums;
  min-width: 0; overflow-wrap: anywhere; }
.vd-kv dt:first-of-type, .vd-kv dt:first-of-type + dd { border-top: 0; }
.vd-hr { height: 0; margin: var(--sp-3) 0; border: 0; border-top: 1px solid var(--border); }
/* Abgelehnte Wettbewerbs-Zuordnung (#191): sichtbar, aber zurückgenommen — sie ist Beleg, keine Aussage.
   Ausblenden wäre schlimmer als dämpfen, dann verschwände die Arbeit aus dem Blick (dieselbe Begründung
   wie bei `verified` in get_wettbewerb_abstand). Keine Ampelfarbe: die gehört der Marge (Entscheidung D). */
.vd-kv.is-dim { opacity: .55; }

/* Zwischenüberschrift in einem Quell-Panel („Bezugsquellen (EK)", „Zuordnung Shop ⟷ WaWi"). */
.vd-sub-h { margin: var(--sp-4) 0 var(--sp-1);
  font: 600 var(--fs-micro)/16px var(--f-ui); letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); }
.vd-sub { margin-top: var(--sp-3); }
.vd-sub-lbl { display: block; margin-bottom: 3px; font: 400 var(--fs-micro) var(--f-ui); color: var(--text-muted); }
.vd-ai { margin-top: var(--sp-2); }

/* ── 5 ARTIKELTEXT ───────────────────────────────────────────────────────────
   Zwei Referenztexte nebeneinander: der Sinn ist der VERGLEICH (Shop ⟷ WaWi), und den kann man
   untereinander nicht ziehen.
   ─────────────────────────────────────────────────────────────────────────── */
.vd-text-edit { margin-left: auto; }
.vd-texts { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--sp-4); margin-top: var(--sp-3); }
.vd-text-ref { min-width: 0; }
.vd-text-final { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }
/* Der Text kommt aus Shop/WaWi und bringt eigenes HTML mit (u. a. <br/> — ohne sie laufen die Werte zu
   „135L1“ zusammen). Er wird deshalb gerendert, nicht escaped, und hier nur eingerahmt. */
.vd-text-body { margin-top: var(--sp-2); padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border); border-radius: var(--r-1); background: var(--sunken);
  font-size: var(--fs-dense); line-height: 1.55; color: var(--text); overflow-wrap: anywhere; }
.vd-text-body p { margin: var(--sp-2) 0; }
.vd-text-body p:first-child { margin-top: 0; }
.vd-text-body p:last-child { margin-bottom: 0; }
.vd-text-body ul, .vd-text-body ol { margin: var(--sp-2) 0; padding-left: var(--sp-5); }
.vd-text-body table { border-collapse: collapse; }
.vd-text-body td, .vd-text-body th { padding: 2px var(--sp-2); border: 1px solid var(--border); text-align: left; }

/* ── 6 ZUBEHÖR ───────────────────────────────────────────────────────────────
   Das Widget kommt vom Shopapi-Service und bringt sein eigenes CSS mit — hier nur der Abstand. */
.vd-cs { margin-top: var(--sp-2); }

/* Unterzeile der Wiki-Einleitung (ersetzt ein rem-Inline-style aus der alten Welt). */
.wk-lede-sub { margin: 0 0 var(--sp-4); font-size: var(--fs-dense); color: var(--text-muted); }

/* Klappbarer Abschnitt — die UA-Markierung raus, ein eigenes Dreieck rein. Stand als .ex-fold,
   .cr-fold und .tx-fold dreimal identisch in der Datei (Cutover 17.07.); drei Kopien derselben
   Figur sind genau das, was hier abgeschafft wird. */
.fold > summary { cursor: pointer; list-style: none; }
.fold > summary::-webkit-details-marker { display: none; }
.fold > summary::before { content: "▸"; color: var(--text-faint); font-size: var(--fs-micro); margin-right: 2px; }
.fold[open] > summary::before { content: "▾"; }

/* ── /preise — Preistabelle · Regeln · Massenaktion ──────────────────────────
   Die Hub-Reiter (Preistabelle/Wettbewerb/Regeln/Massenaktion/Crawler) stehen seit dem Cutover in
   der Schiene, nicht auf der Seite — `_preise_subnav.html` ist damit gestorben. Der tabs-Block
   trägt hier nur noch die seiteneigene Ansicht (Shop vs. Potenzial). */

/* Spaltenbreiten: kein <colgroup> mehr. <col> bindet an den Spaltenindex — eine ausgeblendete
   Spalte verschiebt alle folgenden lautlos. `[data-c=…]` klebt an der Spalte selbst. */
.grid [data-c="pr-prod"] { width: 260px; }
.grid [data-c="pr-ekw"]  { width: 104px; }
.grid [data-c="pr-ekl"]  { width: 104px; }
.grid [data-c="pr-vks"]  { width: 96px; }
.grid [data-c="pr-vkw"]  { width: 96px; }
.grid [data-c="pr-vkr"]  { width: 96px; }
.grid [data-c="pr-db"]   { width: 88px; }
.grid [data-c="pr-todo"] { width: 220px; }
.grid [data-c="pr-act"]  { width: 128px; }

/* Die Aktionsspalte klebt rechts (#359). Die Preistabelle passt nicht in die Fensterbreite — der Knopf lag
   hinter dem waagerechten Scrollen und war ohne Umweg nicht erreichbar. Gleiches Muster wie `.col-act` auf
   der Arbeitsfläche, inklusive der DREI Hintergründe: eine sticky-Zelle ohne eigenen Hintergrund lässt die
   durchlaufenden Zellen darunter durchscheinen, und hover/is-sel müssen mitziehen, sonst bleibt beim
   Überfahren genau diese eine Zelle hell stehen (derselbe Grund wie in Zeile 916/917 für col-*).
   Adressiert über die Zeilenklasse statt data-c: die Datenzellen der Preistabelle tragen keins — und
   `tr > *:last-child` allein träfe auch die colspan-Zelle der Gruppenköpfe. */
.pr-table thead th:last-child,
.pr-table tbody tr.price-row > td:last-child {
  position: sticky; right: 0; z-index: 1; background: var(--canvas);
  box-shadow: -1px 0 0 var(--border), -8px 0 12px -6px rgba(0,0,0,.16);
}
.pr-table thead th:last-child { z-index: 4; }   /* über den Datenzellen, wie der übrige Kopf (z-index 3) */
.grid.pr-table tbody tr.price-row:hover > td:last-child { background: var(--hover); }
.grid.pr-table tbody tr.price-row.is-sel > td:last-child { background: var(--selected); }

.pr-sub { font-size: var(--fs-micro); line-height: 1.35; }
.pr-table .p-name { font-weight: 500; }
/* Regel 11: genau EINE zweizeilige Zelle je Tabelle — die Identität in Spalte 1. */
.pr-table .p-prod { line-height: 1.35; }

/* Aufgeklappte Varianten eines Produkts. Die Sub-Tabelle erbt die Breite ihrer Zelle statt
   `width:auto` — sonst driften ihre Spalten gegen die Elterntabelle (derselbe Fehler, der auf der
   Arbeitsfläche zum Tree-Grid geführt hat; hier bleibt sie, weil die Spaltensätze verschieden sind). */
.variant-panel > td { padding: 0; background: var(--sunken); }
.variant-table { width: 100%; }
.variant-table th, .variant-table td { border-bottom: 1px solid var(--row-line); }
.variant-table tr:last-child td { border-bottom: 0; }

/* ── Regeln & Leitplanken ─────────────────────────────────────────────────── */
.pr-guards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--sp-3); margin: var(--sp-2) 0 var(--sp-6); }
.pr-guard { padding: var(--sp-3) 14px; gap: 2px; }
.pr-guard-val { font: 600 var(--fs-title)/1.2 var(--f-ui); font-variant-numeric: tabular-nums; }
.pr-guard-val .dim { font-size: var(--fs-micro); font-weight: 400; }
.pr-guard-sub { font-size: var(--fs-micro); line-height: 1.45; margin: 0 0 var(--sp-2); }

.pr-form { padding: var(--sp-3) 14px; margin-bottom: var(--sp-4); }
.pr-note { font-size: var(--fs-micro); line-height: 1.5; margin: var(--sp-2) 0 0; }
.pr-inline { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-ui); }
/* Die Formularzeile bricht um, statt die Seite zu sprengen: 9 Felder passen auf keinen Laptop. */
.pr-rule { display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--sp-2) var(--sp-3); }
.pr-rule > label, .pr-rule .attr-pair > label { display: flex; flex-direction: column; gap: 3px;
  font-size: var(--fs-micro); color: var(--text-muted); }
.pr-rule .attr-pair { display: flex; align-items: flex-end; gap: var(--sp-2); }
.pr-w-s { width: 72px; }
.pr-w-m { width: 96px; }
.pr-check { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-dense);
  padding-bottom: 5px; }
.pr-check input { accent-color: var(--primary); margin: 0; }
.pr-acts { display: flex; align-items: flex-start; gap: var(--sp-4); margin-top: var(--sp-3);
  padding-top: var(--sp-3); border-top: 1px solid var(--border); }
.pr-acts .pr-note { flex: 1; margin: 0; }
.pr-acts-btns { flex: none; display: flex; gap: var(--sp-2); }

/* ── Massenaktions-Vorschau (/preise?tab=massenaktion, von massenaenderungen.js gebaut) ──────
   Die Tabelle steht VOR einer Änderung an tausenden Preisen — sie ist der letzte Blick, bevor es
   ernst wird. Bis zum Cutover kam ihr CSS aus app.css; die Klassen stehen in keinem Template
   (JS-Markup), deshalb hat sie beim Neubau niemand vermisst: rohe <th>, und die Preise standen
   linksbündig mit Proportionalziffern (Regel 6). Gefunden vom QA-Reviewer, 17.07. */
.mass-preview { margin-top: var(--sp-3); }
/* Zeile, deren neuer Preis unter der Mindestmarge landet. Tönung + linke Kante statt roter Schrift:
   die Zahlen der Zeile sind nicht falsch, nur ihr Ergebnis ist es — und eine rot eingefärbte Zahl
   läse sich als „dieser Wert stimmt nicht". Die Kante markiert die ZEILE, nicht den Wert. */
.mass-preview tr.is-below-min td { background: color-mix(in srgb, var(--bad) 7%, var(--canvas)); }
.mass-preview tr.is-below-min td:first-child { box-shadow: inset 3px 0 0 var(--bad); }
.grid [data-c="ms-sku"]  { width: 200px; }
.grid [data-c="ms-name"] { width: auto; }
.grid [data-c="ms-ek"]   { width: 104px; }
.grid [data-c="ms-pr"]   { width: 180px; }
.grid [data-c="ms-db"]   { width: 160px; }
/* Der Aufklapp-Pfeil der Warengruppe: feste Breite, damit die Namen darunter fluchten, und in der
   Akzentfarbe — er ist das einzige Bedienelement der Zeile. Stand als `.wg-toggle` in app.css. */
.wg-toggle { display: inline-block; width: 1em; color: var(--accent); font-weight: 600; }

/* Größen-Lücken / fehlende Eigenschaften — von cockpit.js gebaut („Welche Größen fehlen?" /
   „Welche Eigenschaften fehlen?"). Stand als `.ck-chips`/`.ck-chip` in app.css; im JS erzeugtes
   Markup wird beim Neubau nicht vermisst, weil es in keinem Template steht. Ohne die Regeln waren
   es 29 aneinandergeklebte Wörter: „16×2 10 fehlten16×4 8 fehlten…". QA-Befund 17.07. */
.ck-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-2); }
.ck-chip { display: inline-flex; align-items: center; gap: var(--sp-1); padding: 2px var(--sp-2);
  border: 1px solid var(--border); border-radius: var(--r-full); background: var(--sunken);
  font-size: var(--fs-micro); white-space: nowrap; }
.ck-chip b { font-weight: 600; font-variant-numeric: tabular-nums; }

/* Bezugsmenge der Aufgaben-Zahlen (Dominik 19.07.): sie zählen über ALLE Produkte, während „Alle Produkte"
   daneben dem Reiter folgt. Zwei Bezugsmengen in einem Kopf muss man beschriften. */
.chipbar-scope { display: block; font-weight: 400; letter-spacing: .02em; text-transform: none;
                 font-size: var(--fs-micro); color: var(--text-faint); opacity: .85; margin-top: 1px; }
.chipbar-lbl { line-height: 1.15; }
span.tchip.is-zero { cursor: default; }
/* Beleg-Zeile in Tabellenzellen (Lieferantenname, Ersparnis) — die Zahl allein sagt nicht, WER und WIE VIEL. */
.wt-sub { display: block; font-size: var(--fs-micro); line-height: 1.3; color: var(--text-faint);
          font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 15ch; }
.delta-good .wt-sub { color: inherit; opacity: .75; }
/* Sternchen an der Marge: gerechnet mit dem günstigsten Angebot, nicht mit dem Preis des Hauptlieferanten.
   Bewusst ein Marker und keine Umfärbung — die Zahl selbst ist richtig gerechnet, nur ihre Basis ist eine
   andere als die, bei der eingekauft wird (Dominik 19.07., Entscheidung offen). */

/* Preis-Leiste der Wettbewerbs-Seite (20.07.). EIN Prozentfeld speist die Einzelknöpfe UND die
   Sammelaktion — zwei getrennte Felder wären zwei Wahrheiten über denselben Abstand. */
.wb-pricebar { flex-wrap: wrap; gap: var(--sp-2); }
.wb-pricebar .wb-below { width: 70px; text-align: right; font-variant-numeric: tabular-nums; }
.wb-pricebar .wb-minus { font-weight: 600; color: var(--text-muted); }
.wb-pricebar-hint { flex-basis: 100%; }
@media (min-width: 900px) { .wb-pricebar-hint { flex-basis: auto; } }
/* Die „dann X %"-Marge gilt für den vollen Wettbewerbspreis. Sobald ein Abzug gesetzt ist, stimmt sie
   nicht mehr — sie hier nachzurechnen hieße, die Margenformel ein zweites Mal zu schreiben. Also
   gedämpft statt falsch: eine Zahl, der man ansieht, dass sie gerade nicht die ganze Antwort ist. */
.db.is-stale { opacity: .45; }

/* Warengruppe an der Identität der Wettbewerbs-Zeile — dritte Zeile unter Name und Artikelnummer.
   Gedämpft: sie ordnet ein, sie ist nicht die Aussage der Zeile. */
.ident-wg { display: block; font-size: var(--fs-micro); color: var(--text-faint); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ident-wg:hover { color: var(--primary); text-decoration: underline; }

/* Werkstoff-Begriffe (#341): WaWi-Begriff → Shop-Begriff. Drei Spalten, damit die Paare fluchten —
   ohne Raster liest man bei zehn Zeilen nicht mehr, was zu was gehört. */
.kf-terme { display: grid; grid-template-columns: 1fr 1fr auto; gap: var(--sp-2); align-items: center; max-width: 720px; }
.kf-term { display: contents; }
.kf-term-head { display: contents; font-size: var(--fs-micro); color: var(--text-muted); }
.kf-term-head > span { padding-bottom: 2px; }

/* Widerspruch zwischen Zeile und Produkt (29.07.). Steht direkt unter dem Kalkulationsband, weil er
   dessen Zahlen relativiert — ein Hinweis, der erst nach den Prüfaufträgen käme, würde die Zahl über
   ihm ungebremst wirken lassen. Trägt die Bad-Kante wie die anderen Prüfaufträge (Entscheidung D:
   Grün/Gelb/Rot als SKALA bleibt der Marge, eine Kante ist keine Skala). */
.vd-widerspruch { margin: var(--sp-3) 0 var(--sp-2); }

/* ⛔ BEZUGSQUELLEN SIND KEINE SCHLÜSSEL-WERT-LISTE (29.07.).
   Gemessen auf `LB-KA-28` bei 1920 px: die Lieferantennamen steckten in der 140-px-Label-Spur von
   `.vd-kv` und brachen auf bis zu SECHS Zeilen um („Nanjing Pusher Machinery Co.,Ltd" + Kennzeichen +
   Lieferanten-Nummer). Die Karte breiter zu machen half nur begrenzt — die Spur ist fix.
   Hier ist der Name nicht die Beschriftung eines Werts, sondern der Inhalt selbst; der Preis ist die
   Beschriftung dazu. Also: Name nimmt den Platz, Betrag hängt sich rechts an. */
.vd-kv--quellen { grid-template-columns: minmax(0, 1fr) auto; column-gap: var(--sp-3); }
.vd-kv--quellen dt { font-weight: 500; color: var(--text); }
.vd-kv--quellen dd { text-align: right; white-space: nowrap; }

/* .wm-norm — Sub-Mapping je DIN/ISO-Norm auf der Zuordnungs-Seite (01.08.).
   ⛔ Ein RASTER, keine Tabelle: die Zuordnungs-Tabelle darüber hat feste Spaltenbreiten, und eine
      zweite Tabelle in ihrer colspan-Zelle sprengt sie — gemessen verschob es die ganze Seite
      waagerecht. Der eigene Scroll-Container hält die Mutter-Tabelle unangetastet. */
/* ⛔ `max-width: 0` auf der ZELLE ist der Trick, ohne den ein breites Kind die Tabelle sprengt:
   eine Tabellenzelle waechst sonst mit ihrem Inhalt, und `max-width: 100%` im Kind bezieht sich
   dann auf die bereits gewachsene Zelle. Mit 0 haengt die Breite an der TABELLE, und der Wrapper
   scrollt in sich. Gemessen: ohne das verschob das Raster die ganze Seite waagerecht. */
.norm-sub-row > td { max-width: 0 }
.wm-norm-wrap { overflow: auto; max-width: 100%; max-height: 420px }
.wm-norm { display: grid; grid-template-columns: minmax(190px,1fr) 70px 80px 80px minmax(190px,240px);
           gap: 2px 10px; align-items: center; min-width: 600px }
.wm-norm > div { padding: 5px 0; border-bottom: 1px solid var(--border) }
.wm-norm-h { font: 600 var(--fs-micro)/1.4 var(--f-ui); color: var(--text-muted);
             text-transform: uppercase; letter-spacing: .04em; position: sticky; top: 0;
             background: var(--canvas); z-index: 1 }

/* ── Attribute & Schreibweisen (/einrichten/attribute, 01.08.) ──────────────────────────────────
   Die Seite lebt von Wiedererkennung: eine Einordnung (verzinkt), die Kuerzel, die darauf zeigen
   (ZN, VZ), und die Schreibweisen der Lieferanten. Drei Sorten Text — ohne unterschiedliche Form
   liest man sie als eine Liste. */
.count-pill--warn { color: var(--badge-warn-fg); background: var(--badge-warn-bg) }
a.count-pill--warn { text-decoration: none }

/* ⛔ ZUM VIERTEN MAL DIESELBE URSACHE — deshalb steht die Regel jetzt an BEIDEN Tabellenformen.
   .grid ist width:max-content: die letzte Spalte waechst mit ihrem Inhalt, bis die ganze SEITE
   waagerecht scrollt. Bei elf Schreibweisen in einer Zeile war das bei 950 px erreicht.
   max-width:0 zwingt die Zelle in die verbleibende Breite, damit ihr Kind (.attr-aliase mit
   flex-wrap) umbrechen KANN statt zu schieben.
   Die Regel stand zuerst nur an .attr-kand — die drei Woerterbuch-Tabellen daneben hatten sie
   nicht. Genau der Fall aus CLAUDE.md: die Ausnahme einmal richtig erkannt und fuer die Nachbarn
   vergessen. Beim naechsten Mal gehoert sie an .grid selbst. */
.attr-kand td:last-child, .attr-dict td:last-child { max-width: 0 }

/* ⛔ UND DAS ALLEIN REICHT NICHT. `.grid` ist `width: max-content` — die Tabelle waechst mit ihrem
   Inhalt, und `max-width: 0` auf einer Zelle kann das nicht abfangen, solange die TABELLE selbst
   waechst. Gemessen bei 950 px: die Spalte „Schreibweisen" stand ausserhalb des Sichtfelds, die
   Chips brachen dort unsichtbar um — und im Bild blieben verschieden hohe, scheinbar leere Zeilen
   (A4 rund 150 px neben C15 mit 44 px). Das sah nach Renderfehler aus, wo nur Text weggeschoben war.
   `width: 100%` holt die Tabelle in den Rahmen, `table-layout: fixed` verteilt nach der colgroup und
   gibt der letzten Spalte den Rest — dann bricht sie SICHTBAR um. */
.attr-dict { width: 100%; table-layout: fixed }

.attr-token { font: 600 var(--fs-dense)/18px var(--f-mono); letter-spacing: .01em; margin-right: 6px }
/* Der Rohtext aus dem Katalog ist ZITAT, nicht Eingabe — er wird gelesen, nicht getippt. */
.attr-roh { font: 400 var(--fs-dense)/18px var(--f-mono); color: var(--text); background: var(--sunken);
            padding: 2px 6px; border-radius: var(--r-1) }
.attr-bsp { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }

/* ⛔ Die Affordanz muss VOR dem Hover da sein. Zuerst war der Rahmen durchsichtig und erschien erst
   beim Darueberfahren — in siebzig Zeilen gab es damit null Hinweis, dass dort etwas bearbeitbar
   ist, und man musste zufaellig darauf stossen. */
.attr-label { display: inline-block; min-width: 120px; padding: 1px 4px; border-radius: var(--r-1);
              border: 1px solid transparent }
.attr-label[contenteditable] { border-bottom-color: var(--border); border-bottom-style: dotted; cursor: text }
.attr-label[contenteditable]:hover { border-color: var(--border); background: var(--sunken) }
.attr-label[contenteditable]:focus { border-color: var(--accent); background: var(--canvas); outline: none }

/* „Gespeichert, aber noch nicht in den Artikeln" — die Gleichsetzung wirkt fuer den Vergleich
   sofort, fuer material_class erst mit dem naechsten Neuaufbau. */
.attr-pending { margin-left: 6px; font: 500 var(--fs-micro)/16px var(--f-ui); color: var(--warn);
                white-space: nowrap }

/* Die Aufteilung einer Einordnung auf ihre Kürzel (#482). Sie steht UNTER der Einordnung, nicht in
   der Zahlenspalte: dort ist die Zusammenlegung erklärt, und dort wird sie in Frage gestellt. Klein
   und gedämpft — sie belegt die Kopfzahl, sie konkurriert nicht mit ihr. */
.attr-split { margin-top: 2px; font: 400 var(--fs-micro)/1.5 var(--f-ui); color: var(--text-muted) }
.attr-split-t { white-space: nowrap; font-variant-numeric: tabular-nums }
.attr-split-t code { font: 600 var(--fs-micro)/1 var(--f-mono); color: var(--text) }
/* Die Regel über allen drei Wörterbuch-Tabellen: ein Absatz Fließtext, also mit Zeilenmaß. */
.attr-regel { padding: var(--sp-3) var(--sp-4) }
.attr-regel .hint { max-width: var(--measure); margin: 0 }

.attr-aliase { display: flex; flex-wrap: wrap; gap: 4px; align-items: center }
.attr-chip { display: inline-flex; align-items: center; gap: 3px; font: 400 var(--fs-micro)/16px var(--f-ui);
             background: var(--sunken); border: 1px solid var(--border); border-radius: var(--r-1);
             padding: 1px 4px 1px 6px; color: var(--text) }
.attr-chip-x { border: none; background: none; cursor: pointer; color: var(--text-muted); padding: 0 2px;
               font: 600 var(--fs-micro)/1 var(--f-ui) }
.attr-chip-x:hover:not(:disabled) { color: var(--bad) }
.attr-chip-x:disabled { cursor: not-allowed; opacity: .45 }
.attr-chip--add { cursor: pointer; border-style: dashed; padding: 1px 7px }
.attr-chip--add:hover:not(:disabled) { border-color: var(--accent); color: var(--accent) }
.attr-chip--add:disabled { cursor: not-allowed; opacity: .45 }

/* Die Zuordnen-Zeile bricht auf schmalen Fenstern um, statt die Tabelle zu sprengen. */
/* Pflicht-Eigenschaften je Warengruppe (E4, 14.08.). Eine Zeile je Warengruppe; die wartenden stehen
   oben und tragen einen linken Balken — sie sind die einzigen, bei denen die Liste keine Feineinstellung
   ist, sondern der einzige Weg in den Shop. */
.attr-pflicht { padding: var(--sp-2) 0; border-bottom: 1px solid var(--border) }
.attr-pflicht:last-child { border-bottom: 0 }
.attr-pflicht--wartet { border-left: 3px solid var(--warn-mark); padding-left: var(--sp-2) }
.attr-pflicht-kopf { display: flex; flex-wrap: wrap; gap: var(--sp-1); align-items: center;
  margin-bottom: 4px; font-size: var(--fs-dense) }
.attr-pflicht .fset { display: flex; flex-wrap: wrap; gap: 4px; align-items: center }
.attr-pflicht .pf-liste { flex: 1 1 320px; min-width: 200px; padding: 4px 8px;
  border: 1px solid var(--border); border-radius: var(--r-2); background: var(--canvas);
  color: var(--text); font: 400 var(--fs-dense)/18px var(--f-ui) }
.attr-pflicht .pf-ergebnis { flex: 1 1 100%; font-size: var(--fs-micro); line-height: 16px }
/* „Was ist hier lieferbar?" (Phase 3.1): die gemessenen Kandidaten als anklickbare Vorschläge.
   ⛔ Die Zahl steht IM Chip, nicht im Tooltip — „Material 21/59" ist der ganze Unterschied zwischen
      „das nehme ich" und „damit blockiere ich 38 Artikel". Eine Zahl, die man erst beim Überfahren
      sieht, liest niemand, bevor er klickt. */
.pf-kand { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
  margin-top: var(--sp-2); font-size: var(--fs-micro); }
.pf-kand-t { flex: 1 1 100%; color: var(--text-muted); }
.pf-kand-chip { cursor: pointer; }
/* Voll lieferbar = hält niemanden auf. Bewusst nur eine ruhige Betonung und KEIN Grün: die einzige
   Ampel im System ist die Marge (Entscheidung D). */
.pf-kand-chip.is-voll { border-color: var(--border-strong); font-weight: 600; }
/* Ohne Schreibrecht bleiben die Vorschlaege reine Auskunft — sie sehen dann nicht wie Knoepfe aus. */
.pf-kand-chip.is-nurinfo { cursor: default; opacity: .75; }
/* Das Eingabefeld + Speichern bilden ein eigenes, gesperrtes Fieldset INNERHALB der Zeile. */
.attr-pflicht .fset--inline { display: contents; }
.attr-zuordnen { display: flex; flex-wrap: wrap; gap: 4px; align-items: center }
.attr-zuordnen .input { min-width: 130px; flex: 1 1 130px }
/* ⛔ `min-width: 0` (19.08.): ohne das kann ein <select> als Flex-Element nicht unter die Breite
   seines laengsten Eintrags schrumpfen. Gemessen ragte es 54 px ueber den rechten Seitenrand — der
   Aufklapp-Pfeil und der halbe Knopf "Einordnen" lagen ausserhalb, und weil die Zeilen verschieden
   lange Werkstoffnamen fuehren, standen die Bedienelemente von Zeile zu Zeile versetzt. */
.attr-zuordnen .select { max-width: 190px; min-width: 0 }

/* Vier gleich aussehende Panels untereinander lassen die ARBEIT im Bestand verschwinden. Dasselbe
   Muster wie .ck-alarm im Cockpit: getoenter Rahmen, damit der Blick zuerst dort landet. */
.panel--todo { border-color: var(--badge-warn-bd) }
.panel--todo > .panel-head { background: var(--badge-warn-bg) }

/* ── Einkaufspreise (/preise?tab=ek, 02.08.) ───────────────────────────────── */
/* Die Toepfe stehen im Raster, nicht im Flex-Umbruch — dieselbe Lehre wie bei den Normen-Kacheln:
   inhaltsbreite Kaesten teilen ueber zwei Zeilen keine Spaltenkante. */
.ek-toepfe { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--sp-2); margin: var(--sp-3) 0; }
.ek-topf { border: 1px solid var(--border); border-radius: var(--r-1); padding: var(--sp-2) var(--sp-3);
  background: var(--canvas); display: flex; flex-direction: column; gap: 2px; }
.ek-topf b { font: 600 var(--fs-kpi)/1.1 var(--f-ui); font-variant-numeric: tabular-nums; }
.ek-topf span { font: 600 var(--fs-dense)/1.35 var(--f-ui); }
/* Die Handlungsanweisung ist der Zweck der Kachel — sie steht nicht kleiner als die Ueberschrift,
   nur ruhiger. Eine Zahl ohne den naechsten Schritt ist eine Zahl zum Anschauen. */
.ek-topf em { font: 400 var(--fs-micro)/1.45 var(--f-ui); color: var(--text-muted); font-style: normal; }
/* Der Rest-Topf ist der groesste und der einzige, gegen den man heute nichts tun kann. Er steht
   deshalb sichtbar anders da — sonst liest er sich als vierte Aufgabe. */
.ek-topf--rest { background: var(--sunken); border-style: dashed; }
.ek-filter { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin: var(--sp-3) 0 var(--sp-2); }
.ek-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-top: var(--sp-3); }


/* ── Bilder & Zeichnungen (/einrichten/bilder) ──────────────────────────────────────────────────
   Gliederung Warengruppe > Bauform > Werkstoff mit Bildvorschau. Die Vorschau ist der Grund fuer
   die Zeilenhoehe: eine URL ohne das Bild daneben beantwortet nicht, ob sie die richtige ist. */
.jsp-legend { display: grid; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; max-width: var(--measure); }
.jsp-legend li { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2); align-items: baseline;
                 font-size: var(--fs-dense); color: var(--text-muted); }

.bi-wg { margin-bottom: var(--sp-3); }
.bi-wg-sum { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; cursor: pointer;
             padding: var(--sp-2) 0; list-style: none; }
.bi-wg-sum::-webkit-details-marker { display: none; }
.bi-wg-name { font-weight: 600; font-size: var(--fs-sect); }
.bi-wg[open] > .bi-wg-sum .pk-caret { transform: rotate(90deg); }
.bi-wg-sum .pk-caret { width: var(--fs-icon); height: var(--fs-icon); flex: none; transition: transform .12s; }
/* Der Deckungsgrad bricht als Ganzes um, statt mitten in einer Zahl: "3 von 6" ueber zwei Zeilen
   liest sich als zwei verschiedene Angaben. */
.bi-deckung { font-size: var(--fs-micro); white-space: normal; }

.bi-table .is-child [data-c="bi-reihe"] { padding-left: var(--sp-4); }
/* ⛔ UMBRECHEN STATT HINAUSSCHIEBEN (12.08.). Ohne `wrap` blieb die Zeile Bilder | Felder | Speichern
   starr: unterhalb von ~1100 px schob sie den Speichern-Knopf aus dem scrollenden Tabellen-Container.
   Die Seite selbst lief nicht ueber (der Waechter in seiten.test.js schlug zu Recht nicht an) — nur
   die AKTION war ausserhalb des Bildes, und das ist der schlechteste Kandidat zum Abschneiden.
   Mit `wrap` faellt der Knopf in eine eigene Zeile, statt zu verschwinden. */
.bi-slot { display: flex; align-items: flex-start; gap: var(--sp-2); flex-wrap: wrap; }
.bi-slot .bi-save { margin-left: auto; }
.bi-thumbs { display: flex; gap: 4px; flex: none; }
.bi-thumb { width: 38px; height: 38px; object-fit: contain; background: var(--sunken);
            border: 1px solid var(--border); border-radius: 3px; display: block; }
.bi-thumb--zchg { border-style: dashed; }
.bi-thumb--leer { display: grid; place-items: center; color: var(--text-muted); font-size: var(--fs-micro); }

/* ── Ablegen statt URL tippen (Phase 2.1, 15.08.) ────────────────────────────────────────────────
   Jede Vorschau ist ihr eigenes Drop-Ziel; die Rolle steht an der Position. Im Ruhezustand sieht man
   davon NICHTS — die Kachel ist eine Übersicht, keine Werkzeugleiste, und ein gestrichelter Rahmen um
   jedes der 65 Felder wäre lauter als der Inhalt. Erst beim Ziehen zeigt sich das Ziel. */
.bi-drop { position: relative; border-radius: var(--r-1); }
.bi-drop.is-ueber { outline: 2px dashed var(--accent); outline-offset: 2px; background: var(--selected); }
/* Ein leeres Feld ist anklickbar (Dateiauswahl) — der Zeiger sagt das, sonst sieht es aus wie ein
   Platzhalter, den man nur anschauen kann. */
.bi-drop > .bi-thumb--leer { cursor: pointer; }
.bi-drop > .bi-thumb--leer:hover { border-color: var(--accent); color: var(--accent); }
/* Während des Hochladens bleibt die Kachel an ihrem Platz und wird nur gedämpft: ein Spinner, der die
   Vorschau ersetzt, ließe die Zeile springen — bei 65 Zeilen verliert man dabei die Stelle. */
.bi-drop.is-laedt { opacity: .45; pointer-events: none; }
/* Zwei Felder untereinander statt nebeneinander: bei 950 px waeren zwei URL-Felder in einer Zelle
   je 90 px breit -- also wieder der abgeschnittene Link aus der ersten Fassung. */
.bi-fields { display: grid; gap: 4px; flex: 1 1 200px; min-width: 0; }
.bi-fields .input { width: 100%; min-width: 0; }
.bi-copy { width: 100%; min-width: 0; }
.bi-table td[data-c="bi-slot"] { width: 46%; }
[data-c="bi-vor"] { width: 52px; }

/* Blocker-Chip in der Spalte „Warum nicht im Shop" (E3, 14.08.). Bewusst KEIN `.badge`: die Badges tragen
   in dieser Tabelle Farbe mit Bedeutung (Ampel), und ein Blocker ist keine Bewertung, sondern eine
   Feststellung. Mehrere stehen untereinander — ein Artikel kann von zweien aufgehalten werden. */
.blocker-chip { display: block; font-size: var(--fs-micro); line-height: 15px; color: var(--text-muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 190px; }
.blocker-chip + .blocker-chip { margin-top: 2px; }
/* Führt der Blocker an einen Ort, an dem er sich beheben lässt, ist der Chip ein Link (15.08.).
   Zurückhaltend: gepunktete Unterlinie statt Linkfarbe — es bleibt eine Feststellung, keine Bewertung,
   und eine ganze Spalte in Linkblau würde die Ampel-Badges daneben erschlagen. */
a.blocker-chip--fix { color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; cursor: pointer; }
a.blocker-chip--fix:hover, a.blocker-chip--fix:focus-visible { color: var(--accent); border-bottom-color: var(--accent); }
.grid [data-c="blocker"] { max-width: 200px; }
/* Der Reifegrad braucht eine feste, schmale Spur: der Balken soll über die Zeilen hinweg auf gleicher
   Länge liegen, sonst vergleicht man Balken verschiedener Maßstäbe (dann ist die Grafik schlechter als
   die blanke Zahl). 92 px = Balken + „3/4" ohne Umbruch. */
.grid [data-c="reife"] { width: 92px; }

/* ══ SHOP-SANDBOX (Phase 4.1, 16.08.) ═══════════════════════════════════════════════════════════════
   „Wie es im Shop ankommt" — Bild links, Konfigurator rechts. Dieselbe Figur auf der Vollansicht und
   im Seitenpanel; `--kompakt` stapelt sie nur, statt eine zweite Karte zu bauen. */
.shop-sandbox { margin-bottom: var(--sp-4); }
.sb-body { display: grid; grid-template-columns: 200px 1fr; gap: var(--sp-4); padding: var(--sp-3); }
.sb-bild { position: relative; aspect-ratio: 4 / 3; background: var(--sunken); border: 1px solid var(--border);
  border-radius: var(--r-2); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.sb-bild img { width: 100%; height: 100%; object-fit: contain; padding: var(--sp-2); }
.sb-bild-leer { font-size: var(--fs-micro); color: var(--text-faint); text-align: center; padding: var(--sp-2); }
.sb-bild-mark { position: absolute; left: var(--sp-2); bottom: var(--sp-2); padding: 1px 5px; border-radius: var(--r-1);
  background: var(--canvas); border: 1px solid var(--border); font-size: var(--fs-micro); color: var(--text-muted); }
/* Gedeckelt, obwohl die Karte randfüllend ist: Grundpreis/Zuschnitt/Richten sind Label-Wert-Paare, und
   auf 1920 px lagen zwischen „Zuschnitt" und „7,00 €" anderthalb Handbreit leere Fläche — man verliert
   die Zeile beim Hinübersehen. Das ist dieselbe Überlegung wie bei `--measure`, nur für Zahlenpaare
   statt für Fließtext (deshalb ein eigener Wert und NICHT `--measure`: das gilt laut Hausregel
   ausdrücklich nicht für Tabellarisches). */
.sb-konf { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; max-width: 560px; }
.sb-titel { font-size: var(--fs-sect); font-weight: 600; }
.sb-feld { display: flex; align-items: center; gap: var(--sp-2); }
.sb-k { flex: 0 0 58px; font-size: var(--fs-micro); color: var(--text-muted); }
.sb-in { display: flex; align-items: center; gap: var(--sp-2); flex: 1 1 auto; min-width: 0; }
.sb-slider { flex: 1 1 auto; min-width: 80px; accent-color: var(--accent); }
.sb-num { width: 88px; padding: 3px 6px; border: 1px solid var(--border); border-radius: var(--r-1);
  background: var(--canvas); color: var(--text); font: 500 var(--fs-dense) var(--f-ui); text-align: right;
  font-variant-numeric: tabular-nums; }
.sb-num:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 16%, transparent); }
.sb-einheit { font-size: var(--fs-micro); color: var(--text-muted); }
.sb-zeile { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.sb-feld--eng .sb-k { flex-basis: 58px; }
.sb-check { display: flex; align-items: center; gap: 5px; font-size: var(--fs-dense); cursor: pointer; }

/* Die Rechnung. Tabellarische Ziffern, damit die Beträge auf der Kante stehen — sie werden untereinander
   gelesen und sollen sich sichtbar zur Summe addieren. */
.sb-rechnung { margin: var(--sp-1) 0 0; display: grid; gap: 2px; }
.sb-r { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); font-size: var(--fs-dense); }
.sb-r dt { display: flex; align-items: baseline; gap: 5px; min-width: 0; }
.sb-r dd { margin: 0; font-variant-numeric: tabular-nums; white-space: nowrap; }
.sb-hint { font-size: var(--fs-micro); color: var(--text-faint); }
/* Null-Positionen bleiben STEHEN und werden gedämpft: „hier fällt nichts an" ist eine Auskunft, eine
   verschwundene Zeile wäre keine — der Leser wüsste nicht, ob es die Position überhaupt gibt. */
.sb-r.is-null { color: var(--text-faint); }
.sb-r--summe { margin-top: var(--sp-1); padding-top: var(--sp-1); border-top: 1px solid var(--border);
  font-size: var(--fs-ui); font-weight: 600; }
.sb-r--gesamt { font-size: var(--fs-ui); font-weight: 600; }
.sb-liefer { margin: var(--sp-1) 0 0; font-size: var(--fs-dense); color: var(--text-muted); }
.sb-fuss { margin: 0; font-size: var(--fs-micro); color: var(--text-faint); }
/* Während gerechnet wird, bleiben die alten Zahlen stehen und werden nur blass — ein Aufblitzen von
   „—" bei jedem Schieber-Pixel wäre unruhiger als der kurze Verzug. */
.shop-sandbox.is-rechnet .sb-rechnung { opacity: .5; }

/* Im Seitenpanel ist kein Platz für zwei Spalten. */
.shop-sandbox--kompakt .sb-body { grid-template-columns: 1fr; gap: var(--sp-2); padding: var(--sp-2); }
.shop-sandbox--kompakt .sb-bild { aspect-ratio: 16 / 7; }
.shop-sandbox--kompakt .sb-titel { display: none; }   /* der Panel-Kopf trägt den Namen schon */

@media (max-width: 780px) {
  .sb-body { grid-template-columns: 1fr; }
}

/* ══ MARGEN-SIMULATOR (Phase 5.1, 16.08.) ═══════════════════════════════════════════════════════════
   Steht über der Preistabelle und unter dem Zielmarge-Feld.
   ⛔ UMBAU 16.08.: Er war ein hoher Block, der rechts eine Handbreit leer ließ — auf der Seite, die man
   wegen der TABELLE aufmacht. Jetzt eine LEISTE: Schieber links, Kennzahlen rechts daneben, Erklärung
   hinter einer Klappe. Ein Werkzeug über der Arbeit, kein Kapitel vor ihr. */
.pr-sim { margin-bottom: var(--sp-3); }
.pr-sim-leiste { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3); }
.pr-sim-schieber { display: flex; align-items: center; gap: var(--sp-2); flex: 0 1 320px; min-width: 220px; }
.pr-sim-k { flex: 0 0 auto; font-size: var(--fs-micro); color: var(--text-muted); }
.pr-sim-schieber input[type="range"] { flex: 1 1 auto; min-width: 90px; accent-color: var(--accent); }
/* Feste Spur, sonst zappelt die ganze Leiste beim Schieben (3 Stellen + Zeichen). */
.pr-sim-schieber output { flex: 0 0 62px; text-align: right; font-size: var(--fs-sect); font-weight: 600;
  font-variant-numeric: tabular-nums; }

/* Die Kennzahlen als Reihe. `auto-fit` statt fester Spalten: bei 950 px bricht sie um, statt zu quetschen. */
.pr-sim-zahlen { flex: 1 1 420px; display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-1) var(--sp-4); }
.pr-sim-z { display: flex; flex-direction: column; gap: 1px; font-size: var(--fs-micro); color: var(--text-muted); }
.pr-sim-z b { font-size: var(--fs-ui); font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
/* Nur die Trefferzahl ist ein Link — sie ist die einzige, die irgendwo hinführt. Deshalb trägt auch nur
   sie die Akzentfarbe: eine Farbe, die überall steht, sagt nichts mehr. */
a.pr-sim-z--tat { text-decoration: none; }
a.pr-sim-z--tat b { color: var(--accent); }
a.pr-sim-z--tat:hover b { text-decoration: underline; }

/* ⛔ KEINE Ampelfarbe auf diesen Zahlen (Entscheidung D: Grün/Gelb/Rot gehört der Marge-Ampel). „194
   Artikel unter Ziel" ist weder gut noch schlecht — es ist die Folge einer Einstellung, die der Nutzer
   gerade selbst wählt. Eine Färbung würde seine eigene Eingabe bewerten. */
.pr-sim-warn { margin: 0 var(--sp-3) var(--sp-2); }
.pr-sim-mehr { padding: 0 var(--sp-3) var(--sp-2); }
.pr-sim-mehr > summary { font-size: var(--fs-micro); color: var(--text-muted); cursor: pointer; }
.pr-sim-mehr p { margin: var(--sp-2) 0 0; font-size: var(--fs-micro); line-height: 1.5; max-width: var(--measure); }
.pr-sim.is-rechnet .pr-sim-zahlen { opacity: .5; }

@media (max-width: 780px) { .pr-sim-schieber { flex-basis: 100%; } }

/* ══ TEXTVORLAGEN-ÜBERSICHT (16.08.) ════════════════════════════════════════════════════════════════
   Der Einstieg auf /sortiment/texte ohne gewählte Warengruppe. Vorher stand dort eine leere Seite. */
.tx-uebersicht { margin-bottom: var(--sp-4); }
.tx-ub-kopf h2 { margin: 0 0 var(--sp-1); font-size: var(--fs-sect); }
.tx-ub-kopf p { margin: 0 0 var(--sp-3); font-size: var(--fs-dense); max-width: var(--measure); }
.tx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(264px, 1fr)); gap: var(--sp-3); }
.tx-kachel { position: relative; display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-3);
  background: var(--canvas); border: 1px solid var(--border); border-radius: var(--r-2);
  color: var(--text); text-decoration: none; }
.tx-kachel:hover { border-color: var(--border-strong); box-shadow: 0 1px 3px rgb(0 0 0 / .07); text-decoration: none; }
/* Der Link spannt sich ueber die ganze Kachel — sie war frueher selbst ein <a>, und das soll sie sich
   anfuehlen. Seit der Bake-Knopf darin sitzt, geht das nicht mehr als Verschachtelung (interaktiv in
   interaktiv), also uebernimmt ein gestreckter ::after die Flaeche. */
.tx-k-flaeche::after { content: ''; position: absolute; inset: 0; border-radius: inherit; }
.tx-k-flaeche:focus-visible::after { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Alles, was der Nutzer SELBST anfassen koennen muss, liegt ueber der gestreckten Flaeche.
   ⛔ `margin-top: auto` statt eines festen Abstands: das Raster zieht jede Kachel einer Reihe auf die
   Hoehe der hoechsten, und ohne das stand der Knopf mitten in der Kachel waehrend die Nachbarkachel
   ohne Knopf 60 px Leerraum unten trug — das las sich als Loch, nicht als Fusszone. So sitzen alle
   Knoepfe einer Reihe auf einer Linie und der Leerraum ist unten. */
.tx-k-bake { position: relative; z-index: 1; align-self: flex-start; margin-top: auto; }
/* Der Zustand steht am Rand, nicht als Fläche: die Kachel bleibt lesbar, und drei volle Farbflächen
   nebeneinander wären ein Ampel-Raster über etwas, das keine Ampel ist (Entscheidung D). */
.tx-kachel--ohne { border-left: 3px solid var(--warn); }
.tx-kachel--fertig { border-left: 3px solid var(--good); }
.tx-k-kopf { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-2); }
.tx-k-kopf strong { font-size: var(--fs-ui); overflow-wrap: anywhere; }

.tx-k-balken { height: 4px; border-radius: var(--r-full); background: var(--sunken); overflow: hidden; }
.tx-k-balken > span { display: block; height: 100%; background: var(--accent); }
.tx-k-zahlen { display: flex; flex-direction: column; gap: 1px; font-size: var(--fs-micro); color: var(--text-muted); }
.tx-k-zahlen b { color: var(--text); font-variant-numeric: tabular-nums; }
.tx-k-fuss { font-size: var(--fs-micro); color: var(--text-faint); line-height: 1.45; }
.tx-k-warn { color: var(--warn); }
