/* =============================================================
   DRYL Design System — Core Tokens & Primitives
   Glassmorphism · Dark · Indirect Light · Motion
   ============================================================= */

/* Registered so theme seeds interpolate — the derived color-mix chain glides
   on a theme change. Die Ausgangswerte tragen den Akzent dieser Fassung
   (Teal statt Violett/Cyan); sie greifen, wenn die Kaskade den Token nicht
   setzt, und dürfen deshalb nicht auf der alten Palette stehenbleiben. */
@property --accent-a { syntax: "<color>"; inherits: true; initial-value: #2bbfac; }
@property --accent-b { syntax: "<color>"; inherits: true; initial-value: #63dcc9; }
@property --ai-a     { syntax: "<color>"; inherits: true; initial-value: #2bbfac; }
@property --ai-b     { syntax: "<color>"; inherits: true; initial-value: #63dcc9; }

/* Chart series tokens are registered too: explicit theme overrides
   (DrylTheme.Charts) glide like the seeds, and the initial-value doubles as
   the fallback palette in engines without relative color syntax. */
@property --chart-1 { syntax: "<color>"; inherits: true; initial-value: #2bbfac; }
@property --chart-2 { syntax: "<color>"; inherits: true; initial-value: #63dcc9; }
@property --chart-3 { syntax: "<color>"; inherits: true; initial-value: #bd7a12; }
@property --chart-4 { syntax: "<color>"; inherits: true; initial-value: #26a058; }
@property --chart-5 { syntax: "<color>"; inherits: true; initial-value: #d6428e; }
@property --chart-6 { syntax: "<color>"; inherits: true; initial-value: #5583e3; }

/* Neutral tokens are registered so a color-mode switch glides exactly like
   an accent-theme switch. Initial values mirror the built-in dark set. */
@property --ground   { syntax: "<color>"; inherits: true; initial-value: #000000; }
@property --bg-0     { syntax: "<color>"; inherits: true; initial-value: #000000; }
@property --bg-1     { syntax: "<color>"; inherits: true; initial-value: #07070a; }
@property --bg-2     { syntax: "<color>"; inherits: true; initial-value: #0c0c12; }
@property --bg-3     { syntax: "<color>"; inherits: true; initial-value: #14141c; }
@property --line        { syntax: "<color>"; inherits: true; initial-value: rgba(255,255,255,0.06); }
@property --line-strong { syntax: "<color>"; inherits: true; initial-value: rgba(255,255,255,0.12); }
@property --line-soft   { syntax: "<color>"; inherits: true; initial-value: rgba(255,255,255,0.04); }
@property --glass-1  { syntax: "<color>"; inherits: true; initial-value: rgba(255,255,255,0.03); }
@property --glass-2  { syntax: "<color>"; inherits: true; initial-value: rgba(255,255,255,0.05); }
@property --glass-3  { syntax: "<color>"; inherits: true; initial-value: rgba(255,255,255,0.08); }
@property --fg       { syntax: "<color>"; inherits: true; initial-value: #f4f4f7; }
@property --fg-muted { syntax: "<color>"; inherits: true; initial-value: rgba(244,244,247,0.62); }
@property --fg-dim   { syntax: "<color>"; inherits: true; initial-value: rgba(244,244,247,0.38); }
@property --fg-faint { syntax: "<color>"; inherits: true; initial-value: rgba(244,244,247,0.22); }

:root {
  /* Tell the browser this surface is dark — applies to native UA controls (<select> dropdowns, scrollbars, form autofill) so they don't ship light-mode defaults onto our glass. */
  color-scheme: dark;

  /* Palette — warme Sepia-Schwarz-Leiter (Mac-Variante).
     Alle Neutraltöne liegen auf Farbton 40, die Sättigung nimmt mit
     steigender Helligkeit ab (24 % am Grund → 18 % auf der obersten
     Ebene), damit helle Ebenen nicht sandfarben kippen. Kein reines
     Schwarz: der Grund ist sichtbar bräunlich, aber klar dunkler als
     ein mittleres Sepia. */
  --ground:        #090806;   /* hsl(40, 24%, 3%)   */
  --bg-0:          #090806;   /* hsl(40, 24%, 3%)   */
  --bg-1:          #100e0a;   /* hsl(40, 22%, 5%)   */
  --bg-2:          #17140f;   /* hsl(40, 20%, 7.5%) */
  --bg-3:          #201c16;   /* hsl(40, 18%, 10.5%)*/

  /* Ränder und Glasflächen: warm getönte Auflagen statt neutraler
     Weiß-Overlays. rgb(255,238,204) ist ein warmes Off-White auf
     Farbton 40; damit bleibt der Braunton über alle Ebenen erhalten,
     statt mit jeder Schicht ausgewaschen zu werden. */
  --line:          rgba(255, 238, 204, 0.07);
  --line-strong:   rgba(255, 238, 204, 0.14);
  --line-soft:     rgba(255, 238, 204, 0.045);

  /* Glass surfaces */
  --glass-1:       rgba(255, 238, 204, 0.035);
  --glass-2:       rgba(255, 238, 204, 0.055);
  --glass-3:       rgba(255, 238, 204, 0.085);
  --glass-blur:    18px;
  /* NEU (additiv): die drei Unschärfe-Stärken, die vorher als Rohwerte
     in den Regeln standen. Werte unverändert, nur benannt, damit der
     Deckend-Schalter am Ende der Datei sie alle auf einmal erreicht. */
  --glass-blur-soft:   12px;   /* .btn-secondary, .alert */
  --glass-blur-strong: 20px;   /* .topbar, .sidebar */
  --scrim-blur:         8px;   /* Modal-Schleier, .dg-surface */
  /* NEU (additiv): deckende Gegenstücke zu --glass-1..3, gerechnet als
     Komposit der Glasauflage über --bg-2. Sie treten in Kraft, wenn die
     Unschärfe wegfällt: über die Klasse .glass-solid, über die
     Systemeinstellung „Transparenz reduzieren" und als Fallback ohne
     backdrop-filter. Ohne Unschärfe muss die Fläche decken, sonst
     scheint der Seitenhintergrund durch die Karte. */
  --glass-solid-1: #1f1c16;
  --glass-solid-2: #242019;
  --glass-solid-3: #2b271f;

  /* Text: warmes Off-White statt reinem Weiß. Kontrast auf --bg-1
     nachgerechnet: --fg 17.53:1, --fg-muted 7.91:1, --fg-dim 4.95:1
     (im Original lag --fg-dim bei 3.31:1, also unter der Schwelle). */
  --fg:            #f6f4ee;
  --fg-muted:      rgba(246, 244, 238, 0.66);
  --fg-dim:        rgba(246, 244, 238, 0.50);
  --fg-faint:      rgba(246, 244, 238, 0.28);

  /* Akzent — Teal statt Violett/Cyan. Der iOS-Systemblauton liegt
     farbkreislich fast gegenüber von Farbton 40 und vibriert auf
     warmem Untergrund; Teal hält Abstand zum Braun, ohne zu flirren,
     und lässt Amber für den Warnzustand frei. Kontrast auf --bg-1:
     8.3:1, Text auf Akzentfüllung (--on-accent) 7.2:1. */
  --accent-a:      #2bbfac;
  --accent-b:      #63dcc9;
  --accent:        var(--accent-a);
  --accent-soft:   color-mix(in srgb, var(--accent-a) 18%, transparent);
  --accent-line:   color-mix(in srgb, var(--accent-a) 45%, transparent);
  --accent-grad:   linear-gradient(135deg, var(--accent-a) 0%, var(--accent-b) 100%);
  --accent-grad-r: linear-gradient(135deg, var(--accent-b) 0%, var(--accent-a) 100%);

  /* AI accent — defaults to the brand accent (opt-in divergence via DrylTheme.AiAccent) */
  --ai-a:          var(--accent-a);
  --ai-b:          var(--accent-b);

  /* Semantic */
  --success:       #34d399;
  --warning:       #fbbf24;
  --danger:        #f87171;
  --info:          var(--accent-b);

  /* Charts — categorical series palette (dark-validated: lightness band,
     chroma floor, CVD adjacent ΔE ≥ 12, contrast ≥ 3:1 on black).
     Fixed order, assigned in sequence, never cycled; series 7+ → --fg-dim.
     Series 1/2 follow the theme seeds: hue from the accent, lightness snapped
     to the validated band (L 0.65) and chroma clamped, so any DrylTheme stays
     chart-legible without per-theme tuning. Slots 3–6 are fixed anchors;
     themes whose hues collide with them override via DrylTheme.Charts. */
  --chart-1:       oklch(from var(--accent-a) 0.65 clamp(0.1, c, 0.19) h);
  --chart-2:       oklch(from var(--accent-b) 0.65 clamp(0.1, c, 0.19) h);
  --chart-3:       #bd7a12;
  --chart-4:       #26a058;
  --chart-5:       #d6428e;
  --chart-6:       #5583e3;

  /* Type — Systemschrift zuerst. Auf macOS/iOS greift damit echtes
     SF Pro, sonst fällt es auf das mitgelieferte Inter zurück. Die
     Schriftdateien bleiben im Verzeichnis, nur die Reihenfolge dreht. */
  --font-sans:     -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display',
                   'Pretendard Std', 'Inter', 'Inter Placeholder', 'Segoe UI',
                   system-ui, sans-serif;
  --font-mono:     ui-monospace, 'SF Mono', SFMono-Regular, Menlo,
                   'JetBrains Mono', monospace;

  /* Typo-Skala nach den Apple-Text-Styles, in rem statt px, damit
     Dynamic Type und die Browser-Schriftgröße greifen. Referenz:
     1 pt = 1 CSS-px bei 1x, Body liegt bei 17. NEU in dieser Version,
     im Original gab es keine Typo-Token. */
  --fs-caption2:  0.6875rem;  /* 11 */
  --fs-caption1:  0.75rem;    /* 12 */
  --fs-footnote:  0.8125rem;  /* 13 */
  --fs-subhead:   0.9375rem;  /* 15 */
  --fs-callout:   1rem;       /* 16 */
  --fs-body:      1.0625rem;  /* 17 */
  --fs-headline:  1.0625rem;  /* 17, halbfett */
  --fs-title3:    1.25rem;    /* 20 */
  --fs-title2:    1.375rem;   /* 22 */
  --fs-title1:    1.75rem;    /* 28 */
  --fs-large:     2.125rem;   /* 34 */

  /* Scale — durchgehend weichere Rundungen; 6 px wirkte im
     Apple-Kontext technisch, die Leiter beginnt jetzt bei 8. */
  /* NEU (additiv): eine Stufe unterhalb der Flächen-Leiter, für
     Mikro-Bedienelemente und Inline-Auszeichnungen (Kästchen, kbd,
     Inline-Code, Skelett). Apple rundet kleine Steuerelemente im
     Verhältnis zur Grösse, etwa ein Viertel bis ein Drittel der
     Kantenlänge; --r-xs auf einem 18px-Kästchen ergäbe fast einen
     Kreis und damit die Optik eines Radioknopfs. */
  --r-2xs: 6px;
  --r-xs: 8px;
  --r-sm: 12px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-xl: 30px;
  --r-pill: 999px;

  /* Spacing */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 64px;

  /* Shadows / indirect light — deutlich zurückgenommen. Tiefe entsteht
     in dieser Version über geschichtete Materialien und Helligkeits-
     unterschiede, der Schlagschatten setzt nur noch die Kante ab. */
  --shadow-sm:  0 1px 2px rgba(0,0,0,0.35);
  --shadow-md:  0 6px 18px rgba(0,0,0,0.32), 0 1px 3px rgba(0,0,0,0.24);
  --shadow-lg:  0 18px 44px rgba(0,0,0,0.42), 0 4px 10px rgba(0,0,0,0.24);
  --glow-accent: 0 0 0 1px var(--accent-line),
                 0 8px 32px color-mix(in srgb, var(--accent-a) 35%, transparent),
                 0 0 64px color-mix(in srgb, var(--accent-b) 18%, transparent);
  --glow-soft:   0 0 60px color-mix(in srgb, var(--accent-a) 18%, transparent),
                 0 0 120px color-mix(in srgb, var(--accent-b) 8%, transparent);

  /* Background texture opacity (tokenized so light mode can dial it down) */
  --grain-opacity: 0.4;
  /* Der Aurora-Schleier lag bei 0.85 und hat den warmen Grund
     vollständig überfärbt. Apple-Leitprinzip Deference: die Fläche
     tritt hinter den Inhalt zurück, Dekoration ist nachrangig. Der
     Schleier bleibt als Tiefenandeutung erhalten, dominiert aber
     nicht mehr die Farbwirkung. */
  --aurora-opacity: 0.12;
  /* NEU (additiv): eigener Farbton für den Aurora-Schleier. Vorher lief
     er in der Akzentfarbe; mit "mix-blend-mode: screen" hebt er den
     fast schwarzen Grund so stark an, dass die ganze Fläche die
     Akzent-Färbung annahm (gemessen: Farbton 144 statt 40). Ein eigener
     warmer Ton hält den Sepia-Charakter und lässt die Tiefenwirkung. */
  --aurora-tint:   #c98f3a;

  /* Motion */
  /* Kurven und Dauern im Apple-Fenster: Übergänge zwischen 200 und
     350 ms, federnd statt linear. --ease-out folgt der flachen
     Ausschwing-Kurve, die Apple für Ein-/Ausblendungen nutzt. */
  --ease-out:    cubic-bezier(0.32, 0.72, 0, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --ease-viscous: cubic-bezier(0.45, 0.05, 0.15, 1); /* view-transition pseudo-elements only */
  --dur-fast:    200ms;
  --dur-med:     280ms;
  --dur-slow:    350ms;
  /* NEU (additiv): der Druck selbst. Apple lässt ein Bedienelement beim
     Antippen praktisch verzögerungsfrei einsinken und erst beim Loslassen
     gemächlich zurückfedern. --dur-fast (200 ms) wäre für das Einsinken
     spürbar zu träge, das Zurückfedern übernimmt die reguläre Transition
     des Elements. */
  --dur-press:    90ms;
  --press-scale:  0.97;
  --reveal-step: 60ms;   /* stagger step between successive DrylReveal children */

  /* Layering */
  --z-sticky:    5;
  --z-drawer:   50;
  --z-modal:   100;
  --z-popover: 150;
  --z-toast:   200;
  --z-reconnect: 1000; /* DrylReconnectModal — above every other layer; the circuit is down, this must win */

  /* App chrome dimensions — consumed by .topbar / .sidebar / .app-shell.
     Overridable per layout via inline custom properties (DrylLayout.SidebarWidth). */
  --appbar-h:            60px;
  --sidebar-w:          260px;
  --sidebar-collapsed-w: 56px;

  /* NEU (additiv): Mindest-Trefferfläche. 24px ist die Untergrenze, die
     auch für die Maus gilt (WCAG 2.5.8); bei grobem Zeiger, also im
     WKWebView auf iPhone und iPad, gilt Apples 44pt. Die Vergrösserung
     liegt in einem Pseudo-Element, die sichtbare Grafik bleibt klein. */
  --hit: 24px;

  /* Mode-dependent effect tokens — white highlights, sheens, shimmer sweeps
     and hover washes used across glass surfaces. Light-mode overrides live in
     both LIGHT-TOKEN-SET copies below; --knob stays fixed because it sits on
     the accent-gradient fill. --on-accent* used to stay fixed for the same
     reason, but the light copies re-seed the accent to its dark step, so the
     fill DOES change with the mode and they flip with it. */
  /* Sämtliche Auflagen auf demselben warmen Off-White wie --glass-*.
     Neutrale Weiß-Overlays würden den Braunton mit jeder Ebene
     herauswaschen, genau das soll hier nicht passieren. */
  --edge-hi:         rgba(255, 238, 204, 0.07);
  --edge-hi-strong:  rgba(255, 238, 204, 0.20);
  --ai-core:         #fdfaf2;  /* hottest point of the AI aura (comet specular head) — warmes Off-White statt des kühlen #eef3ff */
  --ai-strength:     1;        /* aura presence multiplier — light copies raise it because translucent accents wash out on a bright ground */
  --sheen-grad:      linear-gradient(180deg, rgba(255,238,204,0.045), rgba(255,238,204,0.017));
  --sheen-grad-soft: linear-gradient(180deg, rgba(255,238,204,0.022), rgba(255,238,204,0));
  --shimmer:         rgba(255, 238, 204, 0.22);
  --shimmer-strong:  rgba(255, 238, 204, 0.4);
  --hover-wash:      rgba(255, 238, 204, 0.03);
  --line-hover:      rgba(255, 238, 204, 0.20);
  /* Text auf Akzentfüllung: dunkel statt weiß. Das helle Teal trägt
     kein Weiß (1.9:1), mit dem dunklen Ton sind es 7.2:1. */
  /* Tinte auf Akzentfüllungen, automatisch aus der Akzenthelligkeit
     abgeleitet. Der Ausdruck kippt bei OKLCH-Helligkeit 0.65: liegt der
     Akzent darunter, wird die Tinte hell (0.97), liegt er darüber, dunkel
     (0.12). Chroma 0.02 auf Farbton 70 hält sie warm.
     Warum nicht fest: Die Theme-Presets und der Farbwähler des
     Design-Menüs setzen --accent-a inline auf <html>, und Inline schlägt
     jeden Selektor. Eine feste Tinte stand dann auf einer fremden
     Füllung, bei hellem Amber oder Grau mit unlesbarem Kontrast. */
  --on-accent:       oklch(from var(--accent-a) clamp(0.12, calc((0.65 - l) * 1000), 0.995) 0.015 75);
  --on-accent-line:  rgba(6, 35, 31, 0.16);
  --on-accent-hi:    rgba(255, 255, 255, 0.22);
  --knob:            #fff8ec;
  --backdrop:             rgba(6, 5, 4, 0.6);
  --backdrop-soft:        rgba(6, 5, 4, 0.4);
  --bar-bg:               rgba(9, 8, 6, 0.4);       /* DrylAppBar (flat) surface — themeable independently of the modal scrim */
  --press-wash:           rgba(6, 5, 4, 0.25);
  --scrollbar-thumb:       rgba(255, 238, 204, 0.09);
  --scrollbar-thumb-hover: rgba(255, 238, 204, 0.17);
  --code-bg:         rgba(6, 5, 4, 0.55);
  --code-fg:         #f6f4ee;

  /* Semantic-derived text/edge tokens (Task 7 literal sweep).
     --accent-fg / --danger-fg are the accent/danger-tinted TEXT colors used on
     glass (badges, code, AI indicator, danger menu items) — lighter than the
     solid --accent/--danger fills so they read on translucent surfaces.
     --*-hi are the bright gradient-endpoint tints used on toast progress bars.
     Their dark values are exact literals (the four originals don't sit on the
     seed-to-white mix line, so a color-mix here would shift dark rendering);
     the LIGHT-TOKEN-SET copies override them with seed color-mixes so light
     mode adapts to the semantic seeds. Do NOT remove those light overrides. */
  --accent-fg:  #8fe8d9;
  --danger-fg:  #fca5a5;
  --success-hi: #6ee7b7;
  --warning-hi: #fcd34d;
  --danger-hi:  #fca5a5;
  --info-hi:    #67e8f9;

  /* DepthGlass edge/shadow tokens — the white inset highlights and black drop
     shadow used by .dg-* (DrylDepthGlass / DrylCard Depth). --depth-edge is
     the ~0.18 reference edge; --depth-edge-strong is the brightest inset
     (task-added, same pattern as the pair above — see task-7 report). */
  --depth-edge:        rgba(255, 238, 204, 0.18);
  --depth-edge-strong: rgba(255, 238, 204, 0.55);
  --depth-shadow:      rgba(6, 5, 4, 0.42);
  --accent-ico:        #9fecdf;

  /* Semi-opaque panel surfaces — the near-solid dark planes cards, toasts,
     dialogs, tooltips and sticky headers sit on. Mode-dependent: the light
     set swaps them for near-solid light planes. */
  --panel-tint:        rgba(14, 12, 9, 0.4);
  --panel-soft:        rgba(14, 12, 9, 0.78);
  --panel-solid:       rgba(28, 24, 18, 0.95);
  --panel-sticky:      rgba(10, 9, 7, 0.92);
  --panel-grad:        linear-gradient(180deg, rgba(28, 24, 18, 0.95), rgba(14, 12, 9, 0.95));
  --panel-grad-strong: linear-gradient(180deg, rgba(28, 24, 18, 0.97), rgba(14, 12, 9, 0.97));
}

/* Theme seeds glide on change; instant for reduced-motion users. */
@media (prefers-reduced-motion: no-preference) {
  :root {
    transition: --accent-a var(--dur-slow) var(--ease-in-out),
                --accent-b var(--dur-slow) var(--ease-in-out),
                --ai-a var(--dur-slow) var(--ease-in-out),
                --ai-b var(--dur-slow) var(--ease-in-out),
                --chart-1 var(--dur-slow) var(--ease-in-out),
                --chart-2 var(--dur-slow) var(--ease-in-out),
                --chart-3 var(--dur-slow) var(--ease-in-out),
                --chart-4 var(--dur-slow) var(--ease-in-out),
                --chart-5 var(--dur-slow) var(--ease-in-out),
                --chart-6 var(--dur-slow) var(--ease-in-out),
                --ground var(--dur-slow) var(--ease-in-out),
                --bg-0 var(--dur-slow) var(--ease-in-out),
                --bg-1 var(--dur-slow) var(--ease-in-out),
                --bg-2 var(--dur-slow) var(--ease-in-out),
                --bg-3 var(--dur-slow) var(--ease-in-out),
                --line var(--dur-slow) var(--ease-in-out),
                --line-strong var(--dur-slow) var(--ease-in-out),
                --line-soft var(--dur-slow) var(--ease-in-out),
                --glass-1 var(--dur-slow) var(--ease-in-out),
                --glass-2 var(--dur-slow) var(--ease-in-out),
                --glass-3 var(--dur-slow) var(--ease-in-out),
                --fg var(--dur-slow) var(--ease-in-out),
                --fg-muted var(--dur-slow) var(--ease-in-out),
                --fg-dim var(--dur-slow) var(--ease-in-out),
                --fg-faint var(--dur-slow) var(--ease-in-out);
  }
}

/* =============================================================
   LIGHT MODE — token overrides ("Aurora Light").
   ⚠ THE BLOCK BELOW EXISTS TWICE (system media query + explicit
   attribute). Keep both copies IDENTICAL — checked by
   scripts/check-light-sync.mjs.
   ============================================================= */
@media (prefers-color-scheme: light) {
  :root:not([data-dryl-mode="dark"]) {
    /* LIGHT-TOKEN-SET — copy 1/2 */
    color-scheme: light;
    /* Warme Sepia-Papier-Leiter, das Gegenstück zur dunklen Leiter oben:
       alle Neutraltöne liegen auf Farbton 40, der Farbabstand nimmt nach
       oben ab (13 Punkte am Grund, 6 auf der obersten Ebene), damit die
       hellen Ebenen nicht sandfarben kippen. Tiefe entsteht über die
       wärmer werdenden Flächen, nicht über neutrale Auflagen. */
    --ground:        #f7f3ea;
    --bg-0:          #f7f3ea;
    --bg-1:          #faf7f0;
    --bg-2:          #fcfaf5;
    --bg-3:          #fffdf9;
    /* Ränder, Wischer und Schatten: warmes Dunkelbraun (Farbton 38)
       statt des kühlen Blauschwarz. */
    --line:          rgba(60, 48, 28, 0.10);
    --line-strong:   rgba(60, 48, 28, 0.18);
    --line-soft:     rgba(60, 48, 28, 0.06);
    /* Glasauflagen auf warmem Off-White: neutrales Weiß würde den
       Papierton mit jeder Ebene herauswaschen. */
    --glass-1:       rgba(255, 252, 245, 0.55);
    --glass-2:       rgba(255, 252, 245, 0.64);
    --glass-3:       rgba(255, 252, 245, 0.74);
    --glass-solid-1: #fbf8f0;
    --glass-solid-2: #fcf9f1;
    --glass-solid-3: #fdfaf2;
    /* Text: warmes Tiefbraun. Kontrast auf --bg-1 gerechnet (WCAG,
       Alpha vorher gegen --bg-1 komponiert): --fg 15.8:1,
       --fg-muted 6.9:1, --fg-dim 4.7:1. --fg-faint ist dekorativ. */
    --fg:            #211c14;
    --fg-muted:      rgba(33, 28, 20, 0.74);
    --fg-dim:        rgba(33, 28, 20, 0.62);
    --fg-faint:      rgba(33, 28, 20, 0.40);
    /* Akzent: dasselbe Teal, aber die dunkle Stufe. Das helle Teal des
       Dunkelmodus trägt auf Papierton nur 2.1:1 und verschwindet. Der
       Verlauf bleibt bewusst flach in der Helligkeit, weil --on-accent
       an BEIDEN Enden der Füllung lesbar sein muss: 4.7:1 auf
       --accent-a, 4.6:1 auf --accent-b. */
    --accent-a:      #0c7f71;
    --accent-b:      #0a8276;
    --success:       #0e8a4d;
    --warning:       #b45309;
    --danger:        #dc2626;
    --info:          #0e7490;
    --chart-1:       oklch(from var(--accent-a) 0.52 clamp(0.1, c, 0.17) h);
    --chart-2:       oklch(from var(--accent-b) 0.52 clamp(0.1, c, 0.17) h);
    --chart-3:       #96610e;
    --chart-4:       #1d7f46;
    --chart-5:       #b0316f;
    --chart-6:       #3a63c4;
    --shadow-sm:  0 1px 2px rgba(60, 48, 28, 0.08);
    --shadow-md:  0 8px 24px rgba(60, 48, 28, 0.10), 0 2px 6px rgba(60, 48, 28, 0.06);
    --shadow-lg:  0 24px 64px rgba(60, 48, 28, 0.15), 0 8px 16px rgba(60, 48, 28, 0.08);
    --glow-accent: 0 0 0 1px var(--accent-line),
                   0 8px 32px color-mix(in srgb, var(--accent-a) 22%, transparent),
                   0 0 64px color-mix(in srgb, var(--accent-b) 12%, transparent);
    --glow-soft:   0 0 60px color-mix(in srgb, var(--accent-a) 12%, transparent),
                   0 0 120px color-mix(in srgb, var(--accent-b) 6%, transparent);
    --grain-opacity: 0.25;
    --aurora-opacity: 0.5;
    --edge-hi:         rgba(255, 252, 245, 0.85);
    --edge-hi-strong:  rgba(255, 252, 245, 0.95);
    --ai-core:         color-mix(in srgb, var(--ai-a) 82%, #1a1610 18%);
    --ai-strength:     1.7;
    --sheen-grad:      linear-gradient(180deg, rgba(255,252,245,0.55), rgba(255,252,245,0.15));
    --sheen-grad-soft: linear-gradient(180deg, rgba(255,252,245,0.35), rgba(255,252,245,0));
    --shimmer:         rgba(255, 252, 245, 0.8);
    --shimmer-strong:  rgba(255, 252, 245, 0.95);
    --hover-wash:      rgba(60, 48, 28, 0.03);
    --line-hover:      rgba(60, 48, 28, 0.24);
    /* Text auf Akzentfüllung dreht im Hellmodus um: die Füllung ist hier
       das dunkle Teal, also trägt sie warmes Off-White statt der dunklen
       Tinte aus dem Dunkelmodus. */
    --on-accent:       oklch(from var(--accent-a) clamp(0.12, calc((0.65 - l) * 1000), 0.995) 0.015 75);
    --on-accent-line:  rgba(255, 251, 244, 0.20);
    --on-accent-hi:    rgba(255, 251, 244, 0.22);
    --backdrop:             rgba(46, 38, 24, 0.35);
    --backdrop-soft:        rgba(46, 38, 24, 0.22);
    --bar-bg:               rgba(252, 250, 245, 0.72);
    --press-wash:           rgba(60, 48, 28, 0.08);
    --scrollbar-thumb:       rgba(60, 48, 28, 0.18);
    --scrollbar-thumb-hover: rgba(60, 48, 28, 0.3);
    --code-bg:         #1a1610;
    --accent-fg:  color-mix(in srgb, var(--accent-a) 72%, #241d13);
    --danger-fg:  #b91c1c;
    --depth-edge:        rgba(255, 252, 245, 0.9);
    --depth-edge-strong: rgba(255, 252, 245, 0.95);
    --depth-shadow:      rgba(60, 48, 28, 0.16);
    --success-hi: color-mix(in srgb, var(--success) 69%, white);
    --warning-hi: color-mix(in srgb, var(--warning) 78%, white);
    --danger-hi:  color-mix(in srgb, var(--danger) 63%, white);
    --info-hi:    color-mix(in srgb, var(--info) 66%, white);
    --accent-ico: color-mix(in srgb, var(--accent-a) 78%, #241d13);
    --panel-tint:        rgba(255, 252, 245, 0.4);
    --panel-soft:        rgba(255, 252, 245, 0.78);
    --panel-solid:       rgba(252, 250, 245, 0.95);
    --panel-sticky:      rgba(250, 247, 240, 0.92);
    --panel-grad:        linear-gradient(180deg, rgba(252, 250, 245, 0.95), rgba(247, 243, 234, 0.95));
    --panel-grad-strong: linear-gradient(180deg, rgba(252, 250, 245, 0.97), rgba(247, 243, 234, 0.97));
  }
}
:root[data-dryl-mode="light"] {
  /* LIGHT-TOKEN-SET — copy 2/2 */
  color-scheme: light;
  /* Warme Sepia-Papier-Leiter, das Gegenstück zur dunklen Leiter oben:
     alle Neutraltöne liegen auf Farbton 40, der Farbabstand nimmt nach
     oben ab (13 Punkte am Grund, 6 auf der obersten Ebene), damit die
     hellen Ebenen nicht sandfarben kippen. Tiefe entsteht über die
     wärmer werdenden Flächen, nicht über neutrale Auflagen. */
  --ground:        #f7f3ea;
  --bg-0:          #f7f3ea;
  --bg-1:          #faf7f0;
  --bg-2:          #fcfaf5;
  --bg-3:          #fffdf9;
  /* Ränder, Wischer und Schatten: warmes Dunkelbraun (Farbton 38)
     statt des kühlen Blauschwarz. */
  --line:          rgba(60, 48, 28, 0.10);
  --line-strong:   rgba(60, 48, 28, 0.18);
  --line-soft:     rgba(60, 48, 28, 0.06);
  /* Glasauflagen auf warmem Off-White: neutrales Weiß würde den
     Papierton mit jeder Ebene herauswaschen. */
  --glass-1:       rgba(255, 252, 245, 0.55);
  --glass-2:       rgba(255, 252, 245, 0.64);
  --glass-3:       rgba(255, 252, 245, 0.74);
  --glass-solid-1: #fbf8f0;
  --glass-solid-2: #fcf9f1;
  --glass-solid-3: #fdfaf2;
  /* Text: warmes Tiefbraun. Kontrast auf --bg-1 gerechnet (WCAG,
     Alpha vorher gegen --bg-1 komponiert): --fg 15.8:1,
     --fg-muted 6.9:1, --fg-dim 4.7:1. --fg-faint ist dekorativ. */
  --fg:            #211c14;
  --fg-muted:      rgba(33, 28, 20, 0.74);
  --fg-dim:        rgba(33, 28, 20, 0.62);
  --fg-faint:      rgba(33, 28, 20, 0.40);
  /* Akzent: dasselbe Teal, aber die dunkle Stufe. Das helle Teal des
     Dunkelmodus trägt auf Papierton nur 2.1:1 und verschwindet. Der
     Verlauf bleibt bewusst flach in der Helligkeit, weil --on-accent
     an BEIDEN Enden der Füllung lesbar sein muss: 4.7:1 auf
     --accent-a, 4.6:1 auf --accent-b. */
  --accent-a:      #0c7f71;
  --accent-b:      #0a8276;
  --success:       #0e8a4d;
  --warning:       #b45309;
  --danger:        #dc2626;
  --info:          #0e7490;
  --chart-1:       oklch(from var(--accent-a) 0.52 clamp(0.1, c, 0.17) h);
  --chart-2:       oklch(from var(--accent-b) 0.52 clamp(0.1, c, 0.17) h);
  --chart-3:       #96610e;
  --chart-4:       #1d7f46;
  --chart-5:       #b0316f;
  --chart-6:       #3a63c4;
  --shadow-sm:  0 1px 2px rgba(60, 48, 28, 0.08);
  --shadow-md:  0 8px 24px rgba(60, 48, 28, 0.10), 0 2px 6px rgba(60, 48, 28, 0.06);
  --shadow-lg:  0 24px 64px rgba(60, 48, 28, 0.15), 0 8px 16px rgba(60, 48, 28, 0.08);
  --glow-accent: 0 0 0 1px var(--accent-line),
                 0 8px 32px color-mix(in srgb, var(--accent-a) 22%, transparent),
                 0 0 64px color-mix(in srgb, var(--accent-b) 12%, transparent);
  --glow-soft:   0 0 60px color-mix(in srgb, var(--accent-a) 12%, transparent),
                 0 0 120px color-mix(in srgb, var(--accent-b) 6%, transparent);
  --grain-opacity: 0.25;
  --aurora-opacity: 0.5;
  --edge-hi:         rgba(255, 252, 245, 0.85);
  --edge-hi-strong:  rgba(255, 252, 245, 0.95);
  --ai-core:         color-mix(in srgb, var(--ai-a) 82%, #1a1610 18%);
  --ai-strength:     1.7;
  --sheen-grad:      linear-gradient(180deg, rgba(255,252,245,0.55), rgba(255,252,245,0.15));
  --sheen-grad-soft: linear-gradient(180deg, rgba(255,252,245,0.35), rgba(255,252,245,0));
  --shimmer:         rgba(255, 252, 245, 0.8);
  --shimmer-strong:  rgba(255, 252, 245, 0.95);
  --hover-wash:      rgba(60, 48, 28, 0.03);
  --line-hover:      rgba(60, 48, 28, 0.24);
  /* Text auf Akzentfüllung dreht im Hellmodus um: die Füllung ist hier
     das dunkle Teal, also trägt sie warmes Off-White statt der dunklen
     Tinte aus dem Dunkelmodus. */
  --on-accent:       oklch(from var(--accent-a) clamp(0.12, calc((0.65 - l) * 1000), 0.995) 0.015 75);
  --on-accent-line:  rgba(255, 251, 244, 0.20);
  --on-accent-hi:    rgba(255, 251, 244, 0.22);
  --backdrop:             rgba(46, 38, 24, 0.35);
  --backdrop-soft:        rgba(46, 38, 24, 0.22);
  --bar-bg:               rgba(252, 250, 245, 0.72);
  --press-wash:           rgba(60, 48, 28, 0.08);
  --scrollbar-thumb:       rgba(60, 48, 28, 0.18);
  --scrollbar-thumb-hover: rgba(60, 48, 28, 0.3);
  --code-bg:         #1a1610;
  --accent-fg:  color-mix(in srgb, var(--accent-a) 72%, #241d13);
  --danger-fg:  #b91c1c;
  --depth-edge:        rgba(255, 252, 245, 0.9);
  --depth-edge-strong: rgba(255, 252, 245, 0.95);
  --depth-shadow:      rgba(60, 48, 28, 0.16);
  --success-hi: color-mix(in srgb, var(--success) 69%, white);
  --warning-hi: color-mix(in srgb, var(--warning) 78%, white);
  --danger-hi:  color-mix(in srgb, var(--danger) 63%, white);
  --info-hi:    color-mix(in srgb, var(--info) 66%, white);
  --accent-ico: color-mix(in srgb, var(--accent-a) 78%, #241d13);
  --panel-tint:        rgba(255, 252, 245, 0.4);
  --panel-soft:        rgba(255, 252, 245, 0.78);
  --panel-solid:       rgba(252, 250, 245, 0.95);
  --panel-sticky:      rgba(250, 247, 240, 0.92);
  --panel-grad:        linear-gradient(180deg, rgba(252, 250, 245, 0.95), rgba(247, 243, 234, 0.95));
  --panel-grad-strong: linear-gradient(180deg, rgba(252, 250, 245, 0.97), rgba(247, 243, 234, 0.97));
}

/* =============================================================
   FARBTHEMEN (additiv, nur in dieser Mac-Fassung).
   Zehn Akzentfamilien, umschaltbar ueber data-gh-thema auf <html>.
   Ohne Attribut gilt Tuerkis, das ist der Grundzustand oben in
   dieser Datei; der Block hier wiederholt ihn nur, damit ein
   ausdrueckliches data-gh-thema="teal" dasselbe Ergebnis liefert.

   Warum ein Attribut und nicht ein Inline-Wert auf <html>:
   Inline schlaegt jeden Selektor, ein Thema haette dann im Hell-
   und im Dunkelmodus zwangslaeufig denselben Wert. Genau das geht
   hier nicht, weil der Hellmodus bewusst die dunklere Stufe eines
   Farbtons braucht: das helle Tuerkis des Dunkelmodus traegt auf
   Papierton nur 2.1:1 und verschwindet.

   Je Thema drei Bloecke, gespiegelt zur Modus-Struktur weiter oben:
     1. Dunkelwerte auf :root
     2. Hellwerte, wenn das System hell sagt und nichts dunkel erzwingt
     3. Hellwerte, wenn Hell ausdruecklich gesetzt ist
   Bloecke 2 und 3 sind inhaltsgleich. Diese Doppelung ist dieselbe,
   die der Hell-Tokensatz oben schon betreibt, und aus demselben Grund
   unvermeidlich: ein Selektor kann nicht gleichzeitig "System sagt
   hell" und "hell ist erzwungen" ausdruecken. Wer hier einen Wert
   aendert, muss ihn in BEIDEN Bloecken aendern. Waechter dafuer:
   node werkzeuge/pruefe-themen.js

   --accent-fg und --accent-ico stehen nur im Dunkelblock. Im
   Hellmodus leitet dryl.css beide per color-mix aus --accent-a ab,
   sie folgen dem Thema also von selbst. Im Dunkelmodus sind es feste
   Werte, deshalb muessen sie dort mitwandern.

   Alle Werte sind gerechnet, nicht gegriffen: Kontrast nach WCAG 2.1
   mindestens 4.5:1 fuer Akzent auf Grund, fuer die Tinte an beiden
   Enden der Verlaufsfuellung und fuer inline <code> auf Glas, in
   beiden Modi. Die zehn Toene liegen im Dunkelmodus alle auf oklch-
   Helligkeit 0.726 (Spanne 0.002) und im Hellmodus auf 0.533 (Spanne
   0.009): beim Themenwechsel wandert nur der Farbton, nie die
   Helligkeit. Abstand zu --success, --warning und --danger jeweils
   dE2000 ueber 12. Einzige bekannte Ausnahme: Tuerkis liegt im
   Dunkelmodus bei dE 11.2 zu --success. Das ist der unveraenderte
   Ist-Zustand und bleibt als Anker so stehen.
   ============================================================= */

/* Türkis */
:root[data-gh-thema="teal"] {
  --accent-a:   #2bbfac;
  --accent-b:   #63dcc9;
  --accent-fg:  #8fe8d9;
  --accent-ico: #9fecdf;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="teal"]:not([data-dryl-mode="dark"]) {
    --accent-a: #0c7f71;
    --accent-b: #0a8276;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="teal"] {
  --accent-a: #0c7f71;
  --accent-b: #0a8276;
}

/* Azur */
:root[data-gh-thema="azur"] {
  --accent-a:   #30b7dc;
  --accent-b:   #64d5f8;
  --accent-fg:  #8fe2fe;
  --accent-ico: #a0e7ff;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="azur"]:not([data-dryl-mode="dark"]) {
    --accent-a: #117994;
    --accent-b: #147c97;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="azur"] {
  --accent-a: #117994;
  --accent-b: #147c97;
}

/* Kobalt */
:root[data-gh-thema="kobalt"] {
  --accent-a:   #78a7f4;
  --accent-b:   #a2c6ff;
  --accent-fg:  #bdd6ff;
  --accent-ico: #c6dcff;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="kobalt"]:not([data-dryl-mode="dark"]) {
    --accent-a: #466dad;
    --accent-b: #4870b1;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="kobalt"] {
  --accent-a: #466dad;
  --accent-b: #4870b1;
}

/* Violett */
:root[data-gh-thema="violett"] {
  --accent-a:   #b294eb;
  --accent-b:   #cdb5ff;
  --accent-fg:  #dbcaff;
  --accent-ico: #e0d2ff;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="violett"]:not([data-dryl-mode="dark"]) {
    --accent-a: #775da5;
    --accent-b: #7a60a9;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="violett"] {
  --accent-a: #775da5;
  --accent-b: #7a60a9;
}

/* Malve */
:root[data-gh-thema="malve"] {
  --accent-a:   #dd83c4;
  --accent-b:   #faa3e1;
  --accent-fg:  #ffbceb;
  --accent-ico: #ffc6ed;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="malve"]:not([data-dryl-mode="dark"]) {
    --accent-a: #975284;
    --accent-b: #9a5487;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="malve"] {
  --accent-a: #975284;
  --accent-b: #9a5487;
}

/* Kupfer */
:root[data-gh-thema="kupfer"] {
  --accent-a:   #e98a60;
  --accent-b:   #ffad8a;
  --accent-fg:  #ffc5ad;
  --accent-ico: #ffceb9;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="kupfer"]:not([data-dryl-mode="dark"]) {
    --accent-a: #90604c;
    --accent-b: #93634e;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="kupfer"] {
  --accent-a: #90604c;
  --accent-b: #93634e;
}

/* Bernstein */
:root[data-gh-thema="bernstein"] {
  --accent-a:   #d79842;
  --accent-b:   #f3b86b;
  --accent-fg:  #facb91;
  --accent-ico: #fcd39f;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="bernstein"]:not([data-dryl-mode="dark"]) {
    --accent-a: #906324;
    --accent-b: #936626;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="bernstein"] {
  --accent-a: #906324;
  --accent-b: #936626;
}

/* Olive */
:root[data-gh-thema="olive"] {
  --accent-a:   #b9a83e;
  --accent-b:   #d6c668;
  --accent-fg:  #e2d78f;
  --accent-ico: #e7dc9e;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="olive"]:not([data-dryl-mode="dark"]) {
    --accent-a: #7b6e20;
    --accent-b: #7d7123;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="olive"] {
  --accent-a: #7b6e20;
  --accent-b: #7d7123;
}

/* Farn */
:root[data-gh-thema="farn"] {
  --accent-a:   #91b64e;
  --accent-b:   #b0d375;
  --accent-fg:  #c4e198;
  --accent-ico: #cce6a6;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="farn"]:not([data-dryl-mode="dark"]) {
    --accent-a: #5c762b;
    --accent-b: #5f792d;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="farn"] {
  --accent-a: #5c762b;
  --accent-b: #5f792d;
}

/* Graphit */
:root[data-gh-thema="graphit"] {
  --accent-a:   #aba69b;
  --accent-b:   #c9c4b9;
  --accent-fg:  #d8d4cc;
  --accent-ico: #dedad3;
}
@media (prefers-color-scheme: light) {
  :root[data-gh-thema="graphit"]:not([data-dryl-mode="dark"]) {
    --accent-a: #726d62;
    --accent-b: #757065;
  }
}
:root[data-dryl-mode="light"][data-gh-thema="graphit"] {
  --accent-a: #726d62;
  --accent-b: #757065;
}


* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--ground);
  color: var(--fg);
  font-family: var(--font-sans);
  font-feature-settings: "ss01", "cv11";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Wurzel auf 100 % statt fester 14px: nur so schlägt die vom Nutzer
     eingestellte Browser-Schriftgröße durch (Dynamic Type). Der
     Fließtext bekommt darunter Apples Body-Maß von 17. */
  font-size: 100%;
  line-height: 1.5;
  min-height: 100dvh;
  overflow-x: hidden;
}

body {
  font-size: var(--fs-body);   /* Apple Body = 17 */
  background:
    radial-gradient(1200px 800px at 15% -10%, color-mix(in srgb, var(--aurora-tint) 4%, transparent), transparent 60%),
    radial-gradient(1000px 700px at 100% 0%, color-mix(in srgb, var(--aurora-tint) 3%, transparent), transparent 60%),
    radial-gradient(900px 700px at 50% 110%, color-mix(in srgb, var(--aurora-tint) 2%, transparent), transparent 60%),
    var(--ground);
  background-attachment: fixed;
}

/* Subtle film grain over everything */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: var(--grain-opacity);
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.04 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

/* Aurora — slow drifting orbs in the deep background.
   The blur lives on each orb (not the container): the orbs animate
   transform-only, so each one is a self-contained composited layer whose
   blurred texture the GPU just moves — a container-level filter would force
   re-filtering the whole oversized surface every frame of the drift. */
.aurora {
  position: fixed;
  inset: -20vh -20vw;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  opacity: var(--aurora-opacity);
}
.aurora::before,
.aurora::after,
.aurora .orb {
  content: '';
  position: absolute;
  border-radius: 50%;
  mix-blend-mode: screen;
  filter: blur(70px) saturate(140%);
}
.aurora::before {
  width: 50vw;
  height: 50vw;
  left: -10vw;
  top: 10vh;
  background: radial-gradient(circle, color-mix(in srgb, var(--aurora-tint) 55%, transparent), transparent 60%);
  animation: drift-a 22s ease-in-out infinite alternate;
}
.aurora::after {
  width: 45vw;
  height: 45vw;
  right: -5vw;
  top: -10vh;
  background: radial-gradient(circle, color-mix(in srgb, var(--aurora-tint) 38%, transparent), transparent 60%);
  animation: drift-b 28s ease-in-out infinite alternate;
}
.aurora .orb {
  width: 40vw;
  height: 40vw;
  left: 40%;
  bottom: -20vh;
  background: radial-gradient(circle, color-mix(in srgb, var(--aurora-tint) 30%, transparent), transparent 60%);
  animation: drift-c 34s ease-in-out infinite alternate;
}

@keyframes drift-a {
  0%   { transform: translate3d(0,0,0) scale(1); }
  100% { transform: translate3d(15vw, 10vh, 0) scale(1.15); }
}
@keyframes drift-b {
  0%   { transform: translate3d(0,0,0) scale(1); }
  100% { transform: translate3d(-12vw, 8vh, 0) scale(1.1); }
}
@keyframes drift-c {
  0%   { transform: translate3d(0,0,0) scale(1); }
  100% { transform: translate3d(-8vw, -12vh, 0) scale(1.2); }
}

/* App scaffold */
#root { position: relative; z-index: 2; min-height: 100vh; min-height: 100dvh; }

/* ============== TYPE ============== */
h1, h2, h3, h4 { margin: 0; font-weight: 600; letter-spacing: -0.02em; }
/* 56 bleibt bewusst hart in px: eine Display-Größe oberhalb der Apple-Leiter,
   die über Large Title (34) hinausgeht und deshalb kein --fs-Token hat. */
h1 { font-size: 56px; line-height: 1.05; letter-spacing: -0.035em; font-weight: 700; }
h2 { font-size: var(--fs-large); line-height: 1.15; letter-spacing: -0.025em; }
h3 { font-size: var(--fs-title3); line-height: 1.3; }
h4 { font-size: var(--fs-headline); line-height: 1.4; }

.eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-dim);
  font-weight: 500;
}

.mono { font-family: var(--font-mono); }

.gradient-text {
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

p { margin: 0; color: var(--fg-muted); }
.lead { font-size: var(--fs-title3); line-height: 1.55; color: var(--fg-muted); max-width: 64ch; }

a { color: var(--accent-b); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 4px; }

/* ============== GLASS PRIMITIVES ============== */
.glass {
  background: var(--glass-1);
  border: 1px solid var(--line);
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  border-radius: var(--r-lg);
}

.glass-card {
  position: relative;
  background:
    var(--sheen-grad),
    var(--panel-tint);
  border: 1px solid var(--line);
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  border-radius: var(--r-lg);
  box-shadow:
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md);
  transition: transform var(--dur-med) var(--ease-out),
              border-color var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out);
}
.glass-card::after {
  /* radial glow behind the card */
  content: '';
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  background: radial-gradient(80% 60% at 50% 0%, transparent, transparent 70%);
  pointer-events: none;
  transition: background var(--dur-med) var(--ease-out);
  z-index: -1;
}
.glass-card:hover::after {
  background: radial-gradient(80% 60% at 50% 0%, color-mix(in srgb, var(--accent-a) 18%, transparent), transparent 70%);
}
.glass-card:hover {
  border-color: var(--line-strong);
}

/* Glass-card padding presets (consumed by DrylCard.CardPadding) */
.glass-card.p-0       { padding: 0; }
.glass-card.p-tight   { padding: var(--sp-4); }
.glass-card.p-default { padding: var(--sp-5); }
.glass-card.p-loose   { padding: var(--sp-6); }

/* ============== BUTTONS ============== */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 38px;
  padding: 0 16px;
  border-radius: var(--r-md);
  border: 1px solid transparent;
  background: transparent;
  color: var(--fg);
  font: 500 var(--fs-callout)/1 var(--font-sans);
  letter-spacing: -0.005em;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: transform var(--dur-med) var(--ease-spring),
              background var(--dur-med) var(--ease-out),
              border-color var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out),
              color var(--dur-med) var(--ease-out);
  isolation: isolate;
}
/* Press: drop + shrink, then spring back on release (transform easing above). */
.btn:not(:disabled):active { transform: translateY(0) scale(0.96); }

/* Glass sheen — a soft reflection sweeps across the surface on hover. Clipped by
   border-radius (no overflow:hidden, so the AI-aura ring is untouched). Not on the
   chromeless Ghost variant, and never while disabled. */
.btn:not(.btn-ghost)::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(
    105deg,
    transparent 38%,
    var(--shimmer) 50%,
    transparent 62%
  );
  background-size: 250% 100%;
  background-position: 150% 0;
  opacity: 0;
  pointer-events: none;
  z-index: 2;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.btn:not(.btn-ghost):not(:disabled):hover::after {
  opacity: 1;
  background-position: -50% 0;
  transition: background-position var(--dur-slow) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}

/* Icons spring on hover: trailing slides forward (forward affordance), leading pops,
   icon-only scales. Marker classes come from DrylButton's leading/trailing slots. */
.btn .btn-ico-lead,
.btn .btn-ico-trail { transition: transform var(--dur-med) var(--ease-spring); }
.btn:not(:disabled):hover .btn-ico-trail { transform: translateX(3px); }
.btn:not(:disabled):hover .btn-ico-lead  { transform: translateX(-1px) scale(1.06); }
.btn-icon:not(:disabled):hover .btn-ico-lead,
.btn-icon:not(:disabled):hover .btn-ico-trail { transform: scale(1.12); }

.btn-primary {
  background: var(--accent-grad);
  color: var(--on-accent);
  border-color: var(--on-accent-line);
  box-shadow:
    0 1px 0 var(--on-accent-hi) inset,
    0 0 0 1px color-mix(in srgb, var(--accent-a) 25%, transparent),
    0 6px 24px color-mix(in srgb, var(--accent-a) 35%, transparent),
    0 0 48px color-mix(in srgb, var(--accent-b) 18%, transparent);
}
.btn-primary:hover {
  box-shadow:
    0 1px 0 var(--on-accent-hi) inset,
    0 0 0 1px color-mix(in srgb, var(--accent-a) 40%, transparent),
    0 10px 36px color-mix(in srgb, var(--accent-a) 50%, transparent),
    0 0 64px color-mix(in srgb, var(--accent-b) 28%, transparent);
  transform: translateY(-1px);
}

.btn-secondary {
  background: var(--glass-2);
  border-color: var(--line-strong);
  backdrop-filter: blur(var(--glass-blur-soft));
  -webkit-backdrop-filter: blur(var(--glass-blur-soft));
}
.btn-secondary:hover {
  background: var(--glass-3);
  border-color: var(--accent-line);
  box-shadow: 0 0 0 1px var(--accent-line), 0 0 24px color-mix(in srgb, var(--accent-a) 18%, transparent);
}

.btn-ghost {
  background: transparent;
  color: var(--fg-muted);
}
.btn-ghost:hover {
  background: var(--glass-2);
  color: var(--fg);
}

.btn-danger {
  background: color-mix(in srgb, var(--danger) 12%, transparent);
  border-color: color-mix(in srgb, var(--danger) 30%, transparent);
  color: var(--danger-fg);
}
.btn-danger:hover {
  background: color-mix(in srgb, var(--danger) 20%, transparent);
  box-shadow: 0 0 24px color-mix(in srgb, var(--danger) 25%, transparent);
}

.btn-icon {
  width: 38px;
  padding: 0;
}

/* Pressed / toggled state — driven by DrylButton's Pressed parameter
   (aria-pressed="true"). Accent border + glow, like a secondary on. */
.btn--active {
  border-color: var(--accent-line);
  box-shadow: 0 0 0 1px var(--accent-line), 0 0 20px color-mix(in srgb, var(--accent-a) 18%, transparent);
  color: var(--fg);
}

/* Disabled — must read clearly as inert across every variant: dimmed, flat,
   desaturated, and stripped of the accent glow that otherwise makes a primary
   look active. Hover sheen, press and icon springs are already gated behind
   :not(:disabled). */
.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  box-shadow: none;
  filter: grayscale(0.5);
  transform: none;
}

.btn-sm { height: 30px; padding: 0 12px; font-size: var(--fs-subhead); border-radius: var(--r-sm); }
.btn-sm.btn-icon { width: 30px; padding: 0; }
.btn-lg { height: 46px; padding: 0 22px; font-size: var(--fs-body); }

/* ============== BUTTON GROUP ============== */
/* Joins adjacent DrylButtons into a segmented control: inner corners are
   flattened and borders overlap by 1px so the cluster reads as one outline.
   Outer corners keep each button's own radius (so --sm / --lg are honoured). */
.btn-group { display: inline-flex; align-items: stretch; }
.btn-group--block { display: flex; width: 100%; }
.btn-group--block > .btn { flex: 1 1 0; }

.btn-group > .btn:not(:first-child) {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  margin-left: -1px;
}
.btn-group > .btn:not(:last-child) {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
/* Raise the interacted segment so its border / glow isn't clipped by the
   neighbour overlapping it. */
.btn-group > .btn:hover,
.btn-group > .btn:focus-visible,
.btn-group > .btn.btn--active { z-index: 1; }

/* ============== SPLIT BUTTON ============== */
/* A main action joined to a caret (the caret lives inside a DrylMenu's
   popover-anchor, hence the descendant selectors for the second segment). */
.split-btn { display: inline-flex; align-items: stretch; }
.split-btn--block { display: flex; width: 100%; }
.split-btn--block > .btn { flex: 1 1 auto; }

.split-btn > .btn {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.split-btn > .popover-anchor { margin-left: -1px; }
.split-btn > .popover-anchor .btn {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.split-btn > .btn:hover,
.split-btn > .btn:focus-visible,
.split-btn > .popover-anchor .btn:hover,
.split-btn > .popover-anchor .btn:focus-visible { z-index: 1; }

/* ---- Color-mode toggle ---------------------------------------- */
.color-mode-toggle {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px; height: 34px;
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--glass-1);
  color: var(--fg-muted);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.color-mode-toggle:hover { border-color: var(--line-hover); color: var(--fg); box-shadow: var(--shadow-sm); }
.color-mode-toggle .cmt-core,
.color-mode-toggle .cmt-rays,
.color-mode-toggle .cmt-moon {
  transform-origin: 12px 12px;
  transition: opacity var(--dur-med) var(--ease-out),
              transform var(--dur-med) var(--ease-spring);
}
/* chosen: light → sun */
.color-mode-toggle.is-light .cmt-moon { opacity: 0; transform: rotate(-40deg) scale(0.6); }
.color-mode-toggle.is-light .cmt-core,
.color-mode-toggle.is-light .cmt-rays { opacity: 1; transform: none; }
/* chosen: dark → moon */
.color-mode-toggle.is-dark .cmt-moon { opacity: 1; transform: none; }
.color-mode-toggle.is-dark .cmt-core,
.color-mode-toggle.is-dark .cmt-rays { opacity: 0; transform: rotate(40deg) scale(0.5); }
/* chosen: system → moon base + auto badge */
.color-mode-toggle.is-system .cmt-moon { opacity: 1; transform: none; }
.color-mode-toggle.is-system .cmt-core,
.color-mode-toggle.is-system .cmt-rays { opacity: 0; transform: rotate(40deg) scale(0.5); }
.color-mode-toggle .cmt-auto {
  position: absolute; right: -3px; bottom: -3px;
  font-size: var(--fs-caption2); font-weight: 700; line-height: 1;
  padding: 2px 3px; border-radius: var(--r-xs);
  background: var(--accent-grad); color: var(--on-accent);
  opacity: 0; transform: scale(0.5);
  transition: opacity var(--dur-med) var(--ease-out),
              transform var(--dur-med) var(--ease-spring);
}
.color-mode-toggle.is-system .cmt-auto { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .color-mode-toggle .cmt-core, .color-mode-toggle .cmt-rays,
  .color-mode-toggle .cmt-moon, .color-mode-toggle .cmt-auto { transition: none; }
}

/* ---- Screen-reader-only utility --------------------------------
   Visually hidden but still reachable by assistive tech; layout-only,
   no color tokens needed. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ============== INPUTS ============== */
.input,
.textarea,
.select {
  width: 100%;
  height: 38px;
  padding: 0 14px;
  border-radius: var(--r-md);
  background: var(--glass-1);
  border: 1px solid var(--line-strong);
  color: var(--fg);
  font: 400 var(--fs-body)/1 var(--font-sans);
  transition: border-color var(--dur-med) var(--ease-out),
              background var(--dur-med) var(--ease-out),
              box-shadow var(--dur-med) var(--ease-out);
  outline: none;
}
.input::placeholder, .textarea::placeholder { color: var(--fg-dim); }
.input:hover, .textarea:hover, .select:hover { border-color: var(--line-hover); }
.input:focus, .textarea:focus, .select:focus {
  border-color: var(--accent-line);
  background: var(--glass-2);
  /* 1px flush ring fuses with the border into one crisp accent edge, then a soft
     halo, then ambient glow — reads as a single confident corner, not a thin line
     with a detached block around it. */
  box-shadow: 0 0 0 1px var(--accent-line),
              0 0 0 4px color-mix(in srgb, var(--accent-a) 12%, transparent),
              0 0 24px color-mix(in srgb, var(--accent-a) 18%, transparent);
}
/* The global :focus-visible rule adds a bright cyan outline that clashes with the
   violet glow above. Suppress it here — the glow already satisfies WCAG focus visibility. */
.input:focus-visible,
.textarea:focus-visible,
.select:focus-visible { outline: none; }
.textarea {
  height: auto;
  padding: 12px 14px;
  resize: vertical;
  line-height: 1.5;
  min-height: 96px;
}

/* Select — strip native arrow, draw our own, give <option> a real dark surface */
.select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a0a0b0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-right: 36px;
  cursor: pointer;
}
.select option {
  background: var(--bg-2);
  color: var(--fg);
}

.field-label {
  display: block;
  font-size: var(--fs-subhead);
  color: var(--fg-muted);
  margin-bottom: 6px;
  font-weight: 500;
}

/* Input field wrapper (label + input + icons + helper) */
.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

/* Textarea wrapper — gives <textarea> a positionable host so AI primitives
   (and future extensions) have something to layer on. Visually transparent. */
.textarea-wrapper {
  position: relative;
  display: flex;
  width: 100%;
}
.textarea-wrapper > .textarea { flex: 1; }

/* AI mode on inputs needs an explicit radius on the wrapper so .ai-aura-ring
   (which uses border-radius: inherit) traces the input's rounded corners. */
.input-wrapper.ai-aura,
.textarea-wrapper.ai-aura {
  border-radius: var(--r-md);
}

/* AI mode on the file-upload drop zone — echo the .file-drop radius (--r-lg)
   on the wrapper so the rotating ring and breathing glow trace its rounded
   corners instead of a square box. */
.file-upload-wrapper.ai-aura {
  border-radius: var(--r-lg);
}

/* AI mode on radio groups — give the wrapper a surface + radius so the
   rotating ring has rounded corners and the group reads as a contained panel. */
.radio-group-wrapper.ai-aura {
  border-radius: var(--r-lg);
  padding: var(--sp-3);
  background: var(--glass-1);
}

/* AI mode on the rating stars — a radius + inline-flex box so the ring/glow
   trace the stars as a contained pill instead of a square that overflows them. */
.rating-wrapper.ai-aura {
  display: inline-flex;
  border-radius: var(--r-md);
  padding: var(--sp-2);
}

/* AI mode on buttons uses the shared aura vocabulary (.ai-aura-ring / -glow / -wash
   via <DrylAuraElements>), same as every other host — no button-specific ring. The
   button carries no overflow:hidden and is position:relative + isolate, so the
   default primitive sizing traces its rounded box and glows outside it untouched. */

@media (prefers-reduced-motion: reduce) {
  .btn:not(.btn-ghost)::after { display: none; }
  .btn .btn-ico-lead,
  .btn .btn-ico-trail { transition: none; }
  .btn:hover .btn-ico-lead,
  .btn:hover .btn-ico-trail { transform: none; }
}

.input-wrapper .input-icon,
.tbl-search .input-icon {
  position: absolute;
  color: var(--fg-dim);
  pointer-events: none;
  transition: color var(--dur-med) var(--ease-out);
}
/* Vertically center any absolutely-positioned input icon — the leading glyph and,
   crucially, the trailing spinner. Without an explicit top the trailing element
   resolves its static position at the wrapper's baseline and sinks to the bottom.
   line-height:0 collapses wrapper spans (e.g. the spinner's positioning span) to
   their content height so the centering isn't skewed by inline baseline slack. */
.input-wrapper .input-icon { top: 50%; transform: translateY(-50%); line-height: 0; }
.input-wrapper:focus-within .input-icon,
.tbl-search:focus-within .input-icon { color: var(--fg-muted); }
.input-wrapper .input-icon-leading  { left: 12px; }
.input-wrapper .input-icon-trailing { right: 12px; }

.input.has-leading-icon  { padding-left: 38px; }
.input.has-trailing-icon { padding-right: 38px; }

.input.input-error,
.textarea.input-error,
.select.input-error { border-color: color-mix(in srgb, var(--danger) 50%, transparent); }
.input.input-error:focus,
.textarea.input-error:focus,
.select.input-error:focus {
  border-color: var(--danger);
  background: var(--glass-2);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 12%, transparent), 0 0 24px color-mix(in srgb, var(--danger) 18%, transparent);
}
.input.input-success,
.textarea.input-success,
.select.input-success { border-color: color-mix(in srgb, var(--success) 50%, transparent); }
.input.input-success:focus,
.textarea.input-success:focus,
.select.input-success:focus {
  border-color: var(--success);
  background: var(--glass-2);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--success) 12%, transparent), 0 0 24px color-mix(in srgb, var(--success) 18%, transparent);
}

.input:disabled, .textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.input-helper {
  font-size: var(--fs-footnote);
  color: var(--fg-dim);
  line-height: 1.4;
}
.input-helper-error { color: var(--danger); }

/* Checkbox */
.checkbox {
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: var(--r-2xs);
  background: var(--glass-1);
  border: 1px solid var(--line-strong);
  display: inline-grid;
  place-content: center;
  cursor: pointer;
  transition: all var(--dur-fast) var(--ease-out);
  position: relative;
}
.checkbox:hover { border-color: var(--accent-line); }
.checkbox:checked {
  background: var(--accent-grad);
  border-color: transparent;
  box-shadow: 0 0 16px color-mix(in srgb, var(--accent-a) 50%, transparent);
}
.checkbox::after {
  content: '';
  width: 10px;
  height: 6px;
  border: 2px solid var(--knob);
  border-top: 0;
  border-right: 0;
  transform: rotate(-45deg) translate(1px, -1px) scale(0);
  opacity: 0;
  transition: transform var(--dur-med) var(--ease-spring),
              opacity var(--dur-fast) var(--ease-out);
}
.checkbox:checked::after {
  transform: rotate(-45deg) translate(1px, -1px) scale(1);
  opacity: 1;
}

/* Toggle */
.toggle {
  appearance: none;
  width: 36px;
  height: 20px;
  border-radius: var(--r-pill);
  background: var(--glass-2);
  border: 1px solid var(--line-strong);
  position: relative;
  cursor: pointer;
  transition: background var(--dur-med) var(--ease-out), box-shadow var(--dur-med) var(--ease-out);
}
.toggle::after {
  content: '';
  position: absolute;
  top: 1px;
  left: 1px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--fg-muted);
  transition: transform var(--dur-med) var(--ease-spring), background var(--dur-med) var(--ease-out);
}
.toggle:checked {
  background: var(--accent-grad);
  border-color: transparent;
  box-shadow: 0 0 18px color-mix(in srgb, var(--accent-a) 45%, transparent);
}
.toggle:checked::after {
  background: var(--knob);
  transform: translateX(16px);
}

/* ============== BADGES ============== */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  height: 22px;
  /* Kapsel: die runden Enden fressen waagerecht Fläche, deshalb liegt der
     Innenabstand über der halben Höhe. Netto bleibt die Pille etwa so breit
     wie vorher, weil mit der Systemschrift die Monospace-Dickten wegfallen. */
  padding: 0 var(--sp-3);
  border-radius: var(--r-pill);
  /* Systemschrift statt SF Mono: Apples Antwort auf veränderliche Zahlen sind
     gleich breite ZIFFERN in der Systemschrift (monospacedDigitSystemFont),
     nicht eine Monospace-Familie. Ein Zähler springt damit trotzdem nicht. */
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-caption2);
  font-weight: 500;
  /* Keine Zusatzsperrung: die Systemschrift regelt ihr Tracking je
     Schriftgrad selbst, HIG Typography. */
  letter-spacing: normal;
  border: 1px solid var(--line-strong);
  background: var(--glass-2);
  color: var(--fg-muted);
}
.badge-accent {
  /* Tinte aus dem Akzentkeim abgeleitet statt aus --accent-fg, gleiche Formel
     wie bei den drei semantischen Pillen darunter.
     Grund: --accent-fg steht in den Themenblöcken unbedingt auf dem
     DUNKLEN Wert (:root[data-gh-thema="…"], Gewicht 0-2-0, und weiter unten
     in der Datei als die Hell-Blöcke). Im Hellmodus stand damit helles Teal
     auf heller Teal-Füllung: gerechnet 1.09 bis 1.17:1 über alle zehn Themen,
     praktisch unsichtbar. --accent-a dagegen wird von den Hell-Blöcken sauber
     überschrieben und --fg dreht mit dem Modus, die Ableitung trägt deshalb in
     beiden Modi: hell 4.84 bis 5.24 (Farn 4.10), dunkel 5.79 bis 7.05.
     Der Kaskadenfehler in --accent-fg selbst bleibt bestehen und trifft
     weiterhin Code-Chips, AI-Indikator und Gefahren-Menüpunkte, siehe TODO. */
  color: color-mix(in srgb, var(--accent-a) 80%, var(--fg));
  border-color: var(--accent-line);
  background: var(--accent-soft);
}
/* Tinte der drei semantischen Pillen: die reine Signalfarbe auf ihrer eigenen
   10-Prozent-Füllung fiel im Hellmodus durch Apples Pflichtwert von 4.5:1
   (gerechnet: Erfolg 3.77, Warnung 4.26, Fehler 4.02). Ein Fünftel --fg
   beigemischt hebt sie auf 4.99 / 5.50 / 5.34, im Dunkelmodus auf 8.11 / 8.90
   / 6.35. --fg dreht mit dem Modus, die Tinte wird also genau dort dunkler,
   wo sie dunkler werden muss. Füllung und Rand bleiben unverändert. */
.badge-success { color: color-mix(in srgb, var(--success) 80%, var(--fg)); border-color: color-mix(in srgb, var(--success) 30%, transparent); background: color-mix(in srgb, var(--success) 10%, transparent); }
.badge-warning { color: color-mix(in srgb, var(--warning) 80%, var(--fg)); border-color: color-mix(in srgb, var(--warning) 30%, transparent); background: color-mix(in srgb, var(--warning) 10%, transparent); }
.badge-danger  { color: color-mix(in srgb, var(--danger) 80%, var(--fg));  border-color: color-mix(in srgb, var(--danger) 30%, transparent); background: color-mix(in srgb, var(--danger) 10%, transparent); }
.badge-dot::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  /* Zurückgenommener Schein. Ein Leucht-Effekt kommt in Apples Formensprache
     nicht vor, und der volle Schein über 8px weichte die Kante des 6px-Punkts
     auf, also genau den Kantenkontrast, den die Materialregel für feine
     Elemente verlangt. Radius auf --sp-1, damit er kleiner bleibt als der
     Punkt selbst, Deckung halbiert. Nicht ganz entfernt, weil badge.html den
     Punkt im Fließtext als glühend beschreibt und das Markup unberührt bleibt. */
  box-shadow: 0 0 var(--sp-1) color-mix(in srgb, currentColor 50%, transparent);
}

/* ============== UTILITIES ============== */
.row { display: flex; align-items: center; gap: var(--sp-3); }
.col { display: flex; flex-direction: column; gap: var(--sp-3); }
.between { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.divider { height: 1px; background: var(--line); width: 100%; }
.divider-v { width: 1px; background: var(--line); align-self: stretch; }

.fade-in { animation: fadeIn 480ms var(--ease-out) both; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

.stagger > * { animation: rise 520ms var(--ease-out) both; }
.stagger > *:nth-child(1) { animation-delay: 0ms; }
.stagger > *:nth-child(2) { animation-delay: 60ms; }
.stagger > *:nth-child(3) { animation-delay: 120ms; }
.stagger > *:nth-child(4) { animation-delay: 180ms; }
.stagger > *:nth-child(5) { animation-delay: 240ms; }
.stagger > *:nth-child(6) { animation-delay: 300ms; }
.stagger > *:nth-child(7) { animation-delay: 360ms; }
.stagger > *:nth-child(8) { animation-delay: 420ms; }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }

/* ──────────────────────────────────────────────────────────
 * Motion Foundation — shared primitives for DrylPresence and
 * DrylReveal. All token-driven; all neutralised under
 * prefers-reduced-motion (see the @media block right below).
 * ────────────────────────────────────────────────────────── */

/* Presence — enter/exit for a single child whose unmount is deferred
 * by DrylPresence until the exit animation finishes. The *-out
 * keyframes are matched by name in dryl.motion.onExit, so they must
 * stay prefixed `presence-out`. DrylPresence renders a real wrapping
 * element — animations do not apply to display:contents. */
.presence { min-width: 0; }

.presence-enter { animation: presence-in-fade var(--dur-med) var(--ease-out) both; }
.presence-exit  { animation: presence-out-fade var(--dur-med) var(--ease-out) both; }

.presence--scale.presence-enter      { animation-name: presence-in-scale; }
.presence--scale.presence-exit       { animation-name: presence-out-scale; }
.presence--slide-up.presence-enter   { animation-name: presence-in-up; }
.presence--slide-up.presence-exit    { animation-name: presence-out-up; }
.presence--slide-down.presence-enter { animation-name: presence-in-down; }
.presence--slide-down.presence-exit  { animation-name: presence-out-down; }
.presence--slide-left.presence-enter { animation-name: presence-in-left; }
.presence--slide-left.presence-exit  { animation-name: presence-out-left; }
.presence--slide-right.presence-enter{ animation-name: presence-in-right; }
.presence--slide-right.presence-exit { animation-name: presence-out-right; }

/* Speed modifiers — remap the animation onto the other fixed duration tokens. */
.presence--fast.presence-enter, .presence--fast.presence-exit { animation-duration: var(--dur-fast); }
.presence--slow.presence-enter, .presence--slow.presence-exit { animation-duration: var(--dur-slow); }

@keyframes presence-in-fade   { from { opacity: 0; } to { opacity: 1; } }
@keyframes presence-out-fade  { from { opacity: 1; } to { opacity: 0; } }
@keyframes presence-in-scale  { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: scale(1); } }
@keyframes presence-out-scale { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.94); } }
@keyframes presence-in-up     { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes presence-out-up    { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(12px); } }
@keyframes presence-in-down   { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes presence-out-down  { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-12px); } }
@keyframes presence-in-left   { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: translateX(0); } }
@keyframes presence-out-left  { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(12px); } }
@keyframes presence-in-right  { from { opacity: 0; transform: translateX(-12px); } to { opacity: 1; transform: translateX(0); } }
@keyframes presence-out-right { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(-12px); } }

/* Reveal — scroll-triggered entrance. dryl.motion.observe adds
 * .is-revealed when the wrapper scrolls into view; direct children
 * tween in, optionally staggered via per-child --reveal-i. */
.reveal > * {
  opacity: 0;
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  transition-delay: calc(var(--reveal-i, 0) * var(--reveal-step));
}
.reveal--rise > *    { transform: translateY(16px); }
.reveal--scale-in > *{ transform: scale(0.96); }
.reveal.is-revealed > * { opacity: 1; transform: none; }

/* Reduced motion — show end states instantly, no movement. dryl.motion also
   short-circuits in JS, but this covers the CSS-only paths too. */
@media (prefers-reduced-motion: reduce) {
  .presence-enter,
  .presence-exit { animation: none; }
  .reveal > * {
    transition: none;
    transition-delay: 0ms;
  }
  .reveal--rise > *,
  .reveal--scale-in > * { transform: none; }
  .tabs.is-ink-ready .tab-ink { transition: none; }
  .dialog-backdrop { animation: none; transition: none; }
  .dialog { animation: none; }
  .dialog-layer.is-exiting .dialog { animation: none; }
  .dialog-layer.is-handoff:not(.is-exiting) .dialog { animation-delay: 0ms; }
}

/* ──────────────────────────────────────────────────────────
 * Depth Glass — DrylDepthGlass and the optional Depth warp on
 * DrylCard. A glass pane that warps in 3D toward the pointer: the
 * surface tilts in perspective while the content and gloss parallax in
 * separate layers, so it reads like a sheet of glass floating above
 * the page. Pure CSS transforms driven by --tx/--ty/--mx/--my (written
 * by dryl.depthglass) — composited, reliable, no SVG filters. Stilled
 * under reduced motion.
 *   .dg-warp    — the motion (shared by DrylDepthGlass and DrylCard)
 *   .dg-surface — the standalone glass look (DrylDepthGlass only;
 *                 DrylCard brings its own via .glass-card)
 *   --dg-tilt — max tilt angle   --dg-par — max parallax shift
 * ────────────────────────────────────────────────────────── */
.dg-warp {
  --dg-tilt: 8deg;
  --dg-par: 10px;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transform:
    perspective(900px)
    rotateX(calc(var(--ty, 0) * var(--dg-tilt) * -1))
    rotateY(calc(var(--tx, 0) * var(--dg-tilt)))
    translateY(var(--dg-lift, 0px));
  transition:
    transform var(--dur-med) var(--ease-spring),
    box-shadow var(--dur-med) var(--ease-out);
  will-change: transform;
}
.dg-warp:hover {
  --dg-lift: -4px;
  box-shadow:
    0 22px 50px var(--depth-shadow),
    inset 1px 1px 0 var(--depth-edge-strong),
    inset -1px -1px 0 color-mix(in srgb, var(--depth-edge) 78%, transparent),
    inset 0 0 26px color-mix(in srgb, var(--depth-edge) 39%, transparent);
}

.dg--subtle { --dg-tilt: 4deg;  --dg-par: 6px;  }
.dg--medium { --dg-tilt: 8deg;  --dg-par: 10px; }
.dg--strong { --dg-tilt: 13deg; --dg-par: 16px; }

/* Standalone surface look (DrylDepthGlass). */
.dg-surface {
  border-radius: var(--r-lg);
  background: var(--glass-1);
  backdrop-filter: blur(var(--scrim-blur)) saturate(1.5);
  -webkit-backdrop-filter: blur(var(--scrim-blur)) saturate(1.5);
  border: 1px solid var(--depth-edge);
  box-shadow:
    0 12px 36px color-mix(in srgb, var(--depth-shadow) 76%, transparent),
    inset 1px 1px 0 color-mix(in srgb, var(--depth-edge-strong) 91%, transparent),
    inset -1px -1px 0 color-mix(in srgb, var(--depth-edge) 67%, transparent),
    inset 0 0 20px color-mix(in srgb, var(--depth-edge) 28%, transparent);
}
.dg-surface > .dg-content { padding: var(--sp-5); color: var(--fg); }

.dg-sheen,
.dg-specular {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

/* Top gloss — parallaxes opposite the tilt so the sheen slides across the
   glass as it warps (depth cue). */
.dg-sheen {
  z-index: 1;
  /* Overscan the gloss well past its max parallax travel so its own rectangular
     edge never slides into view as a hard seam near a corner. The parent clips
     the overflow back to the card's rounded shape (overflow:hidden + radius). */
  inset: -32px;
  background: linear-gradient(155deg, color-mix(in srgb, var(--depth-edge-strong) 44%, transparent), transparent 45%);
  mix-blend-mode: screen;
  opacity: 0.7;
  transform: translate(
    calc(var(--tx, 0) * var(--dg-par) * -1.4),
    calc(var(--ty, 0) * var(--dg-par) * -1.4));
  transition: transform var(--dur-med) var(--ease-out);
}

/* Travelling glossy highlight — follows the pointer (falls back to top-centre). */
.dg-specular {
  z-index: 2;
  background: radial-gradient(
    220px 220px at var(--mx, 50%) var(--my, 0%),
    color-mix(in srgb, var(--depth-edge-strong) 73%, transparent), transparent 60%);
  mix-blend-mode: screen;
  opacity: 0.6;
}

/* Content floats above the glass plane — parallaxes WITH the tilt and lifts
   on hover so it reads as raised. */
.dg-content {
  position: relative;
  z-index: 3;
  transform: translate(
    calc(var(--tx, 0) * var(--dg-par)),
    calc(var(--ty, 0) * var(--dg-par)));
  transition: transform var(--dur-med) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .dg-warp,
  .dg-sheen,
  .dg-content { transform: none; transition: none; }
  .dg-warp:hover { --dg-lift: 0px; }
}

/* Scrollbar */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: var(--r-pill);
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); background-clip: padding-box; border: 2px solid transparent; }
::-webkit-scrollbar-track { background: transparent; }

/* Focus ring for keyboard users.
   Do NOT force a border-radius here — modern browsers draw the outline along the
   element's own radius, and clobbering it (every component shares this rule's
   specificity) makes rounded surfaces like .input snap to square corners on focus. */
:focus-visible { outline: 2px solid var(--accent-b); outline-offset: 2px; }

/* ============== APP CHROME ============== */
.app-shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "drawer topbar"
    "drawer main";
  height: 100vh;           /* Fallback für Browser ohne dvh */
  height: 100dvh;
  /* The shell fills the viewport and never scrolls itself, so the sidebar and
     top bar stay fixed in place; only .main scrolls (see its rule below). */
  overflow: hidden;
  transition: grid-template-columns var(--dur-med) var(--ease-in-out);
}
/* Collapsed sidebar: the grid column shrinks to icon width. DrylLayout sets
   this class from its SidebarCollapsed state so the body reflows in step with
   the sidebar's own .is-collapsed width animation. Desktop only — on mobile the
   grid is a single column and the sidebar is an overlay, so collapse is moot. */
@media (min-width: 1024px) {
  .app-shell.is-sidebar-collapsed {
    grid-template-columns: var(--sidebar-collapsed-w) 1fr;
  }
}
.app-shell > .sidebar { grid-area: drawer; }
.app-shell > .topbar  { grid-area: topbar; }
.app-shell > .main    { grid-area: main; }

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;   /* Fallback für Browser ohne dvh */
  height: 100dvh;
  padding: 24px 18px;
  /* Betrag, um den die Rollfläche in das rechte Außenpolster
     hineinreicht. Er entspricht dem Polster selbst, damit der
     Rollbalken am Rand der Leiste sitzt und nicht 18px davor.
     Steht hier, weil das Polster hier steht: wer das eine ändert,
     sieht das andere. */
  --nav-scroll-bleed: 18px;
  border-right: 1px solid var(--line);
  background: var(--sheen-grad-soft);
  backdrop-filter: blur(var(--glass-blur-strong));
  -webkit-backdrop-filter: blur(var(--glass-blur-strong));
  display: flex;
  flex-direction: column;
  gap: 4px;
  z-index: 10;
  overflow: hidden;
  transition: width var(--dur-med) var(--ease-in-out),
              padding var(--dur-med) var(--ease-in-out);
}

/* Structured slots (DrylDrawer Header / Content / Footer). The content area
   scrolls; header and footer stay pinned. Purely additive — drawers that pass
   plain ChildContent never render these wrappers. */
.sidebar-header { flex: 0 0 auto; }
.sidebar-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--sp-1); }
.sidebar-footer {
  flex: 0 0 auto;
  margin-top: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}

