/* ============================================================
   FlowKlar – Global Shared Stylesheet
   ============================================================ */

/* === Design Tokens (CSS Custom Properties) ===
   Alle Farbwerte sind hier zentral definiert.
   Ähnliche Farben wurden zusammengeführt (z. B. grays, reds).
   ============================================================ */
:root {
    /* ── Layout ──────────────────────────────────────────── */
    --ai-panel-width:   450px;   /* AI assistant panel width (user-resizable via the left edge) */
    --plugin-panel-width: 420px; /* a plugin's docked panel (fixed width) */

    /* ── Neutral / Slate ─────────────────────────────────────
       Tinted towards the brand navy (#0F1B3D from the logo) rather than
       a neutral grey. Every step keeps its old role, so the ~2600 existing
       var(--clr-slate-*) references inherit the new tone automatically. */
    --clr-white:        #fff;
    --clr-slate-50:     #fbfbfd;   /* page bg, table headers, very light surfaces */
    --clr-slate-100:    #f2f4f9;   /* tag bg, list separators, subtle fills */
    --clr-slate-200:    #e4e8f0;   /* borders, dividers */
    --clr-slate-300:    #cbd2e0;   /* hover borders, input borders */
    --clr-slate-400:    #98a1b9;   /* placeholder text, muted icons */
    --clr-slate-500:    #6b7590;   /* secondary text, captions */
    --clr-slate-600:    #4a5578;   /* body text, card body */
    --clr-slate-700:    #3c476b;   /* form labels, medium headings */
    --clr-slate-800:    #26325a;   /* primary text, dark surfaces */
    --clr-slate-900:    #0f1b3d;   /* brand navy — headings, sidebar bg */

    /* ── Blue / Primary ──────────────────────────────────────
       Royal blue #2A56D6 from the design language, not the old sky blue. */
    --clr-blue-50:      #f4f7fe;   /* very light blue bg */
    --clr-blue-100:     #eaf0fe;   /* status in-progress bg, tag bg */
    --clr-blue-200:     #c8d6fa;   /* tag pill border, active tag */
    --clr-blue-300:     #94aef2;   /* bar chart, tag hover border */
    --clr-blue-400:     #5c82e6;   /* brand icon, sidebar footer hover */
    --clr-blue-500:     #3d68de;   /* primary accent, focus ring */
    --clr-blue-600:     #2a56d6;   /* links, active tabs, today dot */
    --clr-blue-700:     #1b3ea8;   /* project badge */
    --clr-blue-800:     #1b3ea8;   /* tag text, status in-progress text */
    --clr-blue-900:     #142e7d;   /* active sub-link hover */
    /* "In progress" and the normal priority keep the shipped blue whatever primary colour an
       installation picks (docs/BRANDING.md §3.5): a status is not a brand colour. Literal values,
       not var(--clr-blue-*), because that ramp is what a primary colour replaces. */
    --clr-progress-50:  #f4f7fe;
    --clr-progress-100: #eaf0fe;
    --clr-progress-400: #5c82e6;
    --clr-progress-500: #3d68de;
    --clr-progress-700: #1b3ea8;
    --clr-progress-800: #1b3ea8;

    /* ── Gold (brand accent) ─────────────────────────────────
       The logo's second colour. Used sparingly: active nav marker,
       wordmark, focus highlights. Never as a status colour. */
    --clr-gold-100:     #fdf3df;   /* tinted gold surface */
    --clr-gold-500:     #f4b740;   /* brand gold */
    --clr-gold-800:     #96660c;   /* text on tinted gold */

    /* ── Green / Emerald (success) ───────────────────────── */
    --clr-green-50:     #f0fdf4;   /* success alert bg */
    --clr-green-100:    #dcfce7;   /* status completed bg */
    --clr-green-200:    #bbf7d0;   /* success alert border */
    --clr-green-300:    #86efac;   /* success outline, signature pad valid */
    --clr-green-400:    #4ade80;   /* delivery log success accent */
    --clr-green-500:    #22c55e;   /* status completed dot, chart bar */
    --clr-green-600:    #16a34a;   /* success button, toast success, valid outline */
    --clr-green-700:    #15803d;   /* success button hover */
    --clr-green-800:    #166534;   /* success alert text, status completed text */
    --clr-emerald-100:  #d1fae5;   /* start node badge bg */
    --clr-emerald-500:  #10b981;   /* completed progress bar, project completed dot */
    --clr-emerald-800:  #065f46;   /* project completed text */
    --clr-emerald-900:  #064e3b;   /* start node badge text */

    /* ── Amber / Yellow (warning) ────────────────────────── */
    --clr-amber-50:     #fefaf0;   /* calendar in-progress bg */
    --clr-amber-100:    #fdf3df;   /* locked banner bg, status open bg, draft badge */
    --clr-amber-200:    #fae7bb;   /* tinted amber border */
    --clr-amber-300:    #f7d383;   /* amber hover border */
    --clr-amber-400:    #f4b740;   /* beta badge bg, node open border */
    --clr-amber-500:    #f0a82b;   /* warning button, calendar dot, amber accent */
    --clr-amber-600:    #d18e18;   /* warning button hover */
    --clr-amber-700:    #a8760e;   /* calendar in-progress text */
    --clr-amber-800:    #96660c;   /* locked banner text, status open text */
    --clr-amber-900:    #7a5209;   /* beta badge text */

    /* ── Red (danger / error) ────────────────────────────── */
    --clr-red-50:       #fdf5f4;   /* error alert bg, danger hover bg */
    --clr-red-100:      #fdecec;   /* priority critical bg, btn-icon hover */
    --clr-red-200:      #f8d4d2;   /* error alert border, danger zone border */
    --clr-red-300:      #f0aba7;   /* danger button border */
    --clr-red-400:      #e57a75;   /* danger button hover border */
    --clr-red-500:      #e05a54;   /* indicator dot red, notification badge */
    --clr-red-600:      #dc4a45;   /* danger text, error, invalid outline, overdue */
    --clr-red-800:      #a3322e;   /* fill behind white text (error boundary, danger hover); red text on a tint is --danger-text */

    /* ── Orange (high priority) ──────────────────────────── */
    --clr-orange-100:   #ffedd5;   /* priority high bg */
    --clr-orange-500:   #f97316;   /* high-priority card accent */
    --clr-orange-700:   #c2410c;   /* priority high text */

    /* ── Violet / Purple (roles, admin) ──────────────────── */
    --clr-violet-50:    #f5f3ff;   /* calendar followup bg */
    --clr-violet-100:   #ede9fe;   /* role admin bg */
    --clr-violet-200:   #ddd6fe;   /* role chip bg */
    --clr-violet-300:   #c4b5fd;   /* node accent border */
    --clr-violet-400:   #a78bfa;   /* user bar gradient end */
    --clr-violet-500:   #8b5cf6;   /* stat accent violet, calendar followup dot */
    --clr-violet-600:   #7c3aed;   /* role chip small text */
    --clr-violet-700:   #6d28d9;   /* role admin text */
    --clr-violet-800:   #5b21b6;   /* role chip text */

    /* ── Indigo (tag filter highlights, calendar categories) ─ */
    --clr-indigo-100:   #e0e7ff;   /* tag bar item hover bg */
    --clr-indigo-200:   #c7d2fe;   /* tag bar item hover border */
    --clr-indigo-500:   #6366f1;   /* calendar category dot */
    --clr-indigo-800:   #3730a3;   /* tag bar item hover text */

    /* ── Teal (create / new-item actions) ────────────────── */
    --clr-teal-50:      #f0fdfa;   /* create row hover */
    --clr-teal-100:     #ccfbf1;   /* create badge bg */
    --clr-teal-200:     #99f6e4;   /* lpm toggle hint border */
    --clr-teal-500:     #14b8a6;   /* license "approved" status dot */
    --clr-teal-600:     #0d9488;   /* lpm toggle hint text */
    --clr-teal-700:     #0f766e;   /* create row text, create badge text */

    /* ── Cyan (charts, analytics) ────────────────────────── */
    --clr-cyan-500:     #06b6d4;   /* stat accent cyan */

    /* ── Sky (assignee chips, popover accents) ───────────── */
    --clr-sky-100:      #e0f2fe;   /* assignee chip bg */
    --clr-sky-300:      #7dd3fc;   /* license modal eyebrow */
    --clr-sky-700:      #0369a1;   /* assignee chip text */

    /* ── Pink (inline code) ──────────────────────────────── */
    --clr-pink-700:     #be185d;   /* inline code color */

    /* ── Navy (license pricing modal gradient) ───────────── */
    --clr-navy-900:     #152c48;
    --clr-navy-800:     #172f50;
    --clr-navy-700:     #1e4072;

    /* ── Semantic / Fixed (never change in dark mode) ─────── */
    --clr-always-white: #fff;
    --clr-code-bg:      #0f172a;
    --clr-code-text:    #e2e8f0;

    /* ── Adaptive Shadows ─────────────────────────────────── */
    --shadow-xs:        0 1px 2px rgba(15, 27, 61, 0.04);
    --shadow-sm:        0 1px 2px rgba(15, 27, 61, 0.04), 0 8px 20px -12px rgba(15, 27, 61, 0.16);
    --shadow-md:        0 1px 2px rgba(15, 27, 61, 0.05), 0 14px 32px -14px rgba(15, 27, 61, 0.16);
    --shadow-lg:        0 2px 4px rgba(15, 27, 61, 0.05), 0 20px 44px -16px rgba(15, 27, 61, 0.22);
    --shadow-xl:        0 24px 50px -22px rgba(15, 27, 61, 0.55);
    --shadow-btn:       0 10px 22px -10px rgba(15, 27, 61, 0.55);

    /* ── Radius Scale ──────────────────────────────────────
       Every rounded corner in the app resolves to one of these.
       See docs/DESIGN-SYSTEM.md; do not hardcode new px values. */
    --radius-xs:        6px;    /* dots, markers, progress bars */
    --radius-sm:        8px;    /* small controls, kbd hints */
    --radius-md:        12px;   /* icon buttons, nav pills, chips */
    --radius-lg:        14px;   /* buttons, inputs, list rows, tracks */
    --radius-xl:        18px;   /* tiles, search field */
    --radius-2xl:       22px;   /* cards, modals */
    --radius-pill:      999px;  /* pills, badges */
    --radius-circle:    50%;    /* avatars */

    /* ── Semantic Layer ────────────────────────────────────
       Components read these, not the raw palette above. The raw
       --clr-* values stay legal where a specific hue is the point
       (status badges, chart series, priority colours). */
    --surface-page:     var(--clr-slate-50);    /* page background */
    --surface:          var(--clr-white);       /* cards, rows, fields */
    --surface-sunken:   #f5f7fc;                /* chips, inset fills */
    --surface-track:    #eef0f7;                /* tab/segment/progress tracks */
    --surface-hover:    #f7f9fd;                /* row and list hover */
    /* A menu over the page — a context menu, a bar's panel, a picker's list —: the surface a little
       see-through and the page blurred behind it, so it reads as lying over the page. Follows the theme,
       since --surface does. Most of what lies behind a menu is near-white (the canvas, a list), so the
       glass only shows at about this much see-through; at 82 % / 14px an edge behind a menu vanished. */
    --surface-menu:     color-mix(in srgb, var(--surface) 60%, transparent);
    --menu-blur:        blur(10px) saturate(1.5);
    --border:           var(--clr-slate-200);   /* dividers, card outlines */
    --border-strong:    var(--clr-slate-300);   /* input borders, hover */

    /* A quiet button is a filled shape, not an outline — see the button
       section below. These three carry the whole interaction: they must stay
       distinguishable from each other *and* from --surface in both themes,
       which is why they are their own tokens rather than the sunken/track
       pair (those collapse onto one value under dark mode). */
    --btn-fill:         #eceff8;   /* resting secondary / quiet button */
    --btn-fill-hover:   #e1e7f4;   /* pointer on it */
    --btn-fill-active:  #d4dcef;   /* pressed, or selected in a group */

    --text:             var(--clr-slate-900);   /* headings, primary copy */
    --text-body:        var(--clr-slate-800);   /* body copy */
    --text-secondary:   var(--clr-slate-700);   /* labels, chip text */
    --text-muted:       var(--clr-slate-500);   /* meta, captions */
    --text-faint:       var(--clr-slate-400);   /* placeholders, counters */
    --text-label:       var(--clr-slate-400);   /* uppercase micro labels */

    --primary:          var(--clr-blue-600);    /* links, active states */
    --primary-hover:    var(--clr-blue-700);
    --primary-soft:     var(--clr-blue-50);     /* tinted primary surface */
    --accent:           var(--clr-gold-500);    /* brand gold — active markers */
    --danger:           var(--clr-red-600);
    --danger-text:      var(--clr-red-800);     /* red text on a red tint: badges, banners. Its own
                                                   token because red-800 is also a fill behind white text
                                                   and so cannot turn light under dark mode itself */

    /* Inverted surfaces (tooltips, dark popovers).
       Like the sidebar these stay dark in both themes, so they must not
       resolve to the slate vars — those invert under dark mode and would
       turn a navy button into a white one with a white icon on it. */
    --surface-inverse:        #0f1b3d;   /* fab, tooltip, dark popover bg */
    --surface-inverse-raised: #26325a;   /* rows/items on an inverse surface */
    --surface-inverse-hover:  #313e6b;   /* hover on an inverse surface */
    --border-inverse:         #3c476b;   /* dividers on an inverse surface */
    --text-on-inverse:        #fff;      /* primary text/icons on inverse */
    --text-on-inverse-muted:  #c3cade;   /* body copy on inverse */
    --text-on-inverse-faint:  #8c97bc;   /* meta, captions on inverse */

    /* Sidebar keeps its own scale: it stays dark in both themes, so these
       must not resolve to the slate vars (which invert under dark mode). */
    --sidebar-bg:       #0f1b3d;
    --sidebar-text:     #c3cade;
    --sidebar-text-on:  #fff;
    --sidebar-label:    #68739a;
    --sidebar-foot:     #8c97bc;
    --sidebar-active:   rgba(255, 255, 255, 0.12);
    --sidebar-hover:    rgba(255, 255, 255, 0.07);
    --sidebar-width:    268px;

    /* ── Typography ────────────────────────────────────────── */
    --font-sans:        "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono:        ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    --text-hero:        1.9rem;    /* page titles */
    --tracking-hero:    -0.025em;
    --text-label-size:  0.68rem;   /* uppercase micro labels */
    --tracking-label:   0.09em;

    /* ── Layout Rhythm ─────────────────────────────────────── */
    --page-pad-x:       3rem;
    --page-pad-y:       2.5rem;
    --top-bar-height:   56px;      /* phone; 64px from 768px up (AppShell.razor.css) */
    --section-gap:      2rem;
    --card-pad:         1.6rem;
    --row-pad:          1.1rem;
    --canvas-overscroll: 200px;    /* how far a diagram can be pushed past its origin (canvas-geometry.js) */
}

