/* ═══════════════════════════════════════════════════════════════════════════
   GENERATED FILE — DO NOT EDIT.

   Built by build/bundle-css.sh from src/DR.Simple_UI/css-parts/. Edit the part
   that owns the rule and re-run that script; a guard test fails the build if
   this file and the parts disagree. Adding a part needs no change here — the
   directory is the source of truth.

   Contents, in cascade order:
     00-header.css
     01-tokens.css
     02-theme-light.css
     03-theme-colour-blind.css
     04-theme-contrast.css
     06-base-elements.css
     10-frame-shell.css
     11-frame-sidebar.css
     12-frame-collapsed-rail.css
     13-frame-topbar.css
     15-frame-user-widget.css
     16-frame-page-scroll.css
     17-frame-hover-hints.css
     18-frame-reconnect-banner.css
     19-frame-responsive.css
     20-frame-layouts.css
     21-frame-search.css
     30-cards.css
     31-badges.css
     32-tables.css
     33-buttons.css
     34-forms.css
     35-toolbar.css
     36-modal.css
     37-alerts.css
     38-toasts.css
     39-spinner.css
     40-skeleton.css
     41-progress.css
     42-empty-state.css
     43-content-grids.css
     44-markdown.css
     45-form-controls.css
     46-form-layout.css
     47-form-file.css
     48-form-numeric.css
     50-tabs.css
     51-segmented.css
     52-avatar.css
     53-chip.css
     54-stat.css
     55-page-structure.css
     56-code-block.css
     57-navigation-aids.css
     58-menu.css
     59-table-extensions.css
     60-lists.css
     61-buttons-extra.css
     62-media-and-prose.css
     63-drawer.css
     64-palette-spotlight.css
     65-popover.css
     66-status-indicator.css
     67-output.css
     70-rtl.css
     71-forced-colors.css
     72-print.css
     80-utilities.css
     90-density.css
     95-reduced-motion.css
   ═══════════════════════════════════════════════════════════════════════════ */

/* Layer order, declared once and up front so it does not depend on which layer
   happens to appear first below.

   The consequence that matters to a consuming app: EVERY rule in this file is
   layered, and an unlayered rule beats a layered one whatever its specificity.
   So your own stylesheet — which is unlayered unless you say otherwise — always
   wins, and you no longer have to out-specify anything to override it.

   Two things to know:
     * !important inverts layer order, which is why this library uses none. An
       !important inside a layer becomes HARDER for you to override, not easier.
     * Because your unlayered :root wins outright, a token you set at bare :root
       now also beats this library's [data-theme="light"] value for it. Set both
       blocks, as the rebrand recipe in the catalogue shows. */
@layer dr.tokens, dr.base, dr.frame, dr.paint, dr.utilities, dr.overrides;

/* ── 00-header.css → @layer dr.tokens ──────────────────────────────────────────────── */
@layer dr.tokens {
/* ═══════════════════════════════════════════════════════════════════════════
   DR.Simple_UI — the shared stylesheet
   ───────────────────────────────────────────────────────────────────────────
   The token layer + every tier-2 (page content) class + the CSS for the tier-1
   frame components. One file ships, so a consuming app writes one <link>.

   Two rules govern every edit here:

   1. NO HARD-CODED COLOURS outside the token blocks below. Every colour,
      shadow and tint is a `var(--…)`. A test (CssTokenContractTests) fails the
      build if a hex or rgb() literal appears past the `:root` blocks — that is
      what makes an app able to rebrand by redefining a handful of tokens.
   2. Content UI is a CLASS, never a component. There is no `<DataTable>`, no
      `<Card>` and no `<AppShell>`; pages write markup and apply these classes.

   Consuming apps load this file first, then their own override file which
   redefines only the tokens it needs (typically the --brand* family, --accent
   and --sidebar-active). Apps must not invent tokens; a missing token is added
   here, the version is bumped, and the app upgrades.

   Every theme difference is expressed as a token, so the light and colour-blind
   blocks below contain NO selector overrides and CSS load order is irrelevant.

   Everything here is inside a cascade layer. A consuming app's stylesheet is
   unlayered, so it beats every rule in this file whatever the specificity — which
   means overriding the library never needs a longer selector, and a library rule can
   never out-specify an app. Redefine tokens, not rules.

   Icons: classes size `i` elements (`.btn i`, `.nav-link i`, …). Remix Icon is
   bundled at ../lib/remixicon/ and is the only icon set; link its stylesheet
   alongside this one. See THIRD-PARTY-NOTICES.md for its licence.
   ═══════════════════════════════════════════════════════════════════════════ */


}

/* ── 01-tokens.css → @layer dr.tokens ──────────────────────────────────────────────── */
@layer dr.tokens {
/* ═══ Tokens — the contract ═════════════════════════════════════════════════
   Everything below this block references these and nothing else. */
:root {
    /* ── Appearance ─────────────────────────────────────────────────────────
       Consumed once, as `html { color-scheme: var(--color-scheme) }`, which is
       why it is a token and not a literal: it has to move with the theme. It
       cannot be declared here as `color-scheme: dark` — a token block holds
       custom properties only, and a test enforces that.
       This is what makes the parts of a control the page cannot reach match the
       theme: the native scrollbar, a <select>'s option list, the date picker,
       and the default canvas before any rule applies. */
    --color-scheme: dark;

    /* The UA's date-picker glyph is drawn in a fixed dark colour we cannot reach.
       Inverting it is the only way to see it on a dark surface, so the amount of
       inversion moves with the theme: 1 on dark, 0 on light. A number, not a
       colour — there is nothing here to recolour. */
    --picker-invert: 1;

    /* ── Typography ─────────────────────────────────────────────────────── */
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    --font-mono: "SF Mono", Menlo, Consolas, monospace;

    /* ── Surfaces, text, lines ──────────────────────────────────────────── */
    --bg:              #111827;
    --bg-elevated:     #1f2937;
    --bg-hover:        #283346;
    --fg:              #f3f4f6;
    --fg-soft:         #d1d5db;          /* body copy on cards */
    --muted:           #9ca3af;
    --border:          #374151;
    --border-strong:   #4b5563;
    --border-hover:    #6b7280;
    --divider:         rgba(255, 255, 255, 0.06);
    --card-bg:         #1f2937;
    --surface-soft:    rgba(255, 255, 255, 0.03);
    --surface-strong:  rgba(255, 255, 255, 0.05);
    --on-solid:        #ffffff;          /* text on any filled/solid button */
    --redacted:        #f87171;

    /* ── Brand — THE app override point ─────────────────────────────────────
       Neutral blue by default. An app redefines this family (and --accent /
       --sidebar-active) in its own override file; nothing else needs touching.
       The four ring tokens are separate because the alphas differ per control. */
    --brand:            #2563eb;
    --brand-hover:      #1d4ed8;
    --brand-active:     #1e40af;
    --brand-soft:       #60a5fa;         /* icons/spinners on tinted surfaces */
    --brand-text:       #93c5fd;         /* readable brand text on the theme bg */
    /* Mixed from --brand rather than restated as literals, so redefining --brand
       alone carries the tint and every focus ring with it. An app may still pin any
       of them.

       A derived colour serialises as `color(srgb 0.145098 0.388235 0.921569 / 0.14)`
       rather than `rgba(37, 99, 235, 0.14)` — the painted pixel is the same, only the
       string differs. A computed-style differ that compares strings therefore reports
       every brand-tinted surface as changed. */
    --brand-tint:       color-mix(in srgb, var(--brand) 14%, transparent);
    --brand-ring:       color-mix(in srgb, var(--brand) 50%, transparent);  /* filled primary focus ring */
    --brand-ring-soft:  color-mix(in srgb, var(--brand) 40%, transparent);  /* neutral button / card focus */
    --brand-ring-check: color-mix(in srgb, var(--brand) 35%, transparent);  /* checkbox focus */
    --brand-glow:       color-mix(in srgb, var(--brand) 18%, transparent);  /* text-input focus glow */
    --accent:           #60a5fa;         /* links, decorative icons */

    /* ── Sidebar ───────────────────────────────────────────────────────── */
    --sidebar-bg:      #111827;
    --sidebar-border:  #1f2937;
    --sidebar-fg:      #e5e7eb;
    --sidebar-active:  #2563eb;

    /* ── Semantic status families ───────────────────────────────────────────
       "go"     — the action that sends something outward (approve, apply, send)
       "warn"   — the action that changes who is in control (take over)
       "danger" — destructive / failed
       "info", "secret" — informational and sensitive-value surfaces
       [data-cvd="1"] swaps ONLY the go family to blue, so go-vs-danger reads as
       blue-vs-red for deuteranopia/protanopia. Amber and brand keep their roles. */
    /* The three filled families are one ramp step darker than the palette's own mid
       tone, because --on-solid is white: white on #16a34a is 3.30:1 and on #d97706 is
       3.19:1, both below AA for the 13px/500 label a .btn renders, while #15803d and
       #b45309 reach 5.02:1. Do not lighten them without re-running the contrast audit
       in docs/accessibility.md. */
    --go-solid:   #15803d;
    --go-hover:   #166534;
    --go-active:  #14532d;
    --go-bg:      rgba(34, 197, 94, 0.12);
    --go-border:  #166534;
    --go-fg:      #4ade80;
    --go-ring:    rgba(34, 197, 94, 0.5);

    --warn-solid:      #b45309;
    --warn-hover:      #92400e;
    --warn-active:     #78350f;
    --warn-bg:         rgba(245, 158, 11, 0.12);
    --warn-bg-hover:   rgba(245, 158, 11, 0.2);
    --warn-bg-active:  rgba(245, 158, 11, 0.28);
    --warn-border:     #854d0e;
    --warn-fg:         #fbbf24;
    --warn-ring:       rgba(245, 158, 11, 0.4);
    --warn-ring-solid: rgba(245, 158, 11, 0.5);

    /* #dc2626, not #ef4444: white on #ef4444 is 3.76:1, below AA. Both themes use
       the same value. */
    --danger-solid:         #dc2626;
    --danger-bg:            rgba(127, 29, 29, 0.15);
    --danger-bg-hover:      rgba(185, 28, 28, 0.25);
    --danger-bg-active:     rgba(185, 28, 28, 0.35);
    --danger-border:        rgba(153, 27, 27, 0.6);
    --danger-border-strong: #991b1b;
    --danger-fg:            #f87171;
    --danger-ring:          rgba(239, 68, 68, 0.4);   /* focus ring on a danger control */

    --info-bg:      rgba(96, 165, 250, 0.12);
    --info-border:  #1e40af;
    --info-fg:      #93c5fd;
    --info-solid:   #1d4ed8;                     /* a filled info dot or bar */
    --info-ring:    rgba(96, 165, 250, 0.5);     /* its glow */

    --secret-bg:     rgba(124, 58, 237, 0.12);
    --secret-border: #5b21b6;
    --secret-fg:     #c4b5fd;

    /* Extra badge hues — categorical only, no semantics attached. */
    --badge-cyan-bg:     rgba(6, 182, 212, 0.15);
    --badge-cyan-border: #0e7490;
    --badge-cyan-fg:     #67e8f9;
    --badge-orange-bg:     rgba(234, 88, 12, 0.15);
    --badge-orange-border: #9a3412;
    --badge-orange-fg:     #fb923c;
    --badge-teal-bg:     rgba(15, 118, 110, 0.15);
    --badge-teal-border: #134e4a;
    --badge-teal-fg:     #2dd4bf;

    /* ── Component surfaces ─────────────────────────────────────────────── */
    --badge-bg:      rgba(255, 255, 255, 0.07);
    --table-head-bg: rgba(255, 255, 255, 0.04);

    /* Feedback surfaces. Tinted from the theme rather than the brand: a loading
       placeholder is not a branded element, and a brand-tinted skeleton reads as
       content that is already there. */
    --skeleton-bg:    rgba(255, 255, 255, 0.06);
    --skeleton-sheen: rgba(255, 255, 255, 0.11);
    --progress-track: rgba(255, 255, 255, 0.08);
    --btn-active-bg: #1a2232;
    --code-fg:       #a5b4fc;
    --code-bg:       rgba(99, 102, 241, 0.1);
    --backdrop:      rgba(0, 0, 0, 0.65);       /* modal backdrop */
    --overlay:       rgba(17, 24, 39, 0.66);    /* in-card blocking overlay */
    --spotlight-dim: rgba(3, 7, 18, 0.62);      /* guided-tour / spotlight dim */
    --tip-bg:        #0b1220;
    --tip-border:    #4b5563;
    --tip-fg:        #f3f4f6;                   /* the tip stays dark in both themes */
    --scrollbar:       #374151;
    --scrollbar-hover: #4b5563;
    --scroll-shade:    rgba(0, 0, 0, 0.3);      /* scroll-edge fade */

    /* ── Shadows (full box-shadow values — themes swap the colour) ───────── */
    --shadow-topbar:    0 3px 8px rgba(0, 0, 0, 0.28);
    --shadow-nav-tools: 0 -6px 12px 0 rgba(0, 0, 0, 0.18);
    --shadow-modal:     0 24px 64px rgba(0, 0, 0, 0.6);
    --shadow-tip:       0 8px 22px rgba(0, 0, 0, 0.5);
    --shadow-flyout:    0 6px 18px rgba(0, 0, 0, 0.45);
    --shadow-dropdown:  0 10px 30px rgba(0, 0, 0, 0.35);
    --shadow-float:     0 10px 30px rgba(0, 0, 0, 0.4);
    --shadow-pop:       0 14px 40px rgba(0, 0, 0, 0.55);

    /* ── Blazor reconnect banner (deliberately dark in both themes) ─────── */
    --reconnect-warn-bg:     #422006;
    --reconnect-warn-border: #d97706;
    --reconnect-warn-fg:     #fde68a;
    --reconnect-fail-bg:     #450a0a;
    --reconnect-fail-border: #dc2626;
    --reconnect-fail-fg:     #fca5a5;

    /* ── Spacing scale ──────────────────────────────────────────────────────
       Every padding, margin and gap in the library resolves through one of these.
       Eleven steps: 2px granularity where UI chrome lives, 4px above it, 8px at the
       top. Redefine the scale to make a whole app denser or roomier.

       Sizes are NOT on this scale. A 28px close button and a 30px logo are
       dimensions of a thing, not space between things, and forcing them onto a
       spacing ramp would make the ramp meaningless. */
    --space-1:  2px;
    --space-2:  4px;
    --space-3:  6px;
    --space-4:  8px;
    --space-5:  10px;
    --space-6:  12px;
    --space-7:  16px;
    --space-8:  20px;
    --space-9:  24px;
    --space-10: 32px;
    --space-11: 40px;

    /* ── Control height ─────────────────────────────────────────────────────
       Three heights, shared by everything that can sit in a row with another
       control: the button, the icon button, the text input, the select, the
       read-only value, the search box, the input group and the stepper. A control's
       own padding and line box stay UNDER its tier, so the tier is what decides the
       height rather than a floor the content walks past.

       The chip takes the small and normal tiers because it is interactive. A badge
       does not: it is a label, so it has its own scale in 31-badges.css that lines
       up with text instead of with a target. */
    --control-height-sm: 28px;
    --control-height:    36px;
    --control-height-lg: 44px;   /* the WCAG AAA target size */

    /* How tall a clamped code block is before it scrolls. Raise it for docs, lower it
       for a dense admin page. */
    --code-clamp: 360px;

    /* ── Type scale ─────────────────────────────────────────────────────────
       Ten steps. 13px is the body size for content, 14px for the page. The three
       adjacent sizes in the middle are deliberate: a dense operations UI genuinely
       distinguishes a 13px cell from a 14px paragraph from a 15px panel title. */
    --text-1:  10px;   /* the smallest legible label — nav section headings */
    --text-2:  11px;
    --text-3:  12px;
    --text-4:  13px;
    --text-5:  14px;
    --text-6:  15px;
    --text-7:  16px;
    --text-8:  18px;
    --text-9:  22px;
    --text-10: 26px;
    --text-11: 40px;   /* display only — the empty-state glyph */

    /* ── Motion ─────────────────────────────────────────────────────────────
       Three durations for interaction, and one per looping animation. Set them all
       to 0s to stop every transition without touching a rule — though
       prefers-reduced-motion already does that properly. */
    --motion-fast: 0.12s;   /* hover, focus, colour changes */
    --motion-mid:  0.18s;   /* something entering or leaving — the drawer */
    --motion-slow: 0.25s;   /* a value animating to a new position — the progress bar */
    --spin-duration:     0.6s;
    --progress-duration: 1.1s;
    --skeleton-duration: 1.4s;
    --pulse-duration:    1.6s;

    /* ── Corner rounding ────────────────────────────────────────────────────
       Named by role rather than by size, because "square off the controls" is the
       actual intent and the sizes are not a scale — 4, 5 and 6 differ by a pixel
       each and mean different things. Redefine these to change how rounded a whole
       app is; every border-radius in the library except a literal 50% circle and
       `inherit` resolves through one of them. */
    --radius-control: 5px;   /* buttons, inputs, the input group */
    --radius-surface: 6px;   /* cards, panels, tips, flyouts, toasts */
    --radius-panel:   8px;   /* the modal — the largest surface */
    --radius-inner:   4px;   /* nested one border inside a control or surface */
    --radius-small:   3px;   /* inline code, the checkbox */
    --radius-pill:    999px; /* badges, count pills, the progress track, the switch */

    /* ── Cell padding ───────────────────────────────────────────────────────
       Table cells, remapped by [data-density="compact"]. A token so anything else
       that should tighten with the tables can opt in by using them. */
    --cell-pad-y: 8px;
    --cell-pad-x: 12px;

    /* ── Metrics ────────────────────────────────────────────────────────── */
    --page-max: 1800px;    /* content column cap on ultra-wide screens */
}

}

/* ── 02-theme-light.css → @layer dr.tokens ──────────────────────────────────────────────── */
@layer dr.tokens {
/* ═══ Light theme ═══════════════════════════════════════════════════════════
   Pure token remapping — no selector overrides. `data-theme` rides on <html>;
   apply it before first paint (see README) so no dark flash occurs. */
:root[data-theme="light"] {
    --color-scheme:   light;
    --picker-invert:  0;

    --bg:             #f3f4f6;
    --bg-elevated:    #ffffff;
    --bg-hover:       #e5e7eb;
    --fg:             #111827;
    --fg-soft:        #374151;
    /* #4b5563, not #6b7280: on the light --bg the lighter grey measures 4.39:1,
       which is just under AA and is the single most-used text colour after --fg. */
    --muted:          #4b5563;
    --border:         #d1d5db;
    --border-strong:  #9ca3af;
    --divider:        rgba(0, 0, 0, 0.07);
    --card-bg:        #ffffff;
    --surface-soft:   rgba(0, 0, 0, 0.03);
    --surface-strong: rgba(0, 0, 0, 0.055);
    --redacted:       #b91c1c;

    --brand-soft: #2563eb;
    --brand-text: #1d4ed8;
    --brand-tint: color-mix(in srgb, var(--brand) 10%, transparent);
    --accent:     #1d4ed8;

    --sidebar-bg:     #ffffff;
    --sidebar-border: #e5e7eb;
    --sidebar-fg:     #374151;

    --go-bg:     rgba(22, 163, 74, 0.1);
    --go-border: #86efac;
    --go-fg:     #15803d;

    --warn-bg:     rgba(217, 119, 6, 0.1);
    --warn-border: #fcd34d;
    --warn-fg:     #b45309;

    /* The hover and active tints and the strong border take the same steps as the
       dark theme, scaled for a light surface. All four move together: a tint
       remapped without its hover pair jumps between two unrelated reds. */
    --danger-solid: #dc2626;
    --danger-bg:            rgba(220, 38, 38, 0.08);
    --danger-bg-hover:      rgba(220, 38, 38, 0.14);
    --danger-bg-active:     rgba(220, 38, 38, 0.2);
    --danger-border:        #fca5a5;
    --danger-border-strong: #dc2626;
    --danger-fg:            #b91c1c;
    --danger-ring:          rgba(220, 38, 38, 0.35);

    --info-bg:     rgba(37, 99, 235, 0.08);
    --info-border: #93c5fd;
    --info-fg:     #1d4ed8;
    --info-solid:  #2563eb;
    --info-ring:   rgba(37, 99, 235, 0.4);

    --secret-bg:     rgba(124, 58, 237, 0.08);
    --secret-border: #c4b5fd;
    --secret-fg:     #6d28d9;

    --badge-cyan-bg:     rgba(6, 182, 212, 0.08);
    --badge-cyan-border: #67e8f9;
    --badge-cyan-fg:     #0e7490;
    --badge-orange-bg:     rgba(234, 88, 12, 0.08);
    --badge-orange-border: #fdba74;
    --badge-orange-fg:     #c2410c;
    --badge-teal-bg:     rgba(15, 118, 110, 0.08);
    --badge-teal-border: #5eead4;
    --badge-teal-fg:     #0f766e;

    --badge-bg:      rgba(0, 0, 0, 0.04);
    --table-head-bg: rgba(0, 0, 0, 0.03);

    --skeleton-bg:    rgba(0, 0, 0, 0.06);
    --skeleton-sheen: rgba(0, 0, 0, 0.11);
    --progress-track: rgba(0, 0, 0, 0.08);
    --btn-active-bg: #d1d5db;
    --code-fg:       #4338ca;
    --code-bg:       rgba(99, 102, 241, 0.08);
    --backdrop:      rgba(17, 24, 39, 0.35);
    --overlay:       rgba(243, 244, 246, 0.66);
    --spotlight-dim: rgba(17, 24, 39, 0.42);
    --tip-bg:        #111827;
    --tip-border:    #374151;
    --scrollbar:       #d1d5db;
    --scrollbar-hover: #9ca3af;
    --scroll-shade:    rgba(17, 24, 39, 0.16);

    --shadow-topbar:    0 3px 8px rgba(17, 24, 39, 0.1);
    --shadow-nav-tools: 0 -6px 12px 0 rgba(0, 0, 0, 0.06);
    --shadow-modal:     0 24px 64px rgba(17, 24, 39, 0.25);
    --shadow-tip:       0 8px 22px rgba(0, 0, 0, 0.25);
    --shadow-pop:       0 14px 40px rgba(17, 24, 39, 0.22);
    --shadow-float:     0 10px 30px rgba(17, 24, 39, 0.18);
}

}

/* ── 03-theme-colour-blind.css → @layer dr.tokens ──────────────────────────────────────────────── */
@layer dr.tokens {
/* ═══ Colour-blind-safe palette ═════════════════════════════════════════════
   Independent toggle, composes with either theme. Only the go family moves. */
:root[data-cvd="1"] {
    --go-solid:  #2563eb;
    --go-hover:  #1d4ed8;
    --go-active: #1e40af;
    --go-bg:     rgba(37, 99, 235, 0.14);
    --go-border: #1e40af;
    --go-fg:     #93c5fd;
    --go-ring:   rgba(37, 99, 235, 0.5);
}
:root[data-theme="light"][data-cvd="1"] {
    --go-bg:     rgba(37, 99, 235, 0.08);
    --go-border: #93c5fd;
    --go-fg:     #1d4ed8;
}


}

/* ── 04-theme-contrast.css → @layer dr.tokens ──────────────────────────────────────────────── */
@layer dr.tokens {
/* ═══ High contrast ═════════════════════════════════════════════════════════
   A pure token remap, like every other theme block, so CSS load order stays
   irrelevant and an app's own light/dark blocks keep working underneath.

   Two triggers, and both are needed:
     · `prefers-contrast: more`         — the OS setting, honoured automatically
     · `[data-contrast="more"]` on <html> — a setting inside the app, for the many
       people whose OS is not set but who want it here

   The attribute is deliberately NOT written by boot.js from the media query. A media
   query and an attribute would then both be live, and an app that wanted to offer
   "follow the system / always on / always off" could not express the third state.
   The app sets the attribute; the media query covers everybody who never opens
   settings.

   What changes, and what does not:
     · Borders go to full strength and dividers become real lines. Most of what makes
       this library low-contrast is not the text — it is the 3%-alpha surfaces and the
       6%-alpha dividers, which disappear entirely for anybody who needs this mode.
     · Muted text stops being muted. --muted is the lowest-contrast pair the light
       theme ships, at 4.39:1; here it goes to the body colour.
     · Translucent tints become opaque. A 12% tint over an unknown surface has an
       unknown contrast, which is the one thing this mode cannot have.
     · Shadows are NOT removed. They are the only depth cue left once every surface
       is the same lightness, so they matter more here, not less.
     · Semantic HUES are kept. Turning everything monochrome would destroy the
       go/warn/danger distinction, which is information. Forced colours does that,
       and it has to; this mode does not.
   ─────────────────────────────────────────────────────────────────────────── */

/* Dark, high contrast. */
:root[data-contrast="more"] {
    --bg:              #000000;
    --bg-elevated:     #0b0f18;
    --bg-hover:        #1f2937;
    --fg:              #ffffff;
    --fg-soft:         #ffffff;
    --muted:           #d1d5db;
    --border:          #9ca3af;
    --border-strong:   #d1d5db;
    --border-hover:    #ffffff;
    --divider:         #6b7280;
    --card-bg:         #0b0f18;
    --surface-soft:    #111827;
    --surface-strong:  #1f2937;

    --brand-soft:      #93c5fd;
    --brand-text:      #bfdbfe;
    --brand-tint:      #1e3a8a;
    --accent:          #93c5fd;

    --sidebar-bg:      #000000;
    --sidebar-border:  #9ca3af;
    --sidebar-fg:      #ffffff;

    --go-bg:      #052e16;
    --go-border:  #4ade80;
    --go-fg:      #86efac;
    --warn-bg:     #451a03;
    --warn-border: #fbbf24;
    --warn-fg:     #fcd34d;
    --danger-bg:            #450a0a;
    --danger-border:        #f87171;
    --danger-border-strong: #fca5a5;
    --danger-fg:            #fca5a5;
    --info-bg:     #172554;
    --info-border: #93c5fd;
    --info-fg:     #bfdbfe;
    --info-solid:  #93c5fd;
    --secret-bg:     #2e1065;
    --secret-border: #c4b5fd;
    --secret-fg:     #ddd6fe;

    --badge-bg:      #1f2937;
    --table-head-bg: #1f2937;
    --badge-cyan-bg: #083344;   --badge-cyan-border: #67e8f9;   --badge-cyan-fg: #a5f3fc;
    --badge-orange-bg: #431407; --badge-orange-border: #fdba74; --badge-orange-fg: #fed7aa;
    --badge-teal-bg: #042f2e;   --badge-teal-border: #5eead4;   --badge-teal-fg: #99f6e4;

    --skeleton-bg:    #1f2937;
    --skeleton-sheen: #374151;
    --progress-track: #374151;
    --btn-active-bg:  #374151;
    --code-fg:        #c7d2fe;
    --code-bg:        #1e1b4b;
    --backdrop:       rgba(0, 0, 0, 0.85);
    --overlay:        rgba(0, 0, 0, 0.85);
    --scrollbar:       #9ca3af;
    --scrollbar-hover: #d1d5db;
}

/* Light, high contrast. Composes with the light theme by being more specific. */
:root[data-theme="light"][data-contrast="more"] {
    --bg:              #ffffff;
    --bg-elevated:     #ffffff;
    --bg-hover:        #e5e7eb;
    --fg:              #000000;
    --fg-soft:         #000000;
    --muted:           #1f2937;
    --border:          #4b5563;
    --border-strong:   #1f2937;
    --border-hover:    #000000;
    --divider:         #9ca3af;
    --card-bg:         #ffffff;
    --surface-soft:    #f3f4f6;
    --surface-strong:  #e5e7eb;

    --brand-soft:      #1e40af;
    --brand-text:      #1e3a8a;
    --brand-tint:      #dbeafe;
    --accent:          #1e3a8a;

    --sidebar-bg:      #ffffff;
    --sidebar-border:  #4b5563;
    --sidebar-fg:      #000000;

    --go-bg:      #dcfce7;
    --go-border:  #166534;
    --go-fg:      #14532d;
    --warn-bg:     #fef3c7;
    --warn-border: #92400e;
    --warn-fg:     #78350f;
    --danger-bg:            #fee2e2;
    --danger-border:        #991b1b;
    --danger-border-strong: #7f1d1d;
    --danger-fg:            #7f1d1d;
    --info-bg:     #dbeafe;
    --info-border: #1e40af;
    --info-fg:     #1e3a8a;
    --info-solid:  #1e3a8a;
    --secret-bg:     #ede9fe;
    --secret-border: #5b21b6;
    --secret-fg:     #4c1d95;

    --badge-bg:      #e5e7eb;
    --table-head-bg: #e5e7eb;
    --badge-cyan-bg: #cffafe;   --badge-cyan-border: #155e75;   --badge-cyan-fg: #164e63;
    --badge-orange-bg: #ffedd5; --badge-orange-border: #9a3412; --badge-orange-fg: #7c2d12;
    --badge-teal-bg: #ccfbf1;   --badge-teal-border: #115e59;   --badge-teal-fg: #134e4a;

    --skeleton-bg:    #e5e7eb;
    --skeleton-sheen: #d1d5db;
    --progress-track: #d1d5db;
    --btn-active-bg:  #9ca3af;
    --code-fg:        #312e81;
    --code-bg:        #e0e7ff;
    --backdrop:       rgba(0, 0, 0, 0.6);
    --overlay:        rgba(255, 255, 255, 0.9);
    --scrollbar:       #6b7280;
    --scrollbar-hover: #374151;
    --redacted:        #7f1d1d;
}

/* The OS setting, for anybody who never opens the app's own settings. Only :root
   remaps, so `Appearance_media_queries_only_remap_tokens` holds. */
@media (prefers-contrast: more) {
    :root {
        --border:         #9ca3af;
        --border-strong:  #d1d5db;
        --divider:        #6b7280;
        --fg-soft:        #ffffff;
        --muted:          #d1d5db;
        --surface-soft:   #111827;
        --surface-strong: #1f2937;
    }
    :root[data-theme="light"] {
        --border:         #4b5563;
        --border-strong:  #1f2937;
        --divider:        #9ca3af;
        --fg-soft:        #000000;
        --muted:          #1f2937;
        --surface-soft:   #f3f4f6;
        --surface-strong: #e5e7eb;
    }
}
}