/* Collapsed: icon-only rail. Hide text labels but keep leading icons centred.
   No JS needed — the nav primitives already wrap labels in <span>.
   Confined to desktop so the mobile overlay (below) always shows full labels —
   the collapsed state is a desktop affordance only. */
@media (min-width: 1024px) {
  .sidebar.is-collapsed {
    width: var(--sidebar-collapsed-w);
    padding-left: var(--sp-2);
    padding-right: var(--sp-2);
    /* Eingeklappt schrumpft das Polster auf --sp-2, das Maß muss
       mitgehen. Sonst ragte die Rollfläche über die Polsterkante
       hinaus und würde vom overflow: hidden der Leiste beschnitten. */
    --nav-scroll-bleed: var(--sp-2);
  }
  .sidebar.is-collapsed .nav-item > span,
  .sidebar.is-collapsed .nav-title,
  .sidebar.is-collapsed .nav-section-toggle-label,
  .sidebar.is-collapsed .nav-section-link span,
  .sidebar.is-collapsed .nav-section-chevron,
  .sidebar.is-collapsed .sidebar-collapse-hide {
    display: none;
  }
  .sidebar.is-collapsed .nav-item,
  .sidebar.is-collapsed .nav-section-toggle { justify-content: center; }
}

/* Static mode: never becomes an overlay, stays an in-flow column at any width. */
.sidebar--static { transform: none !important; }