/* ============================================================
   Dark Mode – Variable Overrides
   Applied when:
     • data-theme="dark" is set on <html> (manual Dark)
     • System prefers dark AND no data-theme attribute (Auto)
   ============================================================ */

html[data-theme="dark"] {
    color-scheme: dark;

    /* ── Surfaces ─────────────────────────────────────────────
       Same navy family as light mode, read from the dark end. */
    --clr-white:        #18243f;   /* card / modal bg */
    --clr-slate-50:     #0b1226;   /* page background */
    --clr-slate-100:    #18243f;   /* subtle fills (= card bg) */
    --clr-slate-200:    #24304f;   /* borders */
    --clr-slate-300:    #35446a;   /* hover borders */
    --clr-slate-400:    #6b779b;   /* muted icons */
    --clr-slate-500:    #8b96b8;   /* secondary text */
    --clr-slate-600:    #a9b3ce;   /* body text */
    --clr-slate-700:    #c6cee1;   /* labels */
    --clr-slate-800:    #dee4ef;   /* primary text */
    --clr-slate-900:    #eef1f7;   /* headings */

    /* ── Semantic overrides ───────────────────────────────────
       These carry fixed values in light mode, so they cannot ride
       the palette inversion and must be restated here. */
    --surface-sunken:   #1e2a48;
    --surface-track:    #1e2a48;
    --surface-hover:    #1e2a48;

    /* Lifted well clear of the card (#18243f): with no border left to draw
       the edge, the fill is the only thing separating a button from what it
       sits on. */
    --btn-fill:         #26325a;
    --btn-fill-hover:   #303f6d;
    --btn-fill-active:  #3b4c85;

    /* Inverse surfaces are lifted here instead of deepened: on a dark page
       the light-mode navy would sink into the background. The text tokens
       stay light in both themes and need no override. */
    --surface-inverse:        #26325a;
    --surface-inverse-raised: #313e6b;
    --surface-inverse-hover:  #3b4a80;
    --border-inverse:         #44538a;

    /* ── Blue accents (light bg variants darkened) ────────── */
    --clr-blue-50:      #14224a;
    --clr-blue-100:     #1c2f63;
    --clr-progress-50:  #14224a;
    --clr-progress-100: #1c2f63;

    /* ── Green accents ────────────────────────────────────── */
    --clr-green-50:     #052e16;
    --clr-green-100:    #14532d;
    --clr-green-200:    #166534;

    /* ── Red accents ──────────────────────────────────────── */
    --clr-red-50:       #3b1210;
    --clr-red-100:      #7a2a27;
    --clr-red-200:      #96332f;
    --clr-red-300:      #b03d38;

    /* ── Amber accents ────────────────────────────────────── */
    --clr-amber-50:     #40270a;
    --clr-amber-100:    #6b4310;
    --clr-amber-200:    #8a5a12;

    /* ── Orange ───────────────────────────────────────────── */
    --clr-orange-100:   #431407;

    /* ── Violet / Purple ──────────────────────────────────── */
    --clr-violet-50:    #1a1040;
    --clr-violet-100:   #2d1a60;
    --clr-violet-200:   #4c2d8a;

    /* ── Teal ─────────────────────────────────────────────── */
    --clr-teal-50:      #022c22;
    --clr-teal-100:     #064e3b;
    --clr-teal-200:     #065f46;

    /* ── Sky / Cyan ───────────────────────────────────────── */
    --clr-sky-100:      #0c3357;

    /* ── Indigo ───────────────────────────────────────────── */
    --clr-indigo-100:   #1e2a5e;
    --clr-indigo-200:   #312e81;

    /* ── Emerald ──────────────────────────────────────────── */
    --clr-emerald-100:  #064e3b;

    /* ── Pink (brighter for dark surfaces) ─────────────────── */
    --clr-pink-700:     #f472b6;

    /* ── Accent text (light on dark coloured backgrounds) ──── */
    --clr-blue-700:     #94aef2;
    --clr-blue-800:     #c8d6fa;
    --clr-progress-700: #94aef2;
    --clr-progress-800: #c8d6fa;
    --clr-green-700:    #86efac;
    --clr-green-800:    #bbf7d0;
    --clr-amber-700:    #f4b740;
    --clr-amber-800:    #fbd88a;
    --clr-orange-700:   #fb923c;
    --clr-violet-700:   #c4b5fd;
    --clr-violet-800:   #ddd6fe;
    --clr-emerald-800:  #a7f3d0;
    --clr-emerald-900:  #6ee7b7;
    --clr-sky-700:      #38bdf8;
    --clr-teal-700:     #2dd4bf;
    --clr-indigo-800:   #a5b4fc;

    /* ── Red text on a red tint (see --danger-text in :root) ─ */
    --danger-text:      #fecaca;

    /* ── Shadows (stronger on dark surfaces) ──────────────── */
    --shadow-xs:        0 1px 3px rgba(0, 0, 0, 0.4);
    --shadow-sm:        0 2px 8px rgba(0, 0, 0, 0.4);
    --shadow-md:        0 4px 16px rgba(0, 0, 0, 0.45);
    --shadow-lg:        0 10px 34px rgba(0, 0, 0, 0.5);
    --shadow-xl:        0 24px 50px -22px rgba(0, 0, 0, 0.8);
    --shadow-btn:       0 10px 22px -10px rgba(0, 0, 0, 0.7);
}

/* The sidebar stays dark in both themes. Its colours come from the fixed
   --sidebar-* tokens, which do not ride the palette inversion, so only the
   backdrop needs deepening here. */
html[data-theme="dark"] .sidebar {
    --sidebar-bg: #0a1229;
}

/* ── Auto dark mode (system preference, no manual override) ── */

/* Must stay byte-for-byte in sync with the html[data-theme="dark"] block above.
   CSS cannot share one declaration list across a media query and a plain
   selector, so the values are repeated rather than derived. */
@media (prefers-color-scheme: dark) {
    html:not([data-theme]) {
        color-scheme: dark;

        --clr-white:        #18243f;
        --clr-slate-50:     #0b1226;
        --clr-slate-100:    #18243f;
        --clr-slate-200:    #24304f;
        --clr-slate-300:    #35446a;
        --clr-slate-400:    #6b779b;
        --clr-slate-500:    #8b96b8;
        --clr-slate-600:    #a9b3ce;
        --clr-slate-700:    #c6cee1;
        --clr-slate-800:    #dee4ef;
        --clr-slate-900:    #eef1f7;

        --surface-sunken:   #1e2a48;
        --surface-track:    #1e2a48;
        --surface-hover:    #1e2a48;

        --btn-fill:         #26325a;
        --btn-fill-hover:   #303f6d;
        --btn-fill-active:  #3b4c85;

        --surface-inverse:        #26325a;
        --surface-inverse-raised: #313e6b;
        --surface-inverse-hover:  #3b4a80;
        --border-inverse:         #44538a;

        --clr-blue-50:      #14224a;
        --clr-blue-100:     #1c2f63;
        --clr-progress-50:  #14224a;
        --clr-progress-100: #1c2f63;
        --clr-green-50:     #052e16;
        --clr-green-100:    #14532d;
        --clr-green-200:    #166534;
        --clr-red-50:       #3b1210;
        --clr-red-100:      #7a2a27;
        --clr-red-200:      #96332f;
        --clr-red-300:      #b03d38;
        --clr-amber-50:     #40270a;
        --clr-amber-100:    #6b4310;
        --clr-amber-200:    #8a5a12;
        --clr-orange-100:   #431407;
        --clr-violet-50:    #1a1040;
        --clr-violet-100:   #2d1a60;
        --clr-violet-200:   #4c2d8a;
        --clr-teal-50:      #022c22;
        --clr-teal-100:     #064e3b;
        --clr-teal-200:     #065f46;
        --clr-sky-100:      #0c3357;
        --clr-indigo-100:   #1e2a5e;
        --clr-indigo-200:   #312e81;
        --clr-emerald-100:  #064e3b;

        --clr-pink-700:     #f472b6;

        --clr-blue-700:     #94aef2;
        --clr-blue-800:     #c8d6fa;
        --clr-progress-700: #94aef2;
        --clr-progress-800: #c8d6fa;
        --clr-green-700:    #86efac;
        --clr-green-800:    #bbf7d0;
        --clr-amber-700:    #f4b740;
        --clr-amber-800:    #fbd88a;
        --clr-orange-700:   #fb923c;
        --clr-violet-700:   #c4b5fd;
        --clr-violet-800:   #ddd6fe;
        --clr-emerald-800:  #a7f3d0;
        --clr-emerald-900:  #6ee7b7;
        --clr-sky-700:      #38bdf8;
        --clr-teal-700:     #2dd4bf;
        --clr-indigo-800:   #a5b4fc;

        --danger-text:      #fecaca;

        --shadow-xs:        0 1px 3px rgba(0, 0, 0, 0.4);
        --shadow-sm:        0 2px 8px rgba(0, 0, 0, 0.4);
        --shadow-md:        0 4px 16px rgba(0, 0, 0, 0.45);
        --shadow-lg:        0 10px 34px rgba(0, 0, 0, 0.5);
        --shadow-xl:        0 24px 50px -22px rgba(0, 0, 0, 0.8);
        --shadow-btn:       0 10px 22px -10px rgba(0, 0, 0, 0.7);
    }

    html:not([data-theme]) .sidebar {
        --sidebar-bg: #0a1229;
    }
}

/* === Brand Typeface ===
   Manrope, self-hosted latin subset, variable weight 200–700. One file for
   every weight the UI uses. The About modal deliberately registers the same
   file under "SB Manrope" with its own tokens — that component does not take
   part in this design system, so the two must not be merged. */
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 200 700;
    font-display: swap;
    src: url(/branding/fonts/manrope-latin.woff2) format('woff2');
}

/* === Reset & Base === */
*, *::before, *::after {
    box-sizing: border-box;
}