/* ── 06-base-elements.css → @layer dr.base ──────────────────────────────────────────────── */
@layer dr.base {
/* ═══ Base elements ═════════════════════════════════════════════════════════ */
* { box-sizing: border-box; }

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

/* On <html>, not :root — the selectors are equivalent, but a :root block in this
   library is a token block and may hold custom properties only.

   `scrollbar-width` and `scrollbar-color` are inherited properties, so declaring them
   here reaches every scroll container in the document: a modal body, a drawer, the
   palette list, a code block, a table wrapper, and an app's own scrollers. Declare
   them here rather than on a list of the containers the library creates: that list is
   never complete, and one unthemed OS scrollbar inside a modal is what a gap in it
   looks like. Anything that wants a different bar overrides these two on itself,
   which is what `.breadcrumb` does to hide its own. */
html {
    color-scheme: var(--color-scheme);
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar) transparent;
}

body {
    background: var(--bg);
    color: var(--fg);
    font-family: var(--font-sans);
    font-size: var(--text-5);
    line-height: 1.5;
}

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

/* overflow-wrap: anywhere, because what goes in an inline <code> is exactly the kind
   of string that has no spaces to break at — an asset path, a class list, a token
   name. Without it a long one runs past its container and is clipped, which on a
   phone means the end of the path is simply not there. `anywhere` rather than
   `break-word`: it also shrinks the min-content width, so a <code> in a table cell or
   a flex item stops forcing the whole column wide.

   It does nothing to a code BLOCK. `white-space: pre` there means lines never wrap at
   all, so there is no wrap opportunity for this to take, and the block goes on
   scrolling instead. */
code {
    font-family: var(--font-mono);
    font-size: var(--text-4);
    color: var(--code-fg);
    background: var(--code-bg);
    padding: 1px var(--space-2);
    border-radius: var(--radius-small);
    overflow-wrap: anywhere;
}

h1 { font-size: var(--text-9); margin: 0 0 var(--space-4); }
h2 { font-size: var(--text-7); margin: var(--space-9) 0 var(--space-4); }
p.lede { color: var(--muted); margin: 0 0 var(--space-7); }

/* Keyboard focus keeps a ring; mouse/programmatic focus (e.g. focus restored on
   reload) does not — otherwise an auto-selected block shows a stray outline. */
:focus:not(:focus-visible) { outline: none; }

/* dr-spin is NOT here: it is declared beside its only consumer in
   39-spinner.css. Two identical @keyframes of the same name is a silent-drift
   hazard — the later one wins for the whole document, so editing the earlier
   changes nothing. */
@keyframes dr-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }


}

/* ── 10-frame-shell.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ═══ TIER 1 — the frame ════════════════════════════════════════════════════
   Layout chrome that must be pixel-identical in every app. Do not restyle any
   of this per project; if it is wrong, it is wrong here. */

/* ── Shell ──────────────────────────────────────────────────────────────── */
/* 100dvh, not 100vh: dvh excludes the mobile browser's own chrome, so the shell is
   never partly hidden behind it. No 100vh fallback — dvh predates the supported
   browser floor, so the fallback would be a line that never runs. */
.layout {
    display: flex;
    height: 100dvh;
    overflow: hidden;
}
.content { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }

/* Bare shell (unauthenticated / minimal pages): topbar + body, no sidebar. */
.bare-layout {
    display: flex;
    flex-direction: column;
    height: 100dvh;
    overflow: hidden;
}
.bare-brand  { display: flex; align-items: center; gap: var(--space-5); padding: 0 var(--space-7); font-size: var(--text-6); }
.bare-brand .brand-logo { width: 28px; height: 28px; border-radius: var(--radius-surface); flex-shrink: 0; }

}

/* ── 11-frame-sidebar.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Sidebar ────────────────────────────────────────────────────────────── */
.sidebar {
    width: 220px;
    background: var(--sidebar-bg);
    color: var(--sidebar-fg);
    padding: 0;
    flex-shrink: 0;
    border-inline-end: 1px solid var(--sidebar-border);
    display: flex;
    flex-direction: column;
    transition: width var(--motion-fast) ease;
}

/* flex-shrink: 0 because the sidebar is a flex column and the brand is the only
   item in it that can shrink. A nav taller than the viewport — a collapsed rail
   with more links than fit — would otherwise squeeze the header down onto the
   logo. A fixed-height header stays its height. */
.brand {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    height: 44px;
    flex-shrink: 0;
    padding: 0 var(--space-7);
    border-bottom: 1px solid var(--sidebar-border);
    color: inherit;
    text-decoration: none;
}
.brand:hover { text-decoration: none; }
.brand-logo { width: 30px; height: 30px; border-radius: var(--radius-surface); flex-shrink: 0; }
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.brand-sub {
    display: block; font-size: var(--text-1); color: var(--muted);
    text-transform: uppercase; letter-spacing: 0.06em; margin-top: var(--space-1);
}

/* No margin above the nav. It would put the scroll container's top edge below
   the brand's divider, which reads as a gap the first time the nav is scrolled:
   the content slides under a strip of empty sidebar instead of under the
   divider, and the scrollbar and the scroll shadow both start late. Nothing is
   lost: every element that can be first in the nav (.nav-section-label, .nav-link,
   .nav-status-card) carries its own top padding. */
.nav { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
.nav-scroll { flex: 1; overflow-y: auto; min-height: 0; }
.nav-section { margin-bottom: var(--space-2); }
.nav-section-label {
    display: block;
    padding: var(--space-4) var(--space-7) var(--space-2);
    font-size: var(--text-1);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--muted);
    pointer-events: none;
}
.nav-link {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-7);
    color: var(--sidebar-fg);
    font-size: var(--text-4);
}
.nav-section .nav-link { padding-inline-start: var(--space-9); }
.nav-link i { font-size: var(--text-7); flex-shrink: 0; opacity: 0.6; }
.nav-link:hover { background: var(--bg-elevated); text-decoration: none; }
.nav-link:hover i { opacity: 1; }
/* Tinted rather than a solid brand pill, so the count badge on the same row stays
   readable and any brand hue works underneath. */
.nav-link.active { background: var(--brand-tint); color: var(--brand-text); }
.nav-link.active i { opacity: 1; color: var(--brand-text); }
/* Specificity, not !important: `.nav-link i` (0-1-1) above would otherwise win
   over a bare `.nav-link-ext` (0-1-0). The library uses no !important anywhere —
   inside a cascade layer an !important declaration becomes harder for an app to
   override, not easier, because layer order inverts for important declarations. */
.nav-link .nav-link-ext { margin-inline-start: auto; font-size: var(--text-2); opacity: 0.35; }

/* Count pill on a nav link (pending items, unread, …). */
.nav-count {
    margin-inline-start: auto;
    background: var(--brand);
    color: var(--on-solid);
    border-radius: var(--radius-pill);
    font-size: var(--text-2); font-weight: 600; line-height: 1.6;
    padding: 0 var(--space-4); min-width: 18px; text-align: center;
}

.nav-tools {
    flex-shrink: 0;
    border-top: 1px solid var(--sidebar-border);
    padding: var(--space-2) 0 var(--space-4);
    box-shadow: var(--shadow-nav-tools);
}
/* Quietened by colour, never by opacity. `opacity: 0.55` is theme-blind: on the
   dark sidebar it lands around 4.6:1 and on the light theme's white sidebar around
   3:1, under the 4.5:1 floor for 12px text — and a control at half strength beside
   full-strength siblings reads as disabled rather than as secondary. --muted is the
   token each theme already tunes for exactly this job. */
.nav-link-tool { font-size: var(--text-3); color: var(--muted); }
.nav-link-tool i { opacity: 0.75; }
.nav-link-tool:hover { color: var(--sidebar-fg); }
.nav-link-tool:hover i { opacity: 1; }
.nav-tools-sep { border: none; border-top: 1px solid var(--sidebar-border); margin: var(--space-5) var(--space-8) var(--space-2); }

/* Sidebar status card (a monitored dependency: API reachable, DB connected, …).
   The --ok / --fail modifiers are commonly composed dynamically in the nav. */
.nav-status-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: var(--space-3) var(--space-4);
    padding: var(--space-4) var(--space-5);
    background: var(--surface-soft);
    border: 1px solid var(--border);
    border-inline-start-width: 3px;
    border-radius: var(--radius-surface);
    text-decoration: none;
    transition: background var(--motion-fast), border-color var(--motion-fast);
}
.nav-status-card:hover { background: var(--bg-hover); text-decoration: none; }
.nav-status-label {
    font-size: var(--text-1); font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.08em;
    color: var(--muted);
}
.nav-status-row { display: flex; align-items: center; gap: var(--space-3); font-size: var(--text-3); font-weight: 500; }
.nav-status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--border-hover); }
.nav-status-state { flex: 1; }
.nav-status-card--ok   { border-inline-start-color: var(--go-solid); }
.nav-status-card--ok   .nav-status-dot   { background: var(--go-solid); box-shadow: 0 0 5px var(--go-ring); }
.nav-status-card--ok   .nav-status-state { color: var(--go-fg); }
.nav-status-card--fail { border-inline-start-color: var(--danger-solid); }
.nav-status-card--fail .nav-status-dot   { background: var(--danger-solid); }
.nav-status-card--fail .nav-status-state { color: var(--danger-fg); }

}

/* ── 12-frame-collapsed-rail.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Collapsed sidebar rail ─────────────────────────────────────────────── */
.sidebar.collapsed { width: 56px; }
.sidebar.collapsed .brand { justify-content: center; padding: 0; gap: 0; }
.sidebar.collapsed .brand-text {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}
.sidebar.collapsed .nav-section-label { display: none; }
.sidebar.collapsed .nav-section { margin-bottom: var(--space-4); }
.sidebar.collapsed .nav-link { justify-content: center; padding: var(--space-4) 0; gap: 0; position: relative; }
.sidebar.collapsed .nav-section .nav-link { padding-inline-start: 0; }
.sidebar.collapsed .nav-link > span:not(.nav-count) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}
.sidebar.collapsed .nav-link-ext { display: none; }
.sidebar.collapsed .nav-count {
    position: absolute; top: 2px; inset-inline-end: var(--space-3); margin-inline-start: 0;
    min-width: 15px; padding: 0 var(--space-2); font-size: var(--text-1); line-height: 1.6;
}
.sidebar.collapsed .nav-status-card { align-items: center; padding: var(--space-4) 0; position: relative; }
.sidebar.collapsed .nav-status-label,
.sidebar.collapsed .nav-status-state {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}
.sidebar.collapsed .nav-status-dot { width: 9px; height: 9px; }
.sidebar.collapsed .nav-tools-sep { margin: var(--space-4) var(--space-7) var(--space-2); }

/* Rail-only hover flyout. The JS tip engine skips .sidebar precisely so this
   pure-CSS variant owns the collapsed rail (no double tooltip).

   ANCHOR-POSITIONED, and it has to be. The rail scrolls: .nav-scroll keeps its
   `overflow-y: auto` when collapsed, and a scroll container clips both axes —
   there is no combination of overflow values that scrolls vertically and lets a
   child out sideways. So the flyout is `position: fixed`, which takes the
   viewport as its containing block and is therefore not clipped by any of it.

   That leaves the question of where to put it, and `position-area` answers it
   without a measurement: `inline-end` is the column beside the anchor,
   `align-self: anchor-center` lines it up with the anchor's middle. Both are
   logical, so this mirrors under dir="rtl" with no rule in 70-rtl.css.

   The name has to be scoped. Every rail item declares the SAME anchor-name, and
   without `anchor-scope` the last one in tree order would win for all of them —
   every flyout would appear beside the bottom item. `anchor-scope` confines each
   name to its own element's subtree, which is where its ::after lives. */
.sidebar.collapsed [data-tip] { anchor-name: --dr-rail-tip; anchor-scope: --dr-rail-tip; }
.sidebar.collapsed [data-tip]:hover::after {
    content: attr(data-tip);
    position: fixed;
    position-anchor: --dr-rail-tip;
    position-area: inline-end;
    align-self: anchor-center;
    margin-inline-start: var(--space-5);
    background: var(--card-bg); color: var(--fg); border: 1px solid var(--border);
    padding: var(--space-2) var(--space-4); border-radius: var(--radius-surface); font-size: var(--text-3); font-weight: 500;
    white-space: nowrap; z-index: 400; box-shadow: var(--shadow-flyout);
    pointer-events: none;
}

}

/* ── 13-frame-topbar.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Topbar ─────────────────────────────────────────────────────────────────
   z-index 60 keeps the header (and its own dropdowns) above page content,
   while staying far below the modal/spotlight/toast/tip chain documented at
   "Z-order" in CLAUDE.md. */
.topbar {
    display: flex;
    align-items: stretch;
    height: 44px;
    background: var(--bg-elevated);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
    position: relative;
    z-index: 60;
    box-shadow: var(--shadow-topbar);
}
.topbar-spacer { flex: 1; }
.topbar-section {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: 0 var(--space-8);
    border-inline-start: 1px solid var(--border);
}

/* Square icon button living in the topbar. --start carries its divider on the
   right (for the leading sidebar toggle); the default divides on the left. */
.topbar-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 44px; flex-shrink: 0;
    background: none; border: none; border-inline-start: 1px solid var(--border);
    color: var(--muted); cursor: pointer; font-size: var(--text-8);
    text-decoration: none;
}
.topbar-btn:hover { background: var(--bg-hover); color: var(--fg); text-decoration: none; }
.topbar-btn--start { border-inline-start: none; border-inline-end: 1px solid var(--border); }

}

/* ── 15-frame-user-widget.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── User widget ────────────────────────────────────────────────────────────
   Stretches to the full topbar height so the hover highlight covers the whole
   header area rather than a content strip inside it. */
.user-widget {
    position: relative;
    z-index: 200;
    display: flex;
    align-items: stretch;
    border-inline-start: 1px solid var(--border);
}
.user-trigger {
    display: flex; align-items: center; gap: var(--space-5);
    padding: 0 var(--space-7);
    background: none; border: none; cursor: pointer;
    color: var(--fg); white-space: nowrap;
}
.user-trigger:hover { background: var(--bg-hover); text-decoration: none; }
.user-info  { display: flex; flex-direction: column; align-items: flex-start; }
.user-name  { font-size: var(--text-4); font-weight: 500; line-height: 1.2; }
.user-email { font-size: var(--text-2); color: var(--muted); line-height: 1.2; }
.user-avatar {
    width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--brand-tint); color: var(--brand-text); font-size: var(--text-6);
}
.user-signout {
    display: inline-flex; align-items: center; justify-content: center;
    width: 46px; flex-shrink: 0;
    color: var(--muted); font-size: var(--text-8); text-decoration: none;
    transition: background var(--motion-fast), color var(--motion-fast);
}
.user-signout:hover, .user-signout:focus, .user-signout:hover:focus {
    background: var(--bg-hover); color: var(--fg); text-decoration: none;
}

/* ── The widget's dropdown ──────────────────────────────────────────────────
   The panel itself is the tier-2 `.menu` (58-menu.css) — there is one dropdown
   style in this library, not one per place that needs one. Only the anchoring is
   the frame's business, and it differs from `.menu-anchor` in one way that matters:
   no 4px gap, because the panel hangs off the header's bottom edge rather than
   floating below a button.

   .user-widget is already position: relative, so this needs nothing measured.

   z-index 550 is the popover/dropdown rung. .user-widget also creates a stacking
   context at 200, so 550 orders the panel WITHIN the widget — which is exactly
   right: above everything else in the header, and still covered by a modal backdrop
   opened from inside it. */
.user-widget > .menu {
    position: absolute;
    top: 100%;
    inset-inline-end: 0;
    z-index: 550;
}

}

/* ── 16-frame-page-scroll.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Page surface + scrolling ───────────────────────────────────────────────
   .page is the scroll container. Its width stays 100% and the horizontal
   padding grows instead of capping the width, so the scrollbar stays pinned to
   the far right of the screen while the content is held to --page-max and
   centred. On narrow screens the padding floors at 24px.

   position: relative is load-bearing, not decoration. An `overflow` container
   only clips the absolutely positioned descendants it is the containing block
   for — so without this, a `.visually-hidden` span inside a page (an
   icon-only link's label, a table caption) is laid out against the initial
   containing block instead, escapes the clip, and extends the DOCUMENT's
   scrollable height by however far down the page it sits. The symptom is a
   second scrollbar on <html> that scrolls the whole 100dvh shell off the top
   and reveals empty space below it. `position: relative` with `z-index: auto`
   creates no stacking context, so nothing in the z-order changes. */
.page {
    padding: var(--space-9);
    padding-inline: max(var(--space-9), calc((100% - var(--page-max)) / 2));
    flex: 1;
    overflow-y: auto;
    min-height: 0;
    position: relative;
}

.back-link {
    display: inline-flex; align-items: center; gap: var(--space-2);
    font-size: var(--text-3); color: var(--muted); text-decoration: none; margin-bottom: var(--space-6);
}
.back-link:hover { color: var(--fg); }

/* The scrollbar's SHAPE. Its colour and width are inherited from <html> in
   06-base-elements.css; these pseudo-elements are not inheritable, so the selector is
   the universal one for the same reason — every scroll container in the document, not
   a list of the ones the library creates.

   Thin, track-less thumb with breathing room from a transparent, clipped border. */
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
    background: var(--scrollbar); border-radius: var(--radius-pill);
    border: 2px solid transparent; background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
*::-webkit-scrollbar-corner { background: transparent; }

/* Soft top/bottom scroll shadows so the scroll edge fades instead of cutting
   hard. The two solid cover gradients (background-attachment: local) ride the
   content and hide the shadow at the very top/bottom; the two radial shadows
   (attachment: scroll) are pinned to the box and only show when there IS more
   content that way. --scroll-bg MUST match the surface the scroller sits on, or
   the cover itself reads as a permanent dark band. */
.page, .nav-scroll, .dr-scroll, .drawer-body, .palette-list {
    --scroll-bg: var(--bg);
    background:
        linear-gradient(var(--scroll-bg) 30%, transparent) center top,
        linear-gradient(transparent, var(--scroll-bg) 70%) center bottom,
        radial-gradient(farthest-side at 50% 0, var(--scroll-shade), transparent) center top,
        radial-gradient(farthest-side at 50% 100%, var(--scroll-shade), transparent) center bottom;
    background-repeat: no-repeat;
    background-size: 100% 26px, 100% 26px, 100% 10px, 100% 10px;
    background-attachment: local, local, scroll, scroll;
}
/* Scrollers on an elevated surface match that surface, or the cover gradient reads as
   a permanent dark band across the top of the panel. */
.modal .dr-scroll,
.drawer-body,
.palette-list { --scroll-bg: var(--bg-elevated); }
/* The nav scroller sits on the sidebar, which is its own surface. */
.nav-scroll { --scroll-bg: var(--sidebar-bg); }

}

/* ── 17-frame-hover-hints.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Hover hints ────────────────────────────────────────────────────────────
   One floating bubble, created and positioned by DR.Simple_UI.js (fixed and
   appended to <body>, so a card's or table's overflow never clips it).
   Put data-tip="plain-language what-it-does + consequence" on the control. */
.dr-tip {
    position: fixed;
    /* rtl-ok: DR.Simple_UI.js writes `left` in pixels after measuring, so a
       logical property here would be overwritten by a physical one anyway. */
    top: 0; left: 0;
    z-index: 1000;
    max-width: 280px;
    padding: var(--space-3) var(--space-5);
    background: var(--tip-bg);
    color: var(--tip-fg);
    border: 1px solid var(--tip-border);
    border-radius: var(--radius-surface);
    font-size: var(--text-3);
    font-weight: 500;
    line-height: 1.45;
    box-shadow: var(--shadow-tip);
    pointer-events: none;      /* never intercept the hover it describes */
    opacity: 0;
    transform: translateY(2px);
    transition: opacity var(--motion-fast) ease, transform var(--motion-fast) ease;
}
.dr-tip--visible { opacity: 1; transform: none; }
.btn[data-tip] { cursor: pointer; }

}

/* ── 18-frame-reconnect-banner.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Blazor reconnect banner ──────────────────────────────────────────────────
   Two things, deliberately separated: `.reconnect-banner` is a strip that says the
   connection is in trouble, and `#components-reconnect-modal` is the state machine
   Blazor drives. The strip is visible on its own and goes dark only inside the modal,
   so a row can be shown outside it — which is how the catalogue demonstrates all of
   them at once.

   Blazor sets six state classes on the modal. They map onto four rows:

   | Blazor sets                          | The row that shows                        |
   |--------------------------------------|-------------------------------------------|
   | `components-reconnect-show`          | `.reconnect-attempting`                   |
   | `components-reconnect-retrying`      | `.reconnect-attempting`, plus its counter |
   | `components-reconnect-paused`        | `.reconnect-paused`, or attempting if absent |
   | `components-reconnect-failed`        | `.reconnect-failed`                       |
   | `components-reconnect-resume-failed` | `.reconnect-failed`                       |
   | `components-reconnect-rejected`      | `.reconnect-rejected`                     |
   | `components-reconnect-hide`          | nothing — this is the connected state     |

   `retrying` is added alongside `show`; every other state replaces the last.
   `.reconnect-paused` is optional, so a host page written against the three original
   rows keeps working and only misses the softer wording. */
#components-reconnect-modal {
    display: none;
    position: fixed;
    bottom: 0; inset-inline: 0;
    z-index: 1000;
}
#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-paused,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-resume-failed,
#components-reconnect-modal.components-reconnect-rejected { display: block; }

.reconnect-banner {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-5) var(--space-8);
    font-size: var(--text-4);
    font-weight: 500;
}
.reconnect-banner i { font-size: var(--text-7); flex-shrink: 0; }
.reconnect-banner .btn { margin-inline-start: auto; }

/* The counter. Hidden until Blazor has put a number in it, which it does from the
   second attempt on — before that the markup's placeholder would read as a real count. */
.reconnect-attempt { opacity: 0.75; font-weight: 400; }
#components-reconnect-modal .reconnect-attempt { display: none; }
#components-reconnect-modal.components-reconnect-retrying .reconnect-attempt { display: inline; }

/* Inside the modal, one row at a time. The id carries the specificity, so no
   !important and nothing for an app to fight. */
#components-reconnect-modal .reconnect-banner { display: none; }
/* Blazor adds `retrying` alongside `show`, but it is keyed separately so the row still
   appears if it ever arrives on its own. */
#components-reconnect-modal.components-reconnect-show     .reconnect-attempting { display: flex; }
#components-reconnect-modal.components-reconnect-retrying .reconnect-attempting { display: flex; }
#components-reconnect-modal.components-reconnect-paused  .reconnect-paused     { display: flex; }
#components-reconnect-modal.components-reconnect-failed  .reconnect-failed     { display: flex; }
#components-reconnect-modal.components-reconnect-rejected .reconnect-rejected  { display: flex; }
/* A failed resume reads as a failed reconnect. */
#components-reconnect-modal.components-reconnect-resume-failed .reconnect-failed { display: flex; }
/* Paused with no row of its own falls back to attempting, rather than to an empty bar. */
#components-reconnect-modal.components-reconnect-paused:not(:has(.reconnect-paused))
    .reconnect-attempting { display: flex; }