/* Flyout mode: always an overlay (fixed, off-canvas) regardless of viewport,
   sliding in when .is-open. Pairs with the always-rendered .sidebar-backdrop. */
.sidebar--flyout {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--sidebar-w);
  height: 100vh;   /* Fallback für Browser ohne dvh */
  height: 100dvh;
  transform: translateX(-100%);
  transition: transform var(--dur-med) var(--ease-out);
  z-index: var(--z-drawer);
  border-right: 1px solid var(--line-strong);
}
.sidebar--flyout.is-open { transform: translateX(0); }

/* Desktop-visible sidebar collapse toggle (DrylAppBar ShowSidebarToggle / a
   header button). Reuses the hamburger visual but is shown at all widths. */
.sidebar-toggle {
  display: inline-flex;
  width: 36px;
  height: 36px;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border-radius: var(--r-sm);
  background: transparent;
  border: 1px solid var(--line);
  color: var(--fg-muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.sidebar-toggle:hover {
  background: var(--glass-2);
  border-color: var(--line-strong);
  color: var(--fg);
}
.sidebar-toggle:focus-visible {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: var(--backdrop);
  backdrop-filter: blur(calc(var(--scrim-blur) / 2));
  -webkit-backdrop-filter: blur(calc(var(--scrim-blur) / 2));
  z-index: 40;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-med) var(--ease-out);
}
/* Flyout drawers overlay at every viewport width, so their backdrop must show
   regardless of the mobile media query below. */
.sidebar-backdrop--flyout { display: block; }
.sidebar-backdrop--flyout.is-open { opacity: 1; pointer-events: auto; }

.drawer-toggle {
  display: none;
  width: 36px;
  height: 36px;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border-radius: var(--r-sm);
  background: transparent;
  border: 1px solid var(--line);
  color: var(--fg-muted);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.drawer-toggle:hover {
  background: var(--glass-2);
  border-color: var(--line-strong);
  color: var(--fg);
}
.drawer-toggle:focus-visible {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

@media (max-width: 1023px) {
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-areas:
      "topbar"
      "main";
  }
  .app-shell > .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    width: 280px;
    height: 100vh;   /* Fallback für Browser ohne dvh */
    height: 100dvh;
    transform: translateX(-100%);
    transition: transform var(--dur-med) var(--ease-out);
    z-index: 50;
    border-right: 1px solid var(--line-strong);
  }
  .app-shell > .sidebar.is-open {
    transform: translateX(0);
  }
  .sidebar-backdrop {
    display: block;
  }
  .sidebar-backdrop.is-open {
    opacity: 1;
    pointer-events: auto;
  }
  .drawer-toggle {
    display: inline-flex;
  }
  /* The collapse toggle is a desktop affordance; on mobile the hamburger
     (drawer-toggle) opens the overlay instead, so hide the collapse button. */
  .sidebar-toggle {
    display: none;
  }
  .topbar { padding: 0 16px; }
  .main-inner { padding: 24px 16px 64px; }
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 4px 8px;
}
.brand-mark {
  width: 36px;
  height: 36px;
  border-radius: var(--r-sm);
  background: var(--accent-grad);
  display: grid;
  place-content: center;
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-subhead);
  color: var(--on-accent);
  letter-spacing: -0.02em;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--on-accent) 10%, transparent) inset, 0 8px 24px color-mix(in srgb, var(--accent-a) 40%, transparent), 0 0 32px color-mix(in srgb, var(--accent-b) 20%, transparent);
  position: relative;
  overflow: hidden;
}
.brand-mark::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(120deg, transparent 30%, var(--shimmer-strong) 50%, transparent 70%);
  animation: shimmer 4s ease-in-out infinite;
  z-index: 1;
  pointer-events: none;
}
.brand-mark-logo {
  width: 22px;
  height: 22px;
  object-fit: contain;
  display: block;
  position: relative;
  z-index: 2;
  mix-blend-mode: screen;
}
@keyframes shimmer {
  0%, 100% { transform: translateX(-100%); }
  50% { transform: translateX(100%); }
}
.brand-name { font-weight: 600; font-size: var(--fs-callout); letter-spacing: -0.01em; }
.brand-sub  { font-size: var(--fs-caption2); color: var(--fg-dim); font-family: var(--font-mono); letter-spacing: 0.05em; }