html, body {
    margin: 0;
    padding: 0;
    font-family: var(--font-sans);
    font-size: 15px;
    line-height: 1.6;
    color: var(--text-body);
    background: var(--surface-page);
    height: 100%;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* Reachable for screen readers and FocusOnNavigate, invisible on screen. Use for headings a
   layout does not show but the document structure needs. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

h1, h2, h3, h4 {
    margin-top: 0;
    font-weight: 600;
    color: var(--text);
    line-height: 1.25;
    letter-spacing: -0.015em;
}

/* Page titles carry the design language's weight: large, tightly tracked.
   clamp() keeps them from swallowing a phone screen. */
h1 {
    font-size: clamp(1.5rem, 1.15rem + 1.6vw, var(--text-hero));
    font-weight: 700;
    letter-spacing: var(--tracking-hero);
}

h2 { font-size: 1.2rem; }
h3 { font-size: 1.05rem; }

a {
    color: var(--primary);
    text-decoration: none;
}

a:hover {
    color: var(--primary-hover);
    text-decoration: underline;
}

h1:focus { outline: none; }

/* === SVG Icon === */
.icon {
    display: inline-block;
    vertical-align: middle;
    flex-shrink: 0;
}

/* A tag's symbol in place of its colour dot. The glyph sits inside a 24-unit grid with blank units
   round it, so beside a dot it read as set further in and further off the name; the margins pull
   that blank in, and a list of tags lines its names up whether they carry a dot or a symbol. */
.icon-mark {
    margin: 0 -1px 0 -2px;
}

/* An icon that stands for work in progress ("loader") turns while the work runs. */
.icon-spin {
    animation: icon-spin 1.1s linear infinite;
    transform-origin: 50% 50%;
}

@keyframes icon-spin {
    to {
        transform: rotate(360deg);
    }
}

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


/* === Reveal slot ===
   Wraps a block that comes and goes with a state change, so it grows out of its row rather than
   appearing in it — the same idea as the start badge's slot on the flow canvas, but for content
   whose height is not known up front.

   grid-template-rows interpolates between 0fr and 1fr, which animates to the block's natural
   height. A max-height guess cannot: too low it clips a wrapped line, too high it spends most of
   the duration animating past the end of the content, which reads as a lag.

   The slot takes exactly one child — the track is what animates, and a second child would be given
   a track of its own that no rule here collapses. */
.reveal-slot {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows 0.25s ease, opacity 0.2s ease;
}

/* min-height because a grid item defaults to min-height: auto and refuses to shrink below its
   content, which would hold the slot open at 0fr.

   overflow is the interesting half. It has to be hidden while the slot is closing, or the content
   spills out of a track that no longer has room for it — but it must not stay hidden once open, or
   anything that reaches outside the block is clipped: a dropdown in a filter row, a tooltip above a
   figure. allow-discrete gets both: overflow flips to visible only at the end of the opening, and
   back to hidden at the start of the closing. A browser without it clips nothing and merely shows
   the content a moment early while the block opens. */
.reveal-slot > * {
    min-height: 0;
    overflow: visible;
    transition: overflow 0.25s allow-discrete;
}

.reveal-slot.is-gone {
    grid-template-rows: 0fr;
    opacity: 0;
}

.reveal-slot.is-gone > * {
    overflow: hidden;
}

/* The child's transition goes too: with no height animation left to wait for, overflow has to switch
   at once or the content stays clipped for a quarter second after the block is already open. */
@media (prefers-reduced-motion: reduce) {
    .reveal-slot,
    .reveal-slot > * {
        transition: none;
    }
}


/* === Row leaving a list ===
   An entry that has been dealt with and is on its way off a list — restored from the trash, and
   anything else that earns the same send-off. Without it the only sign is one fewer line in a list
   that redrew, which says neither that it was this entry nor what became of it.

   Global rather than scoped because the row it marks is usually rendered by DataTable: a page's own
   .razor.css cannot reach into a child component, and ::deep needs a scoped ancestor, which a
   component tag does not provide.

   A table row cannot be collapsed to zero height without wrapping every cell, so the rows below it
   still close the gap in one step — by which time this one has gone. */
.row-leaving {
    animation: row-leave 0.26s ease-in both;
    pointer-events: none;
}

@keyframes row-leave {
    from { opacity: 1; transform: translateX(0); }
    to   { opacity: 0; transform: translateX(1.5rem); }
}

/* The toast says what happened; the travel is what goes. */
@media (prefers-reduced-motion: reduce) {
    .row-leaving {
        animation: none;
        opacity: 0;
    }
}


/* === Cards ===
   Soft layers: the card is defined by its shadow, not by an outline. A border
   would flatten the stack the design language builds out of tinted shadows. */
.card-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
    margin-bottom: 1.5rem;
}

/* The grid of ProjectTiles (home page, "Starten"): as many columns as fit rather than a fixed count —
   on a wide screen a fixed three stretch into long strips with a picture smeared across them. 340px
   keeps a tile's figures and its "vor … · Name" on one line each; min() keeps one column on a phone. */
.project-tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
    gap: 0.9rem;
}

/* opacity is in the list for the grace period: deleting a card inside it is reversible, and the card
   receding rather than being cut away is what says so. It has to be declared here and not on
   .is-deleted — a transition the element only gains together with its new value never runs. */
.card {
    background: var(--surface);
    border-radius: var(--radius-2xl);
    border: 1px solid transparent;
    padding: var(--card-pad);
    box-shadow: var(--shadow-md);
    transition: box-shadow 0.2s, transform 0.2s, opacity 0.3s ease;
}

.card:hover {
    box-shadow: var(--shadow-lg);
}

/* === Tags === */
.tag {
    display: inline-block;
    background: var(--surface-sunken);
    color: var(--text-secondary);
    border-radius: var(--radius-sm);
    padding: 0.15rem 0.6rem;
    font-size: 0.78rem;
    font-weight: 500;
}






/* === Buttons === */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    padding: 0.7rem 1.4rem;
    border-radius: var(--radius-lg);
    border: 1px solid transparent;
    font-size: 0.88rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, box-shadow 0.15s, border-color 0.15s;
    text-decoration: none !important;
    line-height: 1.4;
    white-space: nowrap;
    font-family: inherit;
}

.btn:hover {
    text-decoration: none !important;
}

.btn:focus-visible {
    outline: 2px solid var(--clr-blue-500);
    outline-offset: 2px;
}

/* A disabled button has to look disabled. Without this rule the `disabled`
   attribute is invisible — the button keeps its full colour and its hover
   state, so a click that the browser silently swallows reads as a broken app
   rather than as a missing required field. Every :hover below is guarded for
   the same reason: a colour change on hover signals "clickable". */
.btn:disabled,
.btn-icon:disabled,
.btn-icon-box:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* The reference's primary action: brand navy, lifted off the page by a
   deep tinted shadow rather than by a border. */
.btn-primary {
    background: var(--clr-slate-900);
    color: var(--clr-white);   /* inverts with the scale under dark mode */
    border-color: transparent;
    box-shadow: var(--shadow-btn);
}

.btn-primary:hover:not(:disabled) {
    background: var(--clr-slate-800);
}

/* The quiet counterpart to the primary action. It is a *filled shape*, never
   an outline: a border around a button is a second edge competing with the
   shape the fill already draws, and it is what made the old UI read as a grid
   of boxes. Everything secondary in the app resolves to this pattern — fill at
   rest, a step up on hover, a step further when pressed or selected. */
.btn-secondary {
    background: var(--btn-fill);
    color: var(--text-secondary);
    border-color: transparent;
    box-shadow: var(--shadow-xs);
}

.btn-secondary:hover:not(:disabled) {
    background: var(--btn-fill-hover);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}

/* Pressed sits back down on the page — the one place a weaker shadow is right. */
.btn-secondary:active:not(:disabled) {
    background: var(--btn-fill-active);
    box-shadow: var(--shadow-xs);
}

/* Green is what "start" looks like across the app: the play button on a card, "Workflow
   starten", "Aktivieren", "Ausführen". Lifted off the page by a tinted shadow like the
   primary button, so it reads as the most prominent action wherever it sits. */
.btn-success {
    background: var(--clr-green-600);
    color: var(--clr-always-white);
    border-color: transparent;
    box-shadow: 0 10px 22px -10px rgba(22, 163, 74, 0.75);
}

.btn-success:hover:not(:disabled) {
    background: var(--clr-green-700);
}

/* Destructive, but not the page's loudest thing: a red *tint*, not a red
   outline on white. The tint carries the warning on its own, and it deepens
   on hover the way every other filled button here does. */
.btn-danger {
    background: var(--clr-red-50);
    color: var(--clr-red-600);
    border-color: transparent;
    box-shadow: var(--shadow-xs);
}

.btn-danger:hover:not(:disabled) {
    background: var(--clr-red-100);
    box-shadow: var(--shadow-sm);
}

.btn-sm {
    padding: 0.5rem 1rem;
    font-size: 0.82rem;
    border-radius: var(--radius-md);
}

/* "Freigabelink" while a link is out — a form or a protocol: a small green dot, so the button says the
   object can be opened without signing in. Global, so both pages draw the same mark. */
.share-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-left: 0.15rem;
    border-radius: 50%;
    background: var(--clr-green-500);
}

/* Borderless low-emphasis button for inline actions next to titles/fields
   (rename, duplicate, clear). Pair with .btn .btn-sm. */
.btn-ghost {
    background: transparent;
    color: var(--text-muted);
    border-color: transparent;
    box-shadow: none;
}

.btn-ghost:hover:not(:disabled) {
    background: var(--surface-sunken);
    color: var(--text);
}

.btn-group {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.btn-icon {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-faint);
    padding: 0.2rem;
    border-radius: var(--radius-sm);
    display: inline-flex;
    align-items: center;
}

.btn-icon:hover:not(:disabled) {
    color: var(--clr-red-600);
    background: var(--clr-red-100);
}

/* Icon buttons hover red because most of them delete something. Harmless actions – downloading
   above all – take the interactive blue instead, so red keeps meaning "this destroys something".
   Must stay after the rule above: same specificity, source order decides. */
.btn-icon-accent:hover:not(:disabled) {
    color: var(--clr-blue-600);
    background: var(--clr-blue-50);
}

/* Play is green wherever it appears — including as a bare icon button in a table row. */
.btn-icon-play {
    color: var(--clr-green-600);
}

.btn-icon-play:hover:not(:disabled) {
    color: var(--clr-green-700);
    background: var(--clr-green-50);
}

/* An icon button rendered as a link (a download pointing at an endpoint) must not be underlined. */
a.btn-icon {
    text-decoration: none;
}


/* 32px boxed icon button for compact page toolbars (calendar nav, rss, …).
   Global because pages render it inside PageScaffold slots. */
.btn-icon-box {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    background: var(--btn-fill);
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    color: var(--text-muted);
    cursor: pointer;
    box-shadow: var(--shadow-xs);
    transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}

.btn-icon-box:hover:not(:disabled) {
    background: var(--btn-fill-hover);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}

.btn-icon-box:active:not(:disabled) {
    background: var(--btn-fill-active);
    box-shadow: var(--shadow-xs);
}

/* Quiet icon action in a card footer: no box until the pointer is on it. Global because
   pages render these into the <Card> Actions slot, where the card's scoped CSS cannot reach. */
/* Both classes get set together on deliberately quiet actions (a banner's
   dismiss X, a delete next to a row). .btn-icon-box wins on source order, so
   ghost has to be restated at a higher specificity to stay ghost. */
.btn-ghost.btn-icon-box {
    background: transparent;
    box-shadow: none;
}

.btn-ghost.btn-icon-box:hover:not(:disabled) {
    background: var(--surface-sunken);
    box-shadow: none;
}

/* A row of icon actions in a card footer, not a standalone button: it stays
   flat and transparent until hovered, so it must drop the box's elevation. */
.card-act {
    border-color: transparent;
    background: transparent;
    box-shadow: none;
    width: 32px;
    height: 32px;
}

/* === Top bar (docs/TOP-BAR.md) ===
   Global because the actions are separate components — focus, the bell, the assistant, the recorder,
   the plugins, the account — and scoped CSS does not reach across them. The bar's frame and layout are
   AppTopBar.razor.css. */

/* 64px from 768px up; the phone keeps 56px (the token's default). */
@media (min-width: 768px) {
    :root {
        --top-bar-height: 64px;
    }
}

/* A white disc floating on the page ground, set apart by shadow alone. Colour only for state: an
   open panel, an active focus, a count. The --pill variant carries a label beside the icon. */
.tb-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    height: 44px;
    min-width: 44px;
    padding: 0;
    border: none;
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--text-secondary);
    box-shadow: var(--shadow-sm);
    font: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    flex-shrink: 0;
    transition: box-shadow 0.2s, color 0.15s, background 0.15s;
}

.tb-btn:hover {
    box-shadow: var(--shadow-md);
    color: var(--text);
}

.tb-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 3px;
}

.tb-btn.is-open,
.tb-btn.is-active {
    background: var(--primary-soft);
    color: var(--primary);
}

.tb-btn--pill {
    padding: 0 1rem 0 0.85rem;
}

/* A marker on a disc: the bell's count here, the avatar's presence dot in AccountMenu. Both are
   centred on the disc's rim at its lower right (85.4% is the rim at 45°) and cut out of it by a ring
   in the page's colour, so they read as one kind of thing on two discs. `translate`, not `transform`:
   the badge's pop-in animates `transform`, and the two compose instead of one replacing the other. */
.tb-badge {
    position: absolute;
    left: 85.4%;
    top: 85.4%;
    translate: -50% -50%;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: var(--radius-pill);
    background: var(--clr-red-500);
    color: var(--clr-always-white);
    font-size: 0.66rem;
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    box-shadow: 0 0 0 2px var(--surface-page);
}

/* Phone: flat. Shadowed discs crowd a 56px header. */
@media (max-width: 767px) {
    .tb-btn {
        height: 40px;
        min-width: 40px;
        background: none;
        box-shadow: none;
    }

    .tb-btn:hover {
        background: var(--btn-fill);
        box-shadow: none;
    }

    .tb-btn--pill {
        padding: 0 0.6rem;
    }
}

/* A row in one of the bar's panels (the account menu, the overflow). */
.tb-menu-item {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    width: 100%;
    padding: 0.55rem 0.75rem;
    border: none;
    border-radius: var(--radius-md);
    background: none;
    color: var(--text-body);
    font: inherit;
    font-size: 0.88rem;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.12s, color 0.12s;
}

.tb-menu-item:hover,
.tb-menu-item:focus-visible {
    background: var(--surface-hover);
    color: var(--text);
    text-decoration: none;
    outline: none;
}

.tb-menu-item .icon {
    flex-shrink: 0;
    color: var(--text-muted);
}