/* Working on it. */
.reconnect-attempting {
    background: var(--reconnect-warn-bg);
    border-top: 1px solid var(--reconnect-warn-border);
    color: var(--reconnect-warn-fg);
}
.reconnect-attempting i { animation: dr-pulse var(--pulse-duration) ease-in-out infinite; }

/* Paused is not a lost connection: the server released the circuit and kept the state,
   and the next interaction resumes it. Same amber, no pulse — nothing is in progress. */
.reconnect-paused {
    background: var(--reconnect-warn-bg);
    border-top: 1px solid var(--reconnect-warn-border);
    color: var(--reconnect-warn-fg);
}

/* Over. Only a reload helps. */
.reconnect-failed,
.reconnect-rejected {
    background: var(--reconnect-fail-bg);
    border-top: 1px solid var(--reconnect-fail-border);
    color: var(--reconnect-fail-fg);
}
}

/* ── 19-frame-responsive.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Responsive frame — OPT IN ───────────────────────────────────────────────
   Below 900px the sidebar becomes the icon rail, whether or not `.collapsed` is
   set. Add `.layout--responsive` to `.layout` to turn it on.

   OPT-IN, NOT AUTOMATIC. A bare width query would change how an already-released app
   looks on a narrow screen with nobody editing anything. The modifier keeps it a new
   class an app chooses.

   The modifier also carries the specificity. An app that sets
   `.sidebar { width: 260px }` unconditionally beats a bare
   `@media .sidebar { width: 56px }` rule, because equal specificity plus later source
   order goes to the app's own stylesheet. `.layout--responsive .sidebar` is (0,2,0)
   and outranks it.

   Two things to know before switching it on:
     · Below 900px a collapse toggle does nothing, because the rail is forced.
       Hide the toggle at that width, or leave it and accept the no-op.
     · The rules below mirror 12-frame-collapsed-rail.css. CSS cannot alias a
       selector, so the list is repeated rather than shared, and
       `The_responsive_frame_mirrors_the_collapsed_rail` fails if the two drift.

   Geometry only. Nothing here changes a colour: appearance belongs to the token
   blocks, and a width query is the wrong place to decide what something looks
   like. `Layout_media_queries_only_change_geometry` enforces it.
   ─────────────────────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
    .layout--responsive .sidebar { width: 56px; }
    .layout--responsive .sidebar .brand { justify-content: center; padding: 0; gap: 0; }
    .layout--responsive .sidebar .brand-text {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }
    .layout--responsive .sidebar .nav-section-label { display: none; }
    .layout--responsive .sidebar .nav-section { margin-bottom: var(--space-4); }
    .layout--responsive .sidebar .nav-link { justify-content: center; padding: var(--space-4) 0; gap: 0; position: relative; }
    .layout--responsive .sidebar .nav-section .nav-link { padding-inline-start: 0; }
    .layout--responsive .sidebar .nav-link > span:not(.nav-count) {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }
    .layout--responsive .sidebar .nav-link-ext { display: none; }
    .layout--responsive .sidebar .nav-count {
        position: absolute; top: 2px; inset-inline-end: var(--space-3); margin-inline-start: 0;
        min-width: 15px; padding: 0 var(--space-2); font-size: var(--text-1); line-height: 1.6;
    }
    .layout--responsive .sidebar .nav-status-card { align-items: center; padding: var(--space-4) 0; position: relative; }
    .layout--responsive .sidebar .nav-status-label,
    .layout--responsive .sidebar .nav-status-state {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
    }
    .layout--responsive .sidebar .nav-status-dot { width: 9px; height: 9px; }
    .layout--responsive .sidebar .nav-tools-sep { margin: var(--space-4) var(--space-7) var(--space-2); }

    /* The rail's own flyout, which is what carries the label once the text is
       gone. Anchor-positioned for the reason spelled out in
       12-frame-collapsed-rail.css: the rail scrolls, and a scroll container
       clips both axes. Every value here is geometry; the surface comes from the
       tokens the expanded flyout already uses. */
    .layout--responsive .sidebar [data-tip] { anchor-name: --dr-rail-tip; anchor-scope: --dr-rail-tip; }
    .layout--responsive .sidebar [data-tip]:hover::after {
        content: attr(data-tip);
        position: fixed;
        position-anchor: --dr-rail-tip;
        position-area: inline-end;
        align-self: anchor-center;
        margin-inline-start: var(--space-5);
        background: var(--card-bg); color: var(--fg); border: 1px solid var(--border);
        padding: var(--space-2) var(--space-4); border-radius: var(--radius-surface); font-size: var(--text-3); font-weight: 500;
        white-space: nowrap; z-index: 400; box-shadow: var(--shadow-flyout);
        pointer-events: none;
    }

    /* The user widget's second line is the first thing to go: it is the longest
       string in the header and the least load-bearing.

       `.topbar--responsive` reaches the same rules without a `.layout` around it, for
       a header in `.bare-layout`, in `.auth-layout`, or on its own. Same opt-in, same
       specificity — a topbar's own narrow behaviour must not depend on which shell it
       happens to be inside. */
    .layout--responsive .user-email,
    .topbar--responsive .user-email { display: none; }
}

/* Under ~560px the name goes too, leaving the avatar. The sign-out button and any
   header controls keep their full hit area, which is what actually matters on a
   touch screen. */
@media (max-width: 560px) {
    .layout--responsive .user-info,
    .topbar--responsive .user-info { display: none; }
    .layout--responsive .user-trigger,
    .topbar--responsive .user-trigger { padding: 0 var(--space-6); }
}
}

/* ── 20-frame-layouts.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Auth and full-bleed layouts ────────────────────────────────────────────
   Two more shells beside .layout and .bare-layout. Both are tier 1: they must look
   the same in every app.
   ─────────────────────────────────────────────────────────────────────────── */

/* Sign-in, sign-out, "check your e-mail", an invitation. One centred card and
   nothing else — there is no navigation to offer to somebody who is not signed in,
   and a half-populated sidebar behind a sign-in form invites them to try it. */
.auth-layout {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-8);
    min-height: 100dvh;
    padding: var(--space-9);
    background: var(--bg);
}
.auth-brand { display: flex; align-items: center; gap: var(--space-5); font-size: var(--text-7); font-weight: 600; }
.auth-brand img { width: 32px; height: 32px; border-radius: var(--radius-surface); }
/* Capped rather than stretched: a sign-in form the width of a desktop screen has
   inputs nobody can aim at, and reads as an unfinished page. */
.auth-card { width: 100%; max-width: 380px; }
.auth-foot { color: var(--muted); font-size: var(--text-3); text-align: center; }

/* Content that owns the whole area below the header — a map, a board, a diagram
   editor. `.page` is the frame's scroll container and pads its content; this
   replaces it, taking the padding off and handing scrolling to the child.

   Used INSTEAD of .page, never alongside it. */
.full-layout {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    padding: 0;
}

/* ── Nav sub-items ──────────────────────────────────────────────────────────
   A collapsible group inside the sidebar, for a section with more children than fits
   as a flat list.

   Built on <details>/<summary>: open state, keyboard operation and the announcement
   come from the platform, and the group stays usable with scripting blocked — which
   the frame must be. Add `open` in the markup for the group containing the current
   page, so the reader does not land on a page whose own nav entry is hidden.

   In the collapsed rail the whole group flattens to its icons; the summary's caret
   and label drop out with everything else, via the existing rail rules. */
.nav-group > summary {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-7);
    color: var(--sidebar-fg);
    font-size: var(--text-4);
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary:hover { background: var(--bg-elevated); }
.nav-group > summary:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--brand-ring-soft); }
.nav-group > summary > i { font-size: var(--text-7); flex-shrink: 0; opacity: 0.6; }
.nav-group > summary::after {
    content: '';
    margin-inline-start: auto;
    width: 0;
    height: 0;
    border-inline: 4px solid transparent;
    border-top: 5px solid currentColor;
    opacity: 0.45;
    transition: transform var(--motion-fast) ease;
}
.nav-group[open] > summary::after { transform: rotate(-180deg); }
/* Sub-items indent to sit under the parent's label rather than its icon. */
/* The indent is the parent's own padding plus the width of its icon and the gap after
   it. Written as that sum so it stays aligned when the spacing scale is redefined. */
.nav-group .nav-link {
    padding-inline-start: calc(var(--space-7) + var(--text-7) + var(--space-4));
    font-size: var(--text-3);
}
.sidebar.collapsed .nav-group > summary { justify-content: center; padding: var(--space-4) 0; gap: 0; }
.sidebar.collapsed .nav-group > summary::after { display: none; }
/* Clipped, not display:none — the group still needs its name in the rail. */
.sidebar.collapsed .nav-group > summary > span {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}
.sidebar.collapsed .nav-group .nav-link { padding-inline-start: 0; }

/* ── Skip link ──────────────────────────────────────────────────────────────
   The first focusable thing on the page, jumping past the navigation to the content.
   Without it a keyboard user tabs through every nav item on every page.

   Hidden until focused, and then it must be visible ABOVE the header, hence the
   reconnect/tip rung at 1000: it is the one thing that has to be readable whatever
   else is on screen.

   Point it at the id on <main>, and give <main> tabindex="-1" so the jump actually
   moves focus rather than only scrolling. */
.skip-link {
    position: fixed;
    inset-block-start: 0;
    inset-inline-start: 0;
    z-index: 1000;
    padding: var(--space-5) var(--space-7);
    background: var(--brand);
    border-end-end-radius: var(--radius-surface);
    color: var(--on-solid);
    font-size: var(--text-4);
    font-weight: 500;
    text-decoration: none;
    /* Off-screen rather than hidden: it must stay focusable, and it must be the first
       thing Tab reaches. */
    transform: translateY(-110%);
    transition: transform var(--motion-fast) ease;
}
.skip-link:focus {
    transform: none;
    text-decoration: none;
    outline: none;
    box-shadow: 0 0 0 3px var(--brand-ring);
}
}

/* ── 21-frame-search.css → @layer dr.frame ──────────────────────────────────────────────── */
@layer dr.frame {
/* ── Header search ──────────────────────────────────────────────────────────
   The free-text box that sits at the start of the topbar, and the result panel
   that drops out of it.

     <div class="search">
       <i class="ri-search-line search-icon"></i>
       <input class="search-input" type="search" data-search placeholder="Search…">
     </div>

   The clear button and the whole panel are built by drSimpleUi.search — see
   js-parts/25-search.js — so the classes below the input have no markup an app
   writes. An app that wants to render the results itself (a Blazor component
   over its own database, say) uses the same classes and skips data-search.

   The panel is appended to <body> and positioned with inline top/left/width, not
   nested in .search: .topbar creates a stacking context at z-index 60, so a panel
   inside it can never rise above the modal backdrop however high its own z-index,
   and Blazor owns the topbar's subtree while nothing but this library owns <body>'s
   tail. 550 is the popover and dropdown rung on the documented scale.
   ─────────────────────────────────────────────────────────────────────────── */
.search {
    position: relative;
    display: flex;
    align-items: center;
    /* Takes the space it is given up to 480px and gives it back first: the
       status pills and the controls at the far end matter more than a wider
       box. */
    flex: 0 1 480px;
    min-width: 130px;
    /* Logical, so the box keeps its breathing room from the nav toggles on its
       leading side under dir="rtl". A four-value `margin` shorthand would slip
       past the RTL guard and still be wrong. */
    margin-inline: var(--space-6) var(--space-7);
}

.search-icon {
    position: absolute;
    inset-inline-start: var(--space-6);
    color: var(--muted);
    font-size: var(--text-6);
    pointer-events: none;
}

.search-input {
    width: 100%;
    /* The same height as every other control, from the same token, so a .btn beside
       the box — an "advanced" toggle, a saved-search picker — lines up with it. */
    height: var(--control-height);
    /* Symmetric room for the icon at one end and the clear button at the other,
       both of which are positioned over the input rather than beside it. */
    padding: 0 var(--space-10);
    background: var(--surface-strong);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    color: var(--fg);
    font-family: inherit;
    font-size: var(--text-4);
    outline: none;
}
.search-input::placeholder { color: var(--muted); }
/* type="search" brings the platform's own ✕, which sits in the same place as
   .search-clear and does not follow the theme. */
.search-input::-webkit-search-cancel-button { display: none; }
.search-input:focus { border-color: var(--brand); background: var(--card-bg); }

.search-clear {
    position: absolute;
    inset-inline-end: var(--space-3);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    background: none;
    border-radius: var(--radius-control);
    color: var(--muted);
    font-size: var(--text-6);
    cursor: pointer;
}
.search-clear:hover { background: var(--bg-hover); color: var(--fg); }
/* Shown only when there is something to clear, decided by CSS rather than by
   script: the button is markup the app writes, so a rendered-and-hidden button
   is the one form that also works with scripting blocked. It needs the input to
   carry a placeholder — without one :placeholder-shown never matches and the
   button simply stays visible, which is the harmless failure. */
.search-input:placeholder-shown ~ .search-clear { display: none; }

.search-panel {
    position: fixed;
    z-index: 550;
    max-height: 360px;
    overflow-y: auto;
    padding: var(--space-2);
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    box-shadow: var(--shadow-dropdown);
    --scroll-bg: var(--card-bg);
}

/* Searching, and nothing found. Both are a line of muted text rather than an
   empty box: a panel that opens on nothing reads as broken. */
.search-status {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-6);
    color: var(--muted);
    font-size: var(--text-3);
}

.search-item {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    width: 100%;
    padding: var(--space-4) var(--space-5);
    border: none;
    background: none;
    border-radius: var(--radius-surface);
    color: inherit;
    font-family: inherit;
    text-align: start;
    text-decoration: none;
    cursor: pointer;
}
/* Where the keyboard is, which is a different thing from where the pointer is.
   Both land on the same tint here because the two never disagree: the pointer
   moving over a result also moves the selection. */
.search-item--sel { background: var(--bg-hover); }
.search-item:hover { background: var(--bg-hover); text-decoration: none; }

.search-item-title {
    overflow: hidden;
    color: var(--fg);
    font-size: var(--text-4);
    font-weight: 600;
    line-height: 1.35;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.search-item-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    color: var(--muted);
    font-size: var(--text-2);
}

/* A pill inside a result's meta line — "closed", "needs approval". Not a
   .badge: that is a content-sized pill for a table cell, and at 11px inside a
   dropdown row it is the wrong weight. */
.search-tag {
    padding: 0 var(--space-3);
    background: var(--surface-strong);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    color: var(--muted);
    font-size: var(--text-1);
}
.search-tag--warn { border-color: var(--warn-border); color: var(--warn-fg); }

/* Below this the box would crowd the status pills and the controls beside it.
   An app that hides the search here needs another way in — its own list page
   usually already has one. */
@media (max-width: 720px) {
    .search { display: none; }
}
}

/* ── 30-cards.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ═══ TIER 2 — the paint ════════════════════════════════════════════════════
   Page content. Classes only, never components: pages write plain HTML and apply
   these, so the markup — and the spacing that emerges from it — stays visible in
   the .razor file. Copy-pasteable examples for every class below are on the
   catalogue at https://simpleui.dennisrahmen.dev/. */

/* ── Cards ──────────────────────────────────────────────────────────────── */
.card {
    background: var(--card-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    overflow: hidden;
}
.card-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: var(--space-6) var(--space-7);
    border-bottom: 1px solid var(--border);
}
.card-head-right { display: flex; align-items: center; gap: var(--space-4); }
.card-body { padding: var(--space-6) var(--space-7); }

/* A caveat about the card's own content, pinned to its bottom edge — a stale
   figure, a degraded source, a partial result. It is part of the card, so it is
   not an .alert: an alert is about the page. */
.card-warning {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-7);
    border-top: 1px solid var(--warn-border);
    background: var(--warn-bg);
    color: var(--warn-fg);
    font-size: var(--text-3);
}
.card-warning i { font-size: var(--text-5); flex-shrink: 0; }

/* Key/value row inside a card body. */
.kv { display: flex; justify-content: space-between; padding: var(--space-2) 0; }
.kv .k { color: var(--muted); }
.kv .v { font-family: var(--font-mono); }

}

/* ── 31-badges.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Badges ─────────────────────────────────────────────────────────────────
   .badge is the neutral pill; the modifiers are the semantic families plus
   three categorical hues that carry no meaning of their own.

   A badge is a label, not a target, so its three sizes are a type scale rather than
   the control heights: it has to sit inside a line of text, a table cell and a card
   title without setting the line's height. That is why `.badge-lg` is 12px text and
   not a 36px pill. The chip, which is interactive, does take the control heights. */
.badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    background: var(--badge-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    font-size: var(--text-2);
    padding: var(--space-1) var(--space-4);
    color: var(--muted);
    line-height: 1.45;
    white-space: nowrap;
    vertical-align: middle;
}
.badge i { font-size: var(--text-3); }

.badge-sm   { font-size: var(--text-1); padding: 0 var(--space-3); }
.badge-sm i { font-size: var(--text-2); }
.badge-lg   { font-size: var(--text-3); padding: var(--space-2) var(--space-5); }
.badge-lg i { font-size: var(--text-4); }
.badge-go     { background: var(--go-bg);     border-color: var(--go-border);     color: var(--go-fg); }
.badge-warn   { background: var(--warn-bg);   border-color: var(--warn-border);   color: var(--warn-fg); }
.badge-danger { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger-fg); }
.badge-info   { background: var(--info-bg);   border-color: var(--info-border);   color: var(--info-fg); }
.badge-secret { background: var(--secret-bg); border-color: var(--secret-border); color: var(--secret-fg); }
.badge-cyan   { background: var(--badge-cyan-bg);   border-color: var(--badge-cyan-border);   color: var(--badge-cyan-fg); }
.badge-orange { background: var(--badge-orange-bg); border-color: var(--badge-orange-border); color: var(--badge-orange-fg); }
.badge-teal   { background: var(--badge-teal-bg);   border-color: var(--badge-teal-border);   color: var(--badge-teal-fg); }

}

/* ── 32-tables.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Tables ─────────────────────────────────────────────────────────────── */
.table { width: 100%; border-collapse: collapse; }
.table th, .table td {
    text-align: start;
    padding: var(--cell-pad-y) var(--cell-pad-x);
    border-bottom: 1px solid var(--border);
}
.table th {
    background: var(--table-head-bg);
    font-weight: 600;
    color: var(--muted);
    font-size: var(--text-3);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.tr-clickable { cursor: pointer; user-select: none; }
.tr-clickable:hover > td { background: var(--bg-hover); }
.col-muted { color: var(--muted); font-size: var(--text-3); white-space: nowrap; }

}

/* ── 33-buttons.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Buttons ────────────────────────────────────────────────────────────────
   Colour language — a panel's primary CTA is always a FILLED button in its
   semantic colour:
     .btn-primary    filled brand   — the neutral primary
     .btn-go         filled green   — sends something outward (approve, apply)
     .btn-warn-solid filled amber   — changes who is in control (take over)
     .btn-warn       tinted amber   — a SECONDARY of the above, beside a primary
     .btn-danger     tinted red     — destructive

   --control-height is shared with the text input, the select and the search box, so
   a button beside a field is the same height as the field. It also pins an icon-only
   button to the height of its text siblings: a bare icon's line box (15px/1) is
   shorter than the 1.4-line text next to it, so without it an icon-only button sits
   several pixels short in a mixed row. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--control-height);
    padding: var(--space-3) var(--space-7);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
    background: var(--bg-elevated);
    color: var(--fg);
    font-size: var(--text-4);
    font-family: inherit;
    font-weight: 500;
    cursor: pointer;
    line-height: 1.4;
    white-space: nowrap;
    text-decoration: none;
    transition: background var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast);
}
.btn i { font-size: var(--text-6); line-height: 1; }
.btn:hover         { background: var(--bg-hover); border-color: var(--border-strong); text-decoration: none; }
.btn:active        { background: var(--btn-active-bg); }
.btn:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.btn:disabled      { opacity: 0.4; cursor: not-allowed; pointer-events: none; }

/* Three tiers. Each sets its height from the matching token and scales its padding,
   text and icon with it. */
.btn-sm   { min-height: var(--control-height-sm); padding: var(--space-2) var(--space-5); font-size: var(--text-3); }
.btn-sm i { font-size: var(--text-4); }
.btn-lg   { min-height: var(--control-height-lg); padding: var(--space-4) var(--space-9); font-size: var(--text-5); gap: var(--space-4); }
.btn-lg i { font-size: var(--text-8); }

.btn-primary        { background: var(--brand); border-color: var(--brand); color: var(--on-solid); }
.btn-primary:hover  { background: var(--brand-hover); border-color: var(--brand-hover); }
.btn-primary:active { background: var(--brand-active); border-color: var(--brand-active); }
.btn-primary:focus-visible { box-shadow: 0 0 0 2px var(--brand-ring); }

.btn-go        { background: var(--go-solid); border-color: var(--go-solid); color: var(--on-solid); }
.btn-go:hover  { background: var(--go-hover); border-color: var(--go-hover); }
.btn-go:active { background: var(--go-active); border-color: var(--go-active); }
.btn-go:focus-visible { box-shadow: 0 0 0 2px var(--go-ring); }

.btn-warn-solid        { background: var(--warn-solid); border-color: var(--warn-solid); color: var(--on-solid); }
.btn-warn-solid:hover  { background: var(--warn-hover); border-color: var(--warn-hover); }
.btn-warn-solid:active { background: var(--warn-active); border-color: var(--warn-active); }
.btn-warn-solid:focus-visible { box-shadow: 0 0 0 2px var(--warn-ring-solid); }

.btn-warn        { color: var(--warn-fg); border-color: var(--warn-border); background: var(--warn-bg); }
.btn-warn:hover  { background: var(--warn-bg-hover); border-color: var(--warn-border); }
.btn-warn:active { background: var(--warn-bg-active); }
.btn-warn:focus-visible { box-shadow: 0 0 0 2px var(--warn-ring); }

.btn-danger        { color: var(--danger-fg); border-color: var(--danger-border); background: var(--danger-bg); }
.btn-danger:hover  { background: var(--danger-bg-hover); border-color: var(--danger-border-strong); }
.btn-danger:active { background: var(--danger-bg-active); }
/* Its own ring, so focus is not a blue halo around a red destructive button. */
.btn-danger:focus-visible { box-shadow: 0 0 0 2px var(--danger-ring); }

}

/* ── 34-forms.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Forms ──────────────────────────────────────────────────────────────── */
.form-field { display: flex; flex-direction: column; gap: var(--space-3); }

.form-label {
    font-size: var(--text-2);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

/* min-height, not height: a <textarea> and a <select multiple> are taller by nature
   and must stay so. It is the same token the button uses, which is what makes a
   button beside a field line up with it.

   The block padding matches the button's for the same reason, and has to stay small
   enough that the natural height — padding + line box + borders — is UNDER
   --control-height. A min-height is a floor: at 8px of block padding the field
   measures 36.2px, the token never binds, and the field sits a couple of pixels
   taller than everything beside it. */
.form-input {
    min-height: var(--control-height);
    padding: var(--space-3) var(--space-5);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
    color: var(--fg);
    font-size: var(--text-4);
    font-family: inherit;
    line-height: 1.4;
    width: 100%;
    transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
}
.form-input:focus {
    outline: none;
    border-color: var(--brand-soft);
    box-shadow: 0 0 0 2px var(--brand-glow);
}
.form-input::placeholder { color: var(--border-strong); }

/* The same three tiers as the button, so a field and the button beside it agree at
   every size. Padding stays under the tier so the tier is what sets the height. */
.form-input-sm { min-height: var(--control-height-sm); padding: var(--space-2) var(--space-4); font-size: var(--text-3); }
.form-input-lg { min-height: var(--control-height-lg); padding: var(--space-5) var(--space-7); font-size: var(--text-5); }

/* A read-only value rendered in the shape of an input — so it takes the input's
   height too, or a form of mixed editable and read-only rows steps up and down. */
/* display:flex, so min-height applies and the text sits centred in the box. min-height
   does nothing on an inline element, so a bare <span> here comes out several pixels
   shorter than the field above it. */
.form-value-display {
    display: flex;
    align-items: center;
    min-height: var(--control-height);
    padding: var(--space-3) var(--space-5);
    background: var(--surface-soft);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
    font-size: var(--text-4);
}

.form-check {
    display: inline-flex;
    align-items: center;
    gap: var(--space-4);
    font-size: var(--text-4);
    cursor: pointer;
    user-select: none;
}
.form-check input[type="checkbox"] {
    appearance: none;
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    cursor: pointer;
    background: var(--bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-small);
    position: relative;
    transition: background var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast);
}
.form-check input[type="checkbox"]:hover { border-color: var(--border-hover); }
.form-check input[type="checkbox"]:checked {
    background: var(--brand);
    border-color: var(--brand);
}
.form-check input[type="checkbox"]:checked::after {
    content: '';
    position: absolute;
    /* rtl-ok: centring, with translate doing the offset — direction-neutral. */
    left: 50%; top: 50%;
    width: 4px; height: 8px;
    border-inline-end: 2px solid var(--on-solid);
    border-bottom: 2px solid var(--on-solid);
    transform: translate(-50%, -60%) rotate(45deg);
}
.form-check input[type="checkbox"]:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--brand-ring-check);
}

/* Disabled reads as disabled. It applies to the whole label, not just the box:
   dimming the 15px control and leaving its sentence at full contrast makes the row
   look enabled with a faint tick, which is worse than either state.

   The `:has()` is what lets one rule cover the checkbox and the radio without either
   knowing about the other. */
.form-check:has(input:disabled) { color: var(--muted); cursor: not-allowed; }
.form-check input:disabled { opacity: 0.45; cursor: not-allowed; }

.form-input:disabled,
.form-input[readonly] { cursor: not-allowed; background: var(--surface-soft); color: var(--muted); }
/* readonly is not disabled: the value is still selectable and copyable, and still
   submitted, so it keeps the normal cursor and a full-contrast value. Only the
   surface says it cannot be edited. */
.form-input[readonly] { cursor: text; color: var(--fg); }

.form-hint { font-size: var(--text-3); color: var(--muted); margin: 0; }

}

/* ── 35-toolbar.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Toolbar (the filter bar above a table or list) ─────────────────────── */
.toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-6);
    margin-bottom: var(--space-7);
    flex-wrap: wrap;
}
.toolbar-filters { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
.toolbar-input { width: 160px; }
.toolbar-count { font-size: var(--text-3); color: var(--muted); white-space: nowrap; }

}

/* ── 36-modal.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Modal ──────────────────────────────────────────────────────────────── */
.modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--backdrop);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 500;
    backdrop-filter: blur(2px);
}
.modal {
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-panel);
    width: 100%;
    max-width: 480px;
    box-shadow: var(--shadow-modal);
}
/* Width only. A confirmation needs less room than a form, and a diff or a table
   needs more; nothing else about the panel changes. */
.modal-sm { max-width: 360px; }
.modal-lg { max-width: 760px; }