.nav-section {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* Gruppentitel in der Seitenleiste, nach Apple-Art. Nicht gesperrte
   Versalien in Schreibmaschinenschrift, sondern eine kleine, halbfette
   Systemschrift in gedämpftem Ton. Die Gruppe wird nicht dadurch
   erkannt, dass der Titel auffällt, sondern durch den Abstand über ihm
   und die Nähe zu den eigenen Zeilen unter ihm. */
.nav-title {
  /* Oberflächenschrift statt Monospace: gesperrte Versalien liest das
     Auge Buchstabe für Buchstabe, genau das war zu langsam. */
  font-family: var(--font-sans);
  /* 13px, kleiner als die 16px der Zeilen darunter. Der Titel setzt
     sich über das Gewicht ab, nicht über die Größe, sonst
     konkurrierte er mit den Einträgen, die er ordnen soll. */
  font-size: var(--fs-footnote);
  font-weight: 600;
  /* Apples Wert für 13pt Footnote. Er wirkt kaum, gut ein Sechzehntel
     Pixel je Zeichen, gehört aber zur Schriftstufe. */
  letter-spacing: -0.005em;
  text-transform: none;
  /* --fg-faint ist dekorativ (0,28 dunkel, 0,40 hell). Ein Titel muss
     gelesen werden. --fg-dim liegt auf dem tatsächlichen Untergrund
     der Leiste (--sheen-grad-soft über --ground) bei 4,9:1 dunkel und
     4,6:1 hell, beides über der Schwelle für Fließtext. */
  color: var(--fg-dim);
  /* 22px Luft nach oben trennen die Gruppen, 4px nach unten binden den
     Titel an seine Zeilen. Das Verhältnis der beiden Werte macht die
     Gruppe sichtbar, nicht ihre absolute Größe. Links 12px, damit der
     Titel auf der Beschriftung der Zeilen steht und nicht auf deren
     Flächenkante. */
  padding: 22px 12px 4px;
}
.nav-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 34px;
  padding: 0 12px;
  border-radius: var(--r-sm);
  color: var(--fg-muted);
  font-size: var(--fs-callout);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
  user-select: none;
}
.nav-item:hover { background: var(--glass-2); color: var(--fg); }
.nav-item.active {
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent-a) 18%, transparent), color-mix(in srgb, var(--accent-b) 4%, transparent));
  color: var(--fg);
  box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 0 18px color-mix(in srgb, var(--accent-a) 15%, transparent);
}
.nav-item.active::before {
  content: '';
  position: absolute;
  left: -18px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 18px;
  border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: var(--accent-grad);
  box-shadow: 0 0 12px var(--accent-a);
}
.nav-item .ico { width: 16px; height: 16px; flex: 0 0 16px; opacity: 0.8; }
.nav-item .kbd {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  color: var(--fg-faint);
  padding: 2px 6px;
  border-radius: var(--r-2xs);
  border: 1px solid var(--line);
}

/* Navigationszeilen sind keine Fließtextverweise. Die globale Regel
   a:hover { text-decoration: underline } trifft sie nur, weil sie als
   <a> ausgezeichnet sind, gemeint war sie nie. Flächenwechsel und
   .active tragen den Zustand bereits, der Strich wäre ein zweites
   Signal für dieselbe Aussage. Die Gegenregel steht hier an drei
   Klassen statt an der globalen a:hover-Regel, damit Verweise im
   Fließtext ihre Unterstreichung behalten. .nav-section-toggle
   braucht sie nicht, das ist ein <button>, dorthin reicht a:hover
   nicht. Spezifität 0,2,0 gegen 0,1,1, die Klassenzahl entscheidet. */
.nav-item:hover,
.nav-section-link:hover,
.brand:hover {
  text-decoration: none;
}

/* Tastaturfokus: der Ring der globalen :focus-visible-Regel liegt
   außen auf, zusätzlich hinterlegt sich die Zeile wie beim
   Überfahren, damit der Fokus auch innerhalb der Zeile abzulesen ist.
   :not(.active) nimmt die aktive Zeile heraus, deren Akzentfüllung
   sonst durch matte Glasfläche ersetzt würde. Das ist eine Auswahl,
   keine Rangfrage: :not trägt die Spezifität seines Arguments bei,
   der Selektor läge mit 0,3,0 ohnehin über .nav-item.active. */
.nav-item:focus-visible:not(.active) {
  background: var(--glass-2);
  color: var(--fg);
}

/* ── Scrollable nav area ──────────────────────────────── */
.nav-scroll {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* Rollverhalten, Innenabstände und Balken stehen im gemeinsamen Block
   darunter. Grund: in dieser Fassung rollt .sidebar-content,
   .nav-scroll kommt in keiner Seite vor und ist die Variante für
   Fremdprojekte. Beide müssen gleich aussehen, sonst hängt das
   Ergebnis davon ab, welche der zwei Klassen ein Projekt benutzt. */

/* ── Rollfläche der Navigation ─────────────────────────────────────────
   Rechts holt sich die Fläche das Außenpolster der Leiste
   (--nav-scroll-bleed) und gibt --sp-2 davon innen zurück. Der Balken
   sitzt damit im ehemaligen Polster dicht am Rand, statt 18px davor
   mitten in der Fläche zu stehen.
   Links dasselbe Maß, aber ohne Wirkung auf die Lage der Zeilen: das
   negative Außenmaß und das gleich große Innenmaß heben sich auf.
   Zweck ist allein, dass der Fokusring (2px Strich, 2px Abstand, aus
   der globalen :focus-visible-Regel) nicht an der Kante der Rollfläche
   abgeschnitten wird, denn overflow-y: auto zieht overflow-x auf auto
   hoch und macht die Fläche in beiden Achsen zum Rollbehälter. Oben
   und unten dasselbe Maß, aus demselben Grund.
   Warum links genau --sp-2 und nicht mehr: ab 16px begänne der
   Akzentbalken von .nav-item.active (left: -18px, 3px breit) als
   angeschnittener Splitter durchzuscheinen. Bei 8px bleibt er
   vollständig verdeckt, so wie heute. Das ist gewollt: eine
   Seitenleiste nach Apple-Art markiert die aktive Zeile durch ihre
   gefüllte Fläche, der Balken an der Fensterkante ist die Sprache
   anderer Werkzeuge. */
.sidebar > .sidebar-content,
.nav-scroll {
  overflow-y: auto;
  /* Ausdrücklich zu, damit das negative Außenmaß nie einen zweiten,
     waagerechten Balken auslöst. */
  overflow-x: hidden;
  margin-right: calc(var(--nav-scroll-bleed, 0px) * -1);
  margin-left: calc(var(--sp-2) * -1);
  padding: var(--sp-2);
  /* Die Rinne für den Balken bleibt reserviert, auch wenn die Liste
     kurz genug ist, um nicht zu rollen. Ohne das endeten die Zeilen
     mal 20px und mal 8px vor der Kante, je nach Länge der Liste, und
     auf macOS mit überlagernden Balken immer bei 8px, also nie
     spiegelbildlich zu den 18px links. */
  scrollbar-gutter: stable;
  /* Firefox kennt nur diese zwei Eigenschaften, WebKit und Blink nur
     die ::-webkit-Pseudoelemente weiter unten. Jede Maschine nimmt
     sich ihren Teil und übergeht den anderen. */
  scrollbar-width: thin;
  scrollbar-color: transparent transparent;
  /* Beim Ein- und Ausklappen springt --nav-scroll-bleed sofort von
     18px auf --sp-2, das Polster der Leiste braucht dafür --dur-med.
     Ohne diesen Übergang stünde die Rollfläche währenddessen über die
     Polsterkante hinaus und würde vom overflow: hidden der Leiste
     beschnitten. Übergangsfähig ist der berechnete Außenabstand, der
     seinen Wert von -18px auf -8px ändert, nicht die Custom Property
     selbst: die ist nicht registriert und schaltet hart um. */
  transition: margin-right var(--dur-med) var(--ease-in-out),
              scrollbar-color var(--dur-med) var(--ease-out);
}

/* Im Ruhezustand ist der Balken durchsichtig. Er erscheint, sobald der
   Zeiger auf der Leiste liegt oder die Tastatur hineinfährt, also
   genau dann, wenn gerollt werden kann: Rollen mit Rad oder Trackpad
   setzt voraus, dass der Zeiger über der Leiste steht, :hover ist
   damit ein verlässlicher Stellvertreter, und :focus-within fängt den
   Fall ab, dass jemand sich mit der Tabulatortaste durch die Punkte
   arbeitet. Kein JavaScript nötig. */
.sidebar:hover > .sidebar-content,
.sidebar > .sidebar-content:focus-within,
.nav-scroll:hover,
.nav-scroll:focus-within {
  scrollbar-color: var(--fg-faint) transparent;
}

/* WebKit und Blink: 12px Spur, davon 3px durchsichtiger Rahmen auf
   jeder Seite. Sichtbar bleiben 6px Daumen, das ist die Breite des
   macOS-Balkens, greifbar bleibt die volle Spur.
   background-clip: padding-box lässt die Farbe im Rahmen aufhören,
   das ist der Abstand zum Rand.
   Diese Regeln überschreiben die globale Balkenregel weiter oben, die
   entgegen dem ersten Anschein nicht nur den Hauptbereich trifft,
   sondern jeden Rollbehälter. Spezifität 0,2,1 gegen 0,0,1, und die
   globale Überfahrregel (0,1,1) verliert ebenfalls, sonst spränge der
   Daumen beim Überfahren von 6px auf 8px.
   Kein transition hier: Blink und WebKit führen Übergänge auf
   Rollbalken-Pseudoelementen nicht aus. Er steht deshalb oben auf
   scrollbar-color, wo ihn wenigstens Firefox ausführt. */
.sidebar > .sidebar-content::-webkit-scrollbar,
.nav-scroll::-webkit-scrollbar {
  width: 12px;
}
.sidebar > .sidebar-content::-webkit-scrollbar-track,
.nav-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.sidebar > .sidebar-content::-webkit-scrollbar-thumb,
.nav-scroll::-webkit-scrollbar-thumb {
  background-color: transparent;
  background-clip: padding-box;
  border: 3px solid transparent;
  border-radius: var(--r-pill);
  /* Bei rund 90 Einträgen wäre der Daumen sonst ein Strich von wenigen
     Pixeln und mit der Maus nicht mehr zu fassen. */
  min-height: 36px;
}
.sidebar:hover > .sidebar-content::-webkit-scrollbar-thumb,
.sidebar > .sidebar-content:focus-within::-webkit-scrollbar-thumb,
.nav-scroll:hover::-webkit-scrollbar-thumb,
.nav-scroll:focus-within::-webkit-scrollbar-thumb {
  background-color: var(--fg-faint);
}
/* Direkt unter dem Zeiger und beim Ziehen deutlicher, wie unter macOS.
   --fg-faint und --fg-dim statt der --scrollbar-*-Marken, weil letztere
   mit 0,09 Deckung im Dunkeln auf 6px Breite nicht mehr zu sehen sind.
   Beide gewählten Marken kippen mit dem Modus mit. */
.sidebar > .sidebar-content::-webkit-scrollbar-thumb:hover,
.sidebar > .sidebar-content::-webkit-scrollbar-thumb:active,
.nav-scroll::-webkit-scrollbar-thumb:hover,
.nav-scroll::-webkit-scrollbar-thumb:active {
  background-color: var(--fg-dim);
}

/* Grober Zeiger kennt kein Überfahren. Dort bleibt der Daumen sichtbar,
   sonst gäbe es auf dem Tablet keinerlei Hinweis, dass da noch mehr
   kommt. */
@media (hover: none) {
  .sidebar > .sidebar-content,
  .nav-scroll {
    scrollbar-color: var(--fg-faint) transparent;
  }
  .sidebar > .sidebar-content::-webkit-scrollbar-thumb,
  .nav-scroll::-webkit-scrollbar-thumb {
    background-color: var(--fg-faint);
  }
}

/* Eingeklappt ist die Schiene 56px breit. Eine dauerhaft reservierte
   Rinne von 12px zöge die mittig gesetzten Sinnbilder um rund 6px nach
   links. Ohne Rinne und ohne Balken sitzen sie genau in der Mitte,
   gerollt wird trotzdem. Der Balken fehlt dort nicht: eine Schiene aus
   Sinnbildern zeigt ihre Länge über die Sinnbilder. */
@media (min-width: 1024px) {
  .sidebar.is-collapsed > .sidebar-content {
    scrollbar-gutter: auto;
    scrollbar-width: none;
  }
  .sidebar.is-collapsed > .sidebar-content::-webkit-scrollbar {
    width: 0;
  }
}

/* Bei reduzierter Bewegung fällt der Farbwechsel des Balkens weg. Der
   Außenabstand bleibt übergangsfähig, und zwar nicht als Effekt: die
   Leiste bewegt ihr Polster beim Ein- und Ausklappen weiterhin über
   --dur-med, die Rollfläche muss dabei mitgehen, sonst wird sie
   währenddessen beschnitten. */
@media (prefers-reduced-motion: reduce) {
  .sidebar > .sidebar-content,
  .nav-scroll {
    transition: margin-right var(--dur-med) var(--ease-in-out);
  }
}

/* ── Collapsible nav-section ──────────────────────────── */

/* Header used when no Href — entire row is the toggle button */
.nav-section-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  background: none;
  border: none;
  /* Gleiche Gestaltung wie .nav-title, damit aufklappbare und feste
     Gruppen in derselben Leiste nicht zwei verschiedene Titelsorten
     ergeben. Die Begründung der einzelnen Werte steht dort. */
  font-family: var(--font-sans);
  font-size: var(--fs-footnote);
  font-weight: 600;
  letter-spacing: -0.005em;
  text-transform: none;
  color: var(--fg-dim);
  padding: 22px 12px 4px;
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-out);
  line-height: 1;
  text-align: left;
}
/* Ruhezustand ist jetzt --fg-dim, das Überfahren muss also darüber
   liegen, sonst wäre die Regel wirkungslos. */
.nav-section-toggle:hover { color: var(--fg-muted); }
.nav-section-toggle .ico { width: 14px; height: 14px; flex: 0 0 14px; opacity: 0.6; }
.nav-section-toggle-label { flex: 1; }

/* Header row used when Href is set — NavLink + separate chevron button */
.nav-section-header {
  display: flex;
  align-items: center;
  padding: 10px 4px 2px;
  gap: 2px;
}
.nav-section-link {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  height: 34px;
  padding: 0 12px;
  border-radius: var(--r-sm);
  color: var(--fg-muted);
  font-size: var(--fs-callout);
  font-weight: 500;
  text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
  user-select: none;
  min-width: 0;
  position: relative;
}
.nav-section-link:hover { background: var(--glass-2); color: var(--fg); }
.nav-section-link.active {
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent-a) 18%, transparent), color-mix(in srgb, var(--accent-b) 4%, transparent));
  color: var(--fg);
  box-shadow: inset 0 0 0 1px var(--accent-line), inset 0 0 18px color-mix(in srgb, var(--accent-a) 15%, transparent);
}
.nav-section-link.active::before {
  content: '';
  position: absolute;
  left: -22px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 18px;
  border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: var(--accent-grad);
  box-shadow: 0 0 12px var(--accent-a);
}
.nav-section-link .ico { width: 16px; height: 16px; flex: 0 0 16px; opacity: 0.8; }

.nav-section-chevron-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: 0 0 28px;
  background: none;
  border: none;
  border-radius: var(--r-sm);
  color: var(--fg-faint);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.nav-section-chevron-btn:hover { background: var(--glass-2); color: var(--fg-muted); }

/* Rotating chevron (shared by both header variants) */
.nav-section-chevron {
  display: block;
  transition: transform var(--dur-med) var(--ease-out);
  transform: rotate(0deg);
  flex: 0 0 auto;
}
.nav-section-chevron.is-open { transform: rotate(90deg); }

/* Animated collapse container — CSS grid trick (no fixed max-height) */
.nav-children {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-med) var(--ease-out);
}
.nav-children.is-open { grid-template-rows: 1fr; }
.nav-children-inner {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-bottom: 2px;
}

/* Indented sub nav-items */
.nav-item--sub {
  padding-left: 30px;
  height: 30px;
  font-size: var(--fs-subhead);
}

.topbar {
  height: var(--appbar-h);
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 32px;
  border-bottom: 1px solid var(--line);
  background: var(--bar-bg);
  backdrop-filter: blur(var(--glass-blur-strong));
  -webkit-backdrop-filter: blur(var(--glass-blur-strong));
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}
/* Raised elevation: lifts the bar off the content with a shadow and a denser
   glass tint. Flat (default) keeps the original flush look. */
.topbar.is-raised {
  background: var(--glass-2);
  box-shadow: var(--shadow-md);
}
/* Slotted layout (DrylAppBar Start / Center / End). Only rendered when at least
   one of those slots is used; plain ChildContent keeps the flat flex row above.
   Start and End flex equally so Center stays optically centred between them. */
.topbar-start  { display: flex; align-items: center; gap: var(--sp-3); flex: 1 1 0; min-width: 0; }
.topbar-center { display: flex; align-items: center; gap: var(--sp-3); flex: 0 1 auto; justify-content: center; min-width: 0; }
.topbar-end    { display: flex; align-items: center; gap: var(--sp-3); flex: 1 1 0; min-width: 0; justify-content: flex-end; }
.crumb {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: var(--fs-callout);
  color: var(--fg-muted);
}
.crumb .sep { color: var(--fg-faint); }
.crumb b { color: var(--fg); font-weight: 500; }

.search {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 12px;
  border-radius: var(--r-md);
  background: var(--glass-1);
  border: 1px solid var(--line);
  color: var(--fg-muted);
  width: 320px;
  font-size: var(--fs-footnote);
  transition: all var(--dur-med) var(--ease-out);
  cursor: text;
}
.search:hover { border-color: var(--line-strong); }
.search:focus-within { border-color: var(--accent-line); box-shadow: 0 0 0 4px var(--accent-soft); }
.search input { background: transparent; border: 0; outline: 0; color: var(--fg); width: 100%; font: 400 var(--fs-body) var(--font-sans); }
.search input::placeholder { color: var(--fg-dim); }

/* Topbar hardening — page-level chrome, so a viewport media query (consistent
   with the existing app-shell breakpoints). On small screens the bar tightens
   its padding and the fixed-width search becomes flexible so nothing clips off
   the right edge. */
@media (max-width: 768px) {
  .topbar { padding: 0 var(--sp-4); gap: var(--sp-3); }
  .search { width: auto; min-width: 0; flex: 1 1 auto; }
}

.main {
  /* The only scroll region inside the app shell: the sidebar and top bar are
     fixed, page content scrolls here. It spans the full grid track so its
     scrollbar sits flush at the viewport edge — the reading-width cap lives on
     .main-inner, never on the scroll container itself. (A max-width here would
     make .main narrower than its track on wide viewports and centre it, so the
     scrollbar would float inward "in the middle of the page".) min-height:0
     lets this grid row shrink below its content so overflow-y can take effect. */
  width: 100%;
  min-height: 0;
  overflow-y: auto;
}
/* Inner column — caps the reading width and centres it inside the full-width
   scroll region. Padding lives here (not on .main) so the scrollbar stays flush
   against the viewport edge instead of hugging the padded content box. */
.main-inner {
  max-width: 1400px;
  margin-inline: auto;
  padding: 40px 48px 80px;
}

/* ============== KBD / CODE ============== */
.kbd {
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  padding: 2px 6px;
  border-radius: var(--r-2xs);
  border: 1px solid var(--line-strong);
  background: var(--glass-2);
  color: var(--fg-muted);
}
code, .code-inline {
  font-family: var(--font-mono);
  font-size: var(--fs-footnote);
  padding: 1px 6px;
  border-radius: var(--r-2xs);
  background: var(--glass-2);
  border: 1px solid var(--line);
  color: var(--accent-fg);
}

.code-block {
  position: relative;
  border-radius: var(--r-md);
  background: var(--code-bg);
  border: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--fs-footnote);
  line-height: 1.6;
  padding: 16px 18px;
  color: var(--code-fg);
  overflow: auto;
  box-shadow: inset 0 0 60px color-mix(in srgb, var(--accent-a) 5%, transparent);
}
.code-block .tok-keyword { color: #c4b5fd; }
.code-block .tok-tag     { color: #67e8f9; }
.code-block .tok-attr    { color: #fbcfe8; }
.code-block .tok-string  { color: #86efac; }
.code-block .tok-comment { color: var(--fg-faint); font-style: italic; }
.code-block .tok-prop    { color: #fde68a; }

/* ============== SECTIONS ============== */
.section { margin-bottom: 56px; }
.section-head { margin-bottom: 22px; display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
.section-head h2 { letter-spacing: -0.025em; }
.section-head p { max-width: 56ch; }

.swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 12px;
}
.swatch {
  position: relative;
  height: 96px;
  border-radius: var(--r-md);
  border: 1px solid var(--line);
  overflow: hidden;
  padding: 12px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  transition: transform var(--dur-med) var(--ease-out);
  cursor: pointer;
}
.swatch:hover { transform: translateY(-2px); }
.swatch .name { font-size: var(--fs-caption1); font-weight: 500; }
.swatch .val { font-family: var(--font-mono); font-size: var(--fs-caption2); color: var(--fg-dim); margin-top: 2px; }

.type-row {
  display: grid;
  grid-template-columns: 140px 1fr 160px;
  gap: 24px;
  align-items: baseline;
  padding: 18px 0;
  border-bottom: 1px solid var(--line);
}
.type-row:last-child { border-bottom: 0; }
.type-row .label { font-family: var(--font-mono); font-size: var(--fs-caption2); color: var(--fg-dim); letter-spacing: 0.08em; text-transform: uppercase; }
.type-row .spec  { font-family: var(--font-mono); font-size: var(--fs-caption2); color: var(--fg-dim); text-align: right; }

/* Stats / metric cards */
.metric {
  padding: 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 130px;
  position: relative;
  overflow: hidden;
}
.metric .label { font-size: var(--fs-caption1); color: var(--fg-muted); display: flex; align-items: center; gap: 8px; }
.metric .value { font-size: var(--fs-large); font-weight: 600; letter-spacing: -0.025em; line-height: 1; }
.metric .delta { font-family: var(--font-mono); font-size: var(--fs-caption2); }
.metric .delta.up { color: var(--success); }
.metric .delta.down { color: var(--danger); }
.metric .spark {
  position: absolute;
  right: 16px;
  bottom: 16px;
  width: 96px;
  height: 32px;
  opacity: 0.7;
}

/* Tables — base (used by prototype and inline usage) */
.tbl {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--fs-callout);
  /* Zahlenspalten: Apples Antwort auf Zahlenmaterial ist nicht SF Mono,
     sondern die Systemschrift mit gleich breiten Ziffern
     (monospacedDigitSystemFont). Erst dadurch darf die dicktengleiche
     Schrift aus Kopf und Zellen verschwinden, ohne dass Beträge in einer
     Spalte gegeneinander verrutschen. Nicht font-feature-settings, das
     würde die geerbte Liste "ss01","cv11" von <html> überschreiben. */
  font-variant-numeric: tabular-nums;
}
.tbl th {
  text-align: left;
  font-weight: 500;
  font-size: var(--fs-footnote);
  letter-spacing: normal;
  text-transform: none;
  color: var(--fg-dim);
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--line-strong);
  font-family: var(--font-sans);
}
.tbl td {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
  color: var(--fg-muted);
  vertical-align: middle;
  /* Zurück auf den normalen Zeilenumbruch. Die Grundabsicherung setzt auf
     jeder Glaskarte overflow-wrap: anywhere, und in einer Tabelle ist das
     verkehrt herum gedacht: der Browser zerlegt dann lieber ein Wort, als die
     Spalte wachsen zu lassen, aus "kundendienst-triage" wird "kundendien" plus
     "st-triage". Mit dem normalen Umbruch bricht die Zelle an Leerzeichen und
     Bindestrichen, und wenn wirklich nichts mehr passt, rollt .tbl-wrap
     waagerecht, wofür es da ist. */
  overflow-wrap: normal;
}
.tbl tr:hover td { background: var(--hover-wash); color: var(--fg); }
.tbl tr:last-child td { border-bottom: 0; }
.tbl .name { color: var(--fg); font-weight: 500; }

/* ============== TABLE COMPONENT (DrylTable) ============== */

/* Outer wrapper — clips overflow for rounded card corners */
.tbl-root { overflow: hidden; }

/* Sticky header — needs a semi-opaque background so content scrolls under */
.tbl-root .tbl th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--panel-sticky);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
}

/* Horizontal scroll container */
.tbl-wrap { overflow-x: auto; }

/* Row interaction */
.tbl-root .tbl tbody tr {
  transition: background var(--dur-fast) var(--ease-out);
}
.tbl-root .tbl tbody tr:hover td { background: var(--hover-wash); color: var(--fg); }

/* Selected row */
.tbl-root .tbl tbody tr.tbl-row--selected td {
  background: color-mix(in srgb, var(--accent-a) 7%, transparent);
}
.tbl-root .tbl tbody tr.tbl-row--selected:hover td {
  background: color-mix(in srgb, var(--accent-a) 12%, transparent);
}

/* Checkbox column */
.tbl-col-check {
  width: 44px;
  padding-left: var(--sp-4);
  padding-right: 0;
}

/* Primary cell — full fg color + medium weight */
.tbl td.tbl-td-primary { color: var(--fg); font-weight: 500; }

/* Empty state row */
.tbl-root .tbl tbody tr.tbl-empty-row td {
  text-align: center;
  padding: var(--sp-8) var(--sp-4);
  color: var(--fg-dim);
  border-bottom: none;
}

/* Loading state row */
.tbl-root .tbl tbody tr.tbl-loading-row td {
  border-bottom: none;
  padding: 0;
}

/* AI-generated row — fades in with a slight rise and a brief accent flash
   that lingers for ~1.6s before settling. Composes with the table's wrapper
   .ai-aura ring/glow when those are active. */
.tbl-root .tbl tbody tr.tbl-row--ai-enter td {
  animation:
    tbl-row-ai-rise var(--dur-slow) var(--ease-out) both,
    tbl-row-ai-flash 1600ms var(--ease-out) both;
}

/* AI mode on the table wrapper — the .tbl-root has overflow:hidden so it can
   clip content to its rounded corners, which would otherwise eat the outer
   ring and glow. These overrides pull the AI signals inside the bounds:
   the ring traces the very-outermost pixel row, and the glow uses inset
   box-shadows so it lives within the clipped region too. */
.tbl-root .ai-aura-ring { inset: 0; }
.tbl-root .ai-aura-glow {
  box-shadow:
    inset 0 0 0 1px var(--accent-line),
    inset 0 0 48px color-mix(in srgb, var(--ai-a) 18%, transparent),
    inset 0 0 96px color-mix(in srgb, var(--ai-b) 8%, transparent);
}
/* Suppress the Generated lift on the table — visually distracting on a tall
   surface. The wash + row entry animations carry the reveal moment. */
.tbl-root.ai-aura.ai-generated { animation: none; }

@keyframes tbl-row-ai-rise {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes tbl-row-ai-flash {
  0%   { background-color: color-mix(in srgb, var(--ai-a) 16%, transparent); }
  60%  { background-color: color-mix(in srgb, var(--ai-a) 6%, transparent); }
  100% { background-color: transparent; }
}

@media (prefers-reduced-motion: reduce) {
  .tbl-root .tbl tbody tr.tbl-row--ai-enter td { animation: none; }
}

/* ============== TABLE KPI SUMMARY BAR ============== */

/* Grid of KPI tiles above the table header */
.tbl-summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  border-bottom: 1px solid var(--line);
}

/* Individual KPI tile */
.tbl-kpi {
  padding: var(--sp-4);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  min-width: 0;
  overflow: hidden;
}
.tbl-kpi:last-child { border-right: none; }

.tbl-kpi-label {
  font-size: var(--fs-caption1);
  color: var(--fg-muted);
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tbl-kpi-value {
  font-size: var(--fs-large);
  font-weight: 700;
  color: var(--fg);
  line-height: 1.05;
  letter-spacing: -0.025em;
  font-feature-settings: "tnum";
}

.tbl-kpi-footer {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-2);
  min-height: var(--sp-6);
}

.tbl-kpi-delta {
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-caption2);
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: var(--sp-1);
  align-self: flex-end;
}
.tbl-kpi-delta--positive { color: var(--success); }
.tbl-kpi-delta--negative { color: var(--danger); }
.tbl-kpi-delta--neutral  { color: var(--fg-muted); }