.tb-menu-item.is-active {
    color: var(--primary);
}

.tb-menu-item.is-active .icon {
    color: var(--primary);
}

.tb-menu-label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tb-menu-meta {
    flex-shrink: 0;
    max-width: 45%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-faint);
    font-size: 0.8rem;
}

.tb-menu-divider {
    height: 1px;
    margin: 0.35rem 0.25rem;
    background: var(--border);
}

/* A search hit brought on screen (wwwroot/js/top-bar.js): the words marked without touching the DOM. */
::highlight(tb-search) {
    background-color: var(--clr-gold-100);
    color: inherit;
}

/* Wherever the page jumped to — a search hit (top-bar.js), a heading picked in a contents column
   (toc-spy.js) — pulses once so the eye finds where it landed. */
.reveal-flash {
    animation: reveal-flash 1.4s ease-out;
    border-radius: var(--radius-md);
}

/* A ring only: the element keeps its own background, a card its white. */
@keyframes reveal-flash {
    0%   { box-shadow: 0 0 0 0 rgba(42, 86, 214, 0); }
    20%  { box-shadow: 0 0 0 6px rgba(42, 86, 214, 0.22); }
    100% { box-shadow: 0 0 0 0 rgba(42, 86, 214, 0); }
}

@media (prefers-reduced-motion: reduce) {
    .reveal-flash {
        animation: none;
        outline: 2px solid var(--primary);
        outline-offset: 2px;
    }
}

/* === Icon hover motion ===
   Pointing at an entry of the main menu, or at a button of the top bar, makes its icon act out what it
   stands for, once: the gear turns, the ticks are drawn again, the bell swings. The one deliberate
   exception to "movement answers what changed" (DESIGN-SYSTEM.md §9a) — it answers "this is the thing
   you are on". Three hosts: the main menu (.sidebar--motion, which only MainLayout switches on through
   AppShell's IconMotion — the settings menu stays still), the top bar's buttons (.tb-btn and the
   buttons in the search field), and .icon-motion-host — a card whose symbol stands for a whole
   feature, so far only the module cards of the licence page. A bar button marked .tb-btn--still stays
   still: the back arrow points where it goes, and a flourish there would only say "button". Global
   rather than in AppShell.razor.css so all of them share one set of keyframes; the top bar is not the
   shell's markup.

   Icon.razor marks the parts that move with a motion-* class, and each class below is one kind of
   movement. A new glyph picks one of them rather than bringing its own keyframes; one with no marked
   part — a plugin's own outline — gets the plain nudge at the end. motion-step-N staggers the parts of
   one glyph.

   Every movement ends on the shape it started from (the gear turns by a quarter, which is its own
   symmetry), so leaving half-way snaps back by a pixel at most. Parts move in the glyph's units: 24 to
   the icon, 18px on screen, so 3px here are 2.25px there.

   Hover-capable pointers only — on touch a tap would leave the icon stuck mid-flourish — and reduced
   motion drops all of it. */
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
    /* A hopping part may cross the viewBox edge; an inline SVG clips there by default. */
    :is(.sidebar--motion .sidebar-icon-slot, .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host) .icon {
        overflow: visible;
    }

    /* Hop, drop, push: one bounce, told apart only by direction. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) :is(.motion-hop, .motion-drop, .motion-push) {
        animation: nav-icon-hop 0.55s backwards;
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-hop {
        --nav-icon-dx: 0px;
        --nav-icon-dy: -3px;
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-drop {
        --nav-icon-dx: 0px;
        --nav-icon-dy: 3px;
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-push {
        --nav-icon-dx: 3px;
        --nav-icon-dy: 0px;
    }

    /* Shrinks into its own centre and springs back. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-pulse {
        transform-box: fill-box;
        transform-origin: center;
        animation: nav-icon-pulse 0.45s ease-in-out backwards;
    }

    /* Grows with a twist. The origin is the star's centre, which sits below its box's. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-pop {
        transform-box: fill-box;
        transform-origin: 50% 55%;
        animation: nav-icon-pop 0.55s ease-in-out;
    }

    /* Settles onto its bottom edge, as if something had been dropped in. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-squash {
        transform-box: fill-box;
        transform-origin: 50% 100%;
        animation: nav-icon-squash 0.55s ease-in-out;
    }

    /* Folds towards its left edge and back — a page lifted at the spine. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-flip {
        transform-box: fill-box;
        transform-origin: 0% 50%;
        animation: nav-icon-flip 0.55s ease-in-out;
    }

    /* Flickers on. fill-opacity has to rest at 0: the fill stays set for as long as the pointer does. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-glow {
        fill: currentColor;
        fill-opacity: 0;
        animation: nav-icon-glow 0.8s ease-out;
    }

    /* Turn and tilt pivot on the icon's centre, so parts drawn apart (a "?" and its dot) move as one. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) :is(.motion-turn, .motion-tilt) {
        transform-box: view-box;
        transform-origin: 50% 50%;
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-turn {
        animation: nav-icon-turn 0.7s cubic-bezier(0.45, 0, 0.2, 1);
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-tilt {
        animation: nav-icon-tilt 0.7s ease-in-out;
    }

    /* Erased and drawn again along its stroke. Needs pathLength='1' on the part (see Icon.razor);
       backwards keeps a staggered part hidden until its turn instead of showing it twice. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-draw {
        stroke-dasharray: 1;
        animation: nav-icon-draw 0.4s ease-out backwards;
    }

    /* Rolls down 11 units into place, in motion-draw's time and easing: a side drawn from the top
       alongside it, 11 units long, ends on the roll the whole way. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-unroll {
        animation: nav-icon-unroll 0.4s ease-out backwards;
    }

    /* After the movements: the delay has to win over their animation shorthand. */
    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-step-2 {
        animation-delay: 0.08s;
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-step-3 {
        animation-delay: 0.16s;
    }

    :is(.sidebar--motion :is(.sidebar-link, .sidebar-footer-icon), .tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .motion-step-4 {
        animation-delay: 0.24s;
    }

    /* Everything else: the whole icon lifts a little. Screen pixels here — this is the <svg> itself. */
    .sidebar--motion :is(.sidebar-link, .sidebar-footer-icon):is(:hover, :focus-visible) .sidebar-icon-slot > .icon:not(:has([class*="motion-"])),
    :is(.tb-btn:not(.tb-btn--still), .tbs-end-btn, .icon-motion-host):is(:hover, :focus-visible) .icon:not(:has([class*="motion-"])) {
        animation: nav-icon-nudge 0.45s ease-out;
    }
}

@keyframes nav-icon-hop {
    0% {
        transform: translate(0, 0);
        animation-timing-function: cubic-bezier(0.25, 0.8, 0.4, 1);
    }
    40% {
        transform: translate(var(--nav-icon-dx), var(--nav-icon-dy));
        animation-timing-function: cubic-bezier(0.6, 0, 0.9, 0.5);
    }
    70% {
        transform: translate(0, 0);
        animation-timing-function: ease-out;
    }
    85% {
        transform: translate(calc(var(--nav-icon-dx) * 0.25), calc(var(--nav-icon-dy) * 0.25));
        animation-timing-function: ease-in;
    }
    100% {
        transform: translate(0, 0);
    }
}

@keyframes nav-icon-pulse {
    0%, 100% { transform: scale(1); }
    40% { transform: scale(0.7); }
    75% { transform: scale(1.06); }
}

@keyframes nav-icon-pop {
    0%, 100% { transform: scale(1) rotate(0deg); }
    40% { transform: scale(1.2) rotate(14deg); }
    70% { transform: scale(0.95) rotate(-4deg); }
}

@keyframes nav-icon-squash {
    0%, 100% { transform: scale(1, 1); }
    30% { transform: scale(1.08, 0.8); }
    60% { transform: scale(0.96, 1.07); }
    82% { transform: scale(1.02, 0.98); }
}

@keyframes nav-icon-flip {
    0%, 100% { transform: scaleX(1); }
    45% { transform: scaleX(0.25); }
}

@keyframes nav-icon-glow {
    0%, 100% { fill-opacity: 0; }
    20% { fill-opacity: 0.5; }
    35% { fill-opacity: 0.15; }
    55% { fill-opacity: 0.45; }
}

@keyframes nav-icon-turn {
    from { transform: rotate(0deg); }
    to { transform: rotate(90deg); }
}

@keyframes nav-icon-tilt {
    0%, 100% { transform: rotate(0deg); }
    20% { transform: rotate(-14deg); }
    45% { transform: rotate(10deg); }
    70% { transform: rotate(-5deg); }
    88% { transform: rotate(2deg); }
}

@keyframes nav-icon-draw {
    from { stroke-dashoffset: 1; }
    to { stroke-dashoffset: 0; }
}

@keyframes nav-icon-unroll {
    from { transform: translateY(-11px); }
    to { transform: translateY(0); }
}

@keyframes nav-icon-nudge {
    0%, 100% { transform: translateY(0) scale(1); }
    40% { transform: translateY(-2px) scale(1.08); }
    75% { transform: translateY(0.5px) scale(0.98); }
}


/* === Forms === */
.form-group {
    margin-bottom: 1rem;
}

/* "(optional)" after a label. One rule: ten dialogs kept a copy of their own, in four shades, and three used a
   class that had none at all. */
.form-hint-optional {
    font-weight: 400;
    color: var(--text-faint);
    text-transform: none;
    letter-spacing: 0;
}

.form-group label {
    display: block;
    font-weight: 500;
    margin-bottom: 0.3rem;
    font-size: 0.88rem;
    color: var(--clr-slate-700);
}

/* Top margin mirrors the label's gap above the field, so the hint sits as far below the
   control as the label sits above it instead of sticking to the border. */
.form-hint {
    display: block;
    font-size: 0.75rem;
    line-height: 1.35;
    color: var(--clr-slate-400);
    margin: 0.3rem 0 0.4rem;
}

/* A group that closes with a hint ends on small grey text instead of a bordered box, so the
   usual 1rem to the next label reads too tight — give it the missing edge back as distance. */
.form-group:has(> .form-hint:last-child) {
    margin-bottom: 1.35rem;
}

/* The element a design is drawn into (docs/DESIGNS.md D6). Its stylesheets are scoped to it on the server
   (DesignFrame), which also takes position: fixed out of them; here what the design places absolutely is placed
   against the frame and, on screen, cut at its edge — so the design cannot lay a dialog over the page. Not
   `contain: paint`: that would also hold the page's own fixed layers inside the frame, the full-screen signature
   pad and the camera of a form among them; overflow clips only what is positioned within the frame. On paper a
   design is the sheet and nothing to clip. */
.design-frame {
    position: relative;
}

@media screen {
    .design-frame {
        overflow: clip;
        overflow-clip-margin: 1rem;
    }
}

/* The --form-* variables are set only by a form design, on its own markup, and reach the fields it
   frames by inheritance (docs/FORMS.md §11.5). Everywhere else the fallbacks apply. */
.form-control {
    width: 100%;
    padding: var(--form-field-padding, 0.6rem 0.85rem);
    border: 1px solid var(--form-line-color, var(--border-strong));
    border-width: var(--form-line-width, 1px);
    border-radius: var(--form-radius, var(--radius-md));
    font-size: 0.88rem;
    background: var(--surface);
    color: var(--text-body);
    transition: border-color 0.15s, box-shadow 0.15s;
    font-family: inherit;
}

.form-control:focus {
    outline: none;
    border-color: var(--form-accent, var(--primary));
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--form-accent, rgb(42, 86, 214)) 14%, transparent);
}

textarea.form-control {
    /* Block, not inline: kills the phantom descender gap under the box that made the
       distance to the next form group a few pixels larger than everywhere else. */
    display: block;
    resize: vertical;
    min-height: 80px;
}

/* Value violates the field's min/max or validation pattern. */
.form-control.is-invalid {
    border-color: var(--clr-red-300);
    background: var(--clr-red-50);
}

.form-control.is-invalid:focus {
    border-color: var(--clr-red-600);
    box-shadow: 0 0 0 3px rgba(220, 74, 69, 0.12);
}

/* === Select display variants (segmented control / radio group) ===
   Global because the same markup renders in the template editor preview (FormFieldCanvas) and
   in the fill-in sheet (FormFillSheet) — scoped CSS would not reach both. The rest of the form
   body is scoped in Components/Shared/FormFillSheet.razor.css. */
/* Form-field flavour of the SegmentedControl component: same pill, but the options come from
   user data, so they wrap and carry the form-control font size. Keep the two visually in sync –
   see Components/Shared/SegmentedControl.razor.css. */
.fs-segmented {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 3px;
    padding: 5px;
    background: var(--surface-track);
    border: 1px solid transparent;
    border-radius: var(--radius-lg);
    max-width: 100%;
}

.fs-segmented-btn {
    border: none;
    background: transparent;
    padding: 0.55rem 1.1rem;
    border-radius: var(--radius-md);
    font-size: 0.88rem;
    font-family: inherit;
    color: var(--text-muted);
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}

.fs-segmented-btn:hover:not(:disabled):not(.active) {
    color: var(--text);
    background: var(--surface-hover);
}

.fs-segmented-btn.active {
    background: var(--surface);
    color: var(--text);
    font-weight: 600;
    box-shadow: var(--shadow-xs), 0 6px 14px -8px rgba(15, 27, 61, 0.2);
}