/* ── As a <dialog> ──────────────────────────────────────────────────────────
   `.modal` on a <dialog> opened with showModal() gets the top layer, a focus trap,
   Escape-to-close and inert content behind it — none of which a div-based backdrop
   can do without a lot of code that is usually subtly wrong. drSimpleUi.confirm()
   uses this, and an app should too wherever its markup can be a <dialog>.

   No .modal-backdrop is involved: ::backdrop is the platform's own, so the
   dimming comes from the pseudo-element rather than from an element in the page.

   --dialog-backdrop is declared on the dialog itself rather than referencing
   var(--backdrop) from ::backdrop directly. ::backdrop inherits from its originating
   element, so the token has to be one inheritance step away rather than at the
   document root; an unresolved background is a transparent one, which removes the
   dimming without failing visibly. */
/* The entrance animation belongs on `dialog.modal` and never on `.modal`: only a
   dialog has an opening, going from display:none to shown, which is what starts the
   animation. A `.modal` panel sitting in the page has nothing to animate from.

   A resting appearance must never depend on an animation having run. An animation with
   no fill-mode still applies its 0% keyframe while it is PAUSED, and a document is
   paused whenever its tab is in the background — so the same animation on `.modal`
   leaves a panel in a background tab at `scale(0.97) translateY(-10px)` for good. */
dialog.modal {
    padding: 0;
    color: var(--fg);
    --dialog-backdrop: var(--backdrop);
    animation: dr-modal-in var(--motion-fast) ease;
}
dialog.modal::backdrop {
    background: var(--dialog-backdrop);
    backdrop-filter: blur(2px);
}

@keyframes dr-modal-in {
    from { opacity: 0; transform: translateY(-10px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}
.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-7) var(--space-8);
    border-bottom: 1px solid var(--border);
}
.modal-header h3 { margin: 0; font-size: var(--text-6); font-weight: 600; }
.modal-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    background: none;
    border: none;
    border-radius: var(--radius-inner);
    color: var(--muted);
    cursor: pointer;
    font-size: var(--text-8);
    padding: 0;
    transition: background var(--motion-fast), color var(--motion-fast);
}
.modal-close:hover { background: var(--bg-hover); color: var(--fg); }
.modal-body {
    padding: var(--space-8);
    display: flex;
    flex-direction: column;
    gap: var(--space-7);
}
.modal-body p { margin: 0; }
.modal-footer {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-7) var(--space-8);
    border-top: 1px solid var(--border);
}

}

/* ── 37-alerts.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Inline alert banner ────────────────────────────────────────────────── */
.alert {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-7);
    border-radius: var(--radius-surface);
    font-size: var(--text-4);
    margin-bottom: var(--space-7);
    border: 1px solid;
}
.alert i { font-size: var(--text-7); flex-shrink: 0; }
.alert-go     { background: var(--go-bg);     border-color: var(--go-hover);     color: var(--go-fg); }
.alert-warn   { background: var(--warn-bg);   border-color: var(--warn-hover);   color: var(--warn-fg); }
.alert-danger { background: var(--danger-bg); border-color: var(--danger-border-strong); color: var(--danger-fg); }
.alert-info   { background: var(--info-bg);   border-color: var(--info-border);   color: var(--info-fg); }

}

/* ── 38-toasts.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Toasts ─────────────────────────────────────────────────────────────────
   Transient confirmation of something that already happened. Anything the user
   must act on is an .alert, which stays until the state changes.

   The stack is the only fixed element; each toast is a normal block inside it,
   so an app can render the list with @foreach and never position anything.

   Plain `column`, not `column-reverse`: because the bottom edge is pinned, the
   box grows upward on its own, so the newest toast is already nearest the corner
   without reversing anything — and the rendered order matches the source order,
   which a reader comparing the demo to the snippet depends on. */
.toast-stack {
    position: fixed;
    inset-block-end: var(--space-8);
    inset-inline-end: var(--space-8);
    z-index: 600;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    /* The stack spans a corner region; only the toasts themselves take clicks,
       or an invisible column would swallow them for the whole page height. */
    pointer-events: none;
}

.toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: var(--space-5);
    /* 320px, capped by the viewport AND by whatever contains it. The stack is fixed
       to a corner, where `100%` is the viewport and the first term binds; a toast
       rendered inside a narrower box takes the second and shrinks rather than
       overflowing it. */
    width: 320px;
    max-width: min(calc(100vw - 40px), 100%);
    padding: var(--space-5) var(--space-6);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-inline-start: 3px solid var(--border-strong);
    border-radius: var(--radius-surface);
    box-shadow: var(--shadow-float);
    font-size: var(--text-4);
    color: var(--fg-soft);
}
.toast i        { font-size: var(--text-7); flex-shrink: 0; color: var(--muted); }
.toast strong   { display: block; color: var(--fg); font-weight: 600; }
.toast-body     { flex: 1; min-width: 0; }
.toast-close {
    flex-shrink: 0;
    background: none; border: none; padding: 0;
    color: var(--muted); font-size: var(--text-6); cursor: pointer;
    line-height: 1;
}
.toast-close:hover { color: var(--fg); }

/* The semantic families again, carried on the leading rule and the icon. */
.toast-go     { border-inline-start-color: var(--go-solid); }
.toast-go i   { color: var(--go-fg); }
.toast-warn   { border-inline-start-color: var(--warn-solid); }
.toast-warn i { color: var(--warn-fg); }
.toast-danger   { border-inline-start-color: var(--danger-solid); }
.toast-danger i { color: var(--danger-fg); }
.toast-info   { border-inline-start-color: var(--info-border); }
.toast-info i { color: var(--info-fg); }

}

/* ── 39-spinner.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Spinner ────────────────────────────────────────────────────────────────
   A ring with one coloured arc. Sized by font-size so it matches the text or
   the button it sits in without a second class. */
.spinner {
    display: inline-block;
    width: 1em;
    height: 1em;
    border: 2px solid var(--border);
    border-top-color: var(--brand-soft);
    border-radius: 50%;
    animation: dr-spin var(--spin-duration) linear infinite;
    /* Keeps the ring on the text baseline instead of riding above it. */
    vertical-align: -0.125em;
}
.spinner-lg { width: 24px; height: 24px; border-width: 3px; }

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

}

/* ── 40-skeleton.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Skeleton ───────────────────────────────────────────────────────────────
   A placeholder with the shape of the content that is coming, so the page does
   not reflow when it arrives. Give it the height of the real thing. */
.skeleton {
    background: var(--skeleton-bg);
    background-image: linear-gradient(
        90deg,
        var(--skeleton-bg) 0%,
        var(--skeleton-sheen) 50%,
        var(--skeleton-bg) 100%);
    background-size: 200% 100%;
    border-radius: var(--radius-inner);
    animation: dr-skeleton var(--skeleton-duration) ease-in-out infinite;
}
/* A line of text. Two of these with different widths read as a paragraph. */
.skeleton-text   { height: 12px; margin: var(--space-3) 0; }
.skeleton-title  { height: 18px; margin: var(--space-4) 0; border-radius: var(--radius-control); }
.skeleton-avatar { width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0; }

@keyframes dr-skeleton {
    from { background-position: 200% 0; }
    to   { background-position: -200% 0; }
}

}

/* ── 41-progress.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Progress ───────────────────────────────────────────────────────────────
   Determinate: set the inline width on .progress-bar, which is the one value
   only the app knows. Indeterminate: omit the width and add the modifier. */
.progress {
    height: 6px;
    background: var(--progress-track);
    border-radius: var(--radius-pill);
    overflow: hidden;
}
.progress-bar {
    height: 100%;
    background: var(--brand);
    border-radius: inherit;
    transition: width var(--motion-slow) ease;
}
.progress-bar--go     { background: var(--go-solid); }
.progress-bar--warn   { background: var(--warn-solid); }
.progress-bar--danger { background: var(--danger-solid); }

.progress--indeterminate .progress-bar {
    width: 35%;
    animation: dr-progress var(--progress-duration) ease-in-out infinite;
}
@keyframes dr-progress {
    from { transform: translateX(-110%); }
    to   { transform: translateX(340%); }
}

}

/* ── 42-empty-state.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Empty state ────────────────────────────────────────────────────────────
   No data, no permission, or nothing found. Say which of those it is, and give
   the one action that resolves it. */
.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-4);
    padding: var(--space-11) var(--space-9);
    color: var(--muted);
}
.empty-state i {
    font-size: var(--text-11);
    color: var(--border-strong);
    line-height: 1;
}
.empty-state-title { font-size: var(--text-5); font-weight: 600; color: var(--fg); }
.empty-state p     { margin: 0; font-size: var(--text-4); max-width: 44ch; }
/* Sits directly on the page rather than inside a card. */
.empty-state--bare { padding: var(--space-9) 0; }

/* ── Not empty yet, and empty because something broke ───────────────────────
   The same block, for the two states a live view spends most of its time in. Say
   which one it is: "nothing here" and "we could not load it" call for different
   actions, and showing the first when it is the second is how a broken page looks
   fine.

   `--pending` is for a wait with no progress to report — the first event of a stream,
   a long request. Put a `.spinner-lg` where the icon goes. Where there IS progress,
   use `.progress`; where the shape of what is coming is known, use `.skeleton`.

   `--failed` keeps the error's own colour on the icon only. A whole red panel reads as
   destructive rather than as unavailable. */
.empty-state--pending .spinner,
.empty-state--pending .spinner-lg { color: var(--muted); }
.empty-state--failed i { color: var(--danger-fg); }
/* The line under the title, for the detail that makes a failure actionable: the code,
   the endpoint, when it was last tried. */
.empty-state-detail {
    font-family: var(--font-mono);
    font-size: var(--text-2);
    color: var(--muted);
}

}

/* ── 43-content-grids.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Content grids ──────────────────────────────────────────────────────────
   Layout primitives. Page-specific grids belong in the app, not here.

   Every track minimum is `min(Npx, 100%)` rather than a bare `Npx`. A bare track
   minimum does not shrink, so a grid whose column floor exceeds the container
   overflows it instead of dropping to one column — on a phone, and inside any narrow
   card. `min()` keeps the floor above that width and lets the track collapse below it,
   which is what makes these breakpoint-free rather than breakpoint-free above 340px. */

/* Cards of similar height flow across as many balanced columns as the width
   allows and stack to fill each column, instead of piling into one strip. Every
   child is kept whole. auto-fit collapses empty tracks, so the cards that ARE
   present always stretch to fill the row — no stranded empty column. */
.card-masonry {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
    gap: var(--space-7);
    align-items: start;
}

/* Equal-height cards in a plain wrapping grid. Differs from .card-masonry in one
   way that decides which to use: auto-FILL keeps the track count stable, so a
   row of three cards stays three columns wide instead of stretching to fill the
   row. Use this for a set of peers (a dashboard of tiles); use .card-masonry
   when the cards have very different heights. */
.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
    gap: var(--space-7);
}

/* Two-up field rows that collapse to one column when there isn't room. */
.field-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
    gap: var(--space-7);
}

}

/* ── 44-markdown.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Markdown editor ────────────────────────────────────────────────────────
   Toolbar + textarea + rendered preview in one bordered box. The Write/Preview
   tabs flip [data-md-view] on the root (done by the JS). */
.md-editor {
    border: 1px solid var(--border);
    border-radius: var(--radius-panel);
    overflow: hidden;
    background: var(--card-bg);
}
.md-toolbar {
    display: flex; align-items: center; gap: var(--space-1); flex-wrap: wrap;
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border);
    background: var(--bg-elevated);
}
.md-tool {
    background: none; border: none; border-radius: var(--radius-control); cursor: pointer;
    color: var(--fg-soft); font-size: var(--text-6);
    width: 28px; height: 28px;
    display: inline-flex; align-items: center; justify-content: center;
}
.md-tool:hover { background: var(--bg-hover); color: var(--fg); }
/* The Write/Preview switch is the tier-2 .segmented control, not a private set of
   tab classes. It is exactly what a segmented control is for — two mutually
   exclusive options, applied immediately — and using it means the editor gets the
   radio group's keyboard behaviour and announcement for free instead of a pair of
   buttons with a toggled class. Only its position in the toolbar is the editor's
   business. */
.md-toolbar .segmented { margin-inline-start: auto; }
.md-editor .md-input {
    border: none; border-radius: 0; background: transparent;
    width: 100%; display: block;
    font-family: var(--font-mono); font-size: var(--text-3); resize: vertical;
}
.md-editor .md-preview {
    display: none; box-sizing: border-box;
    padding: var(--space-6) var(--space-7); min-height: 140px;
    /* height mirrors the textarea (synced in JS); long content scrolls */
    overflow-y: auto; resize: vertical;
}
.md-editor[data-md-view="preview"] .md-input   { display: none; }
.md-editor[data-md-view="preview"] .md-preview { display: block; }

/* Theme the scroll + resize chrome on both panes so the browser-default white
   scrollbar corner and resizer grip don't show through. */
.md-editor .md-input,
.md-editor .md-preview { scrollbar-width: thin; scrollbar-color: var(--scrollbar) transparent; }
.md-editor .md-input::-webkit-scrollbar,
.md-editor .md-preview::-webkit-scrollbar { width: 10px; height: 10px; }
.md-editor .md-input::-webkit-scrollbar-thumb,
.md-editor .md-preview::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: var(--radius-pill); }
.md-editor .md-input::-webkit-scrollbar-thumb:hover,
.md-editor .md-preview::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
.md-editor .md-input::-webkit-scrollbar-track,
.md-editor .md-preview::-webkit-scrollbar-track,
.md-editor .md-input::-webkit-scrollbar-corner,
.md-editor .md-preview::-webkit-scrollbar-corner { background: transparent; }
.md-editor .md-input::-webkit-resizer,
.md-editor .md-preview::-webkit-resizer {
    background: linear-gradient(-45deg, transparent 0 4px, var(--scrollbar) 4px 5px,
                transparent 5px 7px, var(--scrollbar) 7px 8px, transparent 8px);
}

/* Rendered Markdown — the preview pane, and anywhere else Markdown is shown. */
.markdown-body { color: var(--fg-soft); font-size: var(--text-4); line-height: 1.6; word-break: break-word; }
.markdown-body > :first-child { margin-top: 0; }
.markdown-body > :last-child  { margin-bottom: 0; }
.markdown-body h1, .markdown-body h2, .markdown-body h3,
.markdown-body h4, .markdown-body h5, .markdown-body h6 {
    color: var(--fg); margin: 0.7em 0 0.3em; line-height: 1.3;
}
.markdown-body h1 { font-size: 1.4em; }
.markdown-body h2 { font-size: 1.25em; }
.markdown-body h3 { font-size: 1.1em; }
.markdown-body p  { margin: 0.5em 0; }
.markdown-body ul, .markdown-body ol { margin: 0.5em 0; padding-inline-start: 1.5em; }
.markdown-body li { margin: 0.15em 0; }
/* Underlined, not colour alone: a link inside a block of text must be
   distinguishable without seeing the hue (WCAG 1.4.1). .prose does the same. */
.markdown-body a  { color: var(--brand-soft); text-decoration: underline; }
.markdown-body code {
    font-family: var(--font-mono); font-size: 0.9em;
    background: var(--bg-elevated); border: 1px solid var(--border);
    border-radius: var(--radius-inner); padding: 0.1em 0.35em;
}
.markdown-body pre {
    background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-surface);
    padding: var(--space-5) var(--space-6); overflow-x: auto; margin: 0.6em 0;
}
.markdown-body pre code { background: none; border: none; padding: 0; }
.markdown-body blockquote {
    border-inline-start: 3px solid var(--border);
    margin: 0.6em 0; padding: 0.2em 0 0.2em 0.8em;
    color: var(--muted);
}


}

/* ── 45-form-controls.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Form controls — select, radio, switch ──────────────────────────────────
   The rest of the family is in 34-forms.css. These three are here because each
   one resets the browser's native control and redraws it, which is most of the
   code in the family and none of the shape of it.

   This file must stay numbered ABOVE 34: `.form-select` and `.form-input` have
   equal specificity, so the caret's background-image survives .form-input's
   `background` shorthand on source order alone. Renumber this below 34 and the
   caret silently disappears.
   ─────────────────────────────────────────────────────────────────────────── */

/* Add beside .form-input — `class="form-input form-select"`. .form-input already
   styles a <select>; this replaces the platform caret with one that matches the
   theme.

   The caret is two triangular gradients meeting at a point. Not an SVG: this
   stylesheet loads and inlines nothing, so neither url() nor a data: URI is
   available. Not a pseudo-element either — ::before / ::after do not render on a
   <select> at all, in any browser. */
.form-select {
    appearance: none;
    /* Room for the caret plus its 12px inset, so a long option label stops
       before it rather than running underneath. */
    padding-inline-end: var(--space-10);
    background-image:
        linear-gradient(45deg,  transparent 50%, var(--muted) 50%),
        linear-gradient(135deg, var(--muted) 50%, transparent 50%);
    background-position:
        calc(100% - 17px) calc(50% - 2px),
        calc(100% - 12px) calc(50% - 2px);
    background-size: 5px 5px;
    background-repeat: no-repeat;
    cursor: pointer;
}

/* A list box is not a dropdown: there is nothing to drop, and the caret would
   sit over the first row. */
.form-select[multiple],
.form-select[size]:not([size="1"]) {
    background-image: none;
    padding-inline-end: var(--space-5);
    cursor: auto;
}

/* ── Radio ──────────────────────────────────────────────────────────────────
   Same .form-check label wrapper as the checkbox. Deliberately a brand DOT in an
   unfilled circle rather than a filled disc: shape and fill then both differ from
   the checkbox, so which one is exclusive is readable at a glance and without
   colour. */
.form-check input[type="radio"] {
    appearance: none;
    width: 15px;
    height: 15px;
    flex-shrink: 0;
    cursor: pointer;
    background: var(--bg);
    border: 1px solid var(--border-strong);
    border-radius: 50%;
    position: relative;
    transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
}
.form-check input[type="radio"]:hover { border-color: var(--border-hover); }
.form-check input[type="radio"]:checked { border-color: var(--brand); }
.form-check input[type="radio"]:checked::after {
    content: '';
    position: absolute;
    /* rtl-ok: centring, with translate doing the offset — direction-neutral. */
    left: 50%; top: 50%;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--brand);
    transform: translate(-50%, -50%);
}
.form-check input[type="radio"]:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--brand-ring-check);
}

/* ── Switch ─────────────────────────────────────────────────────────────────
   For a setting that takes effect immediately. A checkbox is for a value that is
   saved with the rest of the form — the difference is when it applies, not how it
   looks, and picking the wrong one misleads about whether Save is still needed.

   Still an <input type="checkbox">, so it is keyboard- and screen-reader-correct
   with no ARIA. */
.switch {
    display: inline-flex;
    align-items: center;
    gap: var(--space-5);
    font-size: var(--text-4);
    cursor: pointer;
    user-select: none;
}
.switch input[type="checkbox"] {
    appearance: none;
    position: relative;
    flex-shrink: 0;
    width: 34px;
    height: 19px;
    background: var(--border-strong);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: background var(--motion-fast), border-color var(--motion-fast), box-shadow var(--motion-fast);
}
.switch input[type="checkbox"]::after {
    content: '';
    position: absolute;
    /* Not on the spacing scale, deliberately: these two, the 34×19 track and the
       13px knob are one interlocking geometry. Rescaling the spacing ramp must not
       move the knob out of its track. */
    top: 2px;
    inset-inline-start: 2px;
    width: 13px;
    height: 13px;
    background: var(--on-solid);
    border-radius: 50%;
    transition: transform var(--motion-fast) ease;
}
.switch input[type="checkbox"]:hover { border-color: var(--border-hover); }
.switch input[type="checkbox"]:checked {
    background: var(--brand);
    border-color: var(--brand);
}
.switch input[type="checkbox"]:checked::after { transform: translateX(15px); }
.switch input[type="checkbox"]:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--brand-ring-check);
}
.switch input[type="checkbox"]:disabled { cursor: not-allowed; opacity: 0.45; }
.switch:has(input:disabled) { cursor: not-allowed; color: var(--muted); }

/* Room for a sentence explaining the consequence, under the label. */
.switch-text  { display: flex; flex-direction: column; gap: var(--space-1); }
.switch-hint  { font-size: var(--text-3); color: var(--muted); }
}

/* ── 46-form-layout.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Form layout and validation ──────────────────────────────────────────────
   How fields are grouped, affixed and closed off, and how a rejected value is
   shown. Numbered above 34 for the same reason as 45: these rules override
   .form-input's own border and ring at equal specificity.
   ─────────────────────────────────────────────────────────────────────────── */

/* ── Validation ─────────────────────────────────────────────────────────────
   Keyed off aria-invalid, not a class. The attribute is what a screen reader
   announces, so if the styling hung off a separate class the two could disagree,
   and the reader who most needs the message would be the one who does not get
   it. Set aria-invalid and the border follows. */
.form-input[aria-invalid="true"] { border-color: var(--danger-border-strong); }
.form-input[aria-invalid="true"]:focus {
    border-color: var(--danger-solid);
    box-shadow: 0 0 0 2px var(--danger-ring);
}

/* The message itself. Put it after the control and point at it with
   aria-describedby, so it is read as part of the field rather than found by
   accident. */
.form-error,
.form-warning {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    margin: 0;
    font-size: var(--text-3);
}
.form-error   { color: var(--danger-fg); }
.form-warning { color: var(--warn-fg); }
.form-error i,
.form-warning i { font-size: var(--text-5); flex-shrink: 0; line-height: 1.35; }

/* A required field's label. The marker is decoration — put `required` on the
   control, which is what is actually announced and enforced, and aria-hidden on
   any marker you write in the markup instead. */
.form-label--required::after {
    content: '*';
    margin-inline-start: var(--space-2);
    color: var(--danger-fg);
}

/* ── Input group ────────────────────────────────────────────────────────────
   One border around a control and its affixes: a leading icon, a trailing unit,
   a button that acts on the value.

   The affixes are real siblings rather than padding plus a background trick, so a
   button stays clickable and a unit stays selectable. The ring moves out to the
   group with :focus-within and the inner control gives up its own border and
   ring — otherwise the group draws two of each, nested. */
/* The GROUP owns the height, and its children give theirs up. The group draws the
   border, so a child still holding min-height: var(--control-height) makes the group
   that height PLUS its own two border pixels. */
.input-group {
    display: flex;
    align-items: stretch;
    width: 100%;
    min-height: var(--control-height);
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
    transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
}
/* The group carries the tier, because it is the group that draws the border. Its
   children have already given up their own height.

   A min-height is a floor, not a cap, so the small tier also has to bring its
   children's block padding and text down — otherwise the field's own 6px + 18.2px line
   box + the group's two borders comes to 32px and the 28px tier never binds. */
.input-group--sm { min-height: var(--control-height-sm); }
.input-group--sm .form-input,
.input-group--sm .btn,
.input-group--sm .input-affix { padding-block: var(--space-1); font-size: var(--text-3); }
.input-group--lg { min-height: var(--control-height-lg); }
.input-group--lg .form-input,
.input-group--lg .btn,
.input-group--lg .input-affix { font-size: var(--text-5); }
.input-group:focus-within {
    border-color: var(--brand-soft);
    box-shadow: 0 0 0 2px var(--brand-glow);
}
.input-group .form-input {
    flex: 1;
    min-width: 0;              /* or a long value pushes the affixes out */
    min-height: 0;             /* the group holds the height — see above */
    border: none;
    border-radius: 0;
    background: none;
}
.input-group .form-input:focus { box-shadow: none; }
/* --radius-inner, not --radius-control: the inner corner sits one border-width
   inside the outer one. */
.input-group > :first-child { border-start-start-radius: var(--radius-inner); border-end-start-radius: var(--radius-inner); }
.input-group > :last-child  { border-start-end-radius: var(--radius-inner);   border-end-end-radius: var(--radius-inner); }

/* A static affix — an icon, a unit, a currency symbol, a fixed prefix. */
.input-affix {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
    padding: 0 var(--space-5);
    color: var(--muted);
    font-size: var(--text-4);
    white-space: nowrap;
}
.input-affix i { font-size: var(--text-6); }
/* The rule goes between the affix and the control, so it is drawn on whichever
   side the control is on. */
.input-affix:first-child { border-inline-end: 1px solid var(--border); }
.input-affix:last-child  { border-inline-start: 1px solid var(--border); }

/* A .btn inside the group gives up its outer edge and keeps its ring inside, so
   focus on the button is still distinguishable from focus in the field. */
.input-group .btn {
    min-height: 0;             /* the group holds the height — see above */
    border: none;
    border-radius: 0;
    background: var(--surface-strong);
}
.input-group .btn:hover { background: var(--bg-hover); }
.input-group .btn:not(:first-child) { border-inline-start: 1px solid var(--border); }
.input-group .btn:not(:last-child)  { border-inline-end:   1px solid var(--border); }
.input-group .btn:focus-visible { box-shadow: inset 0 0 0 2px var(--brand-ring-soft); }

/* ── Form actions ───────────────────────────────────────────────────────────
   The row a form ends with. Right-aligned, because that is where the button that
   commits is looked for. Wraps instead of overflowing, so a three-button row on a
   phone stacks rather than pushing the primary action off-screen. */
.form-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-4);
    margin-top: var(--space-7);
}
/* Two groups: something destructive or tangential on the far side from the
   commit button, so they cannot be hit by accident. Expects exactly two
   children — wrap each side in a <div> if either has more than one button. */
.form-actions--split { justify-content: space-between; }
.form-actions--start { justify-content: flex-start; }

/* ── Form section ───────────────────────────────────────────────────────────
   A titled group of fields. Use <fieldset> + <legend>: the legend is then
   announced together with every control inside the group, which a heading above
   a <div> is not.

   The fieldset itself is left as a block and the fields go in .form-section-body.
   A <legend> is laid out specially and is not a flex or grid item, so making the
   fieldset the flex container puts the legend somewhere different in every
   engine. min-width: 0 undoes the fieldset's intrinsic minimum width, which
   otherwise refuses to shrink inside a flex or grid parent. */
.form-section {
    margin: 0 0 var(--space-8);
    padding: 0;
    border: none;
    min-width: 0;
}
.form-section > legend,
.form-section-title {
    width: 100%;
    padding: 0 0 var(--space-3);
    margin-bottom: var(--space-6);
    border-bottom: 1px solid var(--border);
    font-size: var(--text-4);
    font-weight: 600;
    color: var(--fg);
}
.form-section-hint {
    margin: -6px 0 var(--space-6);
    font-size: var(--text-3);
    color: var(--muted);
}
.form-section-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-7);
}
}