/* Sparkline */
.tbl-kpi-sparkline {
  width: 96px;
  height: 32px;
  flex-shrink: 0;
  margin-left: auto;
  opacity: 0.7;
  transition: opacity var(--dur-med) var(--ease-out);
}
.tbl-kpi:hover .tbl-kpi-sparkline { opacity: 1; }
.tbl-kpi-sparkline polyline {
  stroke: var(--accent-b);
  fill: none;
}

/* ============== TABLE TOOLBAR + SORT (Phase 1) ============== */

/* Toolbar above the table — search input on the left, extras on the right.
   Sits between the optional KPI summary bar and the table header. */
.tbl-toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}

/* Search wrapper — pins icon + input together, takes natural width on the left. */
.tbl-search {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 240px;
  max-width: 360px;
  flex: 0 1 auto;
}
.tbl-search .input { width: 100%; }
.tbl-search .input-icon-leading { left: var(--sp-3); pointer-events: none; }

/* Extras slot — pushed to the far right of the toolbar. */
.tbl-toolbar-extra {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Sortable header — click target, keyboard-focusable. */
.tbl th.tbl-th-sort {
  cursor: pointer;
  user-select: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.tbl th.tbl-th-sort:hover { color: var(--fg-muted); }
.tbl th.tbl-th-sort:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: -1px;
}
.tbl th.tbl-th-sort[aria-sort="ascending"],
.tbl th.tbl-th-sort[aria-sort="descending"] {
  color: var(--fg);
}

/* Header inner layout — label + sort icon side-by-side. */
.tbl th.tbl-th-sort .tbl-th-label { display: inline; }
.tbl-th-sort-icon {
  display: inline-flex;
  align-items: center;
  margin-left: var(--sp-1);
  vertical-align: -2px;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.tbl th.tbl-th-sort:hover .tbl-th-sort-icon { opacity: 0.5; }
.tbl-th-sort-icon.is-active { opacity: 1; }

/* Column alignment helpers */
.tbl-align-center { text-align: center; }
.tbl-align-end    { text-align: right; }

/* EINZIGE NEUE REGEL DIESES DURCHGANGS, alles andere ist reine Wertänderung.
   Sie greift auf Klassen zu, die im Markup längst vergeben sind, und fügt
   weder Klasse noch ID noch data-Haken hinzu; die Drop-in-Prüfung bleibt
   deshalb grün. Wer sie nicht will, löscht diesen Block, sonst ändert sich
   nichts.

   Zweck: .tbl-align-end und .tbl-align-center wiegen 0-1-0 und verlieren in
   der Kopfzelle gegen .tbl th { text-align: left } mit 0-1-1. Die Beschriftung
   einer rechtsbündigen Zahlenspalte stand deshalb links über den Zahlen, an
   36 Stellen im Markup. Zusätzlich ist .tbl-th-inner ein Flex-Kasten, in dem
   text-align allein ohnehin nichts ausrichtet, die Kante braucht also
   justify-content. Apple stellt für Zahlen eigens TableColumnAlignment.numeric
   bereit, die Spaltenkante ist dort nicht dekorativ, sondern die Leselinie. */
.tbl th.tbl-align-end   { text-align: right; }
.tbl th.tbl-align-center { text-align: center; }
.tbl th.tbl-align-end   .tbl-th-inner { justify-content: flex-end; }
.tbl th.tbl-align-center .tbl-th-inner { justify-content: center; }
.tbl th.tbl-align-end   .tbl-th-clickable,
.tbl th.tbl-align-center .tbl-th-clickable { flex: 0 1 auto; }

/* Header inner layout — splits the sort-click area from the filter trigger so
   clicking the filter icon does NOT trigger a sort. */
.tbl-th-inner {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--sp-1);
}
.tbl-th-clickable {
  display: inline-flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
}
.tbl th.tbl-th-sort .tbl-th-clickable {
  cursor: pointer;
  user-select: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.tbl th.tbl-th-sort .tbl-th-clickable:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

/* ============== TABLE FILTERS (Phase 2) ============== */

/* Tiny filter trigger button rendered in the header next to the title. */
.tbl-filter-trigger {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  color: var(--fg-dim);
  cursor: pointer;
  padding: 2px 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tbl-filter-trigger:hover { color: var(--fg-muted); background: var(--glass-2); }
.tbl-filter-trigger:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: 1px;
}
.tbl-filter-trigger.is-active {
  color: var(--accent-a);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

/* Popover anchored to the right edge of the column header. Sits above the
   table body via z-index and the .tbl-th-inner positioning context. */
.tbl-filter-popover {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: var(--z-drawer);
  min-width: 240px;
  max-width: 320px;
  background: var(--bg-2);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  padding: 0;
  /* Inherits from the header cell; the reset below keeps the popover on plain
     body text no matter what the header carries. Since the Apple pass the
     header itself is sans, title-case and untracked, so this is now a guard
     rather than an undo. */
  font-family: var(--font-sans);
  font-size: var(--fs-footnote);
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg);
  animation: tbl-filter-popover-in var(--dur-med) var(--ease-out);
}
@keyframes tbl-filter-popover-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

.tbl-filter-popover-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.tbl-filter-popover-title {
  font-weight: 500;
  color: var(--fg);
}
.tbl-filter-popover-close {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 2px;
  border-radius: var(--r-xs);
  display: inline-flex;
}
.tbl-filter-popover-close:hover { color: var(--fg); }

.tbl-filter-popover-body {
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-height: 280px;
  overflow-y: auto;
}
.tbl-filter-popover-body .input { width: 100%; }

.tbl-filter-option {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 4px 0;
  cursor: pointer;
  color: var(--fg-muted);
}
.tbl-filter-option:hover { color: var(--fg); }
.tbl-filter-option .checkbox { flex: 0 0 auto; }

.tbl-filter-empty {
  color: var(--fg-dim);
  font-style: italic;
}

.tbl-filter-popover-footer {
  display: flex;
  justify-content: space-between;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  border-top: 1px solid var(--line);
}

/* Active-filter chips shown in the toolbar. */
.tbl-filter-chips {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.tbl-filter-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-1) var(--sp-1) var(--sp-2);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  font-size: var(--fs-caption1);
  color: var(--fg);
}
.tbl-filter-chip-label { color: var(--fg-muted); }
.tbl-filter-chip-value { font-weight: 500; }
.tbl-filter-chip-close {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 2px;
  border-radius: var(--r-pill);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.tbl-filter-chip-close:hover { color: var(--fg); background: var(--press-wash); }

/* ============== TABLE FOOTER + PAGINATION (Phase 2) ============== */

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

.tbl-pagination {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  flex-wrap: wrap;
}
.tbl-pagination-info {
  color: var(--fg-muted);
  font-size: var(--fs-caption1);
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
}
.tbl-pagination-size {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--fg-muted);
  font-size: var(--fs-caption1);
}
.tbl-pagination-size .select { padding-top: 4px; padding-bottom: 4px; min-width: 64px; }

.tbl-pagination-pages {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: auto;
}
.tbl-pagination-numbers {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
}
/* On a narrow slot the numbered page buttons collapse, leaving the
   first/prev/next/last controls and the "Showing x–y of z" summary. */
@container (max-width: 480px) {
  .tbl-pagination-numbers { display: none; }
}
.tbl-pagination-gap {
  color: var(--fg-dim);
  font-family: var(--font-sans);
  padding: 0;
  user-select: none;
}

/* ============== TABLE GROUPING / DETAIL / ACTIONS / BULK (Phase 3) ============== */

/* Group header row — sits between groups, title-case label + count badge.
   Sticky under the table header when grouping + sticky-header are both active. */
.tbl-root .tbl tbody tr.tbl-group-header td {
  background: var(--glass-2);
  border-bottom: 1px solid var(--line);
  padding: 0;
}
.tbl-root .tbl tbody tr.tbl-group-header:hover td { background: var(--glass-2); }

.tbl-group-toggle {
  appearance: none;
  background: transparent;
  border: 0;
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4);
  text-align: left;
  cursor: pointer;
  color: var(--fg-muted);
  font-family: var(--font-sans);
  font-size: var(--fs-footnote);
  letter-spacing: normal;
  text-transform: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.tbl-group-toggle:hover { color: var(--fg); }
.tbl-group-toggle:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: -1px;
}

.tbl-group-chevron {
  display: inline-flex;
  align-items: center;
  color: var(--fg-dim);
  transition: transform var(--dur-fast) var(--ease-out);
}
.tbl-group-chevron.is-collapsed { transform: rotate(-90deg); }

.tbl-group-label {
  color: var(--fg);
  flex: 1 1 auto;
}

.tbl-group-count {
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-caption2);
  color: var(--fg-dim);
  /* 2px bleibt bewusst neben der Abstandsskala: --sp-1 machte die Zählpille
     höher als die Gruppenzeile. Der Wert gleicht .tbl-bulk-bar-count an,
     das gleich gemeinte Bauteil. */
  padding: 2px var(--sp-2);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--glass-1);
}

/* Expand column — chevron toggle on the row for DetailTemplate. */
.tbl-col-expand {
  width: 36px;
  padding-left: var(--sp-3);
  padding-right: 0;
}

.tbl-expand-btn {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  color: var(--fg-dim);
  cursor: pointer;
  padding: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition:
    color var(--dur-fast) var(--ease-out),
    transform var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tbl-expand-btn:hover { color: var(--fg); background: var(--glass-2); }
.tbl-expand-btn:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: 1px;
}
.tbl-expand-btn.is-expanded {
  color: var(--accent-a);
  transform: rotate(90deg);
}

/* Reorder grip column — leading drag handle when Reorderable is set. */
.tbl-col-grip {
  width: 32px;
  padding-left: var(--sp-3);
  padding-right: 0;
}

.tbl-grip {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  color: var(--fg-dim);
  cursor: grab;
  padding: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition:
    color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tbl-grip:hover { color: var(--fg); background: var(--glass-2); }
.tbl-grip:active { cursor: grabbing; }
.tbl-grip:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: 1px;
  color: var(--fg);
}
.tbl-grip:disabled {
  cursor: default;
  opacity: 0.4;
  background: transparent;
  color: var(--fg-dim);
}

/* Row being dragged — dims so the drop target reads clearly. */
.tbl-root .tbl tbody tr.tbl-row--dragging td {
  opacity: 0.45;
}

/* Drop target — an accent line traces the row the drag is hovering over. */
.tbl-root .tbl tbody tr.tbl-row--drop-target td {
  box-shadow: inset 0 2px 0 0 var(--accent-a);
}

@media (prefers-reduced-motion: reduce) {
  .tbl-grip { transition: none; }
}

/* Detail row — full-width panel that slides open under the expanded row. */
.tbl-root .tbl tbody tr.tbl-row-detail td {
  background: var(--bg-2);
  padding: 0;
  border-bottom: 1px solid var(--line);
}
.tbl-root .tbl tbody tr.tbl-row-detail:hover td {
  background: var(--bg-2);
  color: var(--fg-muted);
}
.tbl-row-detail-inner {
  padding: var(--sp-4);
  color: var(--fg-muted);
  animation: tbl-detail-in var(--dur-med) var(--ease-out);
}
@keyframes tbl-detail-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Trailing per-row actions column. */
th.tbl-col-actions,
td.tbl-col-actions {
  width: 1%;
  white-space: nowrap;
  text-align: right;
  padding-right: var(--sp-4);
}
.tbl-row-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  justify-content: flex-end;
}

/* ============== TABLE INLINE EDITING ============== */

/* The row currently being edited gets a soft accent wash so it stands apart from
   the rest of the body, including under the hover rule. */
.tbl-root .tbl tbody tr.tbl-row--editing td,
.tbl-root .tbl tbody tr.tbl-row--editing:hover td {
  background: var(--accent-soft);
}

/* A cell hosting an inline editor sheds the text padding so the control fills the
   cell, and lets DRYL inputs stretch to the column width. */
td.tbl-td-editing {
  padding-top: var(--sp-1);
  padding-bottom: var(--sp-1);
}
td.tbl-td-editing .input,
td.tbl-td-editing .select,
td.tbl-td-editing .textarea,
td.tbl-td-editing input,
td.tbl-td-editing select {
  width: 100%;
}

/* Edit affordances in the actions column: pencil (enter), commit, cancel. */
.tbl-edit-btn {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--fg-muted);
  cursor: pointer;
  transition:
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tbl-edit-btn:hover {
  border-color: var(--line-strong);
  color: var(--fg);
  background: var(--glass-1);
}
.tbl-edit-btn:focus-visible {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.tbl-edit-btn--commit {
  border-color: var(--accent-line);
  color: var(--accent-a);
}
.tbl-edit-btn--commit:hover {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent-a);
}
.tbl-edit-btn--cancel:hover {
  border-color: var(--danger);
  color: var(--danger);
  background: transparent;
}

/* ============== TABLE COLUMN PIN / RESIZE / REORDER ============== */

/* Pinned (frozen) columns — the layout helper sets the per-column left/right offset; CSS owns
   the sticky behaviour, opaque backing and layering. */
.tbl .tbl-pin {
  position: sticky;
  z-index: 1;
  background: var(--bg-2);
}
/* Keep pinned body cells opaque through hover so scrolling content can't bleed under them. */
.tbl-root .tbl tbody tr td.tbl-pin,
.tbl-root .tbl tbody tr:hover td.tbl-pin {
  background: var(--bg-2);
}
/* Pinned header cells sit above the sticky header row and the pinned body cells. */
.tbl-root .tbl thead th.tbl-pin {
  z-index: 4;
}
/* Edge rule that separates a frozen column from the scrolling area. */
.tbl .tbl-pin-start { box-shadow: inset -1px 0 0 var(--line); }
.tbl .tbl-pin-end   { box-shadow: inset 1px 0 0 var(--line); }

/* Column resize grip — sits at the right edge of a resizable header. */
.tbl-col-resize {
  position: absolute;
  top: 0;
  right: 0;
  width: 9px;
  height: 100%;
  cursor: col-resize;
  touch-action: none;
  z-index: 2;
}
.tbl-col-resize::after {
  content: "";
  position: absolute;
  top: 20%;
  right: 3px;
  width: 2px;
  height: 60%;
  border-radius: var(--r-pill);
  background: var(--line);
  opacity: 0;
  transition:
    opacity var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tbl th:hover .tbl-col-resize::after { opacity: 1; }
.tbl-col-resize:hover::after { background: var(--accent-line); opacity: 1; }

/* While a resize drag is in progress, lock text selection for a clean drag. */
.tbl-resizing { user-select: none; cursor: col-resize; }

/* Reorderable headers — grab affordance + drag/drop states. */
.tbl th[draggable="true"] .tbl-th-clickable { cursor: grab; }
.tbl th.tbl-th--col-dragging { opacity: 0.5; }
.tbl th.tbl-th--col-drop-target {
  box-shadow: inset 2px 0 0 var(--accent-a);
}

/* Floating bulk-action bar — slides in from the top when a row is selected. */
.tbl-bulk-bar {
  position: sticky;
  top: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background:
    var(--sheen-grad),
    var(--bg-2);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  border-bottom: 1px solid var(--accent-line);
  box-shadow: inset 0 -1px 0 var(--accent-line);
  animation: tbl-bulk-in var(--dur-med) var(--ease-spring);
}
@keyframes tbl-bulk-in {
  from { transform: translateY(-100%); opacity: 0; }
  to   { transform: translateY(0);     opacity: 1; }
}

.tbl-bulk-bar-info {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-2);
}
.tbl-bulk-bar-count {
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-subhead);
  font-weight: 600;
  color: var(--accent-a);
  padding: 2px 8px;
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  background: var(--accent-soft);
}
.tbl-bulk-bar-label {
  color: var(--fg-muted);
  font-size: var(--fs-caption1);
  letter-spacing: 0.02em;
}
.tbl-bulk-bar-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-left: var(--sp-2);
}
.tbl-bulk-bar-close {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 4px;
  border-radius: var(--r-xs);
  display: inline-flex;
  align-items: center;
  margin-left: auto;
  transition: color var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.tbl-bulk-bar-close:hover { color: var(--fg); background: var(--glass-2); }

@media (prefers-reduced-motion: reduce) {
  .tbl-bulk-bar { animation: none; }
  .tbl-row-detail-inner { animation: none; }
}

/* ============== TABLE VIRTUALIZE / STICKY / COLUMN MENU (Phase 4) ============== */

/* Scrollable body — used when Height is set or Virtualize is on. The sticky
   header (default) anchors to the top of this scroll area. */
.tbl-wrap--scroll {
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* Opt-out for the sticky header — applied on the .tbl-root wrapper. */
.tbl-root.tbl-no-sticky .tbl th {
  position: static;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Column-visibility menu trigger sits in the toolbar next to extras. */
.tbl-col-menu-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.tbl-col-menu-wrap--auto { margin-left: auto; }

/* Toolbar action cluster (e.g. CSV export). When it is the first right-aligned
   element (no custom ToolbarContent), it anchors the right group. */
.tbl-toolbar-action {
  display: inline-flex;
  align-items: center;
}
.tbl-toolbar-action--auto { margin-left: auto; }

.tbl-col-menu-trigger {
  appearance: none;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  color: var(--fg-dim);
  cursor: pointer;
  padding: 6px 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition:
    color var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tbl-col-menu-trigger:hover { color: var(--fg); border-color: var(--line-strong); background: var(--glass-2); }
.tbl-col-menu-trigger:focus-visible {
  outline: 1px solid var(--accent-line);
  outline-offset: 1px;
}
.tbl-col-menu-trigger.is-active {
  color: var(--accent-a);
  border-color: var(--accent-line);
  background: var(--accent-soft);
}

/* Column-visibility popover — same surface vocabulary as the filter popover. */
.tbl-col-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: var(--z-drawer);
  min-width: 200px;
  max-width: 280px;
  background: var(--bg-2);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-sans);
  font-size: var(--fs-footnote);
  color: var(--fg);
  animation: tbl-filter-popover-in var(--dur-med) var(--ease-out);
}
.tbl-col-menu-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.tbl-col-menu-title { font-weight: 500; }
.tbl-col-menu-close {
  appearance: none;
  background: transparent;
  border: 0;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 2px;
  border-radius: var(--r-xs);
  display: inline-flex;
}
.tbl-col-menu-close:hover { color: var(--fg); }

.tbl-col-menu-body {
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-height: 280px;
  overflow-y: auto;
}
.tbl-col-menu-option {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 4px 0;
  cursor: pointer;
  color: var(--fg-muted);
}
.tbl-col-menu-option:hover { color: var(--fg); }

/* Avatar */
.avatar {
  width: 28px; height: 28px;
  border-radius: 50%;
  background: var(--accent-a);
  display: inline-grid;
  place-content: center;
  font-family: var(--font-sans);
  font-size: var(--fs-caption2);
  font-weight: 600;
  color: var(--on-accent);
  flex: 0 0 28px;
  overflow: hidden;
  user-select: none;
}
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Sizes (base .avatar is Medium / 28px) */
.avatar--sm { width: 24px; height: 24px; flex-basis: 24px; font-size: var(--fs-caption2); }
.avatar--lg { width: 40px; height: 40px; flex-basis: 40px; font-size: var(--fs-callout); }
/* Square variant */
.avatar--square { border-radius: var(--r-sm); }
/* Counter ("+N") tile in a group */
.avatar--counter {
  /* Deckend statt Glas: die Kachel liegt im Stapel über einem Avatar,
     durchscheinend zeigte sie ihn als farbige Linse. Ausserdem gehört
     kein Glas in die Inhaltsebene. Gleicher Farbwert, opakes Gegenstück. */
  background: var(--glass-solid-3);
  color: var(--fg-muted);
}

/* Wrapper that hosts the presence dot */
.avatar-wrap {
  position: relative;
  display: inline-flex;
  flex: none;
}
.avatar-status {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--bg-0);
}
.avatar-wrap--sm .avatar-status { width: 7px; height: 7px; }
.avatar-wrap--lg .avatar-status { width: 12px; height: 12px; }
.avatar-status--online  { background: var(--success); }
.avatar-status--busy    { background: var(--danger);  }
.avatar-status--away    { background: var(--warning); }
/* Deckend gerechnet: --fg-faint ist durchscheinend und nahm auf der einen
   Hälfte die Avatarfarbe an, der Punkt sah zweifarbig aus. Gleiche Wirkung,
   nur vorab auf den Seitengrund gelegt. */
.avatar-status--offline { background: color-mix(in srgb, var(--fg) 28%, var(--bg-0)); }

/* Stacked group */
.avatar-group {
  display: inline-flex;
  align-items: center;
}
.avatar-group > * + * {
  margin-left: -8px;
  /* Der Trennring wohnt hier statt an .avatar: Apple setzt um ein einzeln
     stehendes Profilbild keinen Rand, braucht ihn im Stapel aber als Lücke
     zum Bild darunter. Getroffen wird jedes Kind ausser dem ersten, also
     genau die, die etwas überlappen. */
  box-shadow: 0 0 0 2px var(--bg-0);
}
.avatar-group--sm > * + * { margin-left: -6px; }
.avatar-group--lg > * + * { margin-left: -12px; }

/* ============== TOASTS (DrylToast + DrylToastProvider) ============== */

/* Fixed-position host that holds the toast stack. pointer-events:none lets
   clicks pass through the empty space; each .toast-slot re-enables them. */
.toast-host {
  position: fixed;
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: var(--sp-5);
  pointer-events: none;
  max-width: min(440px, calc(100vw - var(--sp-7)));
  width: max-content;
}

.toast-host.is-top-right     { top: 0;    right: 0; }
.toast-host.is-top-left      { top: 0;    left: 0; }
.toast-host.is-top-center    { top: 0;    left: 50%; transform: translateX(-50%); }
.toast-host.is-bottom-right  { bottom: 0; right: 0; flex-direction: column-reverse; }
.toast-host.is-bottom-left   { bottom: 0; left: 0; flex-direction: column-reverse; }
.toast-host.is-bottom-center { bottom: 0; left: 50%; transform: translateX(-50%); flex-direction: column-reverse; }

.toast-slot { pointer-events: auto; }

.toast {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--sp-3);
  padding: var(--sp-4);
  padding-right: 38px;          /* room for the close button */
  padding-bottom: calc(var(--sp-4) + 2px); /* room for the progress bar */
  border-radius: var(--r-md);
  background:
    var(--sheen-grad),
    var(--panel-soft);
  border: 1px solid var(--line-strong);
  backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
  box-shadow:
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md);
  min-width: 280px;
  max-width: 100%;
  isolation: isolate;
  overflow: hidden;
  color: var(--fg);
  font-size: var(--fs-callout);
  line-height: 1.45;
  /* Direction-aware entrance is set further down per host position. */
  animation: toast-in-right var(--dur-slow) var(--ease-spring) both;
  transition:
    transform var(--dur-fast) var(--ease-out),
    box-shadow var(--dur-fast) var(--ease-out);
}

/* Subtle lift on hover — combined with the pause on progress this signals
   "I'm holding this open so you can read me". */
.toast:hover {
  transform: translateY(-2px);
  box-shadow:
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-lg);
}

/* One-shot shine sweep across the toast shortly after entry. Pure delight. */
.toast::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(120deg, transparent 30%, color-mix(in srgb, var(--shimmer) 41%, transparent) 50%, transparent 70%);
  transform: translateX(-100%);
  animation: toast-shine 1300ms var(--ease-out) 220ms both;
  pointer-events: none;
  z-index: 3;
  mix-blend-mode: overlay;
}
@keyframes toast-shine {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

/* Direction-aware entry — slide in from the edge nearest the anchor. */
.toast-host.is-top-right .toast,
.toast-host.is-bottom-right .toast { animation-name: toast-in-right; }

.toast-host.is-top-left .toast,
.toast-host.is-bottom-left .toast  { animation-name: toast-in-left; }

.toast-host.is-top-center .toast    { animation-name: toast-in-top; }
.toast-host.is-bottom-center .toast { animation-name: toast-in-bottom; }

@keyframes toast-in-right {
  0%   { opacity: 0; transform: translateX(120%) scale(0.85); filter: blur(6px); }
  60%  { opacity: 1; transform: translateX(-3%)  scale(1.02); filter: blur(0); }
  100% { opacity: 1; transform: translateX(0)    scale(1);    filter: blur(0); }
}
@keyframes toast-in-left {
  0%   { opacity: 0; transform: translateX(-120%) scale(0.85); filter: blur(6px); }
  60%  { opacity: 1; transform: translateX(3%)    scale(1.02); filter: blur(0); }
  100% { opacity: 1; transform: translateX(0)     scale(1);    filter: blur(0); }
}
@keyframes toast-in-top {
  0%   { opacity: 0; transform: translateY(-110%) scale(0.9); filter: blur(6px); }
  60%  { opacity: 1; transform: translateY(5%)    scale(1.02); filter: blur(0); }
  100% { opacity: 1; transform: translateY(0)     scale(1);    filter: blur(0); }
}
@keyframes toast-in-bottom {
  0%   { opacity: 0; transform: translateY(110%) scale(0.9); filter: blur(6px); }
  60%  { opacity: 1; transform: translateY(-5%)  scale(1.02); filter: blur(0); }
  100% { opacity: 1; transform: translateY(0)    scale(1);    filter: blur(0); }
}

/* Exit keyframes — single shared animation. JS listens for `toast-out`
   animationend and tells .NET to drop the entry from the stack. The
   actual exit *rule* is defined further down so it wins over the
   direction-aware and ai-generated animation overrides. */
@keyframes toast-out {
  to { opacity: 0; transform: scale(0.92) translateY(-6px); filter: blur(3px); }
}

/* Icon chip — pops with spring after the toast itself lands. */
.toast-icon-chip {
  width: 30px;
  height: 30px;
  border-radius: var(--r-sm);
  display: grid;
  place-content: center;
  background: var(--glass-2);
  color: var(--fg-muted);
  border: 1px solid var(--line-strong);
  animation: toast-icon-pop var(--dur-slow) var(--ease-spring) 120ms both;
  position: relative;
  z-index: 2;
  flex-shrink: 0;
}
@keyframes toast-icon-pop {
  0%   { opacity: 0; transform: rotate(-18deg) scale(0.3); }
  60%  { opacity: 1; transform: rotate(8deg)   scale(1.15); }
  100% { opacity: 1; transform: rotate(0)      scale(1); }
}

/* Body content column */
.toast-content {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  position: relative;
  z-index: 2;
}
.toast-title {
  font-size: var(--fs-headline);
  font-weight: 600;
  color: var(--fg);
  letter-spacing: -0.005em;
}
.toast-body {
  font-size: var(--fs-subhead);
  color: var(--fg-muted);
  line-height: 1.5;
}
.toast-title + .toast-body { margin-top: 2px; }

/* Close button */
.toast-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 22px;
  height: 22px;
  display: inline-grid;
  place-content: center;
  border-radius: var(--r-xs);
  background: transparent;
  border: 1px solid transparent;
  color: var(--fg-dim);
  cursor: pointer;
  transition:
    color        var(--dur-fast) var(--ease-out),
    background   var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out);
  z-index: 5;
}
.toast-close:hover {
  color: var(--fg);
  background: var(--glass-2);
  border-color: var(--line);
}
.toast-close:focus-visible {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

/* Progress bar — single source of truth for auto-dismiss. The CSS animation
   drives both the visual countdown and (via animationend in dryl.toast.js)
   the actual close. `animation-play-state: paused` on hover keeps the
   timer and the bar perfectly in sync. */
.toast-progress {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 2px;
  width: 100%;
  background: linear-gradient(90deg, var(--accent-a), var(--accent-b));
  transform-origin: left center;
  animation-name: toast-progress;
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  pointer-events: none;
  z-index: 4;
  border-bottom-left-radius: inherit;
  border-bottom-right-radius: inherit;
}
.toast-progress.is-hidden { opacity: 0; }
.toast:hover .toast-progress { animation-play-state: paused; }
@keyframes toast-progress {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

/* ── Variants — semantic accent on icon chip, colored left rail, soft glow.
   The left rail is drawn with an inset box-shadow so the radius still clips
   it cleanly. */
.toast.toast-success {
  border-color: color-mix(in srgb, var(--success) 28%, transparent);
  box-shadow:
    inset 3px 0 0 var(--success),
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md),
    0 0 32px color-mix(in srgb, var(--success) 18%, transparent);
}
.toast.toast-success .toast-icon-chip {
  background: color-mix(in srgb, var(--success) 15%, transparent);
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
}
.toast.toast-success .toast-progress {
  background: linear-gradient(90deg, var(--success), var(--success-hi));
}

.toast.toast-warning {
  border-color: color-mix(in srgb, var(--warning) 28%, transparent);
  box-shadow:
    inset 3px 0 0 var(--warning),
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md),
    0 0 32px color-mix(in srgb, var(--warning) 18%, transparent);
}
.toast.toast-warning .toast-icon-chip {
  background: color-mix(in srgb, var(--warning) 15%, transparent);
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}
.toast.toast-warning .toast-progress {
  background: linear-gradient(90deg, var(--warning), var(--warning-hi));
}