.fs-segmented-btn:disabled {
    cursor: default;
}

.fs-radio-group {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.fs-radio-option {
    display: inline-flex;
    align-items: flex-start;
    gap: 0.5rem;
    font-size: 0.88rem;
    cursor: pointer;
}

.fs-radio-option input[type="radio"] {
    accent-color: var(--clr-slate-800);
    width: 1rem;
    height: 1rem;
    margin-top: 0.1rem;
    flex-shrink: 0;
}

/* A text field that offers a QR scan (FormFieldDefinition.ScanQrCode). Global for the same reason as
   the select variants. The symbol sits inside the box at its right edge, so the field keeps its width
   in the grid; on paper it would be a button nobody can press, so print and PDF leave the plain box. */
.fs-scan {
    position: relative;
}

.fs-scan > .form-control {
    padding-right: 2.75rem;
}

.fs-scan-btn {
    position: absolute;
    top: 50%;
    right: 0.3rem;
    transform: translateY(-50%);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    border: none;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

/* Only the icon answers the pointer: a background would stand out of the box, over a design's colours too. */
.fs-scan-btn:hover:not(:disabled) {
    color: var(--primary);
}

.fs-scan-btn:disabled {
    cursor: default;
}

@media print {
    .fs-scan-btn {
        display: none !important;
    }

    .fs-scan > .form-control {
        padding-right: 0.85rem;
    }
}

/* .pdf-layout: the same as the export, for the copy the form template editor measures (docs/FORMS.md §14). */
body.pdf-exporting .fs-scan-btn,
.pdf-layout .fs-scan-btn {
    display: none !important;
}

body.pdf-exporting .fs-scan > .form-control,
.pdf-layout .fs-scan > .form-control {
    padding-right: 0.85rem;
}

/* ── Image areas (docs/FORM-IMAGE-AREAS.md) ─────────────────────────────────
   A picture with fields at fixed points of it. Global, because the editor (FormFieldCanvas) draws the
   same box and places its cards on it by the same rules. Everything inside is sized relative to the
   area — positions in percent of the picture, text in cqw of its width — so a field lands on the same
   point of the drawing at any size: on a phone, in the editor and on paper alike. The box carries
   --area-ratio (width ÷ height of the picture) and --area-font (the value size as a percentage of the
   width), both set by FormImageArea.StyleOf.

   The area stays a white sheet in dark mode too: it is a picture of paper, and dark text on it is what
   gets printed. */
.fs-area {
    position: relative;
    width: 100%;
    aspect-ratio: var(--area-ratio, 0.7071);
    container-type: inline-size;
    background: #fff;
    color: #0f1b3d;
    line-height: 1.25;
}

.fs-area-image {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    pointer-events: none;
    user-select: none;
}

.fs-area-missing {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1.5px dashed var(--border-strong);
    background: var(--surface-sunken);
    color: var(--text-faint);
    font-size: 0.9rem;
}

/* One field at its place. Its height is the line's unless the author gave it one (.is-sized). The text
   size is set here and not on the area: cqw resolves against the nearest container *above* an element,
   so on the area itself it would follow the window instead of the picture. */
.fs-area-item {
    position: absolute;
    box-sizing: border-box;
    display: flex;
    min-width: 0;
    font-size: calc(var(--area-font, 1.68) * 1cqw);
}

.fs-area-item > * {
    flex: 1 1 auto;
    min-width: 0;
}

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

/* The inputs, compact: the drawing is the label, so a box is all a field is. The size comes from the
   area and is inherited, which the touch rule's 16px floor further down must not override. */
.fs-area .form-control {
    width: 100%;
    height: 1.6em;
    min-height: 0;
    padding: 0 0.25em;
    font-size: inherit;
    line-height: 1.25;
    color: #0f1b3d;
    background: rgba(255, 255, 255, 0.8);
    border: 1px solid rgba(15, 27, 61, 0.35);
    border-radius: 0.15em;
    box-shadow: none;
}

.fs-area .fs-area-item.is-sized .form-control,
.fs-area .fs-area-item.is-sized textarea.form-control {
    height: 100%;
    padding-top: 0.15em;
    resize: none;
}

.fs-area .form-control:focus {
    background: #fff;
    border-color: var(--form-accent, var(--clr-blue-500));
    box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
    outline: none;
}

.fs-area .fs-area-item.has-error .form-control {
    border-color: var(--clr-red-500);
    box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.25);
}

.fs-area input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
}

.fs-area input[type="number"]::-webkit-inner-spin-button,
.fs-area input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.fs-area .fs-scan > .form-control {
    padding-right: 1.5em;
}

.fs-area .fs-scan-btn {
    right: 0.05em;
    width: 1.4em;
    height: 1.4em;
}

.fs-area-check {
    width: 1.1em;
    height: 1.1em;
    margin: 0;
    accent-color: #0f1b3d;
    cursor: pointer;
}

.fs-area-text {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

.fs-area-heading {
    font-size: 1.25em;
    font-weight: 700;
}

.fs-area-sign-box {
    display: flex;
}

.fs-area-sign {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: center;
    gap: 0.3em;
    min-width: 0;
    padding: 0;
    border: 1px dashed rgba(15, 27, 61, 0.4);
    border-radius: 0.15em;
    background: rgba(255, 255, 255, 0.65);
    color: #475569;
    font-size: 0.85em;
    cursor: pointer;
}

.fs-area-sign.has-value {
    border-color: transparent;
    background: transparent;
}

.fs-area-sign:disabled {
    cursor: default;
}

.fs-area-sign img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

/* The answer as text, in the input's place: shown only on paper and in the PDF (see FormFillSheet).
   Never clipped — a value cut off on a measurement sheet ("12," for "12,5") is worse than one that runs
   past its box. The padding repeats the input's border and padding, so the text sits where it was typed. */
.fs-area-print {
    display: none;
    padding: 0 calc(0.25em + 1px);
    line-height: 1.6em;
    white-space: nowrap;
    overflow: visible;
}

.fs-area-item[data-type="TextArea"] .fs-area-print {
    padding-top: calc(0.15em + 1px);
    line-height: 1.25;
    white-space: pre-wrap;
}

/* On a phone the picture would shrink below anything one can type into: it keeps a usable width and
   the sheet scrolls sideways instead. */
.fs-area-scroll {
    overflow-x: auto;
}

@media screen and (max-width: 639px) {
    .fs-area {
        min-width: 600px;
    }
}

/* Paper and PDF: the inputs give way to the text, and the area takes its size from the page — the
   sheet formats exactly the page (a hair less, so rounding never spills onto a second one), an inline
   area the sheet's width, unless it would then be too tall. FormImageArea.PrintWidthMm is the same
   rule in code; keep the two in step. */
@media print {
    .fs-area-scroll {
        overflow: visible !important;
    }

    .fs-area {
        min-width: 0 !important;
        width: min(100%, calc(250mm * var(--area-ratio, 0.7071)));
        margin: 0 auto;
        break-inside: avoid;
    }

    .fs-area.fs-area-sheet {
        width: min(209.8mm, calc(296.8mm * var(--area-ratio, 0.7071)));
        margin: 0 auto;
    }

    .fs-area.fs-area-sheet.fs-area-landscape {
        width: min(296.8mm, calc(209.8mm * var(--area-ratio, 1.4142)));
    }

    .fs-area-input > :not(.fs-area-print),
    .fs-area-sign:not(.has-value) {
        display: none !important;
    }

    .fs-area-print {
        display: block;
    }

    .fs-area-sign.has-value {
        border: none !important;
    }

    .fs-area-problems,
    .fs-area-missing {
        display: none !important;
    }
}

body.pdf-exporting .fs-area-scroll,
.pdf-layout .fs-area-scroll {
    overflow: visible !important;
}

body.pdf-exporting .fs-area,
.pdf-layout .fs-area {
    min-width: 0 !important;
    width: min(100%, calc(250mm * var(--area-ratio, 0.7071)));
    margin: 0 auto;
}

body.pdf-exporting .fs-area.fs-area-sheet,
.pdf-layout .fs-area.fs-area-sheet {
    width: min(209.8mm, calc(296.8mm * var(--area-ratio, 0.7071)));
}

body.pdf-exporting .fs-area.fs-area-sheet.fs-area-landscape,
.pdf-layout .fs-area.fs-area-sheet.fs-area-landscape {
    width: min(296.8mm, calc(209.8mm * var(--area-ratio, 1.4142)));
}

body.pdf-exporting .fs-area-input > :not(.fs-area-print),
body.pdf-exporting .fs-area-sign:not(.has-value),
.pdf-layout .fs-area-input > :not(.fs-area-print),
.pdf-layout .fs-area-sign:not(.has-value) {
    display: none !important;
}

body.pdf-exporting .fs-area-print,
.pdf-layout .fs-area-print {
    display: block;
}

body.pdf-exporting .fs-area-sign.has-value,
.pdf-layout .fs-area-sign.has-value {
    border: none !important;
}

body.pdf-exporting .fs-area-problems,
body.pdf-exporting .fs-area-missing,
.pdf-layout .fs-area-problems,
.pdf-layout .fs-area-missing {
    display: none !important;
}

/* A sheet that is all picture goes on a page without a margin, in its own orientation — named pages,
   so the other sheets of the same printout keep theirs (FormFillSheet puts the class on the sheet).
   !important on purpose: Chrome lets a later plain @page win over a named one — FormFillSheet's own
   "@page { margin: 1cm }", or the paper rule form-design.js adds for the form — and the page then shrinks
   the sheet to fit inside the margin, by 9 %, which moves every value off its mark. */
@page flowklar-sheet {
    size: A4 portrait !important;
    margin: 0 !important;
}

@page flowklar-sheet-landscape {
    size: A4 landscape !important;
    margin: 0 !important;
}

@media print {
    .fs-page-wrapper.fs-sheet-page {
        page: flowklar-sheet;
    }

    .fs-page-wrapper.fs-sheet-page.fs-sheet-landscape {
        page: flowklar-sheet-landscape;
    }
}


/* === Progress === */
.progress-bar-container {
    background: var(--surface-track);
    border-radius: var(--radius-xs);
    height: 7px;
    overflow: hidden;
    margin-top: 0.4rem;
}

.progress-bar-fill {
    background: var(--clr-slate-900);
    height: 100%;
    border-radius: var(--radius-xs);
    transition: width 0.3s;
}

.progress-text {
    font-size: 0.78rem;
    color: var(--clr-slate-500);
    margin-top: 0.2rem;
}

/* === Mentions ===
   One look for @-people and #-content wherever a body is rendered — a comment, a handbook article, a
   tip. Global rather than per page on purpose: mention markup arrives as a MarkupString, which carries
   no scoped-CSS attribute, so every page would otherwise need its own ::deep copy of the same rule and
   they would drift. Mirrors the editor's own styling in quill-theme.css, so what an author sees while
   writing is what a reader gets. */
.fk-mention {
    padding: 0.05em 0.3em;
    border-radius: 4px;
    background: var(--clr-blue-50);
    color: var(--clr-blue-700);
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
}

/* A person opens their profile card (wwwroot/js/user-profile.js); content is a plain link. Both are
   clickable, so both say so. */
.fk-mention-user {
    cursor: pointer;
}

a.fk-mention:hover,
.fk-mention-user:hover {
    background: var(--clr-blue-100);
    text-decoration: none;
}

/* === Empty State ===
   Rendered by the shared EmptyState component; page stylesheets must not
   redefine it (scoped CSS would not reach the component's markup anyway). */
.empty-state {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--clr-slate-500);
}

.empty-state h2,
.empty-state h3 {
    margin-top: 1rem;
    margin-bottom: 0;
    color: var(--clr-slate-700);
}

.empty-state p {
    font-size: 0.95rem;
    margin-top: 0.5rem;
    margin-bottom: 1.25rem;
}

/* The follow-up action sits below the copy; keep a gap even without a <p>. */
.empty-state h2 + .btn,
.empty-state h2 + a,
.empty-state h3 + .btn,
.empty-state h3 + a {
    margin-top: 1.25rem;
}

.indicator-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    display: inline-block;
    flex-shrink: 0;
}

.indicator-dot.red    { background: var(--clr-red-500); }
.indicator-dot.blue   { background: var(--clr-blue-500); }
.indicator-dot.green  { background: var(--clr-green-500); }
.indicator-dot.amber  { background: var(--clr-amber-500); }

/* === Settings: gemeinsame Formular-Bausteine ===
   Werden von allen Einstellungs-Seiten innerhalb von <SettingsCard> genutzt. */
.settings-field > label {
    display: block;
    font-weight: 500;
    margin-bottom: 0.35rem;
    font-size: 0.9rem;
}

.settings-field input[type="range"] {
    width: 100%;
}

.settings-hint {
    display: block;
    color: var(--clr-slate-500);
    font-size: 0.78rem;
    margin-top: 0.3rem;
}

.settings-toggle {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 500;
    cursor: pointer;
}