/* ── 47-form-file.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── File input, dropzone and file list ─────────────────────────────────────
   The native `<input type="file">` cannot be restyled: its button is a shadow-DOM
   part with only `::file-selector-button` exposed, and its "No file chosen" text is
   not addressable at all. So the input is hidden behind a label styled as a button,
   which is the one approach that keeps the real control — a <label> for a file input
   opens the picker on click and on Enter, with no JavaScript and no ARIA.

   `::file-selector-button` is styled too, for anyone who uses the input bare.
   ─────────────────────────────────────────────────────────────────────────── */
.form-file { display: inline-flex; align-items: center; gap: var(--space-5); }
/* Hidden, not display:none — the input must stay focusable and in the accessibility
   tree, or the label becomes a button that a keyboard cannot reach. */
.form-file input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    border: 0;
}
.form-file-name { color: var(--muted); font-size: var(--text-3); }
/* The focus ring has to be drawn on the label, because the focused element is the
   input the reader cannot see. */
.form-file:has(input:focus-visible) .btn { box-shadow: 0 0 0 2px var(--brand-ring-soft); }

/* Almost no block padding. A file input's content is the UA's own button, which is
   taller than a line of text, so `.form-input`'s padding on top of it pushes a bare
   file input past every other control in the row. The button supplies the height, and
   --control-height binds as it does everywhere else. */
.form-input[type="file"] { padding-block: var(--space-1); }

.form-input::file-selector-button {
    margin-inline-end: var(--space-5);
    padding: var(--space-2) var(--space-5);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-inner);
    color: var(--fg);
    font-family: inherit;
    font-size: var(--text-3);
    cursor: pointer;
}
.form-input::file-selector-button:hover { background: var(--bg-hover); }

/* ── Dropzone ───────────────────────────────────────────────────────────────
   A drop target that is ALSO a click target. Drag and drop alone is unusable by
   keyboard and awkward on touch, so the zone wraps a real file input and the drag
   handling is an enhancement on top.

   .dropzone--over says "something is being dragged over me", for which there is no
   CSS pseudo-class. Add `data-dropzone` and DR.Simple_UI.js maintains it, and hands
   a dropped file to the zone's own <input type="file"> as a `change` event; an app
   that would rather own the drag handling leaves the attribute off and sets the
   class itself. */
.dropzone {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-9);
    background: var(--surface-soft);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-surface);
    color: var(--muted);
    font-size: var(--text-4);
    text-align: center;
    cursor: pointer;
    transition: background var(--motion-fast), border-color var(--motion-fast);
}
.dropzone i { font-size: var(--text-10); color: var(--border-hover); }
.dropzone:hover { border-color: var(--border-hover); }
.dropzone--over {
    background: var(--brand-tint);
    border-color: var(--brand-soft);
    border-style: solid;
    color: var(--brand-text);
}
.dropzone--over i { color: var(--brand-soft); }
/* :focus-within rather than :has(input:focus-visible): the zone IS the label, and
   focus-within says exactly what is meant with less machinery. */
.dropzone:focus-within { box-shadow: 0 0 0 2px var(--brand-ring-soft); }

/* ── File list ──────────────────────────────────────────────────────────────
   What was attached, and how to remove it. A real <ul>, so the count is announced.
   The size is monospace so a column of them lines up. */
.file-list { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
.file-item {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    background: var(--surface-soft);
    border: 1px solid var(--border);
    border-radius: var(--radius-inner);
    font-size: var(--text-3);
}
.file-item > i { font-size: var(--text-7); color: var(--muted); flex-shrink: 0; }
.file-item-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-item-size { color: var(--muted); font-family: var(--font-mono); flex-shrink: 0; }
/* Failed upload or a rejected type. The icon carries it too, so it is not colour
   alone. */
.file-item--error { border-color: var(--danger-border); color: var(--danger-fg); }
.file-item--error > i { color: var(--danger-fg); }
}

/* ── 48-form-numeric.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Range, stepper and date/time controls ──────────────────────────────────
   The three native controls whose internals need naming per engine. Everything here
   is a restatement of the same design in different vendor pseudo-elements — the
   duplication is the platform's, not a choice.
   ─────────────────────────────────────────────────────────────────────────── */

/* ── Range ──────────────────────────────────────────────────────────────────
   `appearance: none` removes the whole native widget, track included, so the track
   and thumb are both drawn here. The two engines expose different pseudo-elements
   and CANNOT be combined into one selector list: an unknown pseudo-element
   invalidates the entire rule, so a shared `::-webkit-…, ::-moz-…` list would apply
   in neither browser. Hence two blocks that look copy-pasted.

   A range gives no readout. Put the value in text beside it — the thumb position is
   not a number, and it is unreadable to a screen reader on its own. */
.form-range {
    appearance: none;
    width: 100%;
    height: 18px;
    background: none;
    cursor: pointer;
}
.form-range::-webkit-slider-runnable-track {
    height: 4px;
    background: var(--progress-track);
    border-radius: var(--radius-pill);
}
.form-range::-webkit-slider-thumb {
    appearance: none;
    width: 14px;
    height: 14px;
    margin-top: -5px;              /* centres the thumb on the 4px track */
    background: var(--brand);
    border: none;
    border-radius: 50%;
}
.form-range::-moz-range-track {
    height: 4px;
    background: var(--progress-track);
    border-radius: var(--radius-pill);
}
.form-range::-moz-range-thumb {
    width: 14px;
    height: 14px;
    background: var(--brand);
    border: none;
    border-radius: 50%;
}
.form-range:focus-visible { outline: none; }
.form-range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--brand-ring-check); }
.form-range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px var(--brand-ring-check); }
.form-range:disabled { opacity: 0.45; cursor: not-allowed; }

/* The value readout beside a range. Tabular and fixed-width, so the track does not
   shift as the number's width changes while dragging. */
.form-range-value {
    min-width: 4ch;
    color: var(--fg);
    font-size: var(--text-4);
    font-variant-numeric: tabular-nums;
    text-align: end;
}

/* ── Stepper ────────────────────────────────────────────────────────────────
   A number input with its own buttons, for a value adjusted by one at a time. The
   native spinner is tiny, inconsistent between engines, and invisible until hover in
   some — so it is removed and replaced with two real buttons, which are also a
   usable size on touch.

   Give each button an aria-label: "+" and "−" are announced as "plus" and "minus"
   without saying what of. */
.stepper { display: inline-flex; align-items: stretch; }
.stepper .form-input {
    width: 5ch;
    text-align: center;
    border-radius: 0;
    font-variant-numeric: tabular-nums;
    /* Hides the native spinner. Firefox needs the property, WebKit needs the
       pseudo-element; neither implies the other. */
    appearance: textfield;
}
.stepper .form-input::-webkit-outer-spin-button,
.stepper .form-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.stepper .btn { border-radius: 0; }
.stepper > :first-child { border-start-start-radius: var(--radius-control); border-end-start-radius: var(--radius-control); }
.stepper > :last-child  { border-start-end-radius: var(--radius-control);   border-end-end-radius: var(--radius-control); }
/* One shared border between neighbours rather than two adjacent ones. */
.stepper > * + * { margin-inline-start: -1px; }
/* The focused control has to paint above its neighbour, or half its ring is
   covered. 1 is local stacking, not a rung on the documented scale. */
.stepper .form-input:focus,
.stepper .btn:focus-visible { position: relative; z-index: 1; }

/* ── Date and time ──────────────────────────────────────────────────────────
   `.form-input` already covers the box. What needs naming is the calendar picker
   icon, which is a UA glyph in its own colour and is invisible on a dark surface
   until it is inverted. `color-scheme` (set on <html> from --color-scheme) is what
   makes the picker panel itself dark; this is only the trigger glyph. */
.form-input::-webkit-calendar-picker-indicator {
    cursor: pointer;
    opacity: 0.55;
    /* Not a colour: a filter on a UA glyph we cannot recolour. It flips with the
       theme through the token below rather than being hard-coded to one direction. */
    filter: invert(var(--picker-invert));
}
.form-input::-webkit-calendar-picker-indicator:hover { opacity: 1; }
}

/* ── 50-tabs.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Tabs ───────────────────────────────────────────────────────────────────
   Switching which view of one thing is shown, where the tabs belong to the page.
   Not navigation: a tab that changes the URL is a link wearing a control's
   clothes, and the sidebar is where going somewhere belongs.

   Tabs vs .segmented: tabs swap a region of *content*, a segmented control changes
   a *setting*. If the thing below it is a panel, use tabs.

   The classes do not imply the ARIA and cannot. Write it:
     .tabs        role="tablist"
     .tab         role="tab" aria-selected id="…" aria-controls="panel-id"
     .tab-panel   role="tabpanel" id="panel-id" aria-labelledby="tab-id"
   Without it a screen reader hears a row of unrelated buttons, and arrow-key
   movement between tabs does not exist.
   ─────────────────────────────────────────────────────────────────────────── */
.tabs {
    display: flex;
    align-items: stretch;
    gap: var(--space-1);
    border-bottom: 1px solid var(--border);
    /* Overflow scrolls rather than wraps. A wrapped second row pushes the content
       down and the underline stops reading as one continuous strip. The scrollbar
       is hidden because the tabs themselves are the affordance. */
    overflow-x: auto;
    scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

.tab {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-7);
    background: none;
    border: none;
    /* The indicator is a transparent border that gains a colour, not a border that
       appears — otherwise the label jumps 2px when it is selected. */
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;              /* sit the indicator on the container's line */
    color: var(--muted);
    font-family: inherit;
    font-size: var(--text-4);
    font-weight: 500;
    white-space: nowrap;
    cursor: pointer;
    text-decoration: none;
    transition: color var(--motion-fast), border-color var(--motion-fast), background var(--motion-fast);
}
.tab i { font-size: var(--text-6); }
.tab:hover { color: var(--fg); background: var(--surface-soft); text-decoration: none; }
.tab:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.tab:disabled,
.tab[aria-disabled="true"] { opacity: 0.4; cursor: not-allowed; }

.tab--active,
.tab[aria-selected="true"] { color: var(--brand-text); border-bottom-color: var(--brand); }
.tab--active:hover,
.tab[aria-selected="true"]:hover { color: var(--brand-text); background: none; }

/* A count beside the label — how many rows the tab's panel holds. */
.tab-count {
    padding: 0 var(--space-3);
    background: var(--badge-bg);
    border-radius: var(--radius-pill);
    color: var(--fg-soft);
    font-size: var(--text-2);
    font-weight: 600;
}
.tab--active .tab-count,
.tab[aria-selected="true"] .tab-count { background: var(--brand-tint); color: var(--brand-text); }

.tab-panel { padding-top: var(--space-7); }
}

/* ── 51-segmented.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Segmented control ──────────────────────────────────────────────────────
   Two to four mutually exclusive options, all of them visible, applied the moment
   one is chosen. Prefer it over a <select> when the options are few and short
   enough to read at a glance — the cost of a select is one click before you learn
   what the choices are.

   Built from radios, so it is keyboard- and screen-reader-correct with no ARIA at
   all: arrow keys move between options, the group is announced as a group, and the
   chosen one is announced as chosen. Give every input the same `name`, and wrap the
   control in a <fieldset> or point at a label with aria-labelledby so the group has
   a name.

   The input is a transparent overlay rather than display:none, because display:none
   takes it out of the tab order and out of the accessibility tree — which would leave
   a control that looks operable and is not.

   The chosen state is `:has(input:checked)` on the label, so the whole label — which
   is also the whole hit area — is what gets painted, and the markup stays a label
   wrapping an input and its text.

   When measuring this state from a test, switch transitions off first: everything here
   has a `transition`, and getComputedStyle immediately after the change returns the
   value at t=0, which is the one being transitioned away from.
   ─────────────────────────────────────────────────────────────────────────── */
.segmented {
    display: inline-flex;
    padding: var(--space-1);
    background: var(--surface-strong);
    border: 1px solid var(--border);
    border-radius: var(--radius-control);
}

.segmented-option {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-6);
    border-radius: var(--radius-inner);
    color: var(--muted);
    font-size: var(--text-3);
    font-weight: 500;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    transition: background var(--motion-fast), color var(--motion-fast);
}
.segmented-option i { font-size: var(--text-5); }
.segmented-option input {
    position: absolute;
    inset: 0;
    margin: 0;
    appearance: none;
    opacity: 0;
    cursor: pointer;
}
/* The disabled option is excluded from the hover state, and the input overlay gives up
   its pointer cursor. The overlay is the element actually under the pointer, so its
   cursor wins over the label's `not-allowed` — without both rules a disabled option
   shows a hand and lights up on hover while refusing to be chosen. */
.segmented-option:hover:not(:has(input:disabled)) { color: var(--fg); }
.segmented-option input:disabled { cursor: not-allowed; }

/* Flat, not raised: a shadow inside a 2px track reads as a rendering artefact at
   this size. The elevated surface against the inset track is enough. */
.segmented-option:has(input:checked) { background: var(--bg-elevated); color: var(--fg); }
.segmented-option:has(input:focus-visible) { box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.segmented-option:has(input:disabled) { opacity: 0.45; cursor: not-allowed; }
}

/* ── 52-avatar.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Avatar ─────────────────────────────────────────────────────────────────
   A person or a system actor, in page content. The header's own avatar is
   `.user-avatar` and stays part of the frame; this is the one for tables, lists and
   cards.

   Takes an <img>, initials, or a Remix Icon — in that order of preference, and the
   icon is the honest fallback rather than a generated colour block, which invents a
   distinction that is not in the data.

   An avatar is never the only carrier of a name. Put the name in text beside it, or
   in an <img alt>, or in a .visually-hidden span: initials are not a name, and a
   hover title is not reachable by keyboard or touch.
   ─────────────────────────────────────────────────────────────────────────── */
.avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    /* One source for the diameter, because the group's overlap and its cut-out are
       both derived from it. A size variant sets these two numbers and the geometry
       below follows.

       --avatar-cut-x is the centre of the hole the group masks out: the next
       avatar's centre, measured from this one's own box. Past the right edge in
       LTR; 70-rtl.css mirrors it, because a radial-gradient position is physical
       and margin-inline-start is not. */
    --avatar-size: 28px;
    --avatar-overlap: 8px;
    --avatar-gap: 2px;
    --avatar-cut-x: calc(100% + var(--avatar-size) / 2 - var(--avatar-overlap));
    width: var(--avatar-size);
    height: var(--avatar-size);
    border-radius: 50%;
    overflow: hidden;
    background: var(--brand-tint);
    color: var(--brand-text);
    font-size: var(--text-3);
    font-weight: 600;
    line-height: 1;
    text-transform: uppercase;
    user-select: none;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar i { font-size: var(--text-6); }

.avatar-sm { --avatar-size: 22px; --avatar-overlap: 6px; font-size: var(--text-1); }
.avatar-sm i { font-size: var(--text-4); }
.avatar-lg { --avatar-size: 40px; --avatar-overlap: 11px; font-size: var(--text-6); }
.avatar-lg i { font-size: var(--text-9); }

/* Neutral, for an actor with no brand meaning — a system account, an unknown
   sender. Keeps the brand tint for people, so a page of rows does not read as
   uniformly branded. */
.avatar--muted { background: var(--surface-strong); color: var(--muted); }

/* ── Avatar group ───────────────────────────────────────────────────────────
   Overlapping avatars for "these several people".

   The separating gap is a hole cut out of the avatar BEHIND, not a ring painted on the
   one in front. A ring has to be drawn in the colour of whatever is underneath, and a
   shared component cannot know what that is — `box-shadow: 0 0 0 2px var(--card-bg)`
   is right inside a card and a grey halo on the page background, in a toast and over a
   selected table row. Masking takes colour out of the problem, so the group is correct
   on every surface and in every theme.

   Paint order is plain source order: each avatar overlaps the one before it, the LAST
   one sits on top, and the counter goes last and unoverlapped. Do not reverse it with
   a negative z-index step or with `flex-direction: row-reverse` — the second fixes the
   painting and breaks the reading order. */
/* The default is declared on the group as well as on .avatar, so the counter — which
   is not an .avatar and inherits from here — resolves it too. */
.avatar-group { display: inline-flex; align-items: center; --avatar-overlap: 8px; }
.avatar-group > * + * { margin-inline-start: calc(var(--avatar-overlap) * -1); }

/* :has(+ .avatar), not :not(:last-child) — the last thing in a group is usually the
   .avatar-more counter, which overlaps nothing, so the avatar before it must not be
   cut either. Only an avatar with an avatar after it is. */
.avatar-group > .avatar:has(+ .avatar) {
    mask-image: radial-gradient(
        circle calc(var(--avatar-size) / 2 + var(--avatar-gap))
        at var(--avatar-cut-x) 50%,
        transparent 100%, currentColor 100%);
}

/* The overflow counter. Not an .avatar: it is a number, not an actor, and giving it
   the brand tint would make it look like one more person. */
.avatar-more {
    display: inline-flex;
    align-items: center;
    padding: 0 var(--space-4);
    margin-inline-start: var(--space-2);
    color: var(--muted);
    font-size: var(--text-3);
}
}

/* ── 53-chip.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Chip ───────────────────────────────────────────────────────────────────
   A chip is a *thing*: an applied filter, a selected recipient, a label attached to
   a record. A .badge is a *state*: read-only, describing what something is.

   The test is whether it can be removed. If the user put it there and can take it
   away, it is a chip. If it reflects data, it is a badge. Getting this the wrong way
   round produces a page of pills where nobody can tell which ones are clickable.

   `.chip` rather than `.tag`: a plain name is a claim on a shared namespace, and
   `.tag` is commonly already taken in a consuming app.
   ─────────────────────────────────────────────────────────────────────────── */
/* A chip is interactive, so it takes the control heights rather than a type scale.
   The default is the SMALL tier: a chip's home is a filter bar beside a `.btn-sm`,
   and that is the pairing it has to line up with. `.chip-lg` is the normal tier, for
   a chip beside a full-size button. */
.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    max-width: 100%;
    min-height: var(--control-height-sm);
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-5);
    background: var(--surface-strong);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    color: var(--fg-soft);
    font-size: var(--text-3);
    line-height: 1.6;
}
.chip-lg { min-height: var(--control-height); font-size: var(--text-4); padding-inline-start: var(--space-6); }
.chip-lg .chip-dismiss { width: 22px; height: 22px; }
/* A chip with no dismiss button does not need the trailing room for one. */
.chip:not(:has(.chip-dismiss)) { padding-inline-end: var(--space-5); }
.chip i { font-size: var(--text-5); color: var(--muted); }
.chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A status dot inside a chip, for a filter whose value carries a state. Colour it
   with a semantic token from the markup, or it says nothing. */
.chip-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--border-hover);
}

/* The remove button. A real <button> with an accessible name — "×" alone is
   announced as "times" and gives no clue what it removes, so write
   aria-label="Remove <the value>". */
.chip-dismiss {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 18px;
    height: 18px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 50%;
    color: var(--muted);
    font-size: var(--text-5);
    line-height: 1;
    cursor: pointer;
}
.chip-dismiss:hover { background: var(--bg-hover); color: var(--fg); }
.chip-dismiss:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }

/* Chosen — a filter that is on, rather than one offered. */
.chip--active {
    background: var(--brand-tint);
    border-color: var(--brand-soft);
    color: var(--brand-text);
}
.chip--active i,
.chip--active .chip-dismiss { color: var(--brand-text); }

/* A whole row of them, wrapping. */
.chip-set { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
}

/* ── 54-stat.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Stat tile ──────────────────────────────────────────────────────────────
   One number that matters, with what it is and — optionally — which way it moved.

   The label goes ABOVE the value. A number read before its label is a number the
   reader has to hold in their head while they find out what it means, and on a row
   of four tiles they do that four times.

   The value is tabular-nums: a row of tiles whose digits are different widths
   reflows as the numbers update, and on a polling dashboard that is a page that
   never sits still.
   ─────────────────────────────────────────────────────────────────────────── */
.stat { display: flex; flex-direction: column; gap: var(--space-1); }
.stat-label {
    font-size: var(--text-2);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}
.stat-value {
    font-size: var(--text-9);
    font-weight: 600;
    line-height: 1.2;
    color: var(--fg);
    font-variant-numeric: tabular-nums;
}
/* A unit or denominator that should not compete with the number. */
.stat-unit { font-size: var(--text-4); font-weight: 500; color: var(--muted); }
.stat-note { font-size: var(--text-3); color: var(--muted); }

/* ── Delta ──────────────────────────────────────────────────────────────────
   Which way it moved. Colour alone does not say — put a Remix Icon arrow inside
   (ri-arrow-up-line / ri-arrow-down-line), so the direction survives a
   colour-blind palette, a greyscale print and a forced-colors mode.

   --up and --down are direction, not judgement: more open incidents is up and bad,
   more resolved is up and good. Choose the semantic modifier by whether the reader
   should be pleased, and let the arrow carry the direction. */
.stat-delta {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-3);
    font-weight: 500;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
.stat-delta i { font-size: var(--text-5); }
.stat-delta--good { color: var(--go-fg); }
.stat-delta--bad  { color: var(--danger-fg); }
.stat-delta--warn { color: var(--warn-fg); }

/* ── Against a target ───────────────────────────────────────────────────────
   A number is rarely interesting on its own. `.stat-target` is the line that says what
   it is being measured against — "62 of 80", "of a 120 cap" — and `.stat-bar` is the
   optional filled track under it.

   The bar is a `.progress` with no extra rules: a stat against a target IS progress
   towards it, and a second track class would drift from the first. */
.stat-target { font-size: var(--text-3); color: var(--muted); font-variant-numeric: tabular-nums; }

/* ── A stat that opens something ────────────────────────────────────────────
   For a tile that leads to the thing it counts. An <a>, so it is a real link. The
   whole tile is the target, which is why the padding and the hover are on it rather
   than on the value. */
a.stat {
    padding: var(--space-4) var(--space-5);
    margin: calc(var(--space-4) * -1) calc(var(--space-5) * -1);
    border-radius: var(--radius-surface);
    color: inherit;
    text-decoration: none;
    transition: background var(--motion-fast);
}
a.stat:hover { background: var(--bg-hover); text-decoration: none; }
a.stat:hover .stat-value { color: var(--brand-text); }
a.stat:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }

/* ── Unavailable ────────────────────────────────────────────────────────────
   The number could not be read. An em dash, not a zero: zero is a measurement and
   this is the absence of one, and a dashboard that shows 0 for "the endpoint did not
   answer" is a dashboard that lies. */
.stat--unavailable .stat-value { color: var(--border-strong); font-weight: 500; }
.stat--unavailable .stat-delta,
.stat--unavailable .stat-target { visibility: hidden; }

/* A row of tiles. Breakpoint-free, like the other grids: it responds to the space
   it is given, so it behaves correctly inside a narrow column too. */
.stat-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr));
    gap: var(--space-7);
}
/* Dividers between tiles outside a card too, for a row that sits straight on the
   page and still has to read as one object. */
.stat-row--divided { gap: 0; }
.stat-row--divided > .stat { padding-inline: var(--space-7); border-inline-start: 1px solid var(--divider); }
.stat-row--divided > .stat:first-child { padding-inline-start: 0; border-inline-start: none; }
/* Tiles inside a card get a dividing rule between them rather than a gap, so the
   card reads as one object. */
.card-body > .stat-row { gap: 0; }
.card-body > .stat-row > .stat { padding: var(--space-2) var(--space-7); border-inline-start: 1px solid var(--divider); }
.card-body > .stat-row > .stat:first-child { padding-inline-start: 0; border-inline-start: none; }
}

/* ── 55-page-structure.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Page head ──────────────────────────────────────────────────────────────
   The title row a page opens with: heading, optional supporting line, and the
   actions that apply to the whole page.

   Wraps rather than shrinking the title. On a narrow screen the actions drop below
   the heading, which keeps the heading readable — the alternative is a truncated
   title next to two buttons, and the title is what tells you where you are.
   ─────────────────────────────────────────────────────────────────────────── */
.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-6);
    margin-bottom: var(--space-7);
}
.page-head-text { min-width: 0; }
.page-head-text h1 { margin: 0; }
.page-head-text > p { margin: var(--space-2) 0 0; color: var(--muted); font-size: var(--text-4); }
.page-head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-4); }

/* ── Divider ────────────────────────────────────────────────────────────────
   A rule between sections. Use it when two blocks are related but distinct; use
   whitespace when they are simply sequential, and a .card when one of them is an
   object in its own right.

   With a label it becomes a section break: the text sits in a gap in the rule,
   made with a flex row rather than a background trick, so it works on any surface
   and needs no colour to match. */
.divider { border: none; border-top: 1px solid var(--border); margin: var(--space-8) 0; }

.divider-labelled {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    margin: var(--space-8) 0;
    color: var(--muted);
    font-size: var(--text-2);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.divider-labelled::before,
.divider-labelled::after {
    content: '';
    flex: 1;
    border-top: 1px solid var(--border);
}
/* Label at the start instead of centred, for a run of sections where a centred
   label makes the page look like a series of title cards. */
.divider-labelled--start::before { flex: 0 0 0; }

/* ── Callout ────────────────────────────────────────────────────────────────
   An editorial aside inside prose: a note, a caveat, a worked example. It is part
   of the content and stays there for good.

   Three neighbours, and picking the wrong one misleads:
     .callout       permanent, about the content     — "Note: times are UTC."
     .alert         about the page right now         — "Could not reach the API."
     .card-warning  about one card's own data        — "These counts may lag."
   An .alert that never goes away trains the reader to ignore alerts. */
.callout {
    display: flex;
    gap: var(--space-5);
    padding: var(--space-6) var(--space-7);
    margin: var(--space-7) 0;
    background: var(--surface-soft);
    border-inline-start: 3px solid var(--border-strong);
    border-radius: var(--radius-inner);
    color: var(--fg-soft);
    font-size: var(--text-4);
}
.callout > i { font-size: var(--text-8); flex-shrink: 0; color: var(--muted); line-height: 1.4; }
.callout-body { min-width: 0; }
.callout-body > :first-child { margin-top: 0; }
.callout-body > :last-child { margin-bottom: 0; }
.callout-title { display: block; font-weight: 600; color: var(--fg); margin-bottom: var(--space-1); }

/* The same semantic families as buttons, badges and alerts. Tinted more lightly
   than an .alert: a callout is read as part of the prose, not scanned as a state. */
.callout-info   { background: var(--info-bg);   border-inline-start-color: var(--info-border); }
.callout-info > i   { color: var(--info-fg); }
.callout-warn   { background: var(--warn-bg);   border-inline-start-color: var(--warn-solid); }
.callout-warn > i   { color: var(--warn-fg); }
.callout-danger { background: var(--danger-bg); border-inline-start-color: var(--danger-solid); }
.callout-danger > i { color: var(--danger-fg); }
.callout-go     { background: var(--go-bg);     border-inline-start-color: var(--go-solid); }
.callout-go > i     { color: var(--go-fg); }
}