.toast.toast-error {
  border-color: color-mix(in srgb, var(--danger) 32%, transparent);
  box-shadow:
    inset 3px 0 0 var(--danger),
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md),
    0 0 36px color-mix(in srgb, var(--danger) 22%, transparent);
}
.toast.toast-error .toast-icon-chip {
  background: color-mix(in srgb, var(--danger) 15%, transparent);
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}
.toast.toast-error .toast-progress {
  background: linear-gradient(90deg, var(--danger), var(--danger-hi));
}

.toast.toast-info {
  border-color: color-mix(in srgb, var(--info) 28%, transparent);
  box-shadow:
    inset 3px 0 0 var(--info),
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md),
    0 0 32px color-mix(in srgb, var(--info) 18%, transparent);
}
.toast.toast-info .toast-icon-chip {
  background: color-mix(in srgb, var(--info) 15%, transparent);
  color: var(--info);
  border-color: color-mix(in srgb, var(--info) 35%, transparent);
}
.toast.toast-info .toast-progress {
  background: linear-gradient(90deg, var(--info), var(--info-hi));
}

/* ── AI dominance (CLAUDE.md §2.10) ── When .ai-aura is set, the rotating
   gradient ring and breathing glow take over the visual; the variant left
   rail and tinted border step back so the AI signal reads cleanly. The
   variant accent survives only on the icon chip so a user can still tell
   "AI delivered a success / warning / error". */
.toast.ai-aura {
  border-color: var(--accent-line);
}
.toast.ai-aura.toast-success,
.toast.ai-aura.toast-warning,
.toast.ai-aura.toast-error,
.toast.ai-aura.toast-info {
  box-shadow:
    inset 0 1px 0 var(--edge-hi),
    var(--shadow-md);
}
/* The host has overflow:hidden so the outer ai-aura ring/glow would be
   clipped. Pull them inside the bounds — same trick as DrylTable. */
.toast.ai-aura .ai-aura-ring { inset: 0; }
.toast.ai-aura .ai-aura-glow {
  box-shadow:
    inset 0 0 0 1px var(--accent-line),
    inset 0 0 32px color-mix(in srgb, var(--ai-a) 18%, transparent),
    inset 0 0 64px color-mix(in srgb, var(--ai-b) 8%, transparent);
}
.toast.ai-aura .toast-progress {
  background: var(--accent-grad);
}
/* The standard ai-generated translate would conflict with the entrance lift. */
.toast.ai-aura.ai-generated { animation: none; }

/* Exit animation — placed last with .toast-host scoping so the specificity
   (0,3,0) matches the direction rules and the ai-generated override, and
   source order makes this rule win. Without this, .toast.is-leaving (0,2,0)
   would lose to those, the exit animation would never play, animationend
   would never fire for `toast-out`, and the toast would sit on screen
   forever — close button included. */
.toast-host .toast.is-leaving {
  animation: toast-out var(--dur-med) var(--ease-out) forwards;
  pointer-events: none;
}
.toast-host .toast.is-leaving::after          { animation: none; opacity: 0; }
.toast-host .toast.is-leaving .toast-progress { animation: none; opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .toast,
  .toast::after,
  .toast-icon-chip,
  .toast-progress { animation: none; }
  .toast:hover { transform: none; }
}

/* Skeleton — the shimmer is an oversized gradient strip sliding via transform
   (compositor) instead of a background-position sweep, which repaints every
   frame — noticeable when a loading view shows a whole stack of skeletons.
   Geometry matches the old 200%-tile, two-periods-per-cycle sweep. */
.skel {
  display: block;
  position: relative;
  overflow: hidden;
  background: var(--glass-1);
  border-radius: var(--r-2xs);
  height: 14px;
}
.skel::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 500%;
  background: linear-gradient(90deg, var(--glass-1) 0%, var(--glass-3) 50%, var(--glass-1) 100%);
  background-size: 40% 100%;
  animation: skel 1.4s ease-in-out infinite;
}
@keyframes skel { 0% { translate: -80% 0; } 100% { translate: 0 0; } }

/* Spinner */
.spinner {
  display: block;
  width: 18px; height: 18px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--edge-hi-strong) 56%, transparent);
  border-top-color: var(--accent-b);
  animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* Progress */
.progress {
  height: 6px;
  width: 100%;
  border-radius: var(--r-pill);
  background: var(--glass-2);
  overflow: hidden;
  position: relative;
}
.progress-bar {
  height: 100%;
  background: var(--accent-grad);
  border-radius: var(--r-pill);
  box-shadow: 0 0 12px color-mix(in srgb, var(--accent-a) 50%, transparent);
  transition: width 600ms var(--ease-out);
}

/* DrylProgress — wrapper, label row, sizes, variants, indeterminate */
.progress-field {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  width: 100%;
}
.progress-label-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: var(--fs-caption1);
  color: var(--fg-muted);
}
.progress-pct {
  font-family: var(--font-mono);
  color: var(--fg);
}
/* Aura host sits outside the track so the ring is not clipped by overflow:hidden */
.progress-wrap {
  position: relative;
  width: 100%;
  border-radius: var(--r-pill);
}
.progress--sm { height: 4px; }
.progress--lg { height: 10px; }

.progress-bar--success { background: var(--success); box-shadow: 0 0 12px color-mix(in srgb, var(--success) 50%, transparent); }
.progress-bar--warning { background: var(--warning); box-shadow: 0 0 12px color-mix(in srgb, var(--warning) 50%, transparent); }
.progress-bar--danger  { background: var(--danger);  box-shadow: 0 0 12px color-mix(in srgb, var(--danger) 50%, transparent); }

.progress--indeterminate .progress-bar {
  width: 40%;
  transition: none;
  animation: progress-indeterminate 1.4s var(--ease-in-out) infinite;
}
@keyframes progress-indeterminate {
  0%   { transform: translateX(-120%); }
  100% { transform: translateX(320%); }
}
@media (prefers-reduced-motion: reduce) {
  .progress--indeterminate .progress-bar { animation: none; width: 100%; }
}

/* ============== TABS (DrylTabs / DrylTab) ============== */

.tabs-root {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.tabs {
  position: relative;
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--line);
  padding: 0 var(--sp-1);
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 40px;
  padding: 0 14px;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  font-family: var(--font-sans);
  font-size: var(--fs-callout);
  font-weight: 500;
  letter-spacing: -0.005em;
  color: var(--fg-muted);
  white-space: nowrap;
  flex-shrink: 0;
  cursor: pointer;
  user-select: none;
  border-radius: var(--r-sm) var(--r-sm) 0 0;
  isolation: isolate;
  transition:
    color var(--dur-fast) var(--ease-out),
    background var(--dur-fast) var(--ease-out);
}
.tab:hover { color: var(--fg); background: var(--glass-1); }
.tab:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--accent-soft);
}

.tab.is-active { color: var(--fg); }
.tab.is-active::after {
  content: '';
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: -1px;
  height: 2px;
  background: var(--accent-grad);
  border-radius: var(--r-pill);
  box-shadow: 0 0 12px var(--accent-a);
  transform-origin: center;
  animation: tab-underline-in var(--dur-med) var(--ease-spring) both;
  z-index: 2;
}
@keyframes tab-underline-in {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}

/* Gliding underline (DrylTabs AnimateIndicator, the default). A single shared
   ink element is measured + positioned by dryl.motion.moveIndicator; CSS
   glides it on a spring. When present it replaces the per-tab ::after, which
   stays as the fallback when AnimateIndicator=false or JS is unavailable. */
.tabs--glide .tab.is-active::after { display: none; }
.tab-ink {
  position: absolute;
  bottom: -1px;
  left: 0;
  height: 2px;
  width: 0;
  background: var(--accent-grad);
  border-radius: var(--r-pill);
  box-shadow: 0 0 12px var(--accent-a);
  opacity: 0;
  pointer-events: none;
  z-index: 2;
}
.tabs.is-ink-ready .tab-ink {
  transition:
    transform var(--dur-med) var(--ease-spring),
    width var(--dur-med) var(--ease-spring),
    opacity var(--dur-fast) var(--ease-out);
}

.tab.is-disabled,
.tab[disabled] {
  color: var(--fg-faint);
  cursor: not-allowed;
  pointer-events: none;
}

.tab .tab-icon {
  flex: 0 0 14px;
  opacity: 0.85;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.tab:hover .tab-icon,
.tab.is-active .tab-icon { opacity: 1; }

/* Trailing slot for badges, counters, AI indicators etc. */
.tab-trailing {
  display: inline-flex;
  align-items: center;
  margin-left: 2px;
}

/* AI mode on a single tab — additive ring drawn via a dedicated .tab-comet
   child (kept inside the tab's rounded bounds so the rotating gradient traces
   just this tab, not the whole tab bar). The .is-active ::after underline
   still wins on top, so an active+AI tab reads as both selected and AI-busy.
   The wrapper carries the static border mask; only its oversized conic
   square ::before rotates (transform → compositor, no per-frame paint). */
.tab.ai-aura {
  color: var(--accent-fg);
  background: var(--accent-soft);
}
.tab-comet {
  position: absolute;
  inset: 2px 2px 0 2px;
  border-radius: var(--r-sm);
  padding: 1px;
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  overflow: hidden; /* keep the rotating square's layer bounds inside the tab */
  container-type: size;
  pointer-events: none;
  z-index: 1;
  opacity: 0.85;
}
.tab-comet::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 150cqmax;
  height: 150cqmax;
  translate: -50% -50%;
  background: conic-gradient(
    from 0deg,
    var(--accent-a),
    var(--accent-b),
    var(--accent-a),
    var(--accent-b),
    var(--accent-a)
  );
  animation: ai-comet-spin 6s linear infinite;
}
.tab.ai-thinking .tab-comet  { opacity: 1; }
.tab.ai-generated .tab-comet { opacity: 1; }
.tab.ai-thinking .tab-comet::before  { animation-duration: 1.8s; }
.tab.ai-streaming .tab-comet::before { animation-duration: 3s; }
.tab.ai-generated .tab-comet::before { animation-duration: 4s; }

/* Tab panel — fades in on switch via @key on the panel element */
.tab-panel {
  padding: var(--sp-5) 0 0;
  animation: tab-panel-in var(--dur-med) var(--ease-out) both;
  min-width: 0;
}
@keyframes tab-panel-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Direction-aware content entry: switching forward slides the new panel in
   from the right, switching back from the left (reuses the presence
   keyframes — 12px travel reads clearly without feeling like a carousel). */
.tab-panel--fwd  { animation-name: presence-in-left; }
.tab-panel--back { animation-name: presence-in-right; }

@media (prefers-reduced-motion: reduce) {
  .tab.is-active::after,
  .tab-comet::before,
  .tab-panel,
  .tab-panel--fwd,
  .tab-panel--back { animation: none; }
  .tab-comet { opacity: 0.6; }
}

/* Dialog */
body.dryl-scroll-locked { overflow: hidden; }

.dialog-backdrop {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--backdrop) 92%, transparent);
  backdrop-filter: blur(var(--scrim-blur));
  -webkit-backdrop-filter: blur(var(--scrim-blur));
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  z-index: var(--z-modal);
  animation: fadeIn var(--dur-med) var(--ease-out);
  /* Exit via transition (not animation) so an interrupted fade — a new dialog
     arriving while the last one leaves — reverses smoothly from the current
     opacity instead of restarting. */
  transition: opacity var(--dur-med) var(--ease-out);
}

/* Fullscreen dialogs fill the viewport — kill the backdrop padding so they
   actually reach the edges. */
.dialog-backdrop:has(.dialog-fullscreen) {
  padding: 0;
}

/* Exit — DrylDialogProvider adds .is-exiting once every remaining dialog is
   exiting; entries are removed on dialogOut's animationend (dryl.motion.onExit
   on the layer) with a C#-side watchdog as fallback. pointer-events: none is
   the safety net: a fading (or somehow stuck) overlay must never eat clicks. */
.dialog-backdrop.is-exiting {
  opacity: 0;
  pointer-events: none;
}

/* Each service dialog lives in its own full-viewport layer above the shared
   backdrop, so stacked or hand-off dialogs never double the dark/blur wash. */
.dialog-layer {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
}
.dialog-layer:has(.dialog-fullscreen) { padding: 0; }
.dialog-layer.is-exiting { pointer-events: none; }
.dialog-layer.is-exiting .dialog { animation: dialogOut var(--dur-med) var(--ease-in-out) both; }

/* Handoff — this dialog was opened while its predecessor is still animating
   out: hold the enter for a beat so the swap reads as a sequenced cross-fade
   instead of a collision. */
.dialog-layer.is-handoff:not(.is-exiting) .dialog {
  animation-delay: var(--dur-fast);
  animation-fill-mode: backwards;
}

@keyframes dialogOut {
  from { opacity: 1; transform: scale(1) translateY(0); }
  to   { opacity: 0; transform: scale(0.96) translateY(8px); }
}

.dialog {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 640px;
  max-width: 100%;
  max-height: 90vh; /* Fallback für Browser ohne dvh */
  max-height: 90dvh;
  border-radius: var(--r-lg);
  background: var(--panel-grad);
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-lg), 0 0 80px color-mix(in srgb, var(--accent-a) 25%, transparent);
  animation: dialogIn var(--dur-med) var(--ease-spring);
  color: var(--fg);
}

.dialog-sm         { width: 420px; }
.dialog-md         { width: 640px; }
.dialog-lg         { width: 960px; }
.dialog-fullscreen {
  width: 100vw;
  height: 100vh; /* Fallback für Browser ohne dvh */
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  border-radius: 0;
  border: none;
}

.dialog-header {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}

.dialog-header-icon {
  display: inline-flex;
  width: 28px;
  height: 28px;
  align-items: center;
  justify-content: center;
  border-radius: var(--r-sm);
  background: var(--glass-2);
  border: 1px solid var(--line);
  color: var(--fg-muted);
  flex: 0 0 auto;
}

.dialog-title {
  font-size: var(--fs-headline);
  font-weight: 600;
  color: var(--fg);
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dialog-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--r-sm);
  background: transparent;
  border: 1px solid transparent;
  color: var(--fg-muted);
  cursor: pointer;
  flex: 0 0 auto;
  transition:
    background var(--dur-fast) var(--ease-out),
    border-color var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}
.dialog-close:hover {
  background: var(--glass-2);
  border-color: var(--line);
  color: var(--fg);
}
.dialog-close:focus-visible {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 2px var(--accent-soft);
}

.dialog-body {
  padding: var(--sp-5);
  overflow-y: auto;
  flex: 1 1 auto;
  color: var(--fg-muted);
  line-height: 1.55;
}
.dialog-body > :first-child { margin-top: 0; }
.dialog-body > :last-child  { margin-bottom: 0; }

.dialog-footer {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--line);
  flex: 0 0 auto;
}

.dialog-fullscreen .dialog-body { max-height: none; }

/* Fullscreen-Dialoge respektieren die Safe-Area (iOS Home-Indicator). */
.dialog-fullscreen .dialog-footer {
  padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom));
}

/* Mobile: nicht-Fullscreen-Dialoge docken als Bottom-Sheet an der
   Unterkante an — volle Breite, obere Rundung, dvh-begrenzte Höhe.
   Fullscreen-Dialoge sind davon unberührt (füllen ohnehin alles). */
@media (max-width: 640px) {
  .dialog-backdrop:not(:has(.dialog-fullscreen)),
  .dialog-layer:not(:has(.dialog-fullscreen)) {
    place-items: end center;
    padding: var(--sp-6) 0 0;
  }

  .dialog:not(.dialog-fullscreen) {
    width: 100%;
    max-height: calc(100dvh - var(--sp-8));
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-left: none;
    border-right: none;
    border-bottom: none;
  }

  .dialog:not(.dialog-fullscreen) .dialog-footer {
    padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom));
  }
}

@keyframes dialogIn {
  from { opacity: 0; transform: scale(0.96) translateY(8px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* Tooltip */
.tt-wrap { display: inline-flex; }
/* The bubble lives as a body-level portal (see dryl.tooltip in dryl.js) so it
   escapes ancestor overflow/backdrop-filter clipping; the JS flips the
   preferred placement when the viewport has no room on that side. */
.tt-portal {
  position: fixed;
  z-index: 9000;
  padding: 6px 10px;
  border-radius: var(--r-xs);
  font-size: var(--fs-footnote);
  white-space: nowrap;
  background: var(--panel-solid);
  border: 1px solid var(--line-strong);
  color: var(--fg);
  pointer-events: none;
  box-shadow: var(--shadow-md);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}
.tt-portal.from-below { transform: translateY(-4px); }
.tt-portal.is-open { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .tt-portal { transition: none; }
}

/* Alert */
.alert {
  display: flex;
  gap: 12px;
  padding: 14px 16px;
  border-radius: var(--r-md);
  border: 1px solid var(--line-strong);
  background: var(--glass-1);
  backdrop-filter: blur(var(--glass-blur-soft));
  -webkit-backdrop-filter: blur(var(--glass-blur-soft));
}
.alert .ico {
  width: 28px; height: 28px;
  border-radius: var(--r-xs);
  display: grid; place-content: center;
  flex: 0 0 28px;
}
.alert.info .ico    { background: var(--accent-soft); color: var(--accent-ico); }
.alert.success .ico { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); }
.alert.warning .ico { background: color-mix(in srgb, var(--warning) 15%, transparent); color: var(--warning); }
.alert.danger .ico  { background: color-mix(in srgb, var(--danger) 15%, transparent); color: var(--danger); }
/* KI-Variante — Akzentgradient als Icon-Chip, violett-cyanfarbener Text */
.alert.ai .ico      { background: var(--accent-soft); color: var(--accent-ico); }
.alert .title { font-weight: 500; font-size: var(--fs-callout); margin-bottom: 2px; }
.alert .body { font-size: var(--fs-subhead); color: var(--fg-muted); }
/* Flexibler Textbereich — nimmt verbleibenden Platz ein */
.alert-content { flex: 1; min-width: 0; }
/* Schließen-Button */
.alert.is-dismissible { padding-right: var(--sp-3); }
.alert-dismiss {
  margin-left: auto;
  flex-shrink: 0;
  align-self: flex-start;
  width: 24px; height: 24px;
  display: grid; place-content: center;
  background: transparent;
  border: none;
  border-radius: var(--r-xs);
  color: var(--fg-dim);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color     var(--dur-fast) var(--ease-out);
}
.alert-dismiss:hover { background: var(--glass-3); color: var(--fg); }
.alert-dismiss:focus-visible { outline: 2px solid var(--accent-line); outline-offset: 1px; }
/* AI-Aura auf dem Alert — Rahmen weicht zurück, damit der Gradientring dominiert.
   Der alert hat kein overflow:hidden, daher funktioniert der Standard-inset -1px. */
.alert.ai-aura { border-color: var(--accent-line); }

/* Spotlight card */
.spotlight-card {
  position: relative;
  overflow: hidden;
}
.spotlight-card .spotlight-fx {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(360px circle at var(--mx, 50%) var(--my, 50%),
              color-mix(in srgb, var(--accent-a) 18%, transparent),
              color-mix(in srgb, var(--accent-b) 6%, transparent) 30%,
              transparent 60%);
  transition: opacity var(--dur-med) var(--ease-out);
  z-index: 1;
}
.spotlight-card:hover .spotlight-fx { opacity: 1; }
[data-spotlights="off"] .spotlight-card:hover .spotlight-fx { opacity: 0; }
[data-spotlights="off"] .spotlight-card:hover::before { opacity: 0; }
.spotlight-card .spotlight-content { position: relative; z-index: 2; }
.spotlight-card::before {
  /* Edge light that activates on hover */
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: radial-gradient(360px circle at var(--mx, 50%) var(--my, 50%),
              color-mix(in srgb, var(--accent-a) 60%, transparent), transparent 50%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity var(--dur-med) var(--ease-out);
  pointer-events: none;
  z-index: 3;
}
.spotlight-card:hover::before { opacity: 1; }

/* Chart helpers */
.chart-card { padding: 22px 24px; }
.chart-legend { display: flex; gap: 18px; font-size: var(--fs-caption1); color: var(--fg-muted); }
.legend-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }

/* ============================================================
   AI MODE — ambient signal for AI-driven surfaces
   ============================================================
   Apply alongside any existing surface class (.glass-card, .btn, .input, …).
   Implemented as child elements (not pseudos) so it composes with surfaces
   that already use ::before/::after (spotlight-card, glass-card).

     <div class="glass-card ai-aura">
       <div class="ai-aura-ring"></div>
       <div class="ai-aura-comet"></div>
       <div class="ai-aura-glow"></div>
       …content…
     </div>

   Modifiers on the same host:
     .ai-thinking   — accelerates the ring + glow for active tool calls
     .ai-streaming  — long-running token stream (slightly slower than thinking)
     .ai-generated  — one-shot reveal after generation completes
*/

/* Legacy registered angle property — the comet no longer animates it (it spins
   via transform on the compositor, see .ai-aura-comet); kept registered so any
   consumer CSS reading var(--ai-aura-angle) keeps resolving to a valid angle. */
@property --ai-aura-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }

/* ── Host + per-state design tokens ──────────────────────────────────────
   Every AI-aware surface sets .ai-aura (+ an optional state class). The state
   classes only reset a handful of CSS vars; the ring / glow / wash read them,
   so both aura variants (Comet default, .ai-aura--aurora) share ONE colour +
   motion language and differ only in how the ring paints. Base .ai-aura (no
   state class) == Active. --ai-hot / --ai-cool bias only between the themed
   --ai-a / --ai-b (no new hues); --ai-core is the specular head (theme-aware). */
.ai-aura {
  position: relative;
  isolation: isolate;
  --ai-hot: var(--ai-a);
  --ai-cool: var(--ai-b);
  --aura-base: 26%;          /* even base-saum accent strength   */
  --aura-halo: 26px;         /* halo blur radius                 */
  --aura-halo-a: 18%;        /* halo inner-colour strength       */
  --aura-comet-dur: 9s;      /* comet orbit period               */
  --aura-halo-dur: 6s;       /* halo breathing period            */
  --aura-drift-dur: 12s;     /* aurora field drift period        */
  --aura-sheen: none;        /* directional sheen (Streaming)    */
}
.ai-aura.ai-thinking {       /* concentrating — violet-dominant, fast & tight */
  --ai-hot: var(--ai-a); --ai-cool: var(--ai-a);
  --aura-base: 34%; --aura-halo: 20px; --aura-halo-a: 30%;
  --aura-comet-dur: 3.2s; --aura-halo-dur: 1.6s; --aura-drift-dur: 4s;
}
.ai-aura.ai-streaming {      /* output flowing — cyan-dominant, directional */
  --ai-hot: var(--ai-b); --ai-cool: var(--ai-b);
  --aura-base: 28%; --aura-halo: 34px; --aura-halo-a: 22%;
  --aura-comet-dur: 5s; --aura-halo-dur: 2.6s; --aura-drift-dur: 6s;
  --aura-sheen: block;
}
.ai-aura.ai-generated {      /* done — calm, carries the one-shot bloom */
  --aura-base: 40%; --aura-halo: 30px; --aura-halo-a: 26%;
  --aura-comet-dur: 6s; --aura-halo-dur: 5s; --aura-drift-dur: 5s;
}

/* ── COMET (default) ── even base saum + a travelling specular comet ────── */

/* Even, aspect-independent base saum: a themed hairline via a linear (not
   conic) gradient, so wide surfaces never go patchy on the long edges. */
.ai-aura-ring {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--ai-hot) calc(var(--aura-base) * var(--ai-strength)), transparent),
    color-mix(in srgb, var(--ai-cool) calc(var(--aura-base) * var(--ai-strength)), transparent));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
  z-index: 2;
}
/* The comet: a masked-border conic with one bright, localized arc that orbits —
   a long glowing tail building to a specular white head. The chained drop-shadows
   let the head bloom (white core + wider cool halo) past the 1px border.

   Performance: the wrapper (.ai-aura-comet, a sibling of the ring) is static —
   it carries the border mask and the bloom filter. Only its ::before rotates:
   an oversized conic square spun via transform, which runs on the compositor.
   Animating the conic's angle instead (the old @property approach) repaints
   the gradient + re-runs both drop-shadows on the main thread every frame,
   which multiplied across auras is enough to tank mid-range machines. */
.ai-aura-comet {
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  padding: 1.5px;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  /* Clip the rotating square to the wrapper: without this its composited
     layer bounds span far past the surface and the compositor pays for the
     overlap every frame (measured ~2× frame cost on aura-dense pages). */
  overflow: hidden;
  container-type: size;
  pointer-events: none;
  z-index: 2;
  filter:
    drop-shadow(0 0 6px color-mix(in srgb, var(--ai-core) 78%, transparent))
    drop-shadow(0 0 13px color-mix(in srgb, var(--ai-cool) 45%, transparent));
}
/* 150cqmax ≥ the wrapper's diagonal (√2 · max side), so the rotating square
   covers the border band at every angle regardless of aspect ratio. */
.ai-aura-comet::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: 150cqmax;
  height: 150cqmax;
  translate: -50% -50%;
  background: conic-gradient(from 0deg,
    transparent 0deg 284deg,
    color-mix(in srgb, var(--ai-hot) 60%, transparent) 318deg,
    color-mix(in srgb, var(--ai-cool) 95%, transparent) 343deg,
    var(--ai-core) 351deg,
    color-mix(in srgb, var(--ai-cool) 55%, transparent) 356deg,
    transparent 360deg);
  animation: ai-comet-spin var(--aura-comet-dur) linear infinite;
}

/* Breathing halo behind the host. */
.ai-aura-glow {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--ai-hot) calc(34% * var(--ai-strength)), transparent),
    0 0 var(--aura-halo) color-mix(in srgb, var(--ai-hot) calc(var(--aura-halo-a) * var(--ai-strength)), transparent),
    0 0 calc(var(--aura-halo) * 2.2) color-mix(in srgb, var(--ai-cool) calc(12% * var(--ai-strength)), transparent);
  animation: ai-aura-breathe var(--aura-halo-dur) var(--ease-in-out) infinite;
}
/* Streaming: a directional sheen sweeping in reading direction, clipped to the
   host's rounded box by the glow's overflow. The gradient strip is oversized
   and slides via transform (compositor) instead of a background-position move,
   which would repaint every frame. Travel matches the old -30%→130% position
   sweep on a 220%-wide tile (offsets expressed in the strip's own width). */
.ai-aura-glow::before {
  content: '';
  display: var(--aura-sheen, none);
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 220%;
  background: linear-gradient(100deg,
    transparent 38%,
    color-mix(in srgb, var(--ai-b) 26%, transparent) 50%,
    transparent 62%);
  animation: ai-aura-sheen 2.4s var(--ease-in-out) infinite;
}

@keyframes ai-comet-spin   { to { rotate: 1turn; } }
@keyframes ai-aura-breathe { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }
@keyframes ai-aura-sheen      { 0% { translate: 16.4% 0; } 55%, 100% { translate: -70.9% 0; } }

/* ── AURORA (variant) ── soft, blurred, flowing edge field; no travelling head.
   Calm enough to show many at once. Same states + colour weighting; only the
   ring paint changes (halo, sheen and bloom are shared with Comet).
   The flow is two oversized gradient strips sliding via transform (compositor)
   under the ring's static mask — the counter-drifting layer simply runs the
   same keyframes half a period out of phase. The old multi-layer
   background-position drift repainted three gradients per element per frame. */
.ai-aura--aurora .ai-aura-ring {
  inset: -2px;
  padding: 2.5px;
  background: none;
  overflow: hidden; /* clip the oversized drifting strips' layer bounds */
  filter: blur(1.5px);
}
.ai-aura--aurora .ai-aura-ring::before,
.ai-aura--aurora .ai-aura-ring::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 200%;
  animation: ai-aura-drift var(--aura-drift-dur) var(--ease-in-out) infinite;
}
.ai-aura--aurora .ai-aura-ring::before {
  background:
    radial-gradient(58% 130% at 18% 0%,  color-mix(in srgb, var(--ai-hot) 92%, transparent), transparent 62%),
    radial-gradient(90% 150% at 50% 50%, color-mix(in srgb, var(--ai-hot) calc(34% * var(--ai-strength)), transparent), transparent 72%);
}
.ai-aura--aurora .ai-aura-ring::after {
  background:
    radial-gradient(58% 130% at 82% 100%, color-mix(in srgb, var(--ai-cool) 92%, transparent), transparent 62%);
  animation-delay: calc(var(--aura-drift-dur) / -2);
}
.ai-aura--aurora .ai-aura-comet { display: none; }
@keyframes ai-aura-drift {
  0%, 100% { translate: 0 0; }
  50%      { translate: -50% 0; }
}