.settings-form-actions {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.settings-msg {
    font-size: 0.85rem;
}

.settings-msg--err { color: var(--clr-red-600, #dc2626); }
.settings-msg--ok  { color: var(--clr-green-600, #16a34a); }

/* === Alerts ===
   A notice is its tint, the way a button is its fill (docs/DESIGN-SYSTEM.md §1): the coloured outline
   they used to carry was a second edge around a shape the fill already draws. */
.alert {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-radius: var(--radius-lg);
    font-size: 0.85rem;
    margin-bottom: 1rem;
}

.alert-success {
    background: var(--clr-green-50);
    color: var(--clr-green-800);
}

.alert-error {
    background: var(--clr-red-50);
    color: var(--clr-red-600);
}

.alert-warning {
    background: var(--clr-amber-50);
    color: var(--clr-amber-800);
    align-items: flex-start;
}

.alert-info {
    background: var(--clr-blue-50);
    color: var(--clr-blue-700);
}

/* === Shared by two pages ===
   Rules that sat in one component's scoped stylesheet while another drew the same class too — and showed it
   unstyled: a section heading row, a link to a stored file's origin, the handbook's article list, the empty line
   of a storage list, and a revision's content in either history dialog. */
.section-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}

.section-header h2 {
    margin-bottom: 0;
}

.storage-origin-link {
    color: var(--primary);
    text-decoration: none;
    font-weight: 500;
}

.storage-origin-link:hover {
    text-decoration: underline;
}

.handbook-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-bottom: 2rem;
}

.storage-empty {
    color: var(--clr-slate-400);
    font-size: 0.88rem;
    margin-top: 0.5rem;
}

/* An old revision can carry images and embeds; global, so the raw MarkupString's are reached too. */
.rev-detail-content {
    font-size: 0.85rem;
    color: var(--clr-slate-700);
    line-height: 1.65;
    overflow-wrap: break-word;
}

.rev-detail-content img,
.rev-detail-content iframe,
.rev-detail-content video {
    max-width: 100%;
    height: auto;
    border-radius: var(--radius-sm);
}

/* === List dialogs ===
   What every Select…Modal and the multi-pick share below their list: the empty line and the footer. Global,
   because two components draw them — a scoped rule in one of them never reached the other. */
.list-select-empty {
    text-align: center;
    color: var(--text-faint);
    font-size: 0.85rem;
    padding: 1.5rem 0;
    margin: 0;
}

.list-select-footer {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding-top: 0.75rem;
    margin-top: 0.5rem;
    border-top: 1px solid var(--border);
}

/* === Overdue === */
.overdue {
    color: var(--clr-red-600) !important;
    font-weight: 600;
}



/* === Status Badges ===
   Fully rounded pills in the new language — see the reference's amber "Offen"
   and blue "Aktiv" chips. */
.status-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.25rem 0.7rem;
    border-radius: var(--radius-pill);
    font-size: 0.76rem;
    font-weight: 600;
    white-space: nowrap;
}

.status-open {
    background: var(--clr-amber-100);
    color: var(--clr-amber-800);
}

.status-inprogress {
    background: var(--clr-progress-100);
    color: var(--clr-progress-800);
}

.status-completed {
    background: var(--clr-green-100);
    color: var(--clr-green-800);
}

.status-cancelled {
    background: var(--clr-slate-100);
    color: var(--clr-slate-500);
    text-decoration: line-through;
}

/* A status's own colour (docs/STATUS-MODELS.md §11): any hex, handed over as --status-color and mixed against the
   semantic surface and text tokens like a tinted tag chip, so it stays readable in the dark theme too. */
.status-tinted {
    background: color-mix(in srgb, var(--status-color, var(--clr-slate-500)) 14%, var(--surface));
    color: color-mix(in srgb, var(--status-color, var(--clr-slate-500)) 62%, var(--text));
}
.status-struck { text-decoration: line-through; }

/* === Deleted objects (grace period, see docs/SOFT-DELETE.md) ===
   A deleted object stays in its own list until the grace period runs out: struck through, dimmed and
   read-only — only its restore action stays clickable. */
.is-deleted {
    opacity: 0.72;
    pointer-events: none;
}

/* Two things stay alive inside a deleted object: the way back, and the badge — its tooltip carries the
   "deleted by X on Y" line and needs hover to show it (DeletedMarker's own sheet). */
.is-deleted .deleted-actions,
.is-deleted .deleted-actions * {
    pointer-events: auto;
}

.is-deleted-title {
    color: var(--clr-slate-400);
    text-decoration: line-through;
}

/* === Project Status Badges === */
.project-status-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    font-size: 0.72rem;
    font-weight: 600;
    padding: 0.22rem 0.65rem;
    border-radius: var(--radius-pill);
    white-space: nowrap;
}

.project-status-badge.status-active    { background: var(--clr-progress-100); color: var(--clr-progress-700); }
.project-status-badge.status-completed { background: var(--clr-emerald-100); color: var(--clr-emerald-900); text-decoration: none; }
.project-status-badge.status-cancelled { background: var(--clr-slate-100);   color: var(--clr-slate-500); text-decoration: none; }

/* === Priority Badges === */
.priority-badge {
    display: inline-block;
    padding: 0.22rem 0.65rem;
    border-radius: var(--radius-pill);
    font-size: 0.76rem;
    font-weight: 600;
    white-space: nowrap;
}

.priority-low {
    background: var(--clr-slate-100);
    color: var(--clr-slate-500);
}

.priority-normal {
    background: var(--clr-progress-100);
    color: var(--clr-progress-800);
}

.priority-high {
    background: var(--clr-orange-100);
    color: var(--clr-orange-700);
}

.priority-critical {
    background: var(--clr-red-100);
    color: var(--danger-text);
}

/* === Detail Sections === */
.detail-section {
    background: var(--surface);
    border-radius: var(--radius-2xl);
    border: 1px solid transparent;
    padding: 1.5rem;
    margin-bottom: 1rem;
    box-shadow: var(--shadow-md);
}

/* Uppercase overline, as the reference sets its section titles. */
.detail-section h2 {
    margin: 0 0 1rem;
    font-size: var(--text-label-size);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--tracking-label);
    color: var(--text-label);
}

details.detail-section > summary {
    list-style: none;
}

details.detail-section > summary::-webkit-details-marker {
    display: none;
}

/* === Article Card ===
   The reading-column card of the article pages — handbook, protocols — as opposed to
   .detail-section, which is the full-width card of the object pages. Global because
   ProtocolTranscriptPanel renders one too, and a shared component can never reach a page's
   scoped CSS. */
.article-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-xl);
    padding: 2rem 2.25rem;
    box-shadow: var(--shadow-sm);
    max-width: 820px;
}

/* Sits below the article and shares .article-card, so it lines up with it. Padding is tighter:
   these are lists and controls, not prose. */
.usage-card {
    margin-top: 1.25rem;
    padding: 1.25rem 1.5rem;
}

/* The overline of a card inside the reading column: icon plus a small uppercase heading. */
.usage-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--clr-slate-500);
    margin-bottom: 0.85rem;
}

.usage-header h2 {
    margin: 0;
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--tracking-label);
}

.usage-empty {
    margin: 0;
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--clr-slate-400);
}

/* An action in the overline row belongs at the far end, so the row has two edges instead of a
   ragged middle. Global: section headers appear in pages and in dialogs alike. */
.usage-header-action {
    margin-left: auto;
}

@media (max-width: 640px) {
    .article-card {
        padding: 1.25rem 1rem;
    }

    .usage-card {
        padding: 1rem;
    }
}

.section-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    cursor: pointer;
    user-select: none;
    -webkit-user-select: none;
}

.section-toggle h2 {
    margin: 0;
}

/* Abstand Header → Inhalt nur bei aufgeklappter Sektion (geschlossen bleibt kompakt). */
details.detail-section[open] > summary {
    margin-bottom: 1rem;
}

.toggle-arrow {
    color: var(--clr-slate-400);
    transition: transform 0.2s;
    flex-shrink: 0;
}

details.detail-section:not([open]) .toggle-arrow {
    transform: rotate(-90deg);
}

.detail-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 0.75rem;
    margin-bottom: 0.15rem;
}

.detail-field {
    min-width: 0;
}

.detail-field dt {
    font-size: var(--text-label-size);
    color: var(--text-label);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--tracking-label);
    margin-bottom: 0.15rem;
}

.detail-field dd {
    margin: 0 0 0.5rem 0;
    font-size: 0.85rem;
}

/* === Checklist === */
.checklist {
    list-style: none;
    padding: 0;
    margin: 0;
}

.checklist-item {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.7rem 0.6rem;
    border-bottom: 1px solid var(--clr-slate-100);
    /* Ticking a task moves the turn to the next one. Every property the "turn" state changes is
       listed here, so the highlight travels down the list instead of jumping: without the radius
       the corners would snap while the wash fades, which reads as two separate events. */
    transition: background-color 0.25s ease, border-bottom-color 0.25s ease, border-radius 0.25s ease;
}

.checklist-item:last-child {
    border-bottom: none;
}

.checklist-content {
    flex: 1;
    min-width: 0;
}

.checklist-label {
    font-weight: 500;
    font-size: 0.92rem;
    /* Keep the label text and its status chips on a centered line with an
       even gap; chips wrap below the label instead of overflowing. */
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.45rem;
    /* Fades in step with the strike below, so the task retires as one movement. */
    transition: color 0.25s ease;
}

.checklist-label.done {
    color: var(--clr-slate-400);
}

.checklist-desc {
    font-size: 0.8rem;
    color: var(--clr-slate-500);
    margin-top: 0.1rem;
}

/* Free-text description: keep the line breaks the author typed. */
.checklist-desc-text {
    white-space: pre-wrap;
}

/* Icon-only marker: the coloured check tells that the task has a Definition of Done,
   the criteria themselves open in a dialog so the list stays compact. */
.checklist-dod-btn {
    color: var(--clr-amber-700);
}

.checklist-dod-btn:hover:not(:disabled) {
    background: var(--clr-amber-50);
    color: var(--clr-amber-700);
}

/* Tasks that are not up yet: visibly parked, checkbox disabled. The fade is on the base elements,
   not on the locked state, so a task that is unlocked by the tick above it comes up rather than
   appearing already awake. */
.checklist-item > .checklist-content {
    transition: opacity 0.25s ease;
}

/* === Attachments === */
.attachment-upload {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

.attachment-hint {
    font-size: 0.78rem;
    color: var(--clr-slate-400);
}

.attachment-list {
    list-style: none;
    padding: 0;
    margin: 0.5rem 0 0;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

/* A file row is a floating card like every other row in the language, and its
   icon sits on a tinted tile so the file type is readable at a glance. */
.attachment-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    background: var(--surface);
    border: 1px solid transparent;
    border-radius: var(--radius-xl);
    box-shadow: var(--shadow-sm);
    padding: 0.85rem 1rem;
    transition: box-shadow 0.2s;
}

.attachment-item:hover {
    box-shadow: var(--shadow-md);
}

.attachment-item[data-kind="image"] { --tile-bg: var(--clr-blue-100);    --tile-fg: var(--clr-blue-600); }
.attachment-item[data-kind="pdf"]   { --tile-bg: var(--clr-red-100);     --tile-fg: var(--clr-red-600); }
.attachment-item[data-kind="sheet"] { --tile-bg: var(--clr-green-100);   --tile-fg: var(--clr-green-700); }
.attachment-item[data-kind="doc"]   { --tile-bg: var(--clr-amber-100);   --tile-fg: var(--clr-amber-800); }
.attachment-item[data-kind="video"] { --tile-bg: var(--clr-violet-100);  --tile-fg: var(--clr-violet-700); }

.attachment-meta {
    font-size: 0.78rem;
    color: var(--text-faint);
    margin-top: 0.15rem;
}

/* Separator between meta facts ("Projekt | 4,3 MB | 01.08.2026"). A hairline
   rather than a middot: it sits on the baseline grid instead of floating in
   the middle of the line, and it does not read as punctuation.
   Use as an empty element: <span class="meta-sep"></span> */
.meta-sep {
    display: inline-block;
    width: 1px;
    height: 0.85em;
    vertical-align: -0.12em;
    margin: 0 0.55rem;
    background: var(--border);
}

/* === Shared Editor Styles (Workflow / Project) === */

/* The properties of a node or an edge sit in the CanvasSidebar now (docs/COMPONENTS.md); what stays global
   is the read-only value of a field there, which ControlNodeSettings uses too. */
.panel-info {
    font-size: 0.85rem;
    color: var(--clr-slate-500);
}

.node-connector {
    position: absolute;
    bottom: -8px;
    left: 50%;
    transform: translateX(-50%);
    cursor: crosshair;
    padding: 4px;
    z-index: 5;
    /* A press on the dot is the start of a line, on a tablet as much as with a mouse. Without this the
       browser claims the touch for scrolling, fires pointercancel, and the line vanishes mid-drag. */
    touch-action: none;
}

.connector-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--clr-slate-400);
    /* The ring reads as a cut-out of the node the handle hangs off, so it follows that
       node's surface instead of staying white in dark mode. */
    border: 2px solid var(--surface);
    transition: background 0.15s, transform 0.15s;
}

.node-connector:hover .connector-dot {
    background: var(--clr-blue-500);
    transform: scale(1.3);
}

.node-footer {
    position: relative;
    height: auto;
    margin-top: 0.5rem;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: 0.25rem;
}