/* ── 56-code-block.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Code block ─────────────────────────────────────────────────────────────
   A block of code or output with a copy button. For a command, a payload, a stack
   trace — anything the reader is going to want verbatim.

   No syntax highlighting, and there will not be any: it needs a tokeniser per
   language, which is either a runtime download or a large bundle, and neither is
   allowed here. Monospace with real contrast reads well enough for the payloads and
   commands this is for.

   The block scrolls horizontally rather than wrapping. A wrapped command line is a
   command that no longer copies correctly by eye, and a wrapped stack trace loses
   the frame boundaries.
   ─────────────────────────────────────────────────────────────────────────── */
.code-block {
    position: relative;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    overflow: hidden;
}

/* An optional header naming the file or the language. */
.code-block-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-6);
    background: var(--surface-soft);
    border-bottom: 1px solid var(--border);
    color: var(--muted);
    font-size: var(--text-2);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.code-block pre {
    margin: 0;
    padding: var(--space-6) var(--space-7);
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: var(--text-3);
    line-height: 1.55;
    color: var(--fg-soft);
    /* tab-size 4, because a stack trace or a YAML fragment pasted in with tabs
       otherwise indents by eight and wraps sooner than it has to. */
    tab-size: 4;
}
/* The inline `code` rule gives every <code> a tint and a padding; inside a block
   that would draw a box around the whole thing. */
.code-block pre code {
    background: none;
    border: none;
    padding: 0;
    color: inherit;
    font-size: inherit;
}

/* Floats over the top-right corner. Only visible on hover or keyboard focus at a
   pointer-capable size — a button permanently sitting over the first line is in the
   way of the thing it copies. It stays visible where hover does not exist. */
.code-block-copy {
    position: absolute;
    top: 8px;
    inset-inline-end: var(--space-4);
    z-index: 1;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-inner);
    color: var(--muted);
    font-family: inherit;
    font-size: var(--text-2);
    cursor: pointer;
    opacity: 0;
    transition: opacity var(--motion-fast), color var(--motion-fast), background var(--motion-fast);
}
.code-block:hover .code-block-copy,
.code-block-copy:focus-visible { opacity: 1; }
.code-block-copy:hover { background: var(--bg-hover); color: var(--fg); }
.code-block-copy:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
/* Touch and other hover-less inputs: there is no hover to reveal it. */
@media (hover: none) {
    .code-block-copy { opacity: 1; }
}
/* With a head, the button sits in the head row instead of over the code. */
.code-block-head .code-block-copy { position: static; opacity: 1; }

/* ── Clamped ────────────────────────────────────────────────────────────────
   A long block bounded to a readable height, scrolling inside, with a fading lip and
   a control that opens it in full. Forty lines of markup between two paragraphs is a
   page nobody scrolls past.

   `--clamped` caps it; the expand control removes the class. The height is a token so
   an app can raise it, and the fade is a gradient over the lip rather than a shorter
   box, so the reader can see there is more without a scrollbar being the only clue.

   The pane keeps `tabindex="0"` in both states: a scroll container that a keyboard
   cannot reach is a part of the page a keyboard cannot read. */
.code-block--clamped pre {
    max-height: var(--code-clamp);
    overflow-y: auto;
}
/* The lip. `sticky` inside the scroller, so it sits over the last visible line at
   whatever scroll position rather than at the bottom of the content. */
.code-block--clamped .code-block-lip {
    position: sticky;
    bottom: 0;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    height: 68px;
    margin-top: -68px;
    padding-bottom: var(--space-4);
    background: linear-gradient(transparent, var(--bg) 70%);
    pointer-events: none;
}
/* Not clamped: no fade, and the control still has to be clickable. */
.code-block-lip {
    display: flex;
    justify-content: center;
    padding: var(--space-3) 0 var(--space-4);
}
.code-block-expand {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--control-height-sm);
    padding: var(--space-2) var(--space-6);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    color: var(--fg-soft);
    font-family: inherit;
    font-size: var(--text-2);
    cursor: pointer;
    transition: background var(--motion-fast), color var(--motion-fast);
}
.code-block-expand:hover { background: var(--bg-hover); color: var(--fg); }
.code-block-expand:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.code-block-expand i { font-size: var(--text-4); }
}

/* ── 57-navigation-aids.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Pagination ─────────────────────────────────────────────────────────────
   Page-by-page movement through a long result set.

   Use <a href> for every page that exists, not <button>. A page of results is an
   address: it should be linkable, openable in a new tab, and reachable by the back
   button. Only the disabled ends are <button disabled> or plain spans.

   Mark the list up as <nav aria-label="Pagination"> with the current page carrying
   aria-current="page". The class colours the current page; aria-current is what
   announces it.
   ─────────────────────────────────────────────────────────────────────────── */
.pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}
.page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-width: 32px;
    min-height: 32px;
    padding: 0 var(--space-4);
    background: none;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    color: var(--fg-soft);
    font-family: inherit;
    font-size: var(--text-4);
    font-variant-numeric: tabular-nums;
    text-decoration: none;
    cursor: pointer;
}
.page-link:hover { background: var(--bg-hover); border-color: var(--border); text-decoration: none; }
.page-link:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.page-link[aria-current="page"] {
    background: var(--brand-tint);
    border-color: var(--brand-soft);
    color: var(--brand-text);
    font-weight: 600;
}
.page-link:disabled,
.page-link[aria-disabled="true"] {
    opacity: 0.35;
    cursor: not-allowed;
    pointer-events: none;
}
/* The gap standing in for skipped pages. Not a link, and not focusable. */
.page-gap {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    color: var(--muted);
    user-select: none;
}
/* "121–140 of 1,204" beside the controls. */
.page-count { margin-inline-start: auto; color: var(--muted); font-size: var(--text-3); }

/* ── Breadcrumb ─────────────────────────────────────────────────────────────
   Where this page sits, when the sidebar cannot show it — a record three levels
   inside a section. If the sidebar already highlights the page, a breadcrumb repeats
   it and earns nothing.

   The separator is a pseudo-element on the item, so it is never selected when the
   trail is copied and is never read out. The last item is the current page: mark it
   aria-current="page" and leave it as a <span>, not a link to where you already are.

   Long trails scroll rather than wrap, and the leading edge is what gets cut,
   because the levels nearest the current page are the useful ones. */
.breadcrumb {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-6);
    overflow-x: auto;
    scrollbar-width: none;
    font-size: var(--text-3);
    white-space: nowrap;
}
.breadcrumb::-webkit-scrollbar { display: none; }
.breadcrumb-item {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    flex-shrink: 0;
    color: var(--muted);
    text-decoration: none;
}
.breadcrumb-item i { font-size: var(--text-5); }
/* Colour, not an underline. `text-decoration` propagates to every descendant and
   cannot be cancelled by one, so an underline here also underlines the item's icon and
   the `/` separator after it. */
a.breadcrumb-item:hover { color: var(--fg); text-decoration: none; }
.breadcrumb-item + .breadcrumb-item::before {
    content: '/';
    color: var(--border-strong);
    /* Not a font glyph choice: a slash is one character wide in any typeface, so
       the trail does not reflow when the font falls back. */
}
.breadcrumb-item[aria-current="page"] { color: var(--fg); }
}

/* ── 58-menu.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Menu / dropdown panel ──────────────────────────────────────────────────
   A short list of actions, opened from a control. This is the one dropdown panel
   in the library: the user widget's menu is this class, anchored differently (see
   15-frame-user-widget.css). Two near-identical panel styles is how a design
   system starts to rot, so there is only one.

   A menu is for *actions*. A list of values to pick one of is a <select> or a
   .segmented; a list of places to go is the sidebar.

   Deliberately NOT role="menu". That role promises arrow-key navigation and a
   roving tabindex, and a menu that claims it without implementing it is worse for
   a screen-reader user than one that claims nothing: the items stop being reachable
   the way they appear to be. These are ordinary links and buttons that tab. Put
   aria-expanded on the trigger and leave the panel unlabelled by role.

   Dismissal is the app's: click the scrim, press Escape. There is deliberately no
   JavaScript behind this — the frame must work with scripting blocked.
   ─────────────────────────────────────────────────────────────────────────── */
.menu {
    min-width: 220px;
    padding: var(--space-2);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    box-shadow: var(--shadow-dropdown);
}

/* Wrap trigger + panel in this to anchor the panel under the trigger. It is what
   makes the panel's `position: absolute` resolve against the trigger rather than
   against the page, so nothing has to be measured.

   Deliberately NOT CSS anchor positioning, which the floor does have and which
   `.popover` uses. Anchor positioning earns its keep where the alternative is a
   measurement in script; here the alternative is one `position: relative`. */
/* align-self: start alongside the inline display, because the display alone does not
   hold. A flex or grid item is blockified and then stretched by the default
   `align-items: stretch`, so an `inline-flex` anchor inside a flex column becomes
   full-width — and the panel, which hangs off the anchor's inline-END edge, detaches
   from its trigger and appears at the far side of the container. An app that genuinely
   wants a full-width anchor sets `align-self: stretch` back. */
.menu-anchor { position: relative; display: inline-flex; align-self: start; }
.menu-anchor > .menu {
    position: absolute;
    top: calc(100% + 4px);
    inset-inline-end: 0;
    z-index: 550;
}

/* The panel hangs off the anchor's inline-END edge, so a trigger narrower than the
   panel's 220px overhangs on the leading side. That is right for a trigger at the
   trailing end of a row — a table row's actions, a toolbar's overflow — and wrong for
   one near the leading edge, where the overhang leaves the container. `--start` aligns
   the panel to the anchor's inline-START edge instead. Put it on the panel, so the
   choice sits with the panel that moves. */
.menu-anchor > .menu--start { inset-inline-start: 0; inset-inline-end: auto; }

/* ── Value trigger ──────────────────────────────────────────────────────────
   A control that shows the CURRENT value of something and opens a menu to change it —
   a status, an assignee, a queue, a time range. It sits inside a line of text or a
   table cell, so it is quieter than a button: no border and no fill until hovered.

   Use it where a <select> would be wrong because the options need icons, colour or a
   description. Where a plain list of values would do, use a <select>: it is one
   element and the platform gives you the keyboard.

   The chevron is drawn by CSS, so the markup is the value and nothing else. Pair it
   with data-menu-toggle and a sibling .menu. */
.value-trigger {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    max-width: 100%;
    padding: var(--space-1) var(--space-3);
    margin-inline: calc(var(--space-3) * -1);   /* so the text still lines up with its column */
    background: none;
    border: 1px solid transparent;
    border-radius: var(--radius-inner);
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
}
.value-trigger::after {
    content: '';
    flex-shrink: 0;
    width: 0;
    height: 0;
    border-inline: 4px solid transparent;
    border-top: 5px solid currentColor;
    opacity: 0.5;
}
.value-trigger:hover { background: var(--bg-hover); border-color: var(--border); }
.value-trigger:hover::after { opacity: 1; }
.value-trigger:focus-visible { outline: none; border-color: var(--brand-soft); box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.value-trigger[aria-expanded="true"] { background: var(--bg-hover); border-color: var(--border); }
.value-trigger[aria-expanded="true"]::after { transform: rotate(180deg); }
.value-trigger[aria-disabled="true"] { color: var(--muted); pointer-events: none; }
.value-trigger[aria-disabled="true"]::after { display: none; }
/* The value itself, so a long one truncates instead of pushing the chevron out. */
.value-trigger > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Works on an <a> or a <button>, so an item can navigate or act. */
.menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    width: 100%;
    padding: var(--space-4) var(--space-5);
    background: none;
    border: none;
    border-radius: var(--radius-inner);
    color: var(--fg);
    font-family: inherit;
    font-size: var(--text-4);
    text-align: start;
    text-decoration: none;
    cursor: pointer;
}
.menu-item i { font-size: var(--text-7); color: var(--muted); }
.menu-item:hover { background: var(--bg-hover); text-decoration: none; }
.menu-item:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.menu-item:disabled,
.menu-item[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
.menu-item--danger,
.menu-item--danger i { color: var(--danger-fg); }
/* A trailing shortcut hint or count. */
.menu-item-note { margin-inline-start: auto; color: var(--muted); font-size: var(--text-2); }

.menu-label {
    display: block;
    padding: var(--space-4) var(--space-5) var(--space-2);
    font-size: var(--text-1);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--muted);
}
.menu-sep { border: none; border-top: 1px solid var(--divider); margin: var(--space-2) var(--space-1); }

/* Catches the click that dismisses the panel. Transparent and undecorated: this is
   a menu, not a modal, so it must not dim the page. Put it immediately BEFORE the
   panel in the markup — the panel then paints above it on source order, and the
   scrim needs no z-index of its own. */
.menu-scrim { position: fixed; inset: 0; cursor: default; }
}

/* ── 59-table-extensions.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Table extensions ───────────────────────────────────────────────────────
   Everything beyond "one class on the table". Each is opt-in, because each costs
   something: a sticky header needs a scroll container, zebra stripes fight with row
   selection, and the stacked layout throws away column alignment.
   ─────────────────────────────────────────────────────────────────────────── */

/* ── Sticky header ──────────────────────────────────────────────────────────
   The header stays put while the body scrolls. Two things this needs from the
   markup, and it silently does nothing without them:
     1. A scroll container around the table — `.dr-scroll` with a max-height. The
        header sticks to the nearest scrolling ancestor, and with none it sticks to
        the page, where the topbar is already.
     2. `border-collapse: separate` — a collapsed table's borders belong to the
        table, not the cell, so the header's bottom border scrolls away and leaves
        the rows running into the header text. That is why the border is drawn with
        a box-shadow here rather than a border.
   z-index 1 is local stacking inside the table, not a rung on the documented scale. */
.table--sticky { border-collapse: separate; border-spacing: 0; }
.table--sticky th {
    position: sticky;
    top: 0;
    z-index: 1;
    /* Opaque, not the translucent --table-head-bg: rows scrolling underneath would
       show through it. */
    background: var(--bg-elevated);
    box-shadow: inset 0 -1px 0 var(--border);
}
.table--sticky td { border-bottom: 1px solid var(--border); }

/* ── Zebra ──────────────────────────────────────────────────────────────────
   For wide tables where the eye loses the row between the first and last column.
   Avoid combining it with row selection: two faint background states are hard to
   tell apart, and the reader cannot see which row is chosen.

   The stripe is (0,2,3) — `:nth-child` counts as a class — which is higher than the
   hover highlight in 32-tables.css at (0,2,1). Without the second rule below,
   striped rows would silently stop responding to hover, which reads as a broken
   table rather than as a styling choice. */
.table--zebra tbody tr:nth-child(even) > td { background: var(--surface-soft); }
.table--zebra .tr-clickable:hover > td { background: var(--bg-hover); }

/* ── Sortable header ────────────────────────────────────────────────────────
   The whole header cell is the control, so put a <button> inside it filling the
   cell — a click target that is only the label text is a smaller target than it
   looks. The arrow is keyed off aria-sort, which is what a screen reader announces,
   so the two cannot disagree. */
.table th[aria-sort] { padding: 0; }
.th-sort {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
    padding: var(--cell-pad-y) var(--cell-pad-x);
    background: none;
    border: none;
    color: inherit;
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    text-align: start;
    cursor: pointer;
}
.th-sort:hover { color: var(--fg); background: var(--surface-strong); }
.th-sort:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--brand-ring-soft); }
/* The caret is drawn with borders rather than taken from the icon font. Two
   reasons: `font-family` in this library always rides a token and a test enforces
   it, and pinning a Remix Icon codepoint here would silently point at a different
   glyph the next time the font is re-vendored.

   It reserves its 8px even when unsorted, so the header does not shift sideways as
   the reader sorts by one column after another. */
.th-sort::after {
    content: '';
    flex-shrink: 0;
    width: 0;
    height: 0;
    border-inline: 4px solid transparent;
    opacity: 0;
}
.table th[aria-sort="none"] .th-sort:hover::after { border-top: 5px solid currentColor; opacity: 0.4; }
.table th[aria-sort="ascending"] .th-sort::after { border-bottom: 5px solid currentColor; opacity: 1; }
.table th[aria-sort="descending"] .th-sort::after { border-top: 5px solid currentColor; opacity: 1; }
.table th[aria-sort="ascending"],
.table th[aria-sort="descending"] { color: var(--brand-text); }

/* ── Numeric columns ────────────────────────────────────────────────────────
   Right-aligned and tabular, so digits line up by place value down the column and
   two numbers can be compared by their shape. Put it on the th AND the td.

   Scoped to `.table .col-num`, not bare `.col-num`. A single-purpose class is (0,1,0)
   and `.table th, .table td` is (0,1,1), so the bare form loses the text-align it
   exists to set, silently. A class in `@layer dr.utilities` wins that collision on
   layer order instead; this file is in dr.paint, so it needs the specificity. */
.table .col-num { text-align: end; font-variant-numeric: tabular-nums; }

/* ── Row selection ──────────────────────────────────────────────────────────
   aria-selected on the <tr>, not a class, so the state the reader is told matches
   the state that is drawn. The leading rule is what survives a colour-blind palette
   and a greyscale print.

   `tbody` is in the selector only to reach (0,2,3) and tie with the zebra stripe
   above, which then loses on source order. Without it selection is (0,2,2) and an
   even striped row would show the stripe instead of the selection — the more
   important of the two signals, silently dropped. */
.table tbody tr[aria-selected="true"] > td {
    background: var(--brand-tint);
    box-shadow: inset 3px 0 0 var(--brand);
}

/* ── Expandable and nested rows ─────────────────────────────────────────────
   The detail row is a real <tr> after its parent, so it moves with it when the
   table is sorted and is read after it. Its cell spans every column.

   The inset rule on the left is the only thing saying it belongs to the row above;
   a plain indent reads as an unrelated row that happens to start further in. */
.tr-detail > td {
    padding: 0;
    background: var(--surface-soft);
    box-shadow: inset 3px 0 0 var(--border-strong);
}
.tr-detail-body { padding: var(--space-6) var(--space-7); }

/* A grouped header spanning several columns. */
.table th[colspan] { text-align: center; border-bottom-color: var(--border-strong); }

/* ── Footer totals ──────────────────────────────────────────────────────────
   In a real <tfoot>, so it is announced as a footer and prints on every page. */
.table tfoot td,
.table tfoot th {
    background: var(--surface-soft);
    border-top: 1px solid var(--border-strong);
    border-bottom: none;
    font-weight: 600;
    color: var(--fg);
}

/* ── Stacked layout ────────────────────────────────────────────────────────
   Below 640px each row becomes a card and each cell a label/value pair, taking its
   label from `data-label`. Opt-in, and worth understanding before switching on:
   this throws away column alignment, so comparing a value across rows stops being
   possible. It is right for a list of records to read one at a time, and wrong for
   anything the reader scans down a column of.

   The header is hidden rather than removed, because the cells still reference it
   for screen readers.

   Only geometry and a token-valued surface here — a layout query decides how much
   room there is, never what something looks like. */
@media (max-width: 640px) {
    .table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
    .table--stack tr { display: block; margin-bottom: var(--space-5); border: 1px solid var(--border); border-radius: var(--radius-surface); overflow: hidden; }
    .table--stack td { display: flex; justify-content: space-between; gap: var(--space-6); border-bottom: 1px solid var(--divider); }
    .table--stack tr > td:last-child { border-bottom: none; }
    .table--stack td::before {
        content: attr(data-label);
        flex-shrink: 0;
        color: var(--muted);
        font-size: var(--text-2);
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.06em;
    }
    /* A cell with no data-label — an actions cell — keeps the full width instead of
       leaving an empty gutter where the label would be. */
    .table--stack td:not([data-label])::before { content: none; }
    .table--stack .col-num { text-align: end; }
}
}

/* ── 60-lists.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── List ───────────────────────────────────────────────────────────────────
   Rows of records where a table would be wrong: when the fields differ per row, when
   there is one dominant field and the rest is supporting, or when each row is
   something you open rather than compare.

   A real <ul>/<li>, so the count is announced and the rows are navigable as a list.
   A row that navigates is an <a> filling the <li>, not a click handler on the <li>:
   the whole row is then a real link, openable in a new tab and reachable by keyboard.
   ─────────────────────────────────────────────────────────────────────────── */
.list { list-style: none; margin: 0; padding: 0; }
.list > li + li { border-top: 1px solid var(--divider); }

.list-row {
    display: flex;
    align-items: center;
    gap: var(--space-6);
    padding: var(--space-5) var(--space-6);
    color: inherit;
    text-decoration: none;
}
a.list-row:hover { background: var(--bg-hover); text-decoration: none; }
a.list-row:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--brand-ring-soft); }
.list-row[aria-current="true"],
.list-row[aria-selected="true"] {
    background: var(--brand-tint);
    box-shadow: inset 3px 0 0 var(--brand);
}

/* The dominant field and its supporting line. min-width: 0 is what lets the
   ellipsis work — a flex item defaults to min-content width and refuses to shrink. */
.list-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--space-1); }
.list-title { color: var(--fg); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.list-sub { color: var(--muted); font-size: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Trailing metadata — a timestamp, a badge, a chevron. */
.list-meta { display: flex; align-items: center; gap: var(--space-4); flex-shrink: 0; color: var(--muted); font-size: var(--text-3); }

/* Inside a card the rows meet the card's edges, so the divider spans the full
   width instead of floating with a gap at each end. */
.card > .list > li > .list-row { padding-inline: var(--space-7); }

/* ── The row as a lockup ────────────────────────────────────────────────────
   `.list-row` + `.list-main` + `.list-title` + `.list-sub` + `.list-meta` is a leading
   visual, two stacked lines and a trailing slot. Put an `.avatar` first and it is a
   person; put an icon there and it is a device, a file or a queue. Nothing in it names
   what the thing is, which is why it serves for all of them.

   `--flush` drops the row's own padding, for the places where the surrounding cell
   already provides it: a table cell, a card header, a form row. */
.list-row--flush { padding: 0; gap: var(--space-5); }

/* ── Steps ──────────────────────────────────────────────────────────────────
   Progress through a sequence that has an order — a wizard, an approval chain.

   State is `data-state` on the <li> rather than a modifier class, because a step has
   exactly one of three states and an attribute cannot accidentally carry two. The
   number is drawn in CSS from a counter, so renumbering after inserting a step is
   not a markup edit.

   The connector is the <li>'s own ::after, so the last step has none without a
   modifier. Colour alone does not say which steps are done: the done marker shows a
   tick, not just a fill. */
.steps { list-style: none; margin: 0 0 var(--space-7); padding: 0; display: flex; counter-reset: dr-step; }
.steps > li {
    position: relative;
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding: 0 var(--space-4);
    color: var(--muted);
    font-size: var(--text-3);
    text-align: center;
    counter-increment: dr-step;
}
.steps > li::before {
    content: counter(dr-step);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: 1px solid var(--border-strong);
    border-radius: 50%;
    background: var(--bg);
    color: var(--muted);
    font-weight: 600;
    /* Above the connector, which runs behind it. Local stacking. */
    position: relative;
    z-index: 1;
}
/* The connector runs from this step's marker to the next one's. */
.steps > li:not(:last-child)::after {
    content: '';
    position: absolute;
    top: 13px;
    inset-inline-start: 50%;
    width: 100%;
    border-top: 1px solid var(--border);
}
/* Done is FILLED, pending is outlined. That difference survives a colour-blind
   palette, a greyscale print and forced colours, which a green-vs-grey fill would
   not — so no markup has to be added to carry a tick. */
.steps > li[data-state="done"] { color: var(--fg-soft); }
.steps > li[data-state="done"]::before {
    background: var(--go-solid);
    border-color: var(--go-solid);
    color: var(--on-solid);
}
.steps > li[data-state="done"]:not(:last-child)::after { border-top-color: var(--go-solid); }
.steps > li[data-state="current"] { color: var(--fg); font-weight: 600; }
.steps > li[data-state="current"]::before {
    border-color: var(--brand);
    color: var(--brand-text);
    box-shadow: 0 0 0 3px var(--brand-glow);
}

/* ── Timeline ───────────────────────────────────────────────────────────────
   Things that happened, newest first or oldest first, with when. An audit trail, a
   change history, a comment thread.

   Differs from .steps in what it is for: steps have a fixed sequence with an end,
   a timeline is an open-ended record. If the reader can be "on" one of them, it is
   steps.

   The rule runs down the markers as a border on the <li>, so it stops exactly at
   the last entry rather than needing a length. */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline > li {
    position: relative;
    padding: 0 0 var(--space-7) var(--space-9);
    border-inline-start: 1px solid var(--border);
    margin-inline-start: var(--space-2);
}
.timeline > li:last-child { border-inline-start-color: transparent; padding-bottom: 0; }
.timeline > li::before {
    content: '';
    position: absolute;
    inset-inline-start: -5px;
    top: 4px;
    width: 9px;
    height: 9px;
    background: var(--border-hover);
    border: 2px solid var(--bg);
    border-radius: 50%;
}
.timeline-when { display: block; color: var(--muted); font-size: var(--text-2); font-variant-numeric: tabular-nums; }
.timeline-what { color: var(--fg-soft); font-size: var(--text-4); }
.timeline > li[data-kind="go"]::before     { background: var(--go-solid); }
.timeline > li[data-kind="warn"]::before   { background: var(--warn-solid); }
.timeline > li[data-kind="danger"]::before { background: var(--danger-solid); }

/* ── Accordion ──────────────────────────────────────────────────────────────
   Built on <details>/<summary>, which is the whole point: open and closed state,
   keyboard operation, and the announcement all come from the platform, so there is
   no ARIA to get wrong and nothing to wire.

   Give every <details> the same `name` to make them mutually exclusive — that is a
   platform feature too, and it needs no JavaScript.

   The marker is drawn as a rotating caret. `list-style: none` plus
   `::-webkit-details-marker` is needed because the two engines expose the default
   triangle differently, and neither alone removes it everywhere. */
.accordion { border: 1px solid var(--border); border-radius: var(--radius-surface); overflow: hidden; }
.accordion > details + details { border-top: 1px solid var(--border); }
.accordion summary {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    padding: var(--space-5) var(--space-7);
    background: var(--surface-soft);
    color: var(--fg);
    font-size: var(--text-4);
    font-weight: 500;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.accordion summary::-webkit-details-marker { display: none; }
.accordion summary:hover { background: var(--bg-hover); }
.accordion summary:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--brand-ring-soft); }
/* The caret goes last so the label starts at the same place whatever the state. */
.accordion summary::after {
    content: '';
    margin-inline-start: auto;
    width: 0;
    height: 0;
    border-inline: 4px solid transparent;
    border-top: 5px solid currentColor;
    opacity: 0.5;
    transition: transform var(--motion-fast) ease;
}
.accordion details[open] > summary::after { transform: rotate(-180deg); }
.accordion details[open] > summary { background: var(--surface-strong); }
.accordion-body { padding: var(--space-6) var(--space-7); font-size: var(--text-4); color: var(--fg-soft); }
.accordion-body > :first-child { margin-top: 0; }
.accordion-body > :last-child { margin-bottom: 0; }
}