/* ── Generated one-shot ── gentle lift + a bright bloom flashing inward from the
   edge, then settling. Render <div class="ai-aura-wash"></div> only while Generated. */
.ai-aura.ai-generated { animation: ai-generated-lift 720ms var(--ease-out) both; }
@keyframes ai-generated-lift {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.ai-aura-wash {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 3;
}
.ai-aura.ai-generated .ai-aura-wash {
  opacity: 0;
  box-shadow:
    inset 0 0 0 1px var(--ai-core),
    inset 0 0 22px color-mix(in srgb, var(--ai-hot) 26%, transparent),
    0 0 30px color-mix(in srgb, var(--ai-hot) 34%, transparent);
  animation: ai-aura-bloom 900ms var(--ease-out) both;
}
@keyframes ai-aura-bloom {
  0%   { opacity: 0; transform: scale(1.012); }
  28%  { opacity: 0.95; }
  100% { opacity: 0; transform: scale(1); }
}

/* Afterglow — once Generated has bloomed, the travelling comet retires, leaving a
   calm static saum + gently breathing halo: a non-distracting "AI was here" trace
   a surface can rest in indefinitely (no need to drop to None). The retire fade
   runs on the static wrapper (opacity → compositor); the spin keeps its base
   ai-comet-spin animation on the ::before untouched. */
.ai-aura.ai-generated .ai-aura-comet {
  animation: ai-comet-retire 1100ms var(--ease-out) 800ms forwards;
}
/* visibility: hidden at the end tells the compositor to stop drawing the
   (still spinning) comet surface entirely — a retired comet must not keep
   costing a render pass per frame for the rest of the surface's life. */
@keyframes ai-comet-retire { to { opacity: 0; visibility: hidden; } }

/* Graceful exit — when a surface leaves AI mode the host keeps the aura mounted
   for one --dur-slow beat with .ai-aura--out, dissolving the border + halo instead
   of snapping the DOM away (rule 2.12: mounts AND unmounts animate). */
.ai-aura--out .ai-aura-ring,
.ai-aura--out .ai-aura-comet,
.ai-aura--out .ai-aura-glow {
  animation: none;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  .ai-aura-ring, .ai-aura-ring::before, .ai-aura-ring::after,
  .ai-aura-comet, .ai-aura-comet::before,
  .ai-aura-glow, .ai-aura-glow::before, .ai-aura-wash,
  .ai-aura.ai-generated { animation: none; }
  /* keep the calm, even static saum; hide the now-frozen travelling comet */
  .ai-aura-comet { opacity: 0; }
}

/* ============== EXPANSION PANELS ============== */

/*
  Stacking behaviour
  ──────────────────
  Adjacent closed panels share their border (margin-top: -1px).
  • First/last panels get rounded top/bottom corners.
  • An open panel lifts out: full border-radius + gap margin + stronger shadow.
  • The panel after an open one resets its negative margin so the gap is clean.

  Body animation
  ──────────────
  Uses the grid-template-rows: 0fr → 1fr trick.
  The inner overflow:hidden wrapper collapses to zero height while the content
  itself is never "squeezed" — it always renders at its natural size.
*/

.expansion {
  position: relative;
  background: var(--glass-1);
  border: 1px solid var(--line);
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  border-radius: 0;
  z-index: 0;
  transition:
    margin         var(--dur-med) var(--ease-out),
    border-color   var(--dur-med) var(--ease-out),
    border-radius  var(--dur-med) var(--ease-out),
    box-shadow     var(--dur-med) var(--ease-out),
    z-index        0s             var(--dur-med); /* delay z-index reset until animation is done */
}

/* Collapse shared border between adjacent panels */
.expansion + .expansion { margin-top: -1px; }

/* Round top of first panel in a stack */
.expansion:first-child { border-radius: var(--r-md) var(--r-md) 0 0; }

/* Round bottom of last panel in a stack */
.expansion:last-child { border-radius: 0 0 var(--r-md) var(--r-md); }

/* Single standalone panel */
.expansion:first-child:last-child { border-radius: var(--r-md); }

/* Open state — detach from siblings */
.expansion.is-open {
  margin: var(--sp-3) 0;
  border-radius: var(--r-md);
  border-color: var(--line-strong);
  box-shadow: var(--shadow-md);
  z-index: 1;
  transition:
    margin         var(--dur-med) var(--ease-out),
    border-color   var(--dur-med) var(--ease-out),
    border-radius  var(--dur-med) var(--ease-out),
    box-shadow     var(--dur-med) var(--ease-out);
}

/* Sibling right after an open panel: cancel the shared-border pull-up */
.expansion.is-open + .expansion { margin-top: 0; }

/* Disabled panel */
.expansion.is-disabled { pointer-events: none; }
.expansion.is-disabled .expansion-header { opacity: 0.4; cursor: not-allowed; }

/* ── Header ── */
.expansion-header {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-4) var(--sp-5);
  background: none;
  border: none;
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--fs-callout);
  font-weight: 500;
  letter-spacing: -0.005em;
  cursor: pointer;
  text-align: left;
  transition: color var(--dur-fast) var(--ease-out);
}

.expansion-header:focus-visible {
  outline: 2px solid var(--accent-line);
  outline-offset: -2px;
  border-radius: inherit;
}

.expansion-header:hover .expansion-chevron { color: var(--fg-muted); }

.expansion-icon { color: var(--fg-muted); flex-shrink: 0; }

.expansion-title { flex: 1; }

.expansion-header-trailing {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Chevron flips 180° when open */
.expansion-chevron {
  color: var(--fg-dim);
  flex-shrink: 0;
  transition: transform var(--dur-med) var(--ease-out), color var(--dur-med) var(--ease-out);
}
.expansion.is-open .expansion-chevron {
  transform: rotate(180deg);
  color: var(--fg-muted);
}

/* ── Body (grid-rows animation) ── */
.expansion-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-med) var(--ease-out);
}

.expansion.is-open .expansion-body {
  grid-template-rows: 1fr;
}

/* overflow:hidden here is what allows 0fr to fully collapse */
.expansion-body-inner {
  overflow: hidden;
}

.expansion-body-content {
  padding: 0 var(--sp-5) var(--sp-5);
  color: var(--fg-muted);
}

@media (prefers-reduced-motion: reduce) {
  .expansion,
  .expansion-body,
  .expansion-chevron { transition: none; }
}

/* ============== AI INDICATOR — small status pill ============== */
.ai-indicator {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 10px 0 8px;
  border-radius: var(--r-pill);
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--accent-fg);
  border: 1px solid var(--accent-line);
  background: var(--accent-soft);
  position: relative;
  overflow: hidden;
}
.ai-indicator .ai-indicator-ico {
  flex: 0 0 12px;
  color: var(--ai-b);
  animation: ai-indicator-pulse 2.4s var(--ease-in-out) infinite;
}
.ai-indicator.is-thinking .ai-indicator-ico  { animation-duration: 1s; }
.ai-indicator.is-streaming .ai-indicator-ico { animation-duration: 1.6s; }

/* Subtle sweep across the pill background */
.ai-indicator::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(120deg, transparent 30%, color-mix(in srgb, var(--shimmer) 82%, transparent) 50%, transparent 70%);
  transform: translateX(-100%);
  animation: ai-indicator-shimmer 3.6s var(--ease-in-out) infinite;
  pointer-events: none;
}
.ai-indicator.is-thinking::after  { animation-duration: 1.4s; }
.ai-indicator.is-streaming::after { animation-duration: 2.2s; }

@keyframes ai-indicator-pulse {
  0%, 100% { opacity: 0.7; transform: scale(1); }
  50%      { opacity: 1;   transform: scale(1.12); }
}
@keyframes ai-indicator-shimmer {
  0%   { transform: translateX(-100%); }
  60%  { transform: translateX(100%); }
  100% { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
  .ai-indicator .ai-indicator-ico,
  .ai-indicator::after { animation: none; }
}

/* ============== MENU (DrylMenu / DrylMenuItem) ============== */

/* Positioning anchor — wraps trigger + panel together */
.menu-anchor {
  position: relative;
  display: inline-block;
}

/* Block variant: stretches the anchor full-width */
.menu-anchor--block {
  display: block;
}

/* Transparent pass-through for the trigger slot */
.menu-trigger { display: contents; }

/* Dropdown panel */
.menu-panel {
  position: absolute;
  top: calc(100% + var(--sp-1));
  left: 0;
  z-index: var(--z-drawer);
  min-width: 180px;
  /* Cap to the viewport on a phone so a right-anchored menu can't push off-screen. */
  max-width: min(320px, calc(100vw - var(--sp-4)));
  display: flex;
  flex-direction: column;
  background: var(--bg-2);
  backdrop-filter: blur(var(--glass-blur));
  -webkit-backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-lg);
  padding: var(--sp-1) 0;
  outline: none;
  animation: menu-in var(--dur-med) var(--ease-out) both;
}

/* Placement variants */
.menu-panel--end { left: auto; right: 0; }
.menu-panel--top { top: auto; bottom: calc(100% + var(--sp-1)); }

@keyframes menu-in {
  from { opacity: 0; transform: translateY(-6px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

/* Individual item — rendered as <button> with role="menuitem" */
.menu-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
  padding: var(--sp-2) var(--sp-4);
  background: transparent;
  border: 0;
  color: var(--fg-muted);
  font: 400 var(--fs-callout)/1.4 var(--font-sans);
  text-align: left;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition:
    background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}
.menu-item:hover { background: var(--glass-2); color: var(--fg); }
.menu-item:focus-visible {
  outline: none;
  background: var(--glass-2);
  color: var(--fg);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.menu-item:disabled,
.menu-item[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* Danger variant */
.menu-item--danger { color: var(--danger); }
.menu-item--danger:hover,
.menu-item--danger:focus-visible {
  background: color-mix(in srgb, var(--danger) 10%, transparent);
  color: var(--danger-fg);
}

/* Keyboard shortcut hint — pushed to trailing edge */
.menu-item-shortcut {
  margin-left: auto;
  padding-left: var(--sp-5);
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  color: var(--fg-dim);
}

/* Section separator */
.menu-separator {
  height: 1px;
  background: var(--line);
  border: 0;
  margin: var(--sp-1) 0;
}

/* Section header */
.menu-header {
  padding: var(--sp-2) var(--sp-4) calc(var(--sp-1) / 2);
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-dim);
  user-select: none;
}

@media (prefers-reduced-motion: reduce) {
  .menu-panel { animation: none; }
}

/* ============== NUMBER INPUT STEPPER (DrylInputNumber) ============== */

/* Stretch so the stepper fills the full input height */
.input-wrapper.has-stepper { align-items: stretch; }

/* Input side: take remaining space, square off the right edge */
.input-wrapper.has-stepper .input {
  flex: 1;
  min-width: 0;
  border-radius: var(--r-md) 0 0 var(--r-md);
  border-right: none;
}

/* Sync the separator to the input's border states */
.input-wrapper.has-stepper:hover .num-stepper        { border-left-color: var(--line-hover); }
.input-wrapper.has-stepper:focus-within .num-stepper { border-left-color: var(--accent-line); }

.num-stepper {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  border-left: 1px solid var(--line-strong);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  overflow: hidden;
  transition: border-left-color var(--dur-med) var(--ease-out);
}
.num-step-btn {
  flex: 1;
  display: grid;
  place-content: center;
  width: 28px;
  border: none;
  background: transparent;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 0;
  transition: color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.num-step-btn:hover:not(:disabled)  { color: var(--fg); background: var(--glass-2); }
.num-step-btn:active:not(:disabled) { background: var(--glass-3); color: var(--accent-a); }
.num-step-btn:first-child           { border-bottom: 1px solid var(--line); }
.num-step-btn:disabled              { opacity: 0.4; cursor: not-allowed; }
.num-step-btn:focus-visible {
  position: relative;
  z-index: 1;
  outline: 2px solid color-mix(in srgb, var(--accent-a) 50%, transparent);
  outline-offset: -2px;
}

/* hide native number spinners — we render our own */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type=number] { -moz-appearance: textfield; }

/* ============== RADIO GROUP (DrylRadioGroup / DrylRadio) ============== */

.radio-group {
  display: flex;
  gap: var(--sp-3);
}
.radio-group--vertical   { flex-direction: column; }
.radio-group--horizontal { flex-direction: row; flex-wrap: wrap; align-items: center; }

.radio {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  cursor: pointer;
  user-select: none;
}
.radio-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
  pointer-events: none;
}
.radio-control {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  border-radius: var(--r-pill);
  background: var(--glass-1);
  border: 1px solid var(--line-strong);
  display: grid;
  place-content: center;
  transition: border-color var(--dur-fast) var(--ease-out),
              background   var(--dur-fast) var(--ease-out),
              box-shadow   var(--dur-fast) var(--ease-out);
}
.radio-control::after {
  content: '';
  width: 8px;
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--knob);
  transform: scale(0);
  transition: transform var(--dur-med) var(--ease-spring);
}
.radio:hover .radio-control {
  border-color: var(--accent-line);
}
.radio-input:checked + .radio-control {
  background: var(--accent-grad);
  border-color: transparent;
  box-shadow: 0 0 14px color-mix(in srgb, var(--accent-a) 45%, transparent);
}
.radio-input:checked + .radio-control::after {
  transform: scale(1);
}
.radio:focus-within .radio-control {
  outline: 2px solid color-mix(in srgb, var(--accent-a) 50%, transparent);
  outline-offset: 2px;
}
.radio-label {
  font-size: var(--fs-callout);
  color: var(--fg);
  line-height: 1.4;
}
.radio--disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* ============== CHIPS (used by DrylMultiSelect) ============== */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  background: var(--glass-2);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: var(--fs-caption1);
  color: var(--fg-muted);
  white-space: nowrap;
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chip-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chip-remove {
  flex-shrink: 0;
  display: grid;
  place-content: center;
  width: 14px;
  height: 14px;
  border: none;
  background: transparent;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 0;
  border-radius: var(--r-xs);
  transition: color var(--dur-fast) var(--ease-out);
}
.chip-remove:hover { color: var(--fg); }
.chip-overflow {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--glass-2);
  border: 1px solid var(--line);
  font-size: var(--fs-caption2);
  color: var(--fg-dim);
  white-space: nowrap;
  flex-shrink: 0;
}
.multiselect-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-1);
  flex: 1;
  min-width: 0;
  overflow: hidden;
}

/* ============== FILE UPLOAD (DrylFileUpload) ============== */

.file-drop {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-6) var(--sp-5);
  background: var(--glass-1);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-lg);
  transition: border-color var(--dur-med) var(--ease-out),
              background   var(--dur-med) var(--ease-out),
              box-shadow   var(--dur-med) var(--ease-out);
  text-align: center;
  cursor: pointer;
}
.file-drop:hover,
.file-drop--active {
  border-color: var(--accent-line);
  background: var(--glass-2);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent-a) 8%, transparent), 0 0 28px color-mix(in srgb, var(--accent-a) 14%, transparent);
}
.file-drop--disabled {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
.file-drop-icon {
  color: var(--fg-dim);
  transition: color var(--dur-med) var(--ease-out);
}
.file-drop:hover .file-drop-icon,
.file-drop--active .file-drop-icon { color: var(--fg-muted); }
.file-drop-title {
  font-size: var(--fs-callout);
  font-weight: 500;
  color: var(--fg-muted);
}
.file-drop-sub {
  font-size: var(--fs-subhead);
  color: var(--fg-dim);
}
.file-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}
.file-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--glass-1);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  min-width: 0;
}
.file-item-icon { flex-shrink: 0; color: var(--fg-dim); }
.file-item-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-callout);
  color: var(--fg);
}
.file-item-size {
  flex-shrink: 0;
  font-size: var(--fs-caption1);
  color: var(--fg-dim);
  font-family: var(--font-mono);
}
.file-item-remove {
  flex-shrink: 0;
  display: grid;
  place-content: center;
  width: 24px;
  height: 24px;
  border: none;
  background: transparent;
  color: var(--fg-dim);
  cursor: pointer;
  padding: 0;
  border-radius: var(--r-xs);
  margin-left: auto;
  transition: color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.file-item-remove:hover {
  color: var(--danger);
  background: color-mix(in srgb, var(--danger) 10%, transparent);
}

/* ============== SLIDER (DrylSlider) ============== */

.slider-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.slider-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
.slider-value {
  font-size: var(--fs-caption1);
  color: var(--fg-muted);
  font-family: var(--font-mono);
}

@media (prefers-reduced-motion: reduce) {
  .radio-control, .radio-control::after { transition: none; }
}

/* ─────────────────────────────────────────────────────────
   DrylMarkdown — rendered Markdown content. These rules are global
   (not component-scoped) because the HTML is injected via MarkupString
   and therefore carries no CSS-isolation attribute.
   ───────────────────────────────────────────────────────── */
.md {
  position: relative;
  /* AI mode adds padding + glass (below); transitioning them on the base class
     makes both edges glide — the panel inflates when the aura mounts and, after
     the .ai-aura--out beat unmounts it, deflates over one --dur-slow instead of
     snapping the text back to the bare layout (rule 2.12). */
  background: transparent;
  border-radius: var(--r-lg);
  padding: 0;
  transition: padding var(--dur-slow) var(--ease-out),
              background-color var(--dur-slow) var(--ease-out);
}

/* In AI mode the rotating ring uses border-radius:inherit and the glow draws a
   1px outline — with a bare .md (no radius, no surface) that resolves to a sharp
   rectangle hugging the text. Give the host a glass surface + rounded corners so
   the ring frames a proper panel, mirroring DrylRating / DrylEmptyState. */
.md.ai-aura {
  background: var(--glass-1);
  padding: var(--sp-4);
}

@media (prefers-reduced-motion: reduce) {
  .md { transition: none; }
}

.md-content {
  color: var(--fg-muted);
  font-size: var(--fs-body);
  line-height: 1.65;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.md-content > * { margin: 0; }

/* ── Streaming materialize (AI vocabulary, CLAUDE.md §2.10) ──
   Freshly streamed AI text should not pop in at full opacity — it comes
   alive. Two Blazor-safe reveals, no JS DOM surgery (that corrupts Blazor
   Server's batch diff):

   Plain text (DrylMessage Text=…, no Markdown) — each freshly arrived word is
   rendered server-side as a .stream-token span inside .stream-text; it blur-
   fades to crisp, staggered per word via --stream-i so a burst reads as a
   wave. Already-shown words are never re-created, so they never re-animate.

   Markdown (DrylMarkdown / a Markdown DrylMessage bubble) — words live inside
   Markdig markup that can't be tokenized without touching Blazor-owned nodes,
   so instead the *newest* top-level block (the one currently being written)
   carries a vertical hot→cold temperature: its bottom (the newest line, at the
   writing head) glows hot in --ai-a and cools upward through --ai-b to settled
   --fg. As the block grows, older lines rise into the cold zone — the text
   "cools with the writing head", the block-level echo of the per-word token
   cooldown. As each block finishes and the next begins, :last-child moves on
   and the settled block relaxes to normal text. Pure CSS, no markup change.

   .stream-caret / *::after — a small glowing accent dot riding the tip of the
   stream, the inline "the AI is here" indicator. Both reveals share it. */
.stream-token {
  display: inline;
  animation: stream-token-in var(--dur-slow) var(--ease-out) both;
  animation-delay: calc(var(--stream-i, 0) * 30ms);
}

/* Cooldown glow: a freshly arrived word ignites in the theme's start accent
   (--ai-a, "hot") — the glyph itself is tinted and blooms — then visibly cools
   to the end accent (--ai-b) and settles to normal text (--fg) as the glow dies.
   Words mount one after another behind the moving caret, so it reads as a short
   glowing trail cooling in the cursor's wake. Colour comes only from the AI
   accent tokens, so it follows whatever the active theme derives — never a
   hardcoded hue. Same --dur-slow / --ease-out as the reveal (no new motion).

   The reveal (opacity 0→1, blur 5→0) finishes early, in the first ~30%, so the
   hot phase is seen on a fully-visible word — opacity/blur would otherwise dim
   the glow exactly when it should be brightest. Every keyframe keeps two shadow
   layers so text-shadow interpolates cleanly; the settled word ends glowless. */
@keyframes stream-token-in {
  0% {
    opacity: 0;
    filter: blur(5px);
    color: var(--ai-a);
    text-shadow:
      0 0 8px  color-mix(in srgb, var(--ai-a) 75%, transparent),
      0 0 18px color-mix(in srgb, var(--ai-a) 45%, transparent);
  }
  30% {
    opacity: 1;
    filter: blur(0);
    color: var(--ai-a);
    text-shadow:
      0 0 8px  color-mix(in srgb, var(--ai-a) 75%, transparent),
      0 0 18px color-mix(in srgb, var(--ai-a) 45%, transparent);
  }
  65% {
    color: var(--ai-b);
    text-shadow:
      0 0 6px  color-mix(in srgb, var(--ai-b) 42%, transparent),
      0 0 13px color-mix(in srgb, var(--ai-b) 20%, transparent);
  }
  100% {
    opacity: 1;
    filter: blur(0);
    color: var(--fg);
    text-shadow:
      0 0 0 transparent,
      0 0 0 transparent;
  }
}

/* Newest-block cooldown. Excludes DrylCodeBlock (.code-block) — clipping a code
   surface to its text would erase its chrome. A vertical hot→cold gradient is
   anchored to the block box (0deg = bottom→top): the bottom (newest line, at
   the writing head) is hot --ai-a, cooling through --ai-b to settled --fg for
   the upper (older) lines. A short one-line block reads warm throughout; a tall
   one has most of its body cold with only the fresh bottom glowing.

   The gradient covers the box 1:1 (background-size 100% 100%, no-repeat), so
   EVERY glyph is painted and nothing goes transparent — the invisibility /
   caret-drift trap of an oversized swept image (see the 2.4.1 sheen fix) can't
   arise, because background-position is never animated. Instead the hot zone
   just breathes in brightness (stream-cooldown) so the glow stays alive between
   token bursts, while the actual cooling is driven by the text growing: each
   line rises out of the hot bottom into the cold body as the writing head
   descends. Colour comes only from the AI accent tokens + --fg. */
.md.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block),
.chat-bubble.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block) {
  background-image: linear-gradient(
    0deg,
    var(--ai-a) 0%, var(--ai-b) 22%,
    var(--fg) 58%, var(--fg) 100%);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
          color: transparent;
  animation: stream-cooldown var(--dur-slow) var(--ease-in-out) infinite alternate;
}

@keyframes stream-cooldown {
  from { filter: brightness(1); }
  to   { filter: brightness(1.12); }
}

.stream-caret,
.stream-text::after,
.md.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block)::after,
.chat-bubble.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block)::after {
  content: '';
  display: inline-block;
  width: 0.5em;
  height: 0.5em;
  margin-left: 0.14em;
  border-radius: var(--r-pill);
  vertical-align: baseline;
  background: linear-gradient(135deg, var(--ai-a), var(--ai-b));
  -webkit-text-fill-color: initial;   /* immune to the parent's text-fill on the sheen block */
  box-shadow:
    0 0 10px color-mix(in srgb, var(--ai-a) 60%, transparent),
    0 0 22px color-mix(in srgb, var(--ai-b) 35%, transparent);
  animation: stream-caret-pulse 1.2s var(--ease-in-out) infinite;
}

@keyframes stream-caret-pulse {
  0%, 100% { opacity: 1;    transform: scale(1); }
  50%      { opacity: 0.4;  transform: scale(0.68); }
}

@media (prefers-reduced-motion: reduce) {
  .stream-token { animation: none; }
  .md.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block),
  .chat-bubble.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block) {
    animation: none;
    /* Drop the clip so text returns to its normal colour, no frozen hot zone. */
    -webkit-text-fill-color: currentColor;
            color: var(--fg-muted);
    background-image: none;
  }
  /* Keep the caret as a static position marker; just stop it pulsing. */
  .stream-caret,
  .stream-text::after,
  .md.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block)::after,
  .chat-bubble.ai-aura.ai-streaming .md-content > *:last-child:not(.code-block)::after {
    animation: none;
  }
}

.md-content h1,
.md-content h2,
.md-content h3,
.md-content h4 {
  color: var(--fg);
  line-height: 1.3;
  font-weight: 600;
}
.md-content h1 { font-size: var(--fs-title1); }
.md-content h2 { font-size: var(--fs-title2); }
.md-content h3 { font-size: var(--fs-title3); }
.md-content h4 { font-size: var(--fs-headline); }

.md-content p { color: var(--fg-muted); }

.md-content a {
  color: var(--accent-b);
  text-decoration: none;
  border-bottom: 1px solid var(--accent-line);
  transition: color var(--dur-fast) var(--ease-out);
}
.md-content a:hover { color: var(--fg); }

.md-content strong { color: var(--fg); font-weight: 600; }

.md-content ul,
.md-content ol {
  padding-left: var(--sp-5);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}
.md-content li { color: var(--fg-muted); }

.md-content blockquote {
  padding: var(--sp-1) var(--sp-3);
  border-left: 2px solid var(--accent-line);
  color: var(--fg-dim);
  background: var(--glass-1);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
}

/* Inline code (fenced blocks are delegated to DrylCodeBlock). */
.md-content :not(pre) > code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  padding: 0.1em 0.4em;
  color: var(--fg);
  background: var(--glass-2);
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
}

.md-content hr { border: none; border-top: 1px solid var(--line); }

.md-content table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-callout);
}
.md-content th,
.md-content td {
  text-align: left;
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
}
.md-content th { color: var(--fg); font-weight: 600; }
.md-content td { color: var(--fg-muted); }

/* ─────────────────────────────────────────────────────────
   DrylCitation / DrylCitationList — source attribution for AI answers.
   ───────────────────────────────────────────────────────── */
.citation-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  margin: 0 1px;
  vertical-align: super;
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  line-height: 1;
  color: var(--accent-b);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-xs);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.citation-chip:hover,
.citation-chip--open {
  color: var(--fg);
  background: var(--accent-grad);
}

.citation-pop {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  max-width: 280px;
  padding: var(--sp-1);
}
.citation-pop-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  color: var(--fg);
  font-size: var(--fs-callout);
  font-weight: 600;
}
.citation-pop-title svg { color: var(--accent-b); flex: none; }
.citation-pop-snippet {
  margin: 0;
  color: var(--fg-muted);
  font-size: var(--fs-subhead);
  line-height: 1.5;
}
.citation-pop-link,
.citation-item-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  color: var(--accent-b);
  font-size: var(--fs-footnote);
  text-decoration: none;
  word-break: break-all;
}
.citation-pop-link:hover,
.citation-item-link:hover { color: var(--fg); }

.citation-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}
.citation-list-title {
  font-size: var(--fs-caption2);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.citation-list-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  counter-reset: none;
}
.citation-item {
  display: flex;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  background: var(--glass-1);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.citation-item-index {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  font-family: var(--font-mono);
  font-size: var(--fs-caption2);
  color: var(--accent-b);
  background: var(--accent-soft);
  border-radius: var(--r-xs);
}
.citation-item-body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  min-width: 0;
}
.citation-item-title { color: var(--fg); font-size: var(--fs-callout); font-weight: 600; }
.citation-item-snippet { margin: 0; color: var(--fg-muted); font-size: var(--fs-subhead); line-height: 1.5; }

/* ── DrylList / DrylListItem ───────────────────────────────────
   Token-driven list. The container (.list) sets the row gap via a
   density custom property; each item (.list-item) is a flex row of
   [marker | content | end]. Markers, dividers, selected and
   interactive states are all token-driven. Counter scoping per
   .list keeps Decimal numbering correct across nested sub-lists. */
.list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  counter-reset: dryl-list;
  --list-pad-y: var(--sp-2);
  --list-gap: var(--sp-1);
}
.list--compact     { --list-pad-y: var(--sp-1); --list-gap: 0; }
.list--default     { --list-pad-y: var(--sp-2); --list-gap: var(--sp-1); }
.list--comfortable { --list-pad-y: var(--sp-3); --list-gap: var(--sp-2); }

.list-item {
  counter-increment: dryl-list;
  display: flex;
  align-items: stretch;
  gap: var(--sp-2);
  margin-top: var(--list-gap);
  border-radius: var(--r-sm);
}
.list-item:first-child { margin-top: 0; }

/* Dividers — a hairline rule between adjacent rows (no inter-row gap). */
.list--dividers { --list-gap: 0; }
.list--dividers > .list-item { margin-top: 0; border-radius: 0; }
.list--dividers > .list-item + .list-item { border-top: 1px solid var(--line); }