.node-action-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    background: transparent;
    border: none;
    color: var(--clr-slate-400);
    cursor: pointer;
    padding: 0;
    transition: color 0.15s, background 0.15s;
    border-radius: var(--radius-xs);
    pointer-events: auto;
}

/* ============================================================
   Form Editor – Drag & Drop (global styles for body-appended elements)
   ============================================================ */

/* Body state: disable text selection & set grab cursor globally while dragging */
body.fe-is-dragging {
    cursor: grabbing !important;
    -webkit-user-select: none;
    user-select: none;
}

body.fe-is-dragging * {
    cursor: grabbing !important;
}

/* Ghost element following cursor */
.fe-ghost {
    position: fixed;
    z-index: 10000;
    padding: 0.4rem 0.75rem;
    background: var(--clr-white, #fff);
    border: 1px solid var(--clr-blue-400, #60a5fa);
    border-radius: var(--radius-md);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(42, 86, 214, 0.2);
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--clr-slate-700, #334155);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0.92;
    transform: scale(1.02);
    transition: opacity 0.1s;
}

/* Drop indicator line between fields */
.fe-drop-indicator {
    position: relative;
    height: 3px;
    background: var(--clr-blue-500, #3b82f6);
    border-radius: 2px;
    grid-column: 1 / -1;
    margin: -1px 0;
    pointer-events: none;
    animation: fe-indicator-pulse 1s ease-in-out infinite;
}

.fe-drop-indicator::before,
.fe-drop-indicator::after {
    content: '';
    position: absolute;
    top: 50%;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--clr-blue-500, #3b82f6);
    transform: translateY(-50%);
}

.fe-drop-indicator::before {
    left: -4px;
}

.fe-drop-indicator::after {
    right: -4px;
}

@keyframes fe-indicator-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

/* ============================================================
   Blazor unhandled-error banner – hidden until an error occurs.
   Rendered once, in App.razor, for every layout.
   ============================================================ */
#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    background: var(--clr-red-50);
    /* Red text on a red tint: the tint darkens in dark mode, and --danger-text lightens with it. */
    color: var(--danger-text);
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    z-index: 1000;
}

#blazor-error-ui .reload {
    color: inherit;
    text-decoration: underline;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* ============================================================
   Print – hide chrome, show only form submission content
   ============================================================ */

/* A print sheet is a page's own layout for paper — a document, not a photograph of the screen
   (ProtocolPrintSheet is the first). A page renders it as a sibling of its screen content, where
   it stays invisible until the printer asks; then it *is* the printout and everything else in the
   main column goes, tabs, toolbars, empty states and all. Always mounted, so a plain Ctrl+P
   produces the same document as the print dialog. */
.print-sheet {
    display: none;
}

@media print {
    .main-content:has(> .print-sheet) > *:not(.print-sheet) {
        display: none !important;
    }

    .print-sheet {
        display: block;
    }

    /* Hide all layout chrome. The top bar and the docked panels are addressed as a whole rather
       than button by button: every docked panel is a fixed overlay, and an overlay lands on the
       first sheet of whatever is printed. */
    .sidebar,
    .sidebar-backdrop,
    .top-bar,
    .page-scaffold-toolbar,
    .canvas-sidebar,
    .ai-chat,
    #blazor-error-ui,
    .toast-container {
        display: none !important;
    }

    /* An article on paper is its title, its meta line and its text: the buttons beside the title
       ("Drucken" among them) mean nothing there. Global because the row is ArticleHero's element. */
    .article-hero-actions {
        display: none !important;
    }

    /* Remove layout constraints so content fills the page */
    .app-layout,
    .main-column,
    .main-content {
        display: block !important;
        margin: 0 !important;
        padding: 0 !important;
        width: 100% !important;
        max-width: none !important;
    }

    /* Paper is white: a branding background on the frame (docs/BRANDING.md §3.7) stays on screen. */
    .app-layout {
        background: none !important;
    }

    body {
        background: #fff !important;
        color: #000 !important;
        font-size: 11pt;
    }

    /* On screen a card is defined by its shadow alone. Shadows do not print,
       so on paper every surface would dissolve into the page — give them a
       hairline outline back and drop the (ink-wasting) shadow.
       Keep this list in step with the shadow-defined surfaces: any rule
       pairing `border: 1px solid transparent` with a `--shadow-*` belongs
       here. A flow node carries no such placeholder — its height is counted
       term by term for the edge geometry, and a border would throw that off —
       so it only shows up here. See docs/DESIGN-SYSTEM.md §10. */
    .card,
    .detail-section,
    .settings-card,
    .table-container,
    .dialog,
    .analytics-stat-card,
    .audit-card,
    .flow-node,
    .help-card,
    .legal-card,
    .lic-card,
    .signature-card {
        box-shadow: none !important;
        border: 1px solid #d4d4d4 !important;
    }

    /* Ensure links print as plain text */
    a[href]::after {
        content: none !important;
    }
}

/* The form template's own PDF (pdf-export.js, blank): a form nobody has filled in, so nothing on it
   points at a record. The QR strip's code, number and date and a design's QR block keep their room,
   empty — the pages break where the editor marks them (docs/FORMS.md §14). Global, like the class:
   it is set on the export's copy of the document only. */
body.pdf-blank .fs-qr-code,
body.pdf-blank .fs-qr-id,
body.pdf-blank .fs-qr-date,
body.pdf-blank .qr-block {
    visibility: hidden;
}
.node-delete-btn:hover {
    color: var(--clr-red-500);
    background: var(--clr-red-100);
}


/* ============================================================
   Touch devices
   ============================================================ */

/* iOS Safari zooms the page in whenever a focused field's text is smaller than 16px — and it
   never zooms back out, so after a single keystroke the whole layout is left oversized and
   scrolling sideways. The house field size is 0.88rem (13.2px), which means every text field
   in the app triggered it.
   !important is deliberate: the alternative is repeating the floor in each of the ~100 scoped
   component stylesheets, because Blazor's scope attribute makes `.some-input[b-xxxxxxx]`
   outweigh any global selector that could be written here. The few fields that were sized
   *above* 16px (inline title editors) come down to 16px on touch — readable, and worth it. */
@media (pointer: coarse) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
    select,
    textarea {
        font-size: 16px !important;
    }

    /* Except on an image area, where a field's text is sized relative to the picture: 16px there would
       run out of the boxes on a tablet. Safari zooms in on such a field when it is focused — on a dense
       measurement sheet that is a help rather than the trap the floor above exists for. */
    .fs-area input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
    .fs-area select,
    .fs-area textarea {
        font-size: inherit !important;
    }
}

/* ============================================================
   Tablet (>= 640px)
   ============================================================ */
@media (min-width: 640px) {
    .card-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .detail-row {
        display: flex;
        flex-direction: row;
        gap: 2rem;
    }

    .detail-field {
        min-width: 160px;
    }

    .detail-field dt {
        font-size: 0.75rem;
    }

    .detail-field dd {
        margin-bottom: 0.75rem;
        font-size: 0.92rem;
    }

    /* Info sections are forced open on desktop (initDetailSections) and not toggleable. */
    details.detail-section > summary {
        pointer-events: none;
        cursor: default;
    }

    .toggle-arrow {
        display: none;
    }
}

/* ============================================================
   Desktop (>= 768px)
   ============================================================ */
@media (min-width: 768px) {
    /* h1 is deliberately absent: it scales continuously via clamp() up to
       --text-hero, and a fixed size here would cap every page title again. */
    h2 { font-size: 1.3rem; }
}

/* ============================================================
   Wide (>= 1024px)
   ============================================================ */
@media (min-width: 1024px) {
    .card-grid {
        grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    }
}

/* The rich-text editor's look (frame, toolbar, menus, dialogs) lives in wwwroot/css/quill-theme.css. */

/* ══════════════════════════════════════════════════════════
   Form Editor – JS-created elements (global, not scoped)
   ══════════════════════════════════════════════════════════ */

/* Grabbing cursor globally while dragging */
body.fe-is-dragging,
body.fe-is-dragging * {
    cursor: grabbing !important;
}

/* Ghost element (dragging clone, appended to body) */
.fe-ghost {
    position: fixed;
    z-index: 9999;
    pointer-events: none;
    background: var(--clr-white);
    border: 1px solid var(--clr-slate-300);
    border-radius: var(--radius-md);
    padding: 0.45rem 0.85rem;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.06);
    font-size: 0.82rem;
    font-weight: 500;
    color: var(--clr-slate-600);
    display: flex;
    align-items: center;
    gap: 0.35rem;
    max-width: 220px;
    white-space: nowrap;
    opacity: 0.9;
    animation: fe-ghost-in 0.12s ease-out;
}

@keyframes fe-ghost-in {
    from { opacity: 0; transform: translateY(-3px); }
    to { opacity: 0.9; transform: translateY(0); }
}

/* Drop indicator (inserted by JS into the field list) */
.fe-drop-indicator {
    grid-column: 1 / -1;
    height: 2px;
    pointer-events: none;
    position: relative;
    background: var(--clr-blue-500);
    border-radius: 1px;
    margin: 0.3rem 0;
    overflow: visible;
}

/* Left endpoint dot */
.fe-drop-indicator::after {
    content: '';
    position: absolute;
    top: 50%;
    left: -3px;
    transform: translateY(-50%);
    width: 6px;
    height: 6px;
    background: var(--clr-blue-500);
    border-radius: 50%;
}


/* Drop indicator inside column zones */
.fe-column-zone > .fe-drop-indicator {
    grid-column: unset;
    margin: 0.15rem 0;
}

/* === External Provider Badge === */
.badge-external-provider {
    display: inline-block;
    padding: 0.15rem 0.4rem;
    background: var(--clr-blue-100, #dbeafe);
    color: var(--clr-blue-700, #1d4ed8);
    border-radius: var(--radius-xs);
    font-size: 0.7rem;
    font-weight: 600;
    margin-left: 0.4rem;
    vertical-align: middle;
}

/* === Legal pages (Impressum, Datenschutz, KI-Agentur) ===
   Global because both pages under LegalLayout share them (LegalNotices, KiAgenturBuhl), and the
   layout renders a signed-in user's page without an element of its own that scoped rules could
   anchor on. They sat in two byte-identical scoped copies, and a page that had neither rendered
   unstyled (DUP-F1). */
.legal-page {
    display: flex;
    justify-content: center;
    padding: 2rem 1rem;
}

.legal-card {
    width: 100%;
    max-width: 720px;
    background: var(--surface);
    border: 1px solid transparent;
    box-shadow: var(--shadow-md);
    border-radius: var(--radius-xl);
    padding: 2rem 2rem 2.5rem;
    align-self: flex-start;
}

.legal-back {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--clr-slate-500);
    text-decoration: none;
    margin-bottom: 1.5rem;
    transition: color 0.15s;
}

.legal-back:hover {
    color: var(--clr-slate-800);
}

.legal-title {
    font-size: 1.6rem;
    font-weight: 700;
    color: var(--clr-slate-900);
    margin: 0 0 1.75rem 0;
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--clr-slate-100);
}

.legal-section {
    margin-bottom: 1.75rem;
}

.legal-section h2 {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--clr-slate-800);
    margin: 0 0 0.6rem 0;
}

.legal-section h3 {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--clr-slate-700);
    margin: 1rem 0 0.4rem 0;
}

.legal-section p,
.legal-section li {
    font-size: 0.9rem;
    color: var(--clr-slate-600);
    line-height: 1.65;
    margin: 0 0 0.5rem 0;
}

.legal-section ul {
    padding-left: 1.35rem;
    margin: 0.4rem 0 0.5rem;
}

.legal-section li {
    margin-bottom: 0.3rem;
}

/* === Brand Badge (Schottler & Buhl logo) ===
   The CI lockup, pixel edition, as schottler-buhl.de draws it (Logo.astro):
   the copper pixel signet beside the wordmark, "hell" (dark wordmark,
   -light.svg) and "dunkel" (white wordmark, -dark.svg). The markup carries
   both and the theme shows one, so no backing plate is needed. The pixels
   want at least ~20px of height to read as pixels; every badge here is larger. */
.brand-badge {
    display: inline-flex;
    align-items: center;
    line-height: 0;
    border-radius: var(--radius-sm);
    /* Rendered as a <button> that opens the "about" modal – strip the UA chrome. */
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font: inherit;
    cursor: pointer;
}

.brand-badge img {
    display: block;
    height: 16px;
    width: auto;
}

.brand-badge .brand-logo-dark {
    display: none;
}

html[data-theme="dark"] .brand-badge .brand-logo-light {
    display: none;
}

html[data-theme="dark"] .brand-badge .brand-logo-dark {
    display: block;
}

@media (prefers-color-scheme: dark) {
    html:not([data-theme]) .brand-badge .brand-logo-light {
        display: none;
    }

    html:not([data-theme]) .brand-badge .brand-logo-dark {
        display: block;
    }
}

/* ── Startup Splash ─────────────────────────────────────────────
   Prerendering is deliberately off, so the page stays blank until the
   SignalR circuit is up. This bridges that gap. Removed from the DOM by
   flowklarSplash.hide() (App.razor), called from Routes.razor on first
   render. Fixed colours – the theme vars are fine here, but the splash
   must also look right before any component has mounted. */