/* ── 61-buttons-extra.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Button group and split button ──────────────────────────────────────────
   Adjacent buttons that act on the same thing. The group shares one border between
   neighbours rather than drawing two adjacent ones, which is what makes it read as
   one control instead of a row.

   A group is NOT a way to pick one of several — that is `.segmented`, and it is
   built from radios so it is announced as a choice. A group is several actions.
   Wrap it in `role="group"` with an `aria-label` saying what they act on.
   ─────────────────────────────────────────────────────────────────────────── */
.btn-group { display: inline-flex; align-items: stretch; }
.btn-group > .btn { border-radius: 0; }
.btn-group > :first-child { border-start-start-radius: var(--radius-control); border-end-start-radius: var(--radius-control); }
.btn-group > :last-child  { border-start-end-radius: var(--radius-control);   border-end-end-radius: var(--radius-control); }
.btn-group > * + * { margin-inline-start: -1px; }
/* The focused or hovered button paints above its neighbour, or half its ring and
   its darker border edge are covered. 1 is local stacking, not a documented rung. */
.btn-group > .btn:hover,
.btn-group > .btn:focus-visible { position: relative; z-index: 1; }

/* ── Split button ───────────────────────────────────────────────────────────
   A primary action plus a menu of its variants. Two real buttons, not one with a
   hit-test: the main action must be operable without ever opening the menu, and the
   caret needs its own accessible name ("More send options") and its own
   aria-expanded. */
.split-btn { display: inline-flex; align-items: stretch; }
.split-btn > .btn:first-child { border-start-end-radius: 0; border-end-end-radius: 0; }
.split-btn > .btn:last-child {
    border-start-start-radius: 0;
    border-end-start-radius: 0;
    margin-inline-start: -1px;
    padding-inline: var(--space-4);
}
.split-btn > .btn:hover,
.split-btn > .btn:focus-visible { position: relative; z-index: 1; }
/* Between two filled buttons of the same colour the shared edge disappears, so the
   split is drawn as a lighter inset line instead. */
.split-btn > .btn-primary + .btn-primary,
.split-btn > .btn-go + .btn-go,
.split-btn > .btn-warn-solid + .btn-warn-solid { box-shadow: inset 1px 0 0 var(--surface-strong); }

/* ── Icon-only and floating buttons ─────────────────────────────────────────
   .btn-icon is a square button with no label. It still needs an aria-label, and
   `.btn`'s min-height already matches it to text buttons beside it — this only
   squares the padding so it is not a wide box around one glyph.

   Square by construction, from the same token as the height. Do not hard-code the
   width: it drifts from the height the moment either tier changes. */
.btn-icon { padding-inline: 0; width: var(--control-height); }
.btn-icon.btn-sm { width: var(--control-height-sm); }
.btn-icon.btn-lg { width: var(--control-height-lg); }

/* .btn-ghost carries no border or fill until hovered, for a dense row of secondary
   actions — a table row's actions, a toolbar. Everything the button does is still
   there; only the chrome is deferred. */
.btn-ghost { background: none; border-color: transparent; color: var(--muted); }
.btn-ghost:hover { background: var(--bg-hover); border-color: transparent; color: var(--fg); }
.btn-ghost:active { background: var(--btn-active-bg); }

/* ── Floating action button ─────────────────────────────────────────────────
   One page-level action, pinned to a corner on a screen too small to show a toolbar.
   Deliberately not a general pattern: a second FAB has no obvious place, and the
   first one already covers whatever is under it.

   z-index 60 is the topbar rung — it is chrome at the same level, and it must stay
   below every overlay so a modal covers it. */
.fab {
    position: fixed;
    inset-block-end: var(--space-8);
    inset-inline-end: var(--space-8);
    z-index: 60;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-4);
    min-height: 48px;
    padding: 0 var(--space-8);
    background: var(--brand);
    border: none;
    border-radius: var(--radius-pill);
    color: var(--on-solid);
    font-family: inherit;
    font-size: var(--text-5);
    font-weight: 500;
    box-shadow: var(--shadow-float);
    cursor: pointer;
}
.fab i { font-size: var(--text-8); }
.fab:hover { background: var(--brand-hover); }
.fab:focus-visible { outline: none; box-shadow: var(--shadow-float), 0 0 0 3px var(--brand-ring); }
/* Icon-only: circular, and the label goes in an aria-label. */
.fab--icon { width: 48px; padding: 0; }
}

/* ── 62-media-and-prose.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Media frame ────────────────────────────────────────────────────────────
   A bounded box for an image or an embed whose size you do not control — a
   screenshot, an attachment preview, a chart from elsewhere.

   `aspect-ratio` plus `object-fit: contain` is what stops the page reflowing as
   images arrive: the box has its height before the image loads. `contain` rather
   than `cover` because these are documents, and cropping a screenshot removes the
   part somebody needed.
   ─────────────────────────────────────────────────────────────────────────── */
.media-frame {
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    aspect-ratio: 16 / 10;
    background: var(--surface-soft);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
}
.media-frame > img,
.media-frame > video,
.media-frame > iframe { max-width: 100%; max-height: 100%; object-fit: contain; border: none; }
.media-frame--square { aspect-ratio: 1; }
.media-frame--wide { aspect-ratio: 21 / 9; }
/* Fills and crops — right for a decorative banner, wrong for a document. */
.media-frame--cover > img { width: 100%; height: 100%; object-fit: cover; }

/* A caption belongs to the figure, so use <figure>/<figcaption> and this styles the
   caption. An image whose meaning is in the caption still needs a real alt. */
.media-figure { margin: 0; }
.media-figure > figcaption { margin-top: var(--space-3); color: var(--muted); font-size: var(--text-3); }

/* ── Gallery ────────────────────────────────────────────────────────────────
   Thumbnails, breakpoint-free like the other grids. Each tile is a link or a button
   opening the full item, never a bare div with a click handler. */
.gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(140px, 100%), 1fr));
    gap: var(--space-5);
}
.gallery > * { position: relative; display: block; text-decoration: none; }
.gallery .media-frame { aspect-ratio: 1; }
.gallery > a:hover .media-frame,
.gallery > button:hover .media-frame { border-color: var(--border-hover); }
.gallery > *:focus-visible { outline: none; }
.gallery > *:focus-visible .media-frame { box-shadow: 0 0 0 2px var(--brand-ring-soft); }

/* ── Prose ──────────────────────────────────────────────────────────────────
   Long-form text an app renders from a source it does not control — a release note,
   a knowledge-base article, an e-mail body.

   `.markdown-body` already styles rendered Markdown; `.prose` is for the same job
   where the HTML did not come from Markdown. They deliberately look the same: two
   typographic voices in one app is worse than one shared rule set.

   The measure is capped at 72 characters. Beyond about that the eye loses the line
   it is returning to, which no amount of line-height fixes. */
.prose { color: var(--fg-soft); font-size: var(--text-5); line-height: 1.65; max-width: 72ch; }
.prose > :first-child { margin-top: 0; }
.prose > :last-child { margin-bottom: 0; }
.prose h1, .prose h2, .prose h3, .prose h4 { color: var(--fg); line-height: 1.3; margin: 1.4em 0 0.4em; }
.prose h1 { font-size: 1.5em; }
.prose h2 { font-size: 1.25em; }
.prose h3 { font-size: 1.1em; }
.prose p, .prose ul, .prose ol { margin: 0.8em 0; }
.prose li { margin: 0.3em 0; }
.prose a { color: var(--accent); text-decoration: underline; }
.prose blockquote {
    margin: 1em 0;
    padding: 0.2em 0 0.2em 1em;
    border-inline-start: 3px solid var(--border);
    color: var(--muted);
}
.prose hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; }
.prose img { max-width: 100%; height: auto; border-radius: var(--radius-inner); }
/* A table in untrusted prose is the one thing that breaks the page width, because
   its own content sets its minimum size. */
.prose table { display: block; overflow-x: auto; max-width: 100%; border-collapse: collapse; }
.prose th, .prose td { padding: var(--space-3) var(--space-5); border: 1px solid var(--border); text-align: start; }
.prose th { background: var(--table-head-bg); color: var(--muted); font-size: 0.9em; }

/* ── Redacted ───────────────────────────────────────────────────────────────
   A value deliberately withheld — a secret, a masked identifier, a field the
   reader's role cannot see.

   It must not merely LOOK hidden. A blur or a low opacity leaves the real text in
   the DOM, where it is readable by view-source, by a screen reader, and by anyone
   who selects it. So the class marks a placeholder the server sent instead of the
   value; the value never reaches the browser. */
.redacted {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: 1px var(--space-3);
    background: var(--secret-bg);
    border: 1px dashed var(--secret-border);
    border-radius: var(--radius-inner);
    color: var(--redacted);
    font-family: var(--font-mono);
    font-size: var(--text-3);
    user-select: none;
}
.redacted i { font-size: var(--text-4); }
}

/* ── 63-drawer.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Drawer / sheet ─────────────────────────────────────────────────────────
   A panel sliding in from an edge, for a secondary flow that should not lose the
   page behind it — a filter panel, a record's detail, a form on a phone.

   Drawer vs modal: a modal demands an answer before anything else happens, a drawer
   is a place to work that can be left. If dismissing it loses nothing, it is a
   drawer.

   The z-order is the documented pair: scrim 480, panel 490. Both sit BELOW the modal
   backdrop at 500 on purpose — a modal opened from inside a drawer must cover it,
   which is exactly the case that a single "overlay" rung gets wrong.

   Open/closed is `.drawer--open` on the wrapper, not a `hidden` attribute, because
   the panel has to stay in the DOM to slide. It is `visibility: hidden` when closed,
   which does take it out of the tab order and the accessibility tree — a panel that
   is merely translated off-screen is still tabbable, and Tab then walks the reader
   into a panel they cannot see.
   ─────────────────────────────────────────────────────────────────────────── */
.drawer-scrim {
    position: fixed;
    inset: 0;
    z-index: 480;
    background: var(--backdrop);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--motion-mid) ease, visibility var(--motion-mid);
}

.drawer {
    position: fixed;
    inset-block: 0;
    inset-inline-end: 0;
    z-index: 490;
    display: flex;
    flex-direction: column;
    width: min(420px, 100vw);
    background: var(--bg-elevated);
    border-inline-start: 1px solid var(--border);
    box-shadow: var(--shadow-modal);
    visibility: hidden;
    transform: translateX(100%);
    transition: transform var(--motion-mid) ease, visibility var(--motion-mid);
}
.drawer--start {
    inset-inline-end: auto;
    inset-inline-start: 0;
    border-inline-start: none;
    border-inline-end: 1px solid var(--border);
    transform: translateX(-100%);
}

/* A sheet comes from the bottom edge instead — the right shape on a phone, where a
   side panel has nowhere to come from. */
.sheet {
    inset-block: auto 0;
    inset-inline: 0;
    width: auto;
    max-height: 85vh;
    border-inline-start: none;
    border-top: 1px solid var(--border);
    border-start-start-radius: var(--radius-panel);
    border-start-end-radius: var(--radius-panel);
    transform: translateY(100%);
}

.drawer--open { visibility: visible; transform: none; }
.drawer--open + .drawer-scrim,
.drawer-scrim--open { opacity: 1; visibility: visible; }

/* ── As a <dialog> — the preferred form ─────────────────────────────────────
   A drawer with a scrim is modal in behaviour: the page behind it should not be
   reachable while it is open. So say so, and let the platform do it —
   `<dialog class="drawer">` opened with showModal() gets the focus trap,
   Escape-to-close and inert content behind, and its ::backdrop replaces
   .drawer-scrim entirely. A hand-rolled trap is a great deal of code that is usually
   subtly wrong.

   A top-layer dialog's containing block is the viewport, so the same insets apply
   unchanged.

   `transition-behavior: allow-discrete` plus `@starting-style` is what keeps the
   slide: a dialog goes from `display: none` to displayed, and a discrete property
   cannot be interpolated without opting in. Without them the panel simply appears —
   which is correct, just abrupt, so this degrades rather than breaks. */
dialog.drawer {
    /* The UA stylesheet centres a dialog with `margin: auto` and sizes it to its
       content with `width`/`height: fit-content`. Both fight the edge anchoring above,
       and both have to be given back here, or the panel is sized to its own text and
       vertically centred while every rule that put it at the edge still applies.
       `height: auto` is what lets `inset-block: 0` mean full height. */
    margin: 0;
    height: auto;
    /* The UA also sets BOTH inline insets to 0 — that is how `margin: auto` centres a
       dialog. With a definite width and no auto margins that over-constrains the box,
       and the inset the spec then ignores is the END one, which puts the panel on the
       START edge. Only one inset may bind. */
    inset-inline-start: auto;
    inset-inline-end: 0;
    max-width: none;
    max-height: none;
    padding: 0;
    /* And the UA gives a dialog `border: solid`, which is 3px of currentColor — a
       near-white frame against the viewport edges. All four edges are restated here
       rather than left to `.drawer`, whose class selector this rule outranks. */
    border: none;
    border-inline-start: 1px solid var(--border);
    color: var(--fg);
    --dialog-backdrop: var(--backdrop);
    transition: transform var(--motion-mid) ease, display var(--motion-mid) allow-discrete, overlay var(--motion-mid) allow-discrete;
}
/* A closed dialog goes back to `display: none`. `.drawer`'s own `display: flex` would
   otherwise keep it in the document as a hidden fixed box, and it is what the
   `display` transition above animates between. */
dialog.drawer:not([open]) { display: none; }

/* The two variants, restated at this specificity because the rule above outranks their
   own class selectors. */
dialog.drawer.drawer--start {
    inset-inline-start: 0;
    inset-inline-end: auto;
    border-inline-start: none;
    border-inline-end: 1px solid var(--border);
}
/* A sheet spans the inline axis, is content-sized in the block axis and capped, and
   carries its edge on top. */
dialog.sheet {
    inset-inline: 0;
    height: fit-content;
    max-height: 85vh;
    border: none;
    border-top: 1px solid var(--border);
}
dialog.drawer::backdrop { background: var(--dialog-backdrop); }
/* The div form hides itself when closed; a dialog is already display:none, so `open`
   is what makes it visible and the visibility rule must not fight it. */
dialog.drawer[open] { visibility: visible; transform: none; }
@starting-style {
    dialog.drawer[open] { transform: translateX(100%); }
    dialog.drawer.drawer--start[open],
    [dir="rtl"] dialog.drawer[open] { transform: translateX(-100%); }
    dialog.sheet[open] { transform: translateY(100%); }
}

.drawer-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    flex-shrink: 0;
    padding: var(--space-7) var(--space-8);
    border-bottom: 1px solid var(--border);
}
.drawer-header h3 { margin: 0; font-size: var(--text-6); font-weight: 600; }
/* The only scrolling part, so the header and footer stay put. */
.drawer-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-7) var(--space-8); }
.drawer-footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-4);
    flex-shrink: 0;
    padding: var(--space-7) var(--space-8);
    border-top: 1px solid var(--border);
}

/* A grab handle at the top of a sheet. Decoration only — it is not a control, so it
   is not focusable and carries no name. Dragging a sheet is a gesture the app
   implements; the close button is what makes it dismissible for everyone else. */
.sheet-handle {
    flex-shrink: 0;
    width: 36px;
    height: 4px;
    margin: var(--space-4) auto 0;
    background: var(--border-strong);
    border-radius: var(--radius-pill);
}

/* ── Tree ───────────────────────────────────────────────────────────────────
   Nested structure the reader expands — a group hierarchy, a folder of files, a
   configuration path.

   Built from nested <ul> inside <details>, so expansion, keyboard operation and the
   announcement come from the platform. A real ARIA `role="tree"` needs single-tab-stop
   navigation with arrow keys through a flattened view, and implementing half of it is
   worse than not claiming it: <details> is honest about being a set of disclosures.

   The indent is one padding step per level, applied by nesting rather than by a
   `--depth` variable the markup has to keep correct. */
.tree, .tree ul { list-style: none; margin: 0; padding: 0; }
.tree ul { padding-inline-start: var(--space-7); border-inline-start: 1px solid var(--divider); margin-inline-start: var(--space-4); }
.tree li { font-size: var(--text-4); }

.tree summary,
.tree-leaf {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-inner);
    color: var(--fg-soft);
    text-decoration: none;
    cursor: pointer;
    list-style: none;
}
.tree summary::-webkit-details-marker { display: none; }
.tree summary:hover,
.tree-leaf:hover { background: var(--bg-hover); color: var(--fg); text-decoration: none; }
.tree summary:focus-visible,
.tree-leaf:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--brand-ring-soft); }
.tree summary::before {
    content: '';
    flex-shrink: 0;
    width: 0;
    height: 0;
    border-block: 4px solid transparent;
    border-inline-start: 5px solid currentColor;
    opacity: 0.55;
    transition: transform var(--motion-fast) ease;
}
.tree details[open] > summary::before { transform: rotate(90deg); }
/* A leaf has no caret, so it needs the same indent to line up with its siblings. */
/* Same derivation as the nav group: a leaf has no caret, so it takes the summary's
   padding plus the space the caret and its gap would have occupied. */
.tree-leaf { padding-inline-start: calc(var(--space-3) + var(--space-6)); }
.tree-leaf[aria-current="true"] { background: var(--brand-tint); color: var(--brand-text); }
.tree-count { margin-inline-start: auto; color: var(--muted); font-size: var(--text-2); font-variant-numeric: tabular-nums; }
}

/* ── 64-palette-spotlight.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Command palette ────────────────────────────────────────────────────────
   The ⌘K / Ctrl-K box: type, see matching commands, run one.

   A <dialog> opened with showModal(), which is the reason it can be this small a
   piece of CSS: the top layer, the focus trap, Escape and inert-behind all come from
   the platform. Positioned near the top rather than centred, because the list grows
   downwards and a centred box moves under the reader's eye as they type.

   The list is a real role="listbox" of role="option" items, with the input as the
   combobox that owns it through aria-activedescendant. That claim is made only
   because drSimpleUi.palette implements the keyboard contract behind it in full:
   arrows, Home/End, Enter, and the highlight moving while focus stays in the input.
   aria-selected on a plain button is invalid and axe rejects it.

   No fuzzy-match library. The scorer is `ui._.score` in js-parts/00-core.js, shared
   with the header search — this package loads nothing at runtime.
   ─────────────────────────────────────────────────────────────────────────── */
/* Any class that gives a <dialog> a display has to gate it on [open], or it overrides
   the UA's `display: none` for a closed dialog. The palette is built once and reused,
   so without the gate it stays in the page as a 560px panel below the content from the
   first Ctrl-K onwards. */
dialog.palette[open] {
    display: flex;
    flex-direction: column;
}
dialog.palette {
    width: min(560px, calc(100vw - 32px));
    max-height: 60vh;
    margin: 12vh auto auto;
    padding: 0;
    overflow: hidden;
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-panel);
    box-shadow: var(--shadow-pop);
    color: var(--fg);
    --dialog-backdrop: var(--backdrop);
}
dialog.palette::backdrop { background: var(--dialog-backdrop); backdrop-filter: blur(2px); }

.palette-input {
    flex-shrink: 0;
    width: 100%;
    padding: var(--space-7) var(--space-7);
    background: none;
    border: none;
    border-bottom: 1px solid var(--border);
    color: var(--fg);
    font-family: inherit;
    font-size: var(--text-6);
}
.palette-input:focus { outline: none; }
.palette-input::placeholder { color: var(--border-strong); }

.palette-list { flex: 1; min-height: 0; overflow-y: auto; padding: var(--space-3); margin: 0; list-style: none; }
.palette-item {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    width: 100%;
    padding: var(--space-4) var(--space-5);
    background: none;
    border: none;
    border-radius: var(--radius-inner);
    color: var(--fg-soft);
    font-family: inherit;
    font-size: var(--text-4);
    text-align: start;
    text-decoration: none;
    cursor: pointer;
}
.palette-item i { font-size: var(--text-7); color: var(--muted); flex-shrink: 0; }
.palette-item-note { margin-inline-start: auto; color: var(--muted); font-size: var(--text-2); flex-shrink: 0; }
/* Highlighted by keyboard, which is a different thing from :hover — the pointer must
   not move the keyboard's position. The app sets aria-selected as it arrows. */
.palette-item[aria-selected="true"] { background: var(--brand-tint); color: var(--brand-text); }
.palette-item[aria-selected="true"] i { color: var(--brand-text); }
.palette-item:hover { background: var(--bg-hover); color: var(--fg); }
.palette-group {
    padding: var(--space-5) var(--space-5) var(--space-2);
    color: var(--muted);
    font-size: var(--text-1);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}
.palette-empty { padding: var(--space-9) var(--space-7); color: var(--muted); font-size: var(--text-4); text-align: center; }
.palette-footer {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: var(--space-6);
    padding: var(--space-4) var(--space-7);
    border-top: 1px solid var(--border);
    background: var(--surface-soft);
    color: var(--muted);
    font-size: var(--text-2);
}

/* A keyboard hint. Not a token-worthy surface of its own — it reuses the code tint,
   because that is what it is: a literal the reader types. */
.kbd {
    display: inline-flex;
    align-items: center;
    padding: 1px var(--space-2);
    background: var(--code-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-small);
    color: var(--code-fg);
    font-family: var(--font-mono);
    font-size: var(--text-2);
    line-height: 1.5;
}

/* ── Spotlight ──────────────────────────────────────────────────────────────
   Dims the page except one element, for a guided tour or a first-run hint.

   z-index 510 is the reserved rung between the modal backdrop (500) and the
   popover/dropdown band (550): a spotlight has to cover a modal it is explaining,
   and still be covered by a dropdown opened inside the thing it highlights.

   The hole is a box-shadow spread rather than a clip-path or four dimming strips: one
   element, no arithmetic, and the ring follows the highlighted element's own border
   radius. The app positions .spotlight-hole over the target.

   pointer-events: none on the dim, so the highlighted control is still operable —
   a tour that blocks the thing it is pointing at teaches nothing. */
.spotlight-hole {
    position: absolute;
    z-index: 510;
    border-radius: var(--radius-surface);
    pointer-events: none;
    /* 100vmax spread: large enough to cover any viewport, so the "hole" is simply
       the un-shadowed box. */
    box-shadow: 0 0 0 100vmax var(--spotlight-dim);
}
.spotlight-ring { box-shadow: 0 0 0 100vmax var(--spotlight-dim), 0 0 0 2px var(--brand); }

/* The explanatory bubble. Positioned by the app beside the hole. */
.spotlight-tip {
    position: absolute;
    z-index: 510;
    max-width: 300px;
    padding: var(--space-6) var(--space-7);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    box-shadow: var(--shadow-pop);
    color: var(--fg-soft);
    font-size: var(--text-4);
}
.spotlight-tip-title { display: block; color: var(--fg); font-weight: 600; margin-bottom: var(--space-2); }
.spotlight-tip-actions { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); margin-top: var(--space-5); }
.spotlight-step { color: var(--muted); font-size: var(--text-2); font-variant-numeric: tabular-nums; }
}

/* ── 65-popover.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Popover ────────────────────────────────────────────────────────────────
   A small panel of *content* anchored to the control that opened it: why a row
   is pending, what a setting does, the detail behind a number.

     <button class="btn" popovertarget="sla-detail">Why is this overdue?</button>

     <div class="popover" id="sla-detail" popover>
       <strong class="popover-title">Breached 2h ago</strong>
       <p>Priority 2 carries a four-hour response target…</p>
     </div>

   Not a .menu. A menu is a list of *actions* and its items are links and
   buttons; this is prose, a definition, a small table. Two panels that look
   alike but mean different things is how a design system starts to rot, so the
   difference is in what goes inside, and the surfaces are deliberately distinct:
   a menu is flush-padded for rows, this one has reading padding.

   THE `popover` ATTRIBUTE IS REQUIRED, and so is opening it from a
   `popovertarget` control. The platform then supplies everything this file would
   otherwise need JavaScript for — the top layer, light dismiss, Escape, focus
   handling, and `:popover-open`. There is no drSimpleUi.popover and there does
   not need to be one.

   `popovertarget` also makes the invoker the popover's IMPLICIT ANCHOR, which is
   what lets `position-area` place this with no `anchor-name` anywhere. That
   matters more than it looks: the alternative is a unique --dashed-ident per
   instance, which means an inline style on every popover in the app.

   The top layer ignores z-index entirely and paints by promotion order, so the
   550 below never applies to a real popover. It is there for the fallback path
   in the z-order table, and so that a panel carrying this class while something
   has gone wrong lands on the rung the documentation says it does.
   ─────────────────────────────────────────────────────────────────────────── */
.popover {
    /* inset: auto and margin: 0 undo the UA rule that centres a popover in the
       viewport. Without both, position-area has nothing to do. */
    inset: auto;
    margin: 0;
    position: fixed;
    position-area: block-end span-inline-end;
    /* Flips above the trigger when there is no room below. The flip takes the
       margin with it, so the gap stays on the side facing the invoker. */
    position-try-fallbacks: flip-block;
    margin-block-start: var(--space-3);
    z-index: 550;

    width: max-content;
    max-width: min(320px, calc(100vw - 32px));
    padding: var(--space-6) var(--space-7);
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    box-shadow: var(--shadow-pop);
    color: var(--fg-soft);
    font-size: var(--text-4);
    line-height: 1.6;
}
.popover > :first-child { margin-top: 0; }
.popover > :last-child { margin-bottom: 0; }

.popover-title {
    display: block;
    margin-bottom: var(--space-3);
    color: var(--fg);
    font-size: var(--text-4);
    font-weight: 600;
}
}

/* ── 66-status-indicator.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Status indicator ───────────────────────────────────────────────────────
   A dot and a word for the state of something the app is connected to: a device, a
   stream, a job runner, an API. Small enough for a topbar, a table cell, a card
   header or above an output pane.

   Six states, in two groups. The three ACTIVE ones pulse, because something is
   happening and a static dot cannot say so; the three SETTLED ones do not.

     --live        connected, nothing in flight        pulses
     --streaming   receiving now                       pulses
     --connecting  establishing, or retrying           pulses
     --idle        connected, deliberately quiet       still
     --degraded    working, but not properly           still
     --down        not connected                      still

   The word is not optional. A colour alone fails for anybody who cannot see it, and
   "green" does not say whether green means connected or healthy or done. */
.health-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-3);
    font-weight: 500;
    white-space: nowrap;
}
.health-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--border-hover);
}