/* Nested sub-list — indent and add a connector rail. */
.list .list-item .list {
  margin-top: var(--sp-1);
  padding-left: var(--sp-4);
  border-left: 1px solid var(--line);
}

/* Row body (div for static, button for interactive) — shared layout. */
.list-item-row {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2);
  padding: var(--list-pad-y) var(--sp-2);
  border-radius: inherit;
  color: var(--fg-muted);
  text-align: left;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.list-item-content { min-width: 0; flex: 1 1 auto; line-height: 1.5; }

/* Marker + leading slots — aligned to the first text line. */
.list-marker,
.list-item-icon,
.list-item-start {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 1.5em;
}
.list-marker { min-width: var(--sp-3); color: var(--fg-dim); }
.list-marker--default::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: var(--r-pill);
  background: var(--fg-dim);
}
.list-marker--disc::before { content: '\2022'; font-size: 1.1em; line-height: 1; }
.list-marker--dash::before { content: '\2013'; }
.list-marker--decimal {
  color: var(--fg-muted);
  font-size: var(--fs-footnote);
  font-variant-numeric: tabular-nums;
}
.list-marker--decimal::before { content: counter(dryl-list) '.'; }
.list-item-icon { color: var(--fg-dim); }

/* Trailing slot — pinned right, vertically centred on the row. */
.list-item-end {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding-right: var(--sp-2);
  align-self: center;
}

/* Interactive rows — button reset + hover/active affordance. */
button.list-item-row {
  width: 100%;
  font: inherit;
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
}
.list-item.is-interactive > .list-item-row:hover {
  background: var(--glass-1);
  border-color: var(--line);
  color: var(--fg);
}
button.list-item-row:focus-visible {
  outline: none;
  border-color: var(--accent-line);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* Selected — accent line + glass tint. */
.list-item.is-selected > .list-item-row {
  background: var(--glass-2);
  border-left: 2px solid var(--accent-line);
  color: var(--fg);
}
.list-item.is-selected .list-marker { color: var(--accent-b); }

/* Disabled — dim and inert. */
.list-item.is-disabled {
  opacity: 0.5;
  pointer-events: none;
}

/* ─────────────────────────────────────────────────────────
   Responsive foundation
   Breakpoint scale (px values intentionally literal — var() is
   illegal inside @container / @media conditions):
     Sm 480  ·  Md 768  ·  Lg 1024  ·  Xl 1280
   ───────────────────────────────────────────────────────── */

/* Make an element a container-query context so descendants can
   adapt to this element's inline size instead of the viewport. */
.cq { container-type: inline-size; }

/* Global safety layer — defensive + additive. Kills the
   "nothing shrinks / clips" bug class without an opinionated reset. */
img, svg, video, canvas { max-width: 100%; height: auto; }
.stack, .row, .col, .between, .glass-card { min-width: 0; }
.glass-card { overflow-wrap: anywhere; }
pre, .code-block, code { overflow-wrap: anywhere; }
pre { overflow-x: auto; }

/* Rows inside a card wrap instead of clipping on a narrow slot. A row that
   fits on one line is unaffected; it only wraps when it would overflow. */
.glass-card .row { flex-wrap: wrap; }

/* DrylStack CollapseBelow — horizontal stack flips to vertical
   below the chosen container width. Default row; query overrides. */
.stack-collapse-sm, .stack-collapse-md,
.stack-collapse-lg, .stack-collapse-xl { flex-direction: row; }
@container (max-width: 480px)  { .stack-collapse-sm { flex-direction: column; } }
@container (max-width: 768px)  { .stack-collapse-md { flex-direction: column; } }
@container (max-width: 1024px) { .stack-collapse-lg { flex-direction: column; } }
@container (max-width: 1280px) { .stack-collapse-xl { flex-direction: column; } }

/* DrylGrid fixed-column modes that step down on narrow slots. */
.grid-cols-2, .grid-cols-3, .grid-cols-4 { display: grid; }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@container (max-width: 768px) {
  .grid-cols-3, .grid-cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container (max-width: 480px) {
  .grid-cols-2, .grid-cols-3, .grid-cols-4 { grid-template-columns: 1fr; }
}

/* ============== CHARTS (DrylLineChart / DrylBarChart / DrylAreaChart / DrylDonutChart) ==============
   Hybrid rendering: marks are SVG (stretched viewBox, non-scaling strokes) or
   percent-positioned HTML; all text (axes, legend, tooltip) is HTML so it never
   distorts. One shared vocabulary for all four chart components. */

.chart {
  position: relative;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}

/* AI mode on charts — like .radio-group-wrapper.ai-aura: give the bare chart
   a surface + radius so the rotating ring traces rounded corners and the
   chart reads as a contained AI panel. */
.chart.ai-aura {
  border-radius: var(--r-lg);
  padding: var(--sp-4);
  background: var(--glass-1);
}

.chart-body {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: 1fr auto;
  height: var(--chart-h, 260px);
  padding-top: var(--sp-2);   /* headroom so the top tick label isn't clipped */
}

/* ── Y axis ──
   Width is established by the invisible .chart-ysizer (the longest tick label)
   so wide values ("12.000") never clip; the visible ticks are absolute. */
.chart-yaxis {
  position: relative;
  grid-row: 1;
  grid-column: 1;
  min-width: 2.6em;
  padding-right: var(--sp-2);
  font-size: var(--fs-caption2);
  color: var(--fg-dim);
}
.chart-ysizer {
  visibility: hidden;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.chart-yaxis > span:not(.chart-ysizer) {
  position: absolute;
  right: var(--sp-2);
  transform: translateY(-50%);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ── Plot area ── */
.chart-plot {
  position: relative;
  grid-row: 1;
  grid-column: 2;
  min-width: 0;
}
.chart-gridline {
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: var(--line);
  pointer-events: none;
}
.chart-zeroline { background: var(--line-strong); }

.chart-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
  /* Left→right wipe reveal — the line/area "draws in". clip-path is in the
     element's own box space, so it survives preserveAspectRatio=none and the
     non-scaling-stroke pathLength quirk (see .chart-line note). Vertical slack
     keeps stroke overflow at peaks from clipping; backwards fill reverts to
     no clip after, so nothing stays masked. */
  animation: chart-draw var(--dur-slow) var(--ease-out) backwards;
}

/* ── X axis ── */
.chart-xaxis {
  position: relative;
  grid-row: 2;
  grid-column: 2;
  height: 1.9em;
  font-size: var(--fs-caption2);
  color: var(--fg-dim);
}
.chart-xaxis span {
  position: absolute;
  top: var(--sp-2);
  transform: translateX(-50%);
  white-space: nowrap;
}

/* ── Legend ── */
.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) var(--sp-5);
  font-size: var(--fs-caption1);
  color: var(--fg-muted);
}
.chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}
.chart-swatch {
  width: 10px;
  height: 10px;
  border-radius: var(--r-xs);
  flex: none;
}

/* ── Hover columns, crosshair, tooltip (pure CSS — zero JS, zero roundtrips) ── */
.chart-col {
  position: absolute;
  top: 0; bottom: 0;
  border-radius: var(--r-xs);
}
.chart-col:focus-visible {
  outline: none;
  box-shadow: 0 0 0 1px var(--accent-line);
}
.chart-crosshair {
  position: absolute;
  left: 50%; top: 0; bottom: 0;
  width: 1px;
  background: var(--line-strong);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
  pointer-events: none;
}
.chart-col:hover .chart-crosshair,
.chart-col:focus-visible .chart-crosshair { opacity: 1; }

.chart-tip {
  position: absolute;
  top: var(--sp-2);
  left: calc(50% + var(--sp-3));
  min-width: 120px;
  max-width: 240px;
  padding: var(--sp-3);
  background: var(--panel-solid);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-md);
  font-size: var(--fs-caption1);
  color: var(--fg);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
  pointer-events: none;
  z-index: 3;
}
.chart-tip-flip {
  left: auto;
  right: calc(50% + var(--sp-3));
}
.chart-col:hover .chart-tip,
.chart-col:focus-visible .chart-tip {
  opacity: 1;
  transform: translateY(0);
}
.chart-tip-title {
  color: var(--fg-dim);
  font-size: var(--fs-caption2);
  margin-bottom: var(--sp-2);
  white-space: nowrap;
}
.chart-tip-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  white-space: nowrap;
}
.chart-tip-row + .chart-tip-row { margin-top: var(--sp-1); }
.chart-tip-row .chart-swatch { width: 8px; height: 8px; }
.chart-tip-name { color: var(--fg-muted); margin-right: auto; }
.chart-tip-value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin-left: var(--sp-4);
}

/* ── Line / area marks ──
   Note: no stroke-dasharray draw-in here — Chromium ignores pathLength for
   dash computation under vector-effect:non-scaling-stroke, which would leave
   the line permanently dashed. The "draw-in" is a clip-path wipe on the whole
   .chart-svg instead (see there); the paths themselves stay fully opaque. */
.chart-line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.chart-area {
  stroke: none;
}
.chart-marker {
  position: absolute;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  border-radius: var(--r-pill);
  box-shadow: 0 0 0 2px var(--bg);   /* surface ring — legibility over lines */
  pointer-events: none;
  /* Pop in left→right, roughly tracking the line wipe above it. */
  animation: chart-fade var(--dur-med) var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 40ms);
}

/* ── Bars ── */
.chart-bars { position: absolute; inset: 0; }
.chart-band {
  position: absolute;
  top: 0; bottom: 0;
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 2px;                 /* surface gap between adjacent bars */
  padding: 0 var(--sp-1);
}
.chart-bar-slot {
  position: relative;
  flex: 1 1 0;
  max-width: 24px;          /* bars never fill the slot */
  min-width: 2px;
}
.chart-bar {
  position: absolute;
  left: 0; right: 0;
  border-radius: var(--r-2xs) var(--r-2xs) 0 0;   /* rounded data-end, square baseline */
  transform-origin: bottom;
  animation: chart-grow var(--dur-slow) var(--ease-spring) both;
  animation-delay: calc(var(--i, 0) * 30ms);
  transition: filter var(--dur-fast) var(--ease-out);
}
.chart-bar-neg {
  border-radius: 0 0 var(--r-2xs) var(--r-2xs);
  transform-origin: top;
  animation-name: chart-grow-neg;
}
.chart-band:hover .chart-bar { filter: brightness(1.15); }

/* Stacked segments — 2px surface gap between segments, cap segment rounded */
.chart-seg {
  position: absolute;
  left: 0; right: 0;
  transform-origin: bottom;
  animation: chart-grow var(--dur-slow) var(--ease-spring) both;
  animation-delay: calc(var(--i, 0) * 30ms);
}
.chart-seg-cap { border-radius: var(--r-2xs) var(--r-2xs) 0 0; }

/* ── Donut ── */
.donut-box {
  position: relative;
  height: var(--chart-h, 260px);
  display: flex;
  justify-content: center;
}
/* Each slice wrapper is a centred square matching the SVG's viewBox, so the
   tooltip's percentage anchors map 1:1 onto donut coordinates (a full-width
   wrapper would stretch the anchor space and fling tips sideways). */
.donut-slice {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  translate: -50%;
  aspect-ratio: 1;
  pointer-events: none;
}
.donut-slice svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.donut-slice path {
  pointer-events: auto;
  cursor: default;
  /* Uniform separator: a bg-colored stroke gives a constant-width gap at every
     radius (angular padding wedges out to a point at the centre — ugly on pies).
     Same --bg "surface" token the markers ring with. */
  stroke: var(--bg);
  stroke-width: 1.5;
  transition: transform var(--dur-fast) var(--ease-spring);
  animation: donut-in var(--dur-med) var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 40ms);
  outline: none;
}
.donut-slice:hover path,
.donut-slice:focus-within path {
  transform: translate(var(--dx, 0), var(--dy, 0));
}
.donut-slice:focus-within path {
  stroke: var(--accent-line);
  stroke-width: 1.5;
}
/* Centred on the slice's mid-angle anchor; translate composes with the
   .chart-tip enter transform, so the fade/slide-in is untouched. */
.donut-tip {
  top: var(--tip-top, 50%);
  left: var(--tip-left, 50%);
  translate: -50% -50%;
}
.donut-slice:hover .donut-tip,
.donut-slice:focus-within .donut-tip {
  opacity: 1;
  transform: translateY(0);
}
.donut-center {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: var(--donut-hole, 60%);   /* sized to the hole so content never covers the ring */
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  pointer-events: none;
}
.donut-center > * { pointer-events: auto; }

/* ── Motion ── */
@keyframes chart-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes chart-grow-neg { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes chart-fade { from { opacity: 0; } to { opacity: 1; } }
/* Horizontal wipe; -10% top/bottom + -2% right slack so non-scaling strokes at
   peaks/last point never clip during the reveal. */
@keyframes chart-draw {
  from { clip-path: inset(-10% 102% -10% 0); }
  to   { clip-path: inset(-10% -2% -10% 0); }
}
@keyframes donut-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .chart-svg,
  .chart-line, .chart-area, .chart-marker,
  .chart-bar, .chart-seg, .donut-slice path {
    animation: none;
  }
  .chart-tip, .chart-crosshair, .chart-bar, .donut-slice path { transition: none; }
}

/* ──────────────────────────────────────────────────────────
   View transitions — the "Depth Glass" morph vocabulary.
   Same-document view transitions started by dryl.viewTransition
   (IDrylViewTransition). Glide tier = viscous shape settle only.
   DepthGlass tier (view-transition-class: dryl-depth) adds the
   translucency pulse + mercury merge; clarity arrives on --dur-med,
   before the --dur-slow shape finishes settling — the glass clears
   before the motion ends.
   ────────────────────────────────────────────────────────── */
::view-transition-group(*) {
  animation-duration: var(--dur-slow);
  animation-timing-function: var(--ease-viscous);
}

/* DepthGlass: mercury-like merge. A merge filter (blur + high-contrast,
   the "goo" technique) pulls the old/new snapshots together with a
   droplet's surface tension instead of a flat cross-fade. Transient —
   the pseudo-tree (and the filter with it) disappears when the
   transition ends, and the clarify pass below lands on filter: none. */
::view-transition-image-pair(*.dryl-depth) {
  filter: url(#dryl-merge);
}

/* DepthGlass: translucency + crystalline clarity. Runs on --dur-med
   (faster than the --dur-slow shape glide above) so the surface is
   always sharp before the shape finishes settling. The opacity ramp
   matters as much as the blur: the "animation" shorthand below fully
   replaces the UA default cross-fade-in, so without it the new snapshot
   would pop in at full opacity on frame one, instantly occluding the
   still-fading-out old one — a hard cut that reads as a flicker instead
   of a merge. Keeping opacity in these keyframes is what lets the two
   snapshots actually overlap while translucent, which is also what
   gives the #dryl-merge goo filter something to blend. */
@keyframes dryl-depth-clarify {
  from { opacity: 0; filter: blur(6px) saturate(1.35); }
  to   { opacity: 1; filter: blur(0) saturate(1); }
}
::view-transition-new(*.dryl-depth) {
  animation: dryl-depth-clarify var(--dur-med) var(--ease-out) both;
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*),
  ::view-transition-image-pair(*) {
    animation: none !important;
    filter: none !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   MAC-FASSUNG, NACHTRAG: BERÜHRUNG, TREFFERFLÄCHEN, DECKENDES GLAS
   ═══════════════════════════════════════════════════════════════════════
   Alles hier ist additiv. Es kommt keine Klasse hinzu ausser .glass-solid,
   und keine bestehende Regel wird umgeschrieben; der Block steht am Ende
   der Datei, damit :active in der Kaskade nach :hover greift.

   Warum überhaupt: Im Bestand standen 128 :hover-Regeln 4 :active-Regeln
   gegenüber. Auf einem Zeigergerät fällt das nicht auf, im WKWebView
   schon: ohne Rückmeldung beim Antippen wirkt eine Web-Oberfläche tot,
   und das ist der häufigste Grund, warum sie sich nicht nativ anfühlt.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── Kein grauer Systemblitz ────────────────────────────────────────────
   iOS legt beim Antippen von sich aus ein graues Rechteck über das
   Element. Das ist der Ersatz für fehlendes Feedback; wir liefern jetzt
   eigenes, also weg damit, sonst überlagern sich beide. */
html {
  -webkit-tap-highlight-color: transparent;
}

/* ── Druckfeedback, zwei Vokabeln ───────────────────────────────────────
   Was wie ein Knopf aussieht, sinkt kurz ein. Was wie eine Zeile
   aussieht, hinterlegt sich, denn eine schrumpfende Listenzeile würde
   ihren Text verschieben. Der Wischer liegt als background-image über
   der vorhandenen Flächenfarbe, damit die :hover-Fläche erhalten bleibt
   und keine bestehende Deklaration überschrieben werden muss. */
.alert-dismiss:active,
.dialog-close:active,
.toast-close:active,
.chip-remove:active,
.file-item-remove:active,
.color-mode-toggle:active,
.drawer-toggle:active,
.sidebar-toggle:active,
.nav-section-chevron-btn:active,
.tbl-bulk-bar-close:active,
.tbl-col-menu-close:active,
.tbl-col-menu-trigger:active,
.tbl-filter-popover-close:active,
.tbl-filter-chip-close:active,
.tbl-filter-trigger:active,
.tbl-expand-btn:active,
.tbl-group-toggle:active,
.tbl-edit-btn:active,
.num-step-btn:active:not(:disabled),
.swatch:active {
  transform: scale(var(--press-scale));
  background-image: linear-gradient(var(--press-wash), var(--press-wash));
  transition: transform var(--dur-press) var(--ease-out);
}

/* Knöpfe mit Verlaufsfüllung: nur einsinken. Ein background-image würde
   hier den Verlauf ersetzen statt ihn zu überlagern. */
.citation-chip:active {
  transform: scale(var(--press-scale));
  transition: transform var(--dur-press) var(--ease-out);
}

/* Kästchen und Schalter sinken ein, ohne ihre Füllung zu verlieren. */
.checkbox:active:not(:disabled),
.toggle:active:not(:disabled),
.radio:active .radio-control {
  transform: scale(0.92);
  transition: transform var(--dur-press) var(--ease-out);
}

/* Zeilen und Flächen: hinterlegen, nicht springen. */
.menu-item:active,
.nav-item:active,
.nav-section-link:active,
.nav-section-toggle:active,
.list-item.is-interactive > .list-item-row:active,
.tab:active,
.expansion-header:active,
.tbl-filter-option:active,
.tbl-col-menu-option:active,
.citation-item-link:active,
.citation-pop-link:active {
  background-image: linear-gradient(var(--press-wash), var(--press-wash));
}

/* ── Trefferflächen ─────────────────────────────────────────────────────
   Die sichtbare Grafik behält ihre Grösse, ein transparentes
   Pseudo-Element vergrössert nur die Fläche, die auf den Finger
   reagiert. --hit steht auf 24px (WCAG 2.5.8) und springt bei grobem
   Zeiger auf Apples 44pt.

   Nicht aufgenommen, jeweils mit Grund:
   · .num-step-btn (18,5px hoch, zwei Stück übereinander): eine
     Vergrösserung würde in den Nachbarknopf reichen, und der später
     gezeichnete gewänne. Das Feld darum wächst bei grobem Zeiger auf 44,
     die beiden Hälften kommen damit auf je 22.
   · .rating-btn (30px, in einer Reihe von fünf): dasselbe Problem
     waagerecht. Wer zwischen zwei Sterne tippt, träfe den falschen.
   · Tabellenkopf-Knöpfe: sie stehen dicht neben dem Ziehgriff für die
     Spaltenbreite, eine Vergrösserung würde ihn schlucken.
   · Verweise im Fliesstext: eine vergrösserte Fläche würde die Zeilen
     darüber und darunter überdecken. WCAG nimmt Inline-Verweise
     ausdrücklich aus. */
/* .toast-close fehlt hier bewusst: es steht schon auf position: absolute
   und ist damit selbst Bezugsrahmen. Ein relative würde es aus der Ecke
   der Meldung an den Textfluss zurückholen. */
.alert-dismiss,
.dialog-close,
.chip-remove,
.file-item-remove,
.color-mode-toggle,
.drawer-toggle,
.sidebar-toggle,
.nav-section-chevron-btn,
.citation-chip,
.checkbox,
.radio-control,
.toggle {
  position: relative;
}
.alert-dismiss::after,
.dialog-close::after,
.toast-close::after,
.chip-remove::after,
.file-item-remove::after,
.color-mode-toggle::after,
.drawer-toggle::after,
.sidebar-toggle::after,
.nav-section-chevron-btn::after,
.citation-chip::after,
.checkbox::before,
.radio-control::before,
.toggle::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, var(--hit));
  height: max(100%, var(--hit));
  transform: translate(-50%, -50%);
  border-radius: inherit;
}

/* Bei grobem Zeiger, also Finger statt Maus, gilt Apples Mindestmass für
   alles, was eine Zeile oder ein Feld ist. Am Zeigergerät bleibt die
   dichte Doku-Oberfläche unverändert: dort ist 44px weder gefordert noch
   gewollt, und jede Abbildung der Bestandsfassung bleibt gültig. */
@media (pointer: coarse) {
  :root {
    --hit: 44px;
  }
  .btn,
  .input,
  .textarea,
  .select,
  .search,
  /* Getippt wird auf das Feld, nicht auf die Pille darum: ohne diese
     Zeile bliebe das Eingabefeld 22px hoch in einer 44px-Hülle. */
  .search input,
  .menu-item,
  .nav-item,
  .nav-section-link,
  .nav-section-toggle,
  .tab,
  .expansion-header,
  .list-item.is-interactive > .list-item-row,
  .citation-item-link,
  .citation-pop-link,
  .tbl-filter-option,
  .tbl-col-menu-option {
    min-height: 44px;
  }
  /* Eine Zeile, die auf 44 wächst, braucht ihren Text mittig. */
  .tbl-filter-option,
  .tbl-col-menu-option,
  .citation-item-link {
    display: flex;
    align-items: center;
  }
}

/* ── Deckendes Glas ─────────────────────────────────────────────────────
   Ein Materialzustand, drei Auslöser: die Klasse .glass-solid (punktuell
   an einzelnen Karten), die Systemeinstellung „Transparenz reduzieren"
   von macOS und iOS, und fehlende backdrop-filter-Unterstützung.

   Entscheidend ist das Paar: Unschärfe aus UND Fläche deckender. Eine
   transluzente Fläche funktioniert nur, weil die Unschärfe den
   Hintergrund unlesbar macht. Fällt sie weg, scheint der Seiteninhalt
   durch die Karte und die Fläche wird unruhig. Deshalb gibt es keine
   reine backdrop-filter:none-Klasse.

   Nebeneffekt, der dafür spricht: dieselbe Schaltung ist die Antwort auf
   die Performance-Frage im WKWebView. Wer eine lange, scrollende Liste
   aus Karten hat, setzt .glass-solid und der Compositor muss den
   Hintergrund nicht mehr je Bild neu weichzeichnen. */
.glass-solid {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: var(--glass-solid-1);
}
.glass-card.glass-solid {
  background: var(--sheen-grad), var(--glass-solid-1);
}

/* Systemeinstellung und Fallback greifen an den Tokens an, nicht an den
   Regeln: eine Wertänderung erreicht alle 28 Glasflächen auf einmal.
   :root:root, damit die Stelle in der Spezifität mit den beiden
   Hellmodus-Blöcken gleichzieht und über die Reihenfolge gewinnt. */
@media (prefers-reduced-transparency: reduce) {
  :root:root {
    --glass-blur: 0px;
    --glass-blur-soft: 0px;
    --glass-blur-strong: 0px;
    --scrim-blur: 0px;
    --glass-1: var(--glass-solid-1);
    --glass-2: var(--glass-solid-2);
    --glass-3: var(--glass-solid-3);
    --panel-tint: var(--glass-solid-1);
    --panel-soft: var(--glass-solid-2);
    --panel-sticky: var(--glass-solid-1);
    --bar-bg: var(--glass-solid-2);
    /* Die Modal-Schleier bleiben, wie sie sind: sie liegen unter einer
       ohnehin deckenden Fläche, und eine hier gesetzte Farbe würde in
       beiden Modi dieselbe sein und im Hellmodus falsch liegen. */
    --grain-opacity: 0;
    --aurora-opacity: 0;
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root:root {
    --glass-1: var(--glass-solid-1);
    --glass-2: var(--glass-solid-2);
    --glass-3: var(--glass-solid-3);
  }
}

/* ── Sichere Ränder ─────────────────────────────────────────────────────
   Aussparung, Dynamic Island, Home-Indikator und die runden Ecken des
   Geräts. `viewport-fit=cover` steht seit Sitzung 1 in allen Seiten, die
   Fläche reicht damit bis unter diese Bereiche; ohne env() liefe Inhalt
   darunter durch. Ausserhalb eines solchen Geräts ist env() gleich 0, am
   Schreibtisch ändern die Regeln also nichts.
   Die Regeln stehen hier gesammelt statt in den Bestandsregeln, damit
   beim Übertragen in ein Fremdprojekt sichtbar bleibt, was zusammengehört. */
/* Die seitlichen Einschübe sitzen am Gerüst, nicht an der Leiste. Grund:
   .topbar setzt sein waagerechtes Padding in zwei Breakpoints neu (32 →
   16 px), eine spätere Regel mit demselben Gewicht würde beide aushebeln
   und die Leiste auf dem Telefon wieder auf 32 px aufblasen. Am Gerüst
   rückt stattdessen die ganze Oberfläche aus dem unsicheren Rand. */
.app-shell {
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}
/* Oben dagegen soll die Leiste unter die Aussparung reichen, statt darunter
   zu beginnen: sie wächst um den Einschub und schiebt ihren Inhalt herunter. */
.topbar {
  height: calc(var(--appbar-h) + env(safe-area-inset-top));
  padding-top: env(safe-area-inset-top);
}
/* Die eingeblendete Seitenleiste liegt fixed über dem Gerüst und ist
   deshalb von dessen Padding nicht erfasst. */
.sidebar--flyout {
  padding-left: env(safe-area-inset-left);
}
.sidebar,
.sidebar--flyout {
  padding-bottom: env(safe-area-inset-bottom);
}
.main {
  padding-bottom: env(safe-area-inset-bottom);
}
/* Die Meldungen sitzen in einer der vier Ecken; ihr Innenabstand hält sie
   aus dem unsicheren Bereich, unabhängig davon, welche Ecke es ist. */
.toast-host {
  padding:
    max(var(--sp-5), env(safe-area-inset-top))
    max(var(--sp-5), env(safe-area-inset-right))
    max(var(--sp-5), env(safe-area-inset-bottom))
    max(var(--sp-5), env(safe-area-inset-left));
}
/* Der Modal-Layer zentriert seinen Dialog; der untere Einschub steht seit
   dem Bestand in .dialog selbst, hier fehlten oben und die Seiten. */
.dialog-layer {
  padding-top: env(safe-area-inset-top);
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* ── Keine Textauswahl an Bedienelementen ───────────────────────────────
   Gemessen über alle 86 Seiten: 25 Klassen von Knöpfen und Zeilen liessen
   ihren Text markieren. Am Zeigergerät fällt das kaum auf, unter dem
   Finger schon: ein etwas längerer Druck öffnet dann die Auswahl-Lupe
   samt „Kopieren"-Blase, statt den Knopf auszulösen.
   -webkit-touch-callout schaltet zusätzlich das lange Drücken auf
   Verweisen ab, das in iOS ein Vorschaufenster öffnet. */
.alert-dismiss,
.chip-remove,
.citation-chip,
.color-mode-toggle,
.dialog-close,
.drawer-toggle,
.expansion-header,
.file-item-remove,
.list-item-row,
.nav-section-chevron-btn,
.nav-section-toggle,
.num-step-btn {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}

/* ── Konzentrische Rundung ──────────────────────────────────────────────
   Innenradius gleich Aussenradius minus Innenabstand. Geprüft wurden alle
   gerundeten Flächen mit bündigen Kindern; die meisten schneiden ihre
   Kinder ohnehin ab (overflow) und sind damit von sich aus konzentrisch,
   oder ihre Kinder tragen gar keine eigene Fläche (Dialogkopf und -fuss
   haben nur eine Trennlinie). Übrig bleibt das Menü-Panel: es rundet mit
   --r-md, hat 4px senkrechten und 0px waagerechten Innenabstand und
   schneidet nicht ab. Die eckige Hinterlegung der ersten und letzten
   Zeile stand deshalb beim Überfahren über die runde Ecke hinaus. */
.menu-panel > :first-child {
  border-radius: calc(var(--r-md) - var(--sp-1)) calc(var(--r-md) - var(--sp-1)) 0 0;
}
.menu-panel > :last-child {
  border-radius: 0 0 calc(var(--r-md) - var(--sp-1)) calc(var(--r-md) - var(--sp-1));
}