#fk-splash {
    position: fixed;
    inset: 0;
    z-index: 20000;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1.25rem;
    background: #f8fafc;
    /* Delayed fade-in: a warm-cache start finishes before this and should
       never flash a spinner. The hide script freezes whatever opacity was
       reached and fades from there, so there is no jump either way. */
    animation: fk-splash-in 0.25s ease-out 0.3s both;
    transition: opacity 0.3s ease;
}

.fk-splash-orbit {
    transform-origin: 32px 32px;
    animation: fk-splash-orbit 2.4s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
}

.fk-splash-wordmark {
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    color: #0f172a;
}

.fk-splash-wordmark span {
    color: #f4b740;
}

.fk-splash-text {
    margin: 0;
    font-size: 0.875rem;
    color: #64748b;
}

/* The installation's suffix under the wordmark (docs/BRANDING.md §3.1) — pulled up to it, so the two
   read as one name. */
.fk-splash-suffix {
    margin-top: -1rem;
    font-size: 1.05rem;
    font-weight: 600;
    color: #475569;
}

html[data-theme="dark"] #fk-splash {
    background: #0d1724;
}

html[data-theme="dark"] .fk-splash-wordmark {
    color: #edf3f8;
}

html[data-theme="dark"] .fk-splash-suffix {
    color: #b8c7d6;
}

html[data-theme="dark"] .fk-splash-text {
    color: #8fa6bd;
}

@media (prefers-color-scheme: dark) {
    html:not([data-theme]) #fk-splash {
        background: #0d1724;
    }

    html:not([data-theme]) .fk-splash-wordmark {
        color: #edf3f8;
    }

    html:not([data-theme]) .fk-splash-suffix {
        color: #b8c7d6;
    }

    html:not([data-theme]) .fk-splash-text {
        color: #8fa6bd;
    }
}

/* ── Instance label (docs/BRANDING.md §3.3) ────────────────────
   FLOWKLAR_INSTANCE_LABEL marks a test or training system: a stripe over the
   top edge of the window and a chip with the label. A fixed hue on purpose —
   a test system has to look the same everywhere — and no height: a dozen
   rules size themselves off the viewport and --top-bar-height, and a band
   that pushed the page down would cut off the bottom of each. */
.instance-stripe {
    position: fixed;
    inset: 0 0 auto 0;
    height: 4px;
    z-index: 19000;
    background: #ea580c;
    pointer-events: none;
}

.instance-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-pill);
    background: #c2410c;
    color: #fff;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
}

@keyframes fk-splash-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}

@keyframes fk-splash-orbit {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .fk-splash-orbit { animation: none; }
}


/* === Protocol blocks ===
   The two blocks minutes are written in — an agenda point and a decision. Global for the same reason
   as mentions above: a rendered body arrives as a MarkupString and carries no scoped-CSS attribute.
   quill-theme.css mirrors these so an author sees while writing what a reader gets.

   The numbering is a CSS counter, never text in the document. Deleting TOP 2 therefore renumbers the
   rest by itself, in the editor and in the rendered protocol alike — a document that counts 1, 3, 4
   is the failure this avoids. The counter is reset on the body wrapper, so every protocol starts at 1. */
.fk-protocol-body {
    counter-reset: fk-top;
}

.fk-top {
    counter-increment: fk-top;
    margin-top: 1.6rem;
    padding-bottom: 0.3rem;
    border-bottom: 1px solid var(--border-subtle);
    font-size: 1.05rem;
}

.fk-top::before {
    content: "TOP " counter(fk-top) " · ";
    color: var(--clr-slate-400);
    font-weight: 600;
}

.fk-decision {
    margin: 0.75rem 0;
    padding: 0.6rem 0.85rem;
    border-left: 3px solid var(--clr-green-500, #16a34a);
    border-radius: 0 6px 6px 0;
    background: var(--surface-track);
}

.fk-decision::before {
    content: "Beschluss";
    display: block;
    margin-bottom: 0.2rem;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--clr-slate-500);
}

/* === Live dictation ===
   The speech-to-text surface, shared by the plugin panel (DictationWidget) and the protocol
   recorder (TranscriptRecorder). Global because two components render the same markup, and scoped
   CSS reaches neither from the other.

   No card and no frame: this sits on the surface of whatever card holds it, and
   docs/DESIGN-SYSTEM.md §8 is explicit that a white card on a white card does not read — inside
   one, a region is *sunken* rather than raised. The accent colour is spent on the two things that
   carry meaning, the status dot and the level; everything else is the app's ordinary greys. */
.dictation {
    display: flex;
    flex-direction: column;
    gap: 14px;
    flex: 1;
    min-height: 0;
}

/* ── Status line ── */

.dictation-status {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.dictation-state {
    flex: 1;
    min-width: 0;
    font-weight: 500;
}

.dictation-elapsed {
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.01em;
}

.dictation-dot {
    width: 8px;
    height: 8px;
    border-radius: var(--radius-circle);
    background: var(--border-strong);
    flex-shrink: 0;
}

.dictation-dot.is-live {
    background: var(--clr-orange-500);
    box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.16);
    animation: dictation-pulse 1.6s ease-in-out infinite;
}

.dictation-dot.is-waiting {
    background: var(--clr-orange-500);
    opacity: 0.5;
}

.dictation-dot.is-paused { background: var(--clr-amber-500); }
.dictation-dot.is-done { background: var(--clr-green-500); }
.dictation-dot.is-error { background: var(--clr-red-500); }

@keyframes dictation-pulse {
    0%, 100% { box-shadow: 0 0 0 4px rgba(249, 115, 22, 0.16); }
    50% { box-shadow: 0 0 0 7px rgba(249, 115, 22, 0.04); }
}

/* "Wird gerade aufgezeichnet", in a badge row. Not a Chip: the pulsing dot is the whole message, and a
   chip would put a border around a thing whose point is that it moves. Global rather than scoped
   because the card and the chronology diagram both show it, and a recording has to look the same
   wherever it is announced. */
.protocol-card-live {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--clr-orange-700);
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--tracking-label);
}

@keyframes dictation-idle {
    0%, 100% { transform: scaleY(0.14); }
    50% { transform: scaleY(0.34); }
}

/* ── Transcript ──
   The one region that grows: a dictation is mostly text, and the panel has the height to give it. */

.dictation-transcript {
    flex: 1;
    min-height: 120px;
    overflow-y: auto;
    padding: 14px 16px;
    border-radius: var(--radius-xl);
    background: var(--surface-sunken);
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--text);
    white-space: pre-wrap;
    word-break: break-word;
}

.dictation-placeholder {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    height: 100%;
    min-height: 92px;
    margin: 0;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.88rem;
}

.dictation-placeholder ::deep svg {
    opacity: 0.4;
}

@keyframes dictation-caret {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
}

/* ── Level ──
   Always mounted so the meter never has to re-attach; at rest the bars lie flat and quiet, which
   also tells the eye that nothing is being heard. */
.dictation-wave {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    height: 44px;
    padding: 0 2px;
}

/* The bar's width and the gap set how many there are: stt.js (fillBars) fits as many as the band has
   room for. */
.dictation-wave .dictation-bar {
    flex: 0 1 3px;
    height: 100%;
    border-radius: var(--radius-pill);
    background: linear-gradient(180deg, var(--clr-gold-500), var(--clr-orange-500));
    transform: scaleY(0.12);
    transform-origin: center;
    transition: opacity 0.3s ease;
}

/* Resting: grey and still — no animation pretending to listen. */
.dictation-wave:not(.is-live) .dictation-bar {
    background: var(--border);
    transform: scaleY(0.12);
}

/* Live but the real meter has not attached yet: a restrained stand-in, never a still bar chart,
   which would read as "not working". Once metered, JS owns every transform. */
.dictation-wave.is-live:not(.is-metered) .dictation-bar {
    animation: dictation-idle 1.6s ease-in-out infinite;
}

.dictation-wave.is-live:not(.is-metered) .dictation-bar:nth-child(odd) { animation-delay: 0.18s; }

.dictation-wave.is-live:not(.is-metered) .dictation-bar:nth-child(3n) { animation-delay: 0.42s; }

/* The band and what lies over it. The band's children are stt.js's bars, counted to fit — nothing of
   Blazor's may stand among them. */
.dictation-level {
    position: relative;
}

/* ── Nothing comes from the microphone ──
   stt.js (judgeLevel, 'silent'): eight seconds without a sample above one step of 16-bit PCM. A round
   mark over the flat bars asks whether the microphone is on; ten seconds later, still nothing, it
   flashes red — once every five seconds, a reminder rather than an alarm going on and on. Gone at the
   first sound — and with it the count, so the red waits again next time. The question itself is the
   mark's tooltip. */
.dictation-mic-alert {
    position: absolute;
    top: 50%;
    left: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    margin: -18px 0 0 -18px;
    border-radius: 50%;
    background: var(--clr-amber-50);
    color: var(--clr-amber-700);
    box-shadow: var(--shadow-sm);
    cursor: help;
    animation: dictation-mic-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both,
               dictation-mic-alarm 5s ease-in-out 10s infinite;
}

@keyframes dictation-mic-in {
    from { opacity: 0; transform: scale(0.4); }
    to { opacity: 1; transform: none; }
}

/* One flash at the start of every five seconds: up in 0.3 s, back in 0.4 s, then rest. */
@keyframes dictation-mic-alarm {
    0%, 14%, 100% {
        background: var(--clr-amber-50);
        color: var(--clr-amber-700);
        box-shadow: var(--shadow-sm);
    }
    6% {
        background: var(--clr-red-500);
        color: #fff;   /* white on red in both themes; --clr-white is the card in dark */
        box-shadow: 0 0 0 6px rgba(224, 90, 84, 0.22);
    }
}

/* Without the motion: the mark simply turns red when the ten seconds are up, and stays so. */
@keyframes dictation-mic-red {
    to {
        background: var(--clr-red-500);
        color: #fff;
    }
}

@media (prefers-reduced-motion: reduce) {
    .dictation-mic-alert {
        animation: dictation-mic-red 0.01s linear 10s both;
    }
}

/* Written by JS, never by Blazor. */
.dictation-partial {
    color: var(--text-muted);
}

.dictation-caret {
    display: inline-block;
    width: 2px;
    height: 1.05em;
    margin-left: 1px;
    vertical-align: text-bottom;
    background: var(--clr-orange-500);
    border-radius: var(--radius-xs);
    animation: dictation-caret 1s step-end infinite;
}

/* ── Notes ── */
.dictation-error,
.dictation-gap,
.dictation-quiet {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    margin: 0;
    padding: 10px 12px;
    border-radius: var(--radius-lg);
    font-size: 0.85rem;
    line-height: 1.45;
}

.dictation-error {
    background: var(--clr-red-50);
    color: var(--clr-red-600);
}

.dictation-gap {
    background: var(--surface-sunken);
    color: var(--text-muted);
}

/* A warning, not an error: the recording goes on, and the fix is in the room, not in the app. */
.dictation-quiet {
    background: var(--clr-amber-50);
    color: var(--clr-amber-800);
}

/* ── Controls ──
   One row, filling the width: in the plugin's panel this is the only thing to do, so it should be easy
   to hit. The protocol recorder gives them their natural width (TranscriptRecorder.razor.css). */
.dictation-actions {
    display: flex;
    gap: 8px;
}

.dictation-record,
.dictation-ctl {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex: 1;
    min-height: 44px;
    padding: 0 14px;
    border: none;
    border-radius: var(--radius-lg);
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

/* Start: the accent, once — the single action on an empty panel. */
.dictation-record {
    background: linear-gradient(135deg, var(--clr-gold-500), var(--clr-orange-500));
    color: var(--clr-always-white);
    box-shadow: var(--shadow-btn);
}

.dictation-record:hover:not(:disabled) {
    transform: translateY(-1px);
    box-shadow: var(--shadow-lg);
}

.dictation-record:disabled {
    opacity: 0.5;
    cursor: default;
}

.dictation-record.is-busy {
    background: var(--surface-sunken);
    color: var(--text-muted);
    box-shadow: none;
    cursor: default;
}

.dictation-ctl {
    background: var(--surface-sunken);
    color: var(--text);
}

.dictation-ctl:hover:not(:disabled) {
    background: var(--surface-hover);
}

.dictation-ctl:disabled {
    opacity: 0.5;
    cursor: default;
}

/* Stop and hand-over are the decisive ones and carry the app's primary fill. */
.dictation-ctl--stop {
    background: var(--clr-slate-900);
    color: var(--clr-white);   /* inverts with the scale under dark mode, like .btn-primary */
    box-shadow: var(--shadow-btn);
}

.dictation-ctl--stop:hover:not(:disabled) {
    background: var(--clr-slate-800);
    transform: translateY(-1px);
}

.dictation-square {
    width: 12px;
    height: 12px;
    border-radius: 3px;
    background: currentColor;
}

/* ── The switches decided before starting: speakers, voices, keeping the audio ── */
.transcript-options {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.transcript-keep {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.88rem;
    cursor: pointer;
}

/* Why the speaker switch is off: a remark on the label, not a warning. */
.transcript-keep-hint {
    color: var(--text-muted);
    font-size: 0.8rem;
}