.health-badge--live       { color: var(--go-fg); }
.health-badge--live       .health-dot { background: var(--go-solid); box-shadow: 0 0 6px var(--go-ring); }
.health-badge--streaming  { color: var(--info-fg); }
.health-badge--streaming  .health-dot { background: var(--info-solid); box-shadow: 0 0 6px var(--info-ring); }
.health-badge--connecting { color: var(--warn-fg); }
.health-badge--connecting .health-dot { background: var(--warn-solid); }
.health-badge--idle       { color: var(--muted); }
.health-badge--idle       .health-dot { background: var(--border-hover); }
.health-badge--degraded   { color: var(--warn-fg); }
.health-badge--degraded   .health-dot { background: var(--warn-solid); }
.health-badge--down       { color: var(--danger-fg); }
.health-badge--down       .health-dot { background: var(--danger-solid); }

/* The active states. One animation, three selectors, so the rhythm is identical
   across a page showing several connections at once. */
.health-badge--live .health-dot,
.health-badge--streaming .health-dot,
.health-badge--connecting .health-dot { animation: dr-pulse var(--pulse-duration) ease-in-out infinite; }

/* `--healthy` is an alias for `--live`, kept for apps already using it. Prefer
   `--live` in new markup. */
.health-badge--healthy { color: var(--go-fg); }
.health-badge--healthy .health-dot {
    background: var(--go-solid);
    box-shadow: 0 0 6px var(--go-ring);
    animation: dr-pulse var(--pulse-duration) ease-in-out infinite;
}

/* ── Freshness ──────────────────────────────────────────────────────────────
   How old the number beside it is. For anything polled or pushed, where the reader
   has to know whether they are looking at now or at four minutes ago.

   `--warn` when it is old enough to distrust. What counts as old is the app's
   judgement, not the library's. */
.stale {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--muted);
    font-size: var(--text-2);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.stale i { font-size: var(--text-3); }
.stale--warn { color: var(--warn-fg); }
}

/* ── 67-output.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ── Output pane ────────────────────────────────────────────────────────────
   Lines of machine output the reader watches or scrolls back through: a build log, a
   command's stdout, an event stream, an audit trail.

   `.output` and not `.log`: a plain name is a claim on a shared namespace, and `.log`
   is commonly already taken in a consuming app.

   A real list, so the count is announced and a screen reader can walk it. The pane is
   the scroll container; add `data-follow` and DR.Simple_UI.js sticks it to the newest
   line, releasing when the reader scrolls up.

   Monospace with tabular figures, because the reader is comparing columns —
   timestamps, levels, ids — down the page rather than reading prose. */
.output {
    display: block;
    max-height: 320px;
    overflow-y: auto;
    margin: 0;
    padding: var(--space-4) 0;
    list-style: none;
    background: var(--code-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-surface);
    color: var(--fg-soft);
    font-family: var(--font-mono);
    font-size: var(--text-3);
    font-variant-numeric: tabular-nums;
    line-height: 1.65;
}

/* A header strip for the pane: what is being watched, and its state. Sits outside
   `.output` so it does not scroll away with the lines. */
.output-head {
    display: flex;
    align-items: center;
    gap: var(--space-5);
    padding: var(--space-4) var(--space-6);
    background: var(--surface-soft);
    border: 1px solid var(--border);
    border-bottom: none;
    border-start-start-radius: var(--radius-surface);
    border-start-end-radius: var(--radius-surface);
    font-size: var(--text-3);
    color: var(--muted);
}
.output-head + .output {
    border-start-start-radius: 0;
    border-start-end-radius: 0;
}

/* One line. `white-space: pre-wrap` keeps the output's own alignment while still
   wrapping a long line rather than forcing the pane to scroll sideways. */
.output-line {
    display: block;
    padding: 0 var(--space-6);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.output-line:hover { background: var(--bg-hover); }

/* A timestamp or a level, quieter than the message it introduces. */
.output-time { color: var(--muted); }

/* Severity. The whole line is tinted rather than a marker at the start, because the
   reader is scanning a wall of text and a coloured word is easy to miss in it. */
.output-line--warn   { background: var(--warn-bg);   color: var(--warn-fg); }
.output-line--error  { background: var(--danger-bg); color: var(--danger-fg); }
.output-line--go     { color: var(--go-fg); }
/* Something the app emitted about itself — "reconnected", "stream closed" — rather
   than a line from the thing being watched. */
.output-line--note   { color: var(--muted); font-style: italic; }
}

/* ── 70-rtl.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ═══ Right-to-left ═════════════════════════════════════════════════════════
   Almost nothing is needed here, and that is the point: every directional property
   in this library is a logical one — `margin-inline-start`, `border-inline-end`,
   `inset-inline`, `text-align: start` — so the whole layout mirrors from `dir="rtl"`
   on <html> with no rules at all.
   `No_physical_direction_properties_without_a_justification` keeps it that way.

   What lands here is the residue that logical properties cannot express, of which
   there are exactly two kinds:

     1. `transform: translateX()` does not flip. There is no logical translate, so
        anything that moves a thing along the inline axis needs its sign inverted.
     2. `background-position` has no logical form. A percentage is always measured
        from the physical left edge.

   Both are real gaps in CSS rather than shortcuts taken here, which is why each one
   is named individually instead of being covered by a sweeping rule.
   ─────────────────────────────────────────────────────────────────────────── */

/* The switch knob travels from the inline start to the inline end. Its resting
   position is already logical; only the journey needs mirroring. */
[dir="rtl"] .switch input[type="checkbox"]:checked::after { transform: translateX(-15px); }

/* The select's caret is two gradients positioned from 100%, which is the physical
   right edge whatever the direction. Mirrored by measuring from 0% instead, and the
   reserved padding swaps sides with it. */
[dir="rtl"] .form-select {
    padding-inline-end: var(--space-5);
    padding-inline-start: var(--space-10);
    background-position:
        calc(0% + 12px) calc(50% - 2px),
        calc(0% + 17px) calc(50% - 2px);
}
/* The two triangles also swap, or the caret points sideways: the left-hand box has
   to be the one filled towards the top-left. */
[dir="rtl"] .form-select {
    background-image:
        linear-gradient(135deg, var(--muted) 50%, transparent 50%),
        linear-gradient(45deg,  transparent 50%, var(--muted) 50%);
}
[dir="rtl"] .form-select[multiple],
[dir="rtl"] .form-select[size]:not([size="1"]) { padding-inline-start: var(--space-5); }

/* The drawer slides in from an inline edge; `inset-inline-end` already puts it on
   the correct side, so only the direction of travel is wrong. */
[dir="rtl"] .drawer { transform: translateX(-100%); }
[dir="rtl"] .drawer--start { transform: translateX(100%); }
[dir="rtl"] .drawer--open { transform: none; }

/* Kind 2 again. An avatar group's separating gap is a hole masked out of the avatar
   behind, centred on where the next one lands — and a radial-gradient position is
   measured from the physical left edge whatever the direction, while the overlap
   that put it there is a `margin-inline-start`. So the offset is restated from the
   other edge; the arithmetic is the same, mirrored. */
[dir="rtl"] .avatar { --avatar-cut-x: calc(var(--avatar-overlap) - var(--avatar-size) / 2); }

/* The tree's disclosure caret points along the inline axis, so it starts mirrored
   and rotates the other way when open. */
[dir="rtl"] .tree summary::before { transform: rotate(180deg); }
[dir="rtl"] .tree details[open] > summary::before { transform: rotate(90deg); }

/* The collapsed rail's flyout needs nothing here, and neither does anything else in
   the rail. `position-area: inline-end` and `align-self: anchor-center` are both
   logical and resolve against the writing mode, and the offset is a
   `margin-inline-start`. */
}

/* ── 71-forced-colors.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ═══ Forced colours ════════════════════════════════════════════════════════
   Windows high-contrast mode. The browser replaces every colour with a small
   user-chosen palette, and it does so whatever this stylesheet says — so the job here
   is not to restyle anything, it is to make sure nothing the library draws
   *disappears* when it does.

   Two failure modes, and both are silent:

     1. Anything drawn with a background instead of a border vanishes. A tinted
        surface becomes Canvas, so a brand-tinted active nav item, a selected table
        row, a checked checkbox and a filled step marker all become indistinguishable
        from their neighbours.
     2. `box-shadow` is not painted at all in forced colours. Every focus ring in this
        library is a box-shadow, so without the rules below the whole library becomes
        unusable by keyboard — which is the exact population most likely to be in
        forced-colors mode.

   Everything below is a token remap on :root plus outline work, which is why it
   passes `Appearance_media_queries_only_remap_tokens`: the only selector rules are
   ones whose declarations are all `forced-color-adjust` or `outline`, the documented
   exception.
   ─────────────────────────────────────────────────────────────────────────── */
@media (forced-colors: active) {
    /* The system keywords are the palette the user chose; they are legal here and
       nowhere else. Mapping the tokens means every class picks the palette up without
       being touched. */
    :root {
        --bg:              Canvas;
        --bg-elevated:     Canvas;
        --bg-hover:        Highlight;
        --fg:              CanvasText;
        --fg-soft:         CanvasText;
        --muted:           GrayText;
        --border:          CanvasText;
        --border-strong:   CanvasText;
        --border-hover:    Highlight;
        --divider:         CanvasText;
        --card-bg:         Canvas;
        --surface-soft:    Canvas;
        --surface-strong:  Canvas;
        --on-solid:        HighlightText;

        --brand:           Highlight;
        --brand-hover:     Highlight;
        --brand-active:    Highlight;
        --brand-soft:      Highlight;
        --brand-text:      LinkText;
        --brand-tint:      Canvas;
        --accent:          LinkText;

        --sidebar-bg:      Canvas;
        --sidebar-border:   CanvasText;
        --sidebar-fg:      CanvasText;
        --sidebar-active:  Highlight;

        /* The semantic families collapse: the user's palette has no green or amber,
           and inventing one defeats the mode. What must survive is the *shape* — the
           leading rules, the icons and the text, all of which are borders and glyphs. */
        --go-solid: CanvasText;   --go-bg: Canvas;     --go-border: CanvasText;   --go-fg: CanvasText;
        --warn-solid: CanvasText; --warn-bg: Canvas;   --warn-border: CanvasText; --warn-fg: CanvasText;
        --danger-solid: CanvasText; --danger-bg: Canvas; --danger-fg: CanvasText;
        --danger-border: CanvasText; --danger-border-strong: CanvasText;
        --info-bg: Canvas;   --info-border: CanvasText;   --info-fg: CanvasText;
        --secret-bg: Canvas; --secret-border: CanvasText; --secret-fg: CanvasText;

        --badge-bg: Canvas;
        --table-head-bg: Canvas;
        --skeleton-bg: Canvas;
        --skeleton-sheen: Canvas;
        --progress-track: Canvas;
        --code-bg: Canvas;
        --code-fg: CanvasText;
        --backdrop: Canvas;
        --overlay: Canvas;
        --tip-bg: Canvas;
        --tip-border: CanvasText;
        --tip-fg: CanvasText;
    }

    /* Focus rings. Every one in this library is a box-shadow, and box-shadow is not
       painted in forced colours — so each of them is restated as an outline, which is.
       An `outline` on top of the existing `outline: none` is why these rules are
       permitted by the appearance-media-query guard. */
    .btn:focus-visible,
    .form-input:focus,
    .form-check input:focus-visible,
    .switch input[type="checkbox"]:focus-visible,
    .form-range:focus-visible,
    .input-group:focus-within,
    .tab:focus-visible,
    .menu-item:focus-visible,
    .page-link:focus-visible,
    .chip-dismiss:focus-visible,
    .th-sort:focus-visible,
    .accordion summary:focus-visible,
    .tree summary:focus-visible,
    .tree-leaf:focus-visible,
    .skip-link:focus,
    .fab:focus-visible,
    a.list-row:focus-visible { outline: 2px solid CanvasText; outline-offset: 1px; }

    /* Selected and current states are backgrounds, which are flattened away. An
       outline is what is left that the mode still paints. */
    .nav-link.active,
    .tab[aria-selected="true"],
    .page-link[aria-current="page"],
    .palette-item[aria-selected="true"],
    .tree-leaf[aria-current="true"],
    .list-row[aria-selected="true"],
    .segmented-option:has(input:checked) { outline: 1px solid Highlight; }

    /* A status dot is the one thing whose actual colour IS the content — it is not
       decoration on a label, it IS the label — so it opts out of the forced palette
       entirely. */
    .nav-status-dot,
    .health-dot,
    .chip-dot,
    .timeline > li::before { forced-color-adjust: none; }
}
}

/* ── 72-print.css → @layer dr.paint ──────────────────────────────────────────────── */
@layer dr.paint {
/* ═══ Print ═════════════════════════════════════════════════════════════════
   An app page printed or saved as PDF. Chrome is the enemy here: a sidebar, a header
   and a toast stack are navigation, and navigation on paper is a waste of the page.

   Colours are left alone rather than forced to black on white. Browsers do not print
   backgrounds by default, so the dark theme's surfaces already come out white; forcing
   text to black as well would break the reader's own "print backgrounds" choice, which
   is the one case where the dark surfaces are wanted.

   This is a layout media query, so `Layout_media_queries_only_change_geometry`
   applies: geometry, or a value that comes from a token.
   ─────────────────────────────────────────────────────────────────────────── */
@media print {
    /* The shell stops being a viewport-height flex box: on paper there is no viewport
       and nothing scrolls, so a `height: 100dvh; overflow: hidden` shell would print
       exactly one screenful and silently drop the rest. */
    .layout,
    .bare-layout,
    .auth-layout { display: block; height: auto; min-height: 0; overflow: visible; }
    .content,
    .page,
    .full-layout { display: block; overflow: visible; min-height: 0; padding: 0; }

    /* Chrome and overlays. Anything that is navigation, transient, or floating over
       the content has no meaning on paper. */
    .sidebar,
    .topbar,
    .skip-link,
    .toast-stack,
    .dr-tip,
    .fab,
    .drawer,
    .drawer-scrim,
    .spotlight-hole,
    .spotlight-tip,
    .reconnect-banner,
    .modal-backdrop,
    .page-head-actions,
    .form-actions,
    .code-block-copy { display: none; }

    /* A card is a box on screen; on paper the border is what keeps it a box, and the
       shadow is nothing. Kept whole across a page break: a card split in half loses
       the connection between its head and its numbers. */
    .card,
    .code-block,
    .callout,
    .media-frame { box-shadow: none; break-inside: avoid; }

    /* A table's header repeats on every page — that is what <thead> is for, and it
       only happens if the header is not sticky. */
    .table--sticky th { position: static; }
    .table thead { display: table-header-group; }
    .table tfoot { display: table-footer-group; }
    .table tr { break-inside: avoid; }

    /* Scroll containers have no scrollbar on paper, so their content must be allowed
       out rather than clipped to the box it had on screen. */
    .dr-scroll,
    .drawer-body,
    .md-editor .md-preview,
    .palette-list { max-height: none; overflow: visible; }

    /* An accordion prints what it is showing. Forcing every <details> open would
       print things the reader deliberately collapsed; leaving them closed loses
       nothing they were reading. */

    /* A link's destination is invisible on paper. Printed after the text, and only
       for absolute URLs — a relative href resolves to nothing useful, and printing
       every in-app link's path makes the page unreadable. */
    .prose a[href^="http"]::after,
    .markdown-body a[href^="http"]::after {
        content: ' (' attr(href) ')';
        font-size: 0.85em;
        word-break: break-all;
    }

    /* Headings should not be the last line on a page. */
    h1, h2, h3, .card-head, .page-head { break-after: avoid; }
}
}

/* ── 80-utilities.css → @layer dr.utilities ──────────────────────────────────────────────── */
@layer dr.utilities {
/* ═══ Utilities ═════════════════════════════════════════════════════════════
   Single-purpose classes, in `@layer dr.utilities`.

   The layer is what makes them work at all. A utility is (0,1,0) and a component rule
   like `.table th, .table td` is (0,1,1), so on specificity alone a utility loses
   every collision — `.text-end` on a table cell would do nothing. dr.utilities sits
   after dr.paint, so it wins regardless of specificity and no cap convention has to be
   remembered.

   Deliberately short, and staying short. This is not a utility framework: the point
   of tier 2 is semantic classes that carry meaning, and a page assembled out of
   fifteen utilities has moved its design decisions into the markup where nobody can
   review them. What is here is the handful of adjustments that are genuinely
   presentational and would otherwise be an inline style.
   ─────────────────────────────────────────────────────────────────────────── */

/* Taken out of the visual rendering, kept in the accessibility tree — a label for
   an icon-only control, a table caption, the target of an aria-describedby.

   Not `display: none` and not `width: 0`: both remove the element from the
   accessibility tree as well, which is the opposite of the point. The 1px box
   with the content clipped away is what every engine still announces. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* For something that must come back when it is reached by keyboard — a skip link
   is the usual case. :focus-within covers a wrapper holding the focusable node. */
.visually-hidden--focusable:focus,
.visually-hidden--focusable:focus-within {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
}

/* ── Text ───────────────────────────────────────────────────────────────────
   Logical, so they mirror with the document direction. `.text-end` on a table cell is
   the common case. */
.text-start  { text-align: start; }
.text-center { text-align: center; }
.text-end    { text-align: end; }

.text-muted { color: var(--muted); }
.text-mono  { font-family: var(--font-mono); }
/* Digits that line up down a column, for anything a reader compares. */
.text-nums  { font-variant-numeric: tabular-nums; }
.text-nowrap { white-space: nowrap; }
/* Two type steps either side of the body size, for a line that has to be quieter or
   louder than its surroundings without becoming a heading. */
.text-sm { font-size: var(--text-3); }
.text-lg { font-size: var(--text-6); }
/* Breaks inside a word when there is nowhere else to break — an id, a URL, a token
   name in a narrow column. `anywhere` rather than `break-word` because it also shrinks
   the min-content width, so the element stops forcing its container wide. */
.text-break { overflow-wrap: anywhere; }
/* Cut after n lines with an ellipsis. `-webkit-` prefixed and `display: -webkit-box`
   are load-bearing: line-clamp has no unprefixed form that works. */
.text-clamp-2,
.text-clamp-3 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.text-clamp-2 { -webkit-line-clamp: 2; }
.text-clamp-3 { -webkit-line-clamp: 3; }
/* One line, cut with an ellipsis. `min-width: 0` is included because the usual
   parent is a flex item, which otherwise refuses to shrink below its content and
   the ellipsis never appears — the single most common reason truncation "does not
   work". */
.text-truncate {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Layout ─────────────────────────────────────────────────────────────────
   A row and a column with the library's own gap, for the arrangements that would
   otherwise be an inline `style="display:flex; gap:8px"` — which is most of them. */
.dr-row { display: flex; align-items: center; gap: var(--space-4); }
.dr-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-4); }
.dr-col { display: flex; flex-direction: column; gap: var(--space-4); }
/* Pushes everything after it to the far end of a flex container. */
.dr-push { margin-inline-start: auto; }

/* Three gap steps and none. `.dr-row` and `.dr-col` set one gap; these are for the
   rows that need a tighter or looser one, which was an inline `style="gap:12px"`. */
.dr-gap-0 { gap: 0; }
.dr-gap-1 { gap: var(--space-4); }
.dr-gap-2 { gap: var(--space-6); }
.dr-gap-3 { gap: var(--space-8); }

/* Cross-axis alignment. `.dr-row` centres; a row of fields with labels wants
   `.dr-start`, and a row of text of different sizes wants `.dr-baseline`. */
.dr-wrap     { flex-wrap: wrap; }
.dr-start    { align-items: flex-start; }
.dr-center   { align-items: center; }
.dr-end      { align-items: flex-end; }
.dr-baseline { align-items: baseline; }
.dr-between  { justify-content: space-between; }

/* The flex child that takes the remaining room. `min-width: 0` comes with it, because
   without it the child refuses to shrink below its content and every ellipsis inside
   stops working — the commonest truncation bug there is. */
.dr-fill { flex: 1; min-width: 0; }
.dr-shrink-0 { flex-shrink: 0; }
.dr-w-full { width: 100%; }

/* Block margins only, and only for removing or adding a step. There is no inline
   pair: horizontal placement belongs to the layout, not to a utility on the child. */
.dr-mt-0 { margin-top: 0; }
.dr-mt-1 { margin-top: var(--space-4); }
.dr-mt-2 { margin-top: var(--space-6); }
.dr-mt-3 { margin-top: var(--space-8); }
.dr-mb-0 { margin-bottom: 0; }
.dr-mb-1 { margin-bottom: var(--space-4); }
.dr-mb-2 { margin-bottom: var(--space-6); }
.dr-mb-3 { margin-bottom: var(--space-8); }

/* A hairline between rows without an <hr> and without a wrapper. */
.dr-divider-top    { border-top: 1px solid var(--divider); }
.dr-divider-bottom { border-bottom: 1px solid var(--divider); }

/* A scroll container on the inline axis — a table too wide for its column, a row of
   cards. `.dr-scroll` styles a scroller; this one creates it. */
.dr-scroll-x { overflow-x: auto; }

/* ── State ──────────────────────────────────────────────────────────────────
   Not a `hidden` replacement: use the `hidden` ATTRIBUTE for that, because it also
   removes the element from the accessibility tree and the tab order. This is for
   hiding something that must stay in the layout's flow calculations. */
.dr-invisible { visibility: hidden; }

/* Marks a block as busy without removing it. Pair it with aria-busy="true", which is
   what is announced; this only stops the reader interacting with stale content. */
.dr-busy { opacity: 0.55; pointer-events: none; cursor: progress; }

/* For something unavailable that is NOT a <button>, <input> or <a> — those have the
   `disabled` attribute, which is announced and removes them from the tab order, and
   this is no substitute for it. Add aria-disabled="true" alongside. */
.dr-disabled {
    color: var(--muted);
    pointer-events: none;
    cursor: not-allowed;
}

/* ── Print ──────────────────────────────────────────────────────────────────
   The two every printed page needs: keep the chrome off the paper, and let a sheet
   carry something the screen does not. 72-print.css handles the frame; these are for
   an app's own content. */
@media print {
    .dr-no-print { display: none; }
}
.dr-print-only { display: none; }
@media print {
    .dr-print-only { display: revert; }
}
}

/* ── 90-density.css → @layer dr.overrides ──────────────────────────────────────────────── */
@layer dr.overrides {
/* ═══ Compact density ═══════════════════════════════════════════════════════
   [data-density="compact"] on <html>. A pure token remap, like every other theme
   block, so it composes with the themes and CSS load order stays irrelevant.

   Anything that should tighten with the tables uses var(--cell-pad-y) /
   var(--cell-pad-x) and gets it for free — an app's own dense card, for instance.

   There is deliberately no selector override alongside this. An app's own stylesheet is
   unlayered and wins whatever this file declares, so an app holding its own
   `.table th, .table td { padding: … }` does not go compact until that rule is deleted.
   No specificity here can substitute for deleting it.
   ─────────────────────────────────────────────────────────────────────────── */
:root[data-density="compact"] {
    --cell-pad-y: var(--space-2);
    --cell-pad-x: var(--space-5);
}
}

/* ── 95-reduced-motion.css → @layer dr.overrides ──────────────────────────────────────────────── */
@layer dr.overrides {
/* ═══ Reduced motion ════════════════════════════════════════════════════════
   Every animation and transition in the library, switched off in one place. It is
   one block on purpose: scattered per-part media queries are how a new transition
   ends up with no reduced-motion counterpart, and nobody notices, because nobody
   testing the feature has the setting on.

   Not a blanket `*, *::before, *::after { transition: none }`. Two reasons:
   without !important — which this library does not use — a sweeping selector at
   (0,0,0) loses to every real rule anyway; and it would also silence the
   consuming app's own motion, which is not this package's call to make.

   So each line below names the selector it is undoing, at the same specificity.
   Numbered 95 rather than 85 so it lands in @layer dr.overrides, which is the last
   layer: motion has to be switched off whatever declared it, including a utility.
   When you add a transition or an animation anywhere in css-parts/, add its off
   switch here in the same edit.

   What is NOT switched off: nothing here removes information. The spinner still
   reads as busy while static, the skeleton still reads as a placeholder, and the
   indeterminate progress bar is filled rather than blanked so it still says "in
   progress, no estimate".
   ─────────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {

    /* ── Tier 1 — the frame ─────────────────────────────────────────────── */
    .sidebar          { transition: none; }   /* 11 — collapse to the icon rail */
    .nav-status-card  { transition: none; }   /* 11 */
    .user-signout     { transition: none; }   /* 15 */
    /* 17 — the hint appears at its final position instead of sliding in. Opacity
       is kept as a step, not a fade, by removing the transition entirely. */
    .dr-tip           { transition: none; transform: none; }
    .reconnect-attempting i { animation: none; }   /* 18 */
    .nav-group > summary::after { transition: none; }   /* 20 — the group caret */
    /* 20 — the skip link jumps into view rather than sliding down. It has to still
       become visible, so only the transition goes. */
    .skip-link        { transition: none; }

    /* ── Tier 2 — the paint ─────────────────────────────────────────────── */
    .btn              { transition: none; }   /* 33 */
    .form-input       { transition: none; }   /* 34 */
    .form-check input[type="checkbox"] { transition: none; }   /* 34 */
    .form-check input[type="radio"]    { transition: none; }   /* 45 */
    .switch input[type="checkbox"]     { transition: none; }   /* 45 */
    .switch input[type="checkbox"]::after { transition: none; }
    .input-group      { transition: none; }   /* 46 */
    .dropzone         { transition: none; }   /* 47 */
    dialog.modal      { animation: none; }    /* 36 */
    .modal-close      { transition: none; }   /* 36 */
    .spinner          { animation: none; }    /* 39 */
    .skeleton         { animation: none; background-image: none; }   /* 40 */
    .progress-bar     { transition: none; }   /* 41 */
    .progress--indeterminate .progress-bar { animation: none; width: 100%; }
    .tab              { transition: none; }   /* 50 */
    a.stat            { transition: none; }   /* 54 */
    /* 66 — the dot keeps its colour and the word keeps saying the state; only the
       pulse stops. Nothing is lost, because the pulse was never the only signal. */
    .health-badge--live .health-dot,
    .health-badge--streaming .health-dot,
    .health-badge--connecting .health-dot { animation: none; }
    .health-badge--healthy .health-dot { animation: none; }
    .segmented-option { transition: none; }   /* 51 */
    .code-block-copy  { transition: none; }   /* 56 */
    .code-block-expand { transition: none; }  /* 56 */
    .accordion summary::after { transition: none; }   /* 60 — the disclosure caret */
    .tree summary::before     { transition: none; }   /* 63 — the same caret in a tree */

    /* 63 — the drawer appears and disappears instead of sliding. `visibility` and
       `display` are in those transition lists only to keep the panel present while it
       moves, so with no movement the whole transition goes and the panel is simply
       there or not. */
    .drawer-scrim     { transition: none; }
    .drawer           { transition: none; }
    dialog.drawer     { transition: none; }
}
}

