*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

:root {
    --bg: #f5f7fa;
    --surface: #ffffff;
    --text: #1a1a2e;
    --text-secondary: #555;
    --primary: #2563eb;
    --primary-hover: #1d4ed8;
    --border: #e2e8f0;
    /* green-700, not -600: white text on green-600 measures 3.30:1 and fails
       AA, and every success badge/button puts --on-accent on this fill.
       green-700 is 5.02:1 (tests/test_color_contrast.py). */
    --success: #15803d;
    --danger: #dc2626;
    --radius: 8px;
    --th-bg: #f8fafc;
    --hover-bg: #f8fafc;
    --bg-secondary: #f8fafc;
    /* Semantic accents + status families. Templates must use these (or badge/
       alert classes) instead of literal hex — every hardcoded color is a
       latent dark-mode bug. */
    /* Foreground for text sitting ON a solid --primary / --success / --danger
       fill (buttons, active toggles, marker tooltips). The one token this
       palette was missing: ~14 places hardcoded `color:#fff`, which is correct
       in light but wrong on the LIGHTER fills the dark theme uses, so those
       either failed AA or had to be special-cased per site. Borrowed from
       airShare's palette, which has exactly this token for the same reason
       (docs/product/UI_COHESION_PASS.md C1). */
    --on-accent: #ffffff;
    /* --primary as TEXT. In the light theme it is the same blue; in the dark
       theme --primary is lightened for use as a FILL, which drops it to 3.98:1
       against the dark surface — under AA — everywhere it is used as a label,
       a heading or a nav link. Same split as --danger / --danger-text.
           python scripts/measure_contrast.py */
    --primary-text: #2563eb;

    /* Risk severity, five steps. Was five hardcoded hexes in
       engine_analysis_results.html, which is why they could not serve both
       themes: darkening the amber steps to reach AA on white took them to
       2.91:1 on the dark surface. A palette entry per theme is the only shape
       that works, and it is what every other status colour here already does. */
    --sev-critical: #b91c1c;
    --sev-high: #c2410c;
    --sev-mod-high: #b45309;
    --sev-moderate: #a16207;
    --sev-low: #15803d;
    --warning: #f59e0b;
    /* amber-700, not -600: amber-600 is 3.19:1 on white and fails AA as small
       text. amber-700 is 5.02:1. */
    --warning-text: #b45309;
    --warning-bg: #fef3c7;
    --warning-border: #fcd34d;
    --success-text: #166534;
    --success-bg: #dcfce7;
    --success-border: #86efac;
    --danger-text: #991b1b;
    --danger-bg: #fee2e2;
    --danger-border: #fca5a5;
    --info-text: #1e40af;
    --info-bg: #dbeafe;
    --info-border: #93c5fd;
    --purple-text: #6b21a8;
    --purple-bg: #f3e8ff;
    --purple-border: #c084fc;
    /* Text-safe variants of the accent family. The raw --accent-* values are
       chart/decoration hues and FAIL WCAG AA as small text on a light surface:
       measured against #fff, amber is 1.67:1, blue 2.54:1, purple 3.96:1. Use
       these whenever an accent hue carries words (section labels, inline
       emphasis); keep the raw token for fills, borders and chart series.
       Mirrors the existing --warning-text / --success-text / --info-text
       pattern, which the accent family was missing. */
    --accent-blue-text: #1d4ed8;     /* 6.31:1 on #fff */
    --accent-amber-text: #a16207;    /* 4.87:1 on #fff */
    --accent-purple-text: #7e22ce;   /* 6.09:1 on #fff */
    --accent-purple: #a855f7;
    --accent-violet: #7c3aed;
    --accent-blue: #60a5fa;
    --accent-amber: #fbbf24;

    /* Categorical chart series (docs/product/UI_COHESION_PASS.md C3).
     *
     * IDENTITY colors — one per series, assigned in fixed slot order and never
     * cycled. Distinct from the status family above, which is reserved for
     * good/warning/bad state and must never stand in for "series 4".
     *
     * The ORDER is the colorblind-safety mechanism, not decoration: adjacent
     * slots are the pairs a reader compares, so they carry the separation
     * budget. This replaces an ad-hoc rainbow (red/orange/yellow/green/blue/
     * violet) that failed four of five checks — slots 5 and 6 measured ΔE 1.3
     * apart under deuteranopia (indistinguishable), slots 1 and 2 measured 10.4
     * for NORMAL vision, and the yellow sat at 1.92:1 on white. On the engine
     * page these slots are the six cylinders, where telling them apart IS the
     * diagnostic task.
     *
     * Validated against THESE surfaces (#ffffff light, #1e293b dark) — both
     * modes pass every check. Re-run the validator after any edit rather than
     * hand-picking a replacement hue; see tests/test_chart_palette.py.
     * Light slots 3/4/5 are under 3:1 on white, so a chart using them owes the
     * reader a legend or visible labels (the relief rule). */
    --chart-1: #2a78d6;   /* blue    */
    --chart-2: #eb6834;   /* orange  */
    --chart-3: #1baf7a;   /* aqua    */
    --chart-4: #eda100;   /* yellow  */
    --chart-5: #e87ba4;   /* magenta */
    --chart-6: #008300;   /* green   */
    --chart-7: #4a3aa7;   /* violet  */
    --chart-8: #e34948;   /* red     */

    /* Translucent primary, for focus rings and range-band fills. It was being
       REFERENCED as `var(--primary-alpha, <rgba>)` in two places without ever
       having been defined, so both silently fell through to their own literal —
       and the two literals disagreed (0.25 vs 0.15 alpha, light --primary vs
       dark). Defining it makes those sites theme-aware, which was the evident
       intent of writing `var(...)` in the first place. */
    --primary-alpha: rgba(37, 99, 235, 0.15);

    /* Two-tone brand wordmark (airframeMetrics) — "airframe" in ink, "Metrics"
       in the coral brand color (matches the app icon + sibling gaShare mark).
       One edit here restyles the mark everywhere; both halves are theme-aware. */
    /* 4.62:1 on --bg and 4.96:1 on --surface. Was #e5484d at 3.65:1 — the
       app's last AA failure, and the only one that was a brand decision
       rather than an engineering one. Hue (358 deg) and saturation (0.75)
       are preserved exactly; only lightness moves, 0.590 -> 0.498, which is
       the smallest change that clears the threshold on both light surfaces.
       The dark theme keeps its own lighter coral below and is untouched:
       it already measured 0 failures, and one hex cannot serve two themes. */
    --brand-coral: #db1f25;          /* app-icon / accent coral (light theme) */
    --brand-airframe: var(--text);
    --brand-metrics: var(--brand-coral);

    /* -----------------------------------------------------------------------
     * Typography (docs/product/UI_COHESION_PASS.md C4a).
     *
     * Aligned with the sibling airShare app so the two read as one product
     * family. Its stack leads with `system-ui`, which is the correct modern
     * first choice: it resolves to the platform UI font everywhere, including
     * Linux and Android where `-apple-system` does nothing at all. The old
     * stack led with `-apple-system` and so fell through to a generic
     * sans-serif on non-Apple platforms.
     *
     * The scale is Tailwind's (what airShare gets by default) plus airShare's
     * own `2xs` step. It replaces TWENTY-FOUR ad-hoc sizes measured across
     * style.css and the templates — 0.55, 0.6, 0.65, 0.68, 0.7, 0.72, 0.75,
     * 0.78, 0.8, 0.82, 0.85, 0.88, 0.9, 0.95, 1, 1.1, 1.15, 1.25, 1.3, 1.35,
     * 1.5, 1.75, 1.8, 2 — differences of 0.02rem that no one can perceive but
     * which guarantee no two pages agree.
     *
     * 2xs is the floor. Everything below it (0.55-0.68rem, i.e. 9-11px) rounds
     * UP, because sub-11px body text is a legibility problem, not a design
     * choice.
     * --------------------------------------------------------------------- */
    --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, "SF Mono", Consolas, Menlo, monospace;

    --fs-2xs: 0.6875rem;   /* 11px — dense badges, table micro-labels */
    --fs-xs: 0.75rem;      /* 12px */
    --fs-sm: 0.875rem;     /* 14px — secondary text, most tables */
    --fs-base: 1rem;       /* 16px — body */
    --fs-lg: 1.125rem;     /* 18px */
    --fs-xl: 1.25rem;      /* 20px — h2 / wordmark */
    --fs-2xl: 1.5rem;      /* 24px */
    --fs-3xl: 1.875rem;    /* 30px — h1 */
}

[data-theme="dark"] {
    --bg: #0f172a;
    --surface: #1e293b;
    --text: #e2e8f0;
    --text-secondary: #94a3b8;
    --primary: #3b82f6;
    --primary-hover: #2563eb;
    --border: #334155;
    --success: #22c55e;
    --danger: #ef4444;
    --th-bg: #1e293b;
    --hover-bg: #253449;
    --bg-secondary: #253449;
    /* Dark ink, not white: the dark theme's --primary/--success/--danger fills
       are lighter, so white text on them drops below AA. */
    --on-accent: #0d1117;
    --primary-text: #93c5fd;   /* 8.6:1 on --surface; --primary is 3.98 */
    --sev-critical: #fca5a5;
    --sev-high: #fdba74;
    --sev-mod-high: #fcd34d;
    --sev-moderate: #fde047;
    --sev-low: #86efac;
    --link: #60a5fa;
    --link-hover: #93c5fd;
    --warning: #f59e0b;
    --warning-text: #fbbf24;
    --warning-bg: #451a03;
    --warning-border: #92400e;
    --success-text: #86efac;
    --success-bg: #052e16;
    --success-border: #166534;
    --danger-text: #fca5a5;
    --danger-bg: #450a0a;
    --danger-border: #7f1d1d;
    --info-text: #93c5fd;
    --info-bg: #172554;
    --info-border: #1e40af;
    --purple-text: #d8b4fe;
    --purple-bg: #3b0764;
    --purple-border: #7e22ce;
    /* On dark surfaces the raw hues are already light enough to read, so the
       text variants stay bright rather than inheriting the light-mode darks. */
    --accent-blue-text: #93c5fd;
    --accent-amber-text: #fcd34d;
    --accent-purple-text: #d8b4fe;
    --accent-purple: #c084fc;
    --accent-violet: #a78bfa;
    --accent-blue: #60a5fa;
    --accent-amber: #fbbf24;
    /* The same six hues re-stepped for the dark surface — a selected dark
       palette, not an automatic flip of the light one. Validated as a set
       against #1e293b. Green is mode-invariant (it already sits in the dark
       band). */
    --chart-1: #3987e5;
    --chart-2: #d95926;
    --chart-3: #199e70;
    --chart-4: #c98500;
    --chart-5: #d55181;
    --chart-6: #008300;
    --chart-7: #9085e9;
    --chart-8: #e66767;
    --primary-alpha: rgba(59, 130, 246, 0.15);
    /* Lighter coral for contrast on the dark surface. */
    --brand-coral: #ff6b70;
}

a { color: var(--link, var(--primary)); }
a:hover { color: var(--link-hover, var(--primary-hover)); }

body {
    font-family: var(--font-sans);
    background: var(--bg);
    color: var(--text);
    line-height: 1.6;
}

.container { max-width: 1920px; margin: 0 auto; padding: 0 1.5rem; overflow-x: auto; }


/* Navbar */
.navbar {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    padding: 0.75rem 0;
    margin-bottom: 2rem;
}
/* overflow: visible, overriding the generic .container's `overflow-x: auto`.
   That rule is right for a page with a wide table and wrong for a bar a menu
   has to escape: a non-visible overflow on EITHER axis makes the element a
   scroll container, the other axis computes to `auto` too, and an
   absolutely-positioned descendant is then clipped by it. So the account
   dropdown could not overlay the page — opening it scrolled the navbar
   instead, which is a menu you cannot read. */
.navbar .container { display: flex; align-items: center; gap: 1rem; overflow: visible; }
.nav-brand { font-weight: 700; font-size: var(--fs-xl); color: var(--primary); text-decoration: none; order: 0; margin-right: auto; }
.nav-links { order: 1; }
.nav-user-menu { order: 2; }
.nav-toggle-btn { order: 3; }
/* Two-tone brand wordmark: `airframe` + `Metrics`, the camel-case seam is the
   color seam. nowrap keeps the two halves from breaking apart on mobile. */
.wordmark { white-space: nowrap; letter-spacing: -0.01em; }
.wm-a { color: var(--brand-airframe); }
.wm-b { color: var(--brand-metrics); }
/* One line, always. Measured on /dashboard before this: the row needed 1324px
   and a 1232px window offers 1184, so "Saved Searches" and "Portal Inbox" broke
   INSIDE themselves — the items were never wrapping past each other, their
   labels were wrapping, because .nav-link computed `white-space: normal`. The
   bar rendered 51px tall instead of 26px.
   nowrap forbids the break; the tighter gap and label size buy back the ~185px
   that makes the row fit. Below NAV_COLLAPSE_BP (1200px) eleven links cannot
   fit at any legible size, so the hamburger takes over there rather than at
   768px — see the collapse block. */
.nav-links { display: flex; gap: 1rem; align-items: center; }
.nav-link { color: var(--text-secondary); text-decoration: none; font-weight: 500; font-size: var(--fs-sm); white-space: nowrap; }
.nav-link:hover { color: var(--primary-text); }
.nav-link[aria-current="page"] { color: var(--primary-text); font-weight: 700; }
/* CSS-only hamburger: the button is hidden on desktop and toggles .nav-links
   visibility on mobile via the adjacent checkbox (no JS needed). */
.nav-toggle-btn {
    display: none;
    font-size: var(--fs-2xl);
    line-height: 1;
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
    padding: 0.4rem 0.6rem;
    min-width: 44px;
    min-height: 44px;
}

/* Visible keyboard focus for all interactive elements (a11y). */
/* ─── Navbar collapse — NAV_COLLAPSE_BP ─────────────────────────────────
   1200px, not 768px. The operator bar carries eleven links; measured at
   --fs-sm with a 1rem gap the row needs ~1150px beside the brand and the
   avatar, so between 769px and ~1200px it had nowhere to go but a second
   line. Raising the threshold is what makes "one line" true at EVERY width
   rather than only on a wide monitor.

   The public override below moves with it, and that pairing is load-bearing:
   landing and sign-in ship no hamburger, so a collapse rule that applied to
   them without one would set display:none on the only "Sign in" link with no
   way to reveal it. Splitting these two blocks across different breakpoints
   makes that page unusable between 769px and 1199px. */
@media (max-width: 1199px) {
    .navbar .container { flex-wrap: wrap; gap: 0.5rem; }
    /* Row 1: brand · avatar · hamburger. Links collapse to a full-width row 2.
       `order` drives the visual layout; DOM order (checkbox before .nav-links)
       is preserved so the CSS-only toggle selector still works. */
    .nav-user-menu { order: 1; }
    .nav-toggle-btn { display: block; order: 2; }
    .nav-links {
        order: 3;
        display: none;
        flex-basis: 100%;
        flex-direction: column;
        align-items: stretch;
        gap: 0.25rem;
        margin-top: 0.5rem;
    }
    .nav-toggle:checked ~ .nav-links { display: flex; }
    .nav-links .nav-link, .nav-links .theme-toggle {
        padding: 0.6rem 0.5rem;
        border-radius: var(--radius);
        min-height: 44px;
        display: flex;
        align-items: center;
    }
    .nav-links .nav-link[aria-current="page"] { background: var(--hover-bg); }
    /* Public pages (landing, sign-in) carry two or three items and deliberately
       have no hamburger, so the collapse above would hide them with no way to
       reveal them. Keep this small set inline at every width instead. */
    .nav-links.nav-public {
        order: 1;
        display: flex;
        flex-basis: auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.75rem;
        margin-top: 0;
    }
    .nav-links.nav-public .nav-link {
        padding: 0.25rem 0.1rem;
        min-height: 0;
        font-size: var(--fs-sm);
        border-radius: 0;
    }
    .nav-links.nav-public .theme-toggle { font-size: var(--fs-xs); padding: 0.2rem 0.4rem; }
}

a:focus-visible, button:focus-visible, .nav-toggle-btn:focus-visible,
label:focus-visible, [tabindex]:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 3px;
}

/* Dark mode toggle */
.theme-toggle {
    background: none;
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.25rem 0.5rem;
    cursor: pointer;
    font-size: var(--fs-sm);
    color: var(--text-secondary);
}
.theme-toggle:hover { background: var(--hover-bg); }

/* Consolidated account menu: initials avatar + dropdown (identity, theme,
   settings, version, sign-out). Replaces the row of nav-tail controls. */
.nav-user-menu { position: relative; }
.nav-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--hover-bg);
    color: var(--text);
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 0.02em;
    cursor: pointer;
    padding: 0;
    text-transform: uppercase;
}
.nav-avatar:hover { border-color: var(--primary); color: var(--primary); }
.user-dropdown {
    position: absolute;
    right: 0;
    top: calc(100% + 8px);
    min-width: 208px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
    padding: 0.35rem;
    z-index: 60;
}
.user-dropdown[hidden] { display: none; }
.user-dropdown-head { padding: 0.4rem 0.6rem 0.5rem; }
.user-dropdown-name { font-weight: 600; font-size: var(--fs-sm); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 16rem; }
.user-dropdown-email { font-size: var(--fs-xs); color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 16rem; }
.user-dropdown-sep { height: 1px; background: var(--border); margin: 0.35rem 0.25rem; }
.user-dropdown-item {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    width: 100%;
    box-sizing: border-box;
    padding: 0.5rem 0.6rem;
    border: none;
    background: none;
    border-radius: 6px;
    font: inherit;
    font-size: var(--fs-sm);
    color: var(--text);
    text-decoration: none;
    text-align: left;
    cursor: pointer;
    min-height: 40px;
}
.user-dropdown-item:hover { background: var(--hover-bg); color: var(--primary); }
.user-dropdown-item[aria-current="page"] { color: var(--primary); font-weight: 600; }
.user-dropdown-item .udi-icon { width: 1.1rem; text-align: center; color: var(--text-secondary); flex: none; }
/* theme.js writes "◐ System" into this button; keep it aligned with the icon items. */
.theme-menu-item { padding-left: 0.6rem; }

/* Cards */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
/* Card rendered as a link (e.g. Activity → FAA shortcuts) */
a.card { text-decoration: none; color: inherit; display: block; transition: border-color 0.12s ease; }
a.card:hover { border-color: var(--primary); }
a.card .card-label { color: var(--primary-text); }
.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.25rem;
}
.card-label { font-size: var(--fs-sm); color: var(--text-secondary); margin-bottom: 0.25rem; }
.card-value { font-size: var(--fs-3xl); font-weight: 700; }

/* Tables */
table { width: 100%; border-collapse: collapse; background: var(--surface); border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); }
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
th { background: var(--th-bg); font-weight: 600; font-size: var(--fs-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; }
/* The pointer cursor belongs to headers that actually sort — 15 tables of 94.
   It was on `th` unconditionally, so every header in the app looked clickable
   and 79 tables' worth of them were not. `user-select: none` stops a
   double-click from selecting the label instead of sorting; it used to be set
   from JS, per header, which is why it never reached a header the script had
   not bound. */
table.sortable th[data-sort] { cursor: pointer; user-select: none; }
table.sortable th[data-sort]:hover { background: var(--hover-bg); color: var(--text); }

/* The at-rest half of the affordance. `cursor: pointer` and a `:hover`
   background answer "can I sort this?" for a mouse only — a phone has no hover
   at all, so on the surface where a 40-row table is hardest to read there was
   nothing to say the columns reorder.

   The ACTIVE half already exists and is not duplicated here: `table_sort.js`
   appends a `.sort-arrow` span (▲/▼) to the clicked header and strips it from
   the others. A second indicator via `::after` would render two arrows.

   A text decoration is the one cue with no box, which is the constraint rather
   than a nicety: 29 tables carry `.sortable`, three tables in the app are
   already wider than a 360px viewport, and when `measure_table_widths.py`
   broke the fixable ones down by column, in three of four the widest thing in
   the table was the <th> itself. A reserved glyph on every sortable header
   would push that count up; an underline cannot. */
table.sortable th[data-sort] {
    text-decoration: underline dotted;
    text-decoration-thickness: 1px;
    text-underline-offset: 3px;
}
table.sortable th[data-sort]:hover { text-decoration-style: solid; }

tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--hover-bg); }
.clickable-row { cursor: pointer; }

/* Forms and Filters */
.filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: end;
    margin-bottom: 1.5rem;
    padding: 1rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.filter-group { display: flex; flex-direction: column; gap: 0.25rem; }
.filter-group > label { font-size: var(--fs-xs); font-weight: 600; color: var(--text-secondary); }
/* The one form-control rule (C2 follow-up, v0.1.118).
 *
 * `textarea` used to appear NOWHERE in this file, so every textarea in the app
 * was styled by hand in a style attribute — which is why the control paddings
 * had drifted into three near-identical values (0.35 / 0.4 / 0.3rem) and two
 * different radii (an inline 4px against this rule's 6px). Controls had no
 * component; 131 inline style attributes were re-implementing one.
 *
 * `font-family: inherit` is part of the rule for the same reason: 23 controls
 * declared it inline and the rest inherited the UA's system font, so the app
 * rendered its inputs in two different typefaces.
 */
input:not([type="checkbox"]), select, textarea {
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: var(--fs-sm);
    font-family: inherit;
    background: var(--surface);
    color: var(--text);
}
input:not([type="checkbox"]):focus, select:focus, textarea:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); }

/* Compact control — the single replacement for those three hand-written
 * paddings.
 *
 * It MUST be written at element+class specificity. The base rule above is
 * (0,1,1), not (0,0,1): `:not()` takes the specificity of its argument and
 * `[type="checkbox"]` is an attribute selector. A bare `.control-sm` is
 * (0,1,0) and would silently lose to it — which is exactly the cascade that
 * shifted the engine-analysis form by 4px in C2 and is why
 * `inline_style_sweep.py` refuses to touch controls at all. At (0,1,1) this
 * ties the base rule and wins on source order, being defined after it.
 */
input.control-sm, select.control-sm, textarea.control-sm { padding: 0.25rem 0.5rem; }
/* Control WIDTHS, as part of the same component rather than a style attribute
   per field. The settings forms had been sizing every numeric input inline
   (`style="width:100px"` and friends), which is the C2 finding in miniature:
   controls had no component, so each field re-declared one. Three sizes cover
   every numeric/select field in the app's forms. */
input.control-num, select.control-num { width: 100px; }
input.control-num-sm, select.control-num-sm { width: 80px; }
input.control-num-md, select.control-num-md { width: 110px; }  /* 5-digit money, e.g. engine overhaul */
input.control-num-lg, select.control-num-lg { width: 140px; }

/* Multi-select dropdown */
.multi-select { position: relative; min-width: 140px; }
.multi-select-toggle {
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: var(--fs-sm);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.5rem;
    white-space: nowrap;
}
.multi-select-toggle:hover { border-color: var(--primary); }
.multi-select-toggle .arrow { font-size: var(--fs-2xs); color: var(--text-secondary); }
.multi-select-dropdown {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 100;
    min-width: 100%;
    max-height: 240px;
    overflow-y: auto;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    margin-top: 2px;
    padding: 0.25rem 0;
}
.multi-select.open .multi-select-dropdown { display: block; }
.multi-select-dropdown label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.75rem;
    cursor: pointer;
    font-size: var(--fs-sm);
    font-weight: normal;
    color: var(--text);
    white-space: nowrap;
}
.multi-select-dropdown label:hover { background: var(--hover-bg); }
.multi-select-dropdown input[type="checkbox"] { margin: 0; width: auto; }

/* Buttons */
/* A row of primary actions that wraps with even spacing on desktop and stacks
   full-width on phones (see the mobile override). */
.stacked-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border: none;
    border-radius: 6px;
    font-size: var(--fs-sm);
    font-weight: 500;
    cursor: pointer;
    /* A button label must never break character-by-character in a narrow mobile
       cell (mobile `td { overflow-wrap: anywhere }`). */
    white-space: nowrap;
    text-decoration: none;
    transition: background 0.15s;
}
/* --on-accent, not `white`. The token exists precisely for this and said so:
   "the dark theme's fills are lighter, so white text on them drops below AA".
   The buttons were hardcoding white anyway, and measured it: white on the dark
   theme's --success is 2.28:1, on --primary 3.68:1, on --danger 3.76:1 — all
   under the 4.5 AA needs. In the light theme --on-accent IS white, so nothing
   there changes.
       python scripts/measure_contrast.py */
.btn-primary { background: var(--primary); color: var(--on-accent); }
.btn-primary:hover { background: var(--primary-hover); }
.btn-success { background: var(--success); color: var(--on-accent); }
.btn-danger { background: var(--danger); color: var(--on-accent); }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { background: var(--hover-bg); }
.btn-sm { padding: 0.35rem 0.75rem; font-size: var(--fs-xs); }

/* Detail page */
.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 1rem;
    margin-bottom: 2rem;
}
.detail-item { padding: 0.5rem 0; }
.detail-label { font-size: var(--fs-xs); color: var(--text-secondary); font-weight: 600; }
.detail-value { font-size: var(--fs-lg); }
/* Weight & Balance table */
.wb-card { padding: 0; margin-bottom: 2rem; overflow: hidden; }
.wb-title { font-size: var(--fs-sm); font-weight: 700; margin: 0; padding: 0.75rem 1rem 0.5rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; }
.wb-table { margin: 0; border: none; border-radius: 0; }
.wb-table th, .wb-table td { white-space: nowrap; padding: 0.5rem 0.75rem; }
.wb-table td { font-size: var(--fs-base); }
.wb-row-base td { font-weight: 600; border-bottom: 2px solid var(--border); }
.wb-deduct { color: var(--text-secondary); }
.wb-payload { font-weight: 600; font-variant-numeric: tabular-nums; }

/* Tabular figures wherever numbers stack in a column (UI_COHESION_PASS C4).
   Digits then share one advance width, so thousands separators and decimal
   points line up down a table and a price column reads as a column rather
   than ragged text. `.text-right` is this codebase's marker for a numeric
   cell; the feature only affects digits, so it is inert on the few
   right-aligned cells that hold buttons or labels instead. */
td.text-right, th.text-right, .card-value, .text-center.wb-payload {
    font-variant-numeric: tabular-nums;
}

/* Section headers */
h1 { font-size: var(--fs-3xl); margin-bottom: 1rem; }
h2 { font-size: var(--fs-xl); margin-bottom: 0.75rem; color: var(--text); }
.section { margin-bottom: 2rem; }

/* Badges */
.badge {
    display: inline-block;
    padding: 0.2rem 0.6rem;
    border-radius: 12px;
    font-size: var(--fs-xs);
    font-weight: 600;
    /* Never let a short badge label ("8 Docs", "G6") break character-by-character
       inside a narrow mobile table cell (mobile `td { overflow-wrap: anywhere }`).
       `overflow-wrap: normal` is what actually guarantees that — it forbids
       breaking inside a word. `nowrap` also forbade breaking at spaces, which is
       fine for "8 Docs" but not for the scan-status badge, whose partial-run
       label reads "pending — 80 of 120 completed, 40 rescheduled" and alone
       measured 366px, wider than the phone (UI_COHESION_PASS.md C6). */
    white-space: normal;
    overflow-wrap: normal;
}
.badge-green { background: #dcfce7; color: #166534; }
.badge-red { background: #fee2e2; color: #991b1b; }
.badge-blue { background: #dbeafe; color: #1e40af; }
.badge-gray { background: #f1f5f9; color: #475569; }
.badge-orange { background: #ffedd5; color: #9a3412; }
.badge-purple { background: #f3e8ff; color: #6b21a8; }
.badge-yellow { background: #fef9c3; color: #854d0e; }

[data-theme="dark"] .badge-green { background: #14532d; color: #86efac; }
[data-theme="dark"] .badge-red { background: #450a0a; color: #fca5a5; }
[data-theme="dark"] .badge-blue { background: #1e3a5f; color: #93c5fd; }
[data-theme="dark"] .badge-gray { background: #334155; color: #cbd5e1; }
[data-theme="dark"] .badge-orange { background: #431407; color: #fdba74; }
[data-theme="dark"] .badge-purple { background: #3b0764; color: #d8b4fe; }
[data-theme="dark"] .badge-yellow { background: #422006; color: #fde047; }

/* Generation-specific badges — colors match chart genColors */
.badge-gen-g1 { background: #dbeafe; color: #1e40af; }  /* Blue */
.badge-gen-g2 { background: #fee2e2; color: #991b1b; }  /* Red */
.badge-gen-g3 { background: #dcfce7; color: #166534; }  /* Green */
.badge-gen-g5 { background: #fef3c7; color: #92400e; }  /* Amber */
.badge-gen-g6 { background: #ede9fe; color: #5b21b6; }  /* Purple */
.badge-gen-g7 { background: #cffafe; color: #0e7490; }  /* Cyan */
.badge-gen-g7p { background: #fce7f3; color: #9d174d; } /* Pink */
[data-theme="dark"] .badge-gen-g1 { background: #1e3a5f; color: #93c5fd; }
[data-theme="dark"] .badge-gen-g2 { background: #450a0a; color: #fca5a5; }
[data-theme="dark"] .badge-gen-g3 { background: #14532d; color: #86efac; }
[data-theme="dark"] .badge-gen-g5 { background: #451a03; color: #fcd34d; }
[data-theme="dark"] .badge-gen-g6 { background: #2e1065; color: #c4b5fd; }
[data-theme="dark"] .badge-gen-g7 { background: #083344; color: #67e8f9; }
[data-theme="dark"] .badge-gen-g7p { background: #500724; color: #f9a8d4; }

/* Dark mode: fix select/option readability */
[data-theme="dark"] select option {
    background: var(--surface);
    color: var(--text);
}
[data-theme="dark"] select option:checked {
    background: var(--primary);
    color: #fff;
}

/* Trend indicators */
.trend-down { color: var(--success); }
.trend-up { color: var(--danger-text); }

/* Utilities */
.text-right { text-align: right; }
.text-center { text-align: center; }
/* Keep a value on one line even inside a narrow mobile cell — overrides the
   mobile `td { overflow-wrap: anywhere }` that otherwise breaks "$1,349,000"
   mid-number. Higher specificity than the bare `td` rule, so no !important. */
.nowrap { white-space: nowrap; overflow-wrap: normal; }
.mb-1 { margin-bottom: 1rem; }
.mt-1 { margin-top: 1rem; }
.flex { display: flex; }
.flex-between { display: flex; justify-content: space-between; align-items: center; }
.hidden { display: none; }
.cursor-pointer { cursor: pointer; }
.fw-600 { font-weight: 600; }

/* ---------------------------------------------------------------------------
 * Utility layer (docs/product/UI_COHESION_PASS.md C2).
 *
 * Sized to the actual problem rather than to a guess: a census of the 1,161
 * inline `style="…"` attributes in app/templates found they are overwhelmingly
 * ATOMIC declarations, not component patterns — text-align:right x84,
 * display:flex x103, font-weight:600 x92, font-size: var(--fs-2xs) x51,
 * margin-top:0.5rem x26. A card()/button() macro library would have absorbed
 * about a tenth of them; these classes absorb the bulk.
 *
 * Extends the naming already in use above (.text-right, .mb-1, .fw-600,
 * .text-sm) instead of introducing a second vocabulary. One seam to know
 * about: the pre-existing `-1` suffix means 1rem, so the finer steps added here
 * are named xs/sm/md rather than renumbering and breaking every existing use.
 *
 * These exist to DELETE inline styles. A new inline style that any of these
 * covers is a regression — tests/test_inline_style_budget.py ratchets the count.
 * ------------------------------------------------------------------------- */

/* Spacing — the four values that actually recur. */
.mt-0 { margin-top: 0; }
.mt-xs { margin-top: 0.25rem; }
.mt-sm { margin-top: 0.5rem; }
.mt-md { margin-top: 0.75rem; }
.mb-0 { margin-bottom: 0; }
.mb-xs { margin-bottom: 0.25rem; }
.mb-sm { margin-bottom: 0.5rem; }
.mb-md { margin-bottom: 0.75rem; }
.m-0 { margin: 0; }

/* Second pass (C2, v0.1.117). Every declaration below was ALREADY canonical
   inline — an on-scale length, a design token, or a keyword — so each of these
   is a pure move from the style attribute to a class with no value change, and
   the whole batch is verifiable by `pixel_diff.py check` reporting zero moved
   pages. Nothing here snaps an off-scale value to the scale; that is a
   deliberate visual change and is not part of this sweep.
   The `lg`/`xl` steps continue the xs/sm/md/-1 naming rather than starting a
   second vocabulary. */
.ml-xs { margin-left: 0.25rem; }
.ml-sm { margin-left: 0.5rem; }
.mt-lg { margin-top: 1.5rem; }
.mt-xl { margin-top: 2rem; }
.mb-lg { margin-bottom: 1.5rem; }

/* Padding — `.p-1` matches the `padding:1rem` panel seen 14 times. */
.p-0 { padding: 0; }
.p-xs { padding: 0.25rem; }
.p-sm { padding: 0.5rem; }
.p-md { padding: 0.75rem; }
.p-1 { padding: 1rem; }
.p-2 { padding: 2rem; }
.pt-md { padding-top: 0.75rem; }
/* The recurring card/list-row inset — a symmetric pair, not a per-value class. */
.p-md-1 { padding: 0.75rem 1rem; }

/* Type scale. `.text-sm` (0.85rem) is defined below with the text utilities;
   these fill in the smaller steps templates reach for constantly. */
.text-2xs { font-size: var(--fs-2xs); }
.text-xs { font-size: var(--fs-xs); }
.text-base { font-size: var(--fs-base); }
.fw-500 { font-weight: 500; }
.fw-700 { font-weight: 700; }
.text-left { text-align: left; }
.uppercase { text-transform: uppercase; letter-spacing: 0.04em; }
.text-lg { font-size: var(--fs-lg); }
.fw-normal { font-weight: normal; }
/* `color:inherit` on a link/span that must take the surrounding colour. */
.text-inherit { color: inherit; }

/* Flex — `.flex` and `.flex-between` are above; these complete the set. */
.flex-col { display: flex; flex-direction: column; }
.flex-wrap { flex-wrap: wrap; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.items-baseline { align-items: baseline; }
.justify-end { justify-content: flex-end; }
.flex-1 { flex: 1; }
.inline-flex { display: inline-flex; }
.gap-0 { gap: 0; }
.gap-2 { gap: 2rem; }
.gap-xs { gap: 0.25rem; }
.gap-sm { gap: 0.5rem; }
.gap-md { gap: 0.75rem; }
.gap-1 { gap: 1rem; }
.inline { display: inline; }
.inline-block { display: inline-block; }

/* Borders and surfaces — `border:none` alone appears 120 times (table cells
   that opt out of the default rule), and the 1px hairline 74 times. */
.bd-0 { border: none; }
.bd { border: 1px solid var(--border); }
.bd-t { border-top: 1px solid var(--border); }
.bd-b { border-bottom: 1px solid var(--border); }
.bd-t-2 { border-top: 2px solid var(--border); }
.bd-danger { border: 1px solid var(--danger-border); }
.rounded { border-radius: var(--radius); }
.rounded-sm { border-radius: 4px; }
.bg-surface { background: var(--surface); }
.bg-secondary { background: var(--bg-secondary); }
.bg-transparent { background: transparent; }
.bg-danger { background: var(--danger-bg); }
.bg-warning { background: var(--warning-bg); }

/* Content wrapping — `white-space:pre-wrap` appears 17 times on log/notes
   blocks that must preserve author line breaks. */
.pre-wrap { white-space: pre-wrap; }
.w-full { width: 100%; }
.block { display: block; }
/* The counterpart to .col-collapse, for the other half of the same trade.
   Collapsing a column removes a fact from the phone entirely, which C6 forbids
   when that column is the only place it appears. This lets the fact move INTO a
   surviving cell instead of vanishing: tag the inline copy .mobile-only and it
   shows exactly where the column is hidden, so neither viewport shows it twice.

   Written as a min-width HIDE, not a max-width show, for two reasons. It never
   has to guess a display value — below the breakpoint the element keeps its
   own (a .badge stays inline-block). And it must sit AFTER .badge (line ~532):
   both selectors are (0,1,0), so source order decides, and declaring it earlier
   let .badge's `display:inline-block` win and leak the badge onto desktop. That
   is the same equal-specificity trap C2 hit three times. */
@media (min-width: 769px) { .mobile-only { display: none; } }
.grid { display: grid; }
.justify-between { justify-content: space-between; }
.font-inherit { font-family: inherit; }
.no-underline { text-decoration: none; }
.relative { position: relative; }
.absolute { position: absolute; }
.overflow-hidden { overflow: hidden; }
.overflow-x-auto { overflow-x: auto; }
.list-none { list-style: none; }
.opacity-80 { opacity: 0.8; }

/* Semantic text colours. These were already `color:var(--token)` written
   inline; moving them to classes is a pure refactor, not a colour change. */
.text-ink { color: var(--text); }
.text-danger { color: var(--danger-text); }
.text-success { color: var(--success); }
.text-primary { color: var(--primary-text); }
.text-warning { color: var(--warning); }
/* The contrast-safe variant of the same hue: --warning is the accent (#f59e0b
   in both themes), --warning-text flips dark-on-light / light-on-dark so it
   stays readable as body text. Both are in the templates; they are not
   interchangeable. */
.text-warning-strong { color: var(--warning-text); }
/* Chart canvas wrapper — Chart.js needs a positioned, fixed-height parent when
   maintainAspectRatio:false. `--chart-h` lets a page override the height without
   a per-canvas inline style (default matches the engine-analysis charts). */
.chart-box { position: relative; height: var(--chart-h, 300px); }
.chart-box-lg { height: 350px; }

/* A review queue's action cell: preview, then publish or reject, each a form
   with its own note field. The 220px floor keeps the two note inputs from
   collapsing into slivers on a desktop — but it was an inline style on the
   cell, so it applied at 360px too, where it made the column 235px against a
   154px content floor and was single-handedly most of what pushed both
   /ai-review tables past the viewport. The floor now applies only where there
   is room for it. */
.review-actions { display: flex; flex-direction: column; gap: 0.4rem; }
@media (min-width: 769px) { .review-actions { min-width: 220px; } }

/* One review action: a note field and the button that submits it. Side by side
   they measure 207px against a 126px floor — the note input alone — because a
   flex ROW sizes to the wider of "both on one line" and "wrapped", and the
   table column then sizes to that. Stacking them on a phone is what gets both
   queues under 360px; `flex-wrap` alone never fires, since nothing is forcing
   the row narrow in the first place. */
.review-form { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.review-form .control-sm { flex: 1; min-width: 120px; }
@media (max-width: 768px) {
    .review-form { flex-direction: column; }
    .review-form .control-sm { min-width: 0; }
}

/* Sort indicators */
th .sort-arrow { margin-left: 0.25rem; font-size: var(--fs-2xs); }
/* Value Index cell: the index restated in dollars, under the badge. Tight
   line-height so it fits inside the existing (photo-driven) row height rather
   than making every row taller. */
.risk-delta {
    font-size: var(--fs-xs); line-height: 1.15; margin-top: 0.1rem;
    color: var(--text-secondary); font-variant-numeric: tabular-nums;
}

/* Responsive — mobile card layout */
@media (max-width: 768px) {
    /* Filters pair up on a phone rather than stacking one per row.
       One-per-row is right for two controls and wrong for seven: on
       /faa/owners it built a 512px wall of form fields, so the page opened
       903px above its first row of data — no data on screen at all. Two
       columns halves the row count, and short selects (State, Airworthy)
       read better side by side than in a seven-deep stack.

       Grid, not the old flex column, because pairing needs equal track widths
       — a flex row with wrapping sizes each group to its content, so "State"
       and "Airworthy" would land on the same line at two different widths.

       Anything that needs the full width opts out with .filter-group-wide:
       free-text inputs (a placeholder truncates at half width) and the
       actions row. Measure the result, don't assume it:
           python scripts/measure_above_fold.py */
    .filters {
        display: grid;
        grid-template-columns: 1fr 1fr;
        align-items: end;
        gap: 0.6rem;
    }
    .filters .filter-group { width: 100%; min-width: 0; }
    .filters .filter-group-wide { grid-column: 1 / -1; }
    .filters .filter-group > label { text-align: left; }
    .filters .filter-group input:not([type="checkbox"]),
    .filters .filter-group select,
    .filters .filter-group .multi-select { width: 100%; min-width: 0; }
    /* Column-priority for wide tables: cells tagged .col-collapse are hidden
       on phones so the 3-4 highest-value columns fit without an inner
       horizontal scroll (the row links through to the full detail). Same
       pattern the listings table established. */
    .col-collapse { display: none; }
    /* 24px gutters cost 13% of a 360px screen, and .container clips with
       overflow-x:auto — so a table that FITS the viewport still loses its
       right-hand column to the indent, without the document ever scrolling
       sideways. That is how /faa/owners hid a cut-off Airworthy column from
       both the layout suite and (until v0.1.149) measure_table_widths.py.

       12px, not 8px. Measured against the corpus, the clipped-table count
       goes 15 → 12 at 12px and 15 → 10 at 8px; the extra two are not worth
       halving the page's breathing room, and the four still clipped after
       that (settings 922px, outreach 602px, compare 543px, listing-detail
       376px) are column-count problems no gutter can reach. Re-measure
       before changing this number:
           python scripts/measure_table_widths.py */
    .container { padding: 0 0.75rem; }
    /* Denser cells on phones — the desktop padding alone pushed 4-column
       tables past 390px.

       0.45rem horizontal, from 0.6rem. Six cells of a three-column row spend
       their padding twice over, so 2.4px per side is ~14px off the table —
       which happens to be the whole distance five tables were short. Measured,
       not guessed: clipped tables go 12 → 7, clearing watchlist (8px over),
       activity-airport (5), diagnostics (2), engine-results (15) and
       deployments (14) exactly, and taking listing-detail's worst from 49 to
       35 and portal-inbox's from 25 to 10.

       What survives is not reachable by padding: settings (541px) and compare
       (195px) are deliberately wide — a form editor with an Actions column and
       a side-by-side matrix — and outreach (226px) has no col-collapse at all.
           python scripts/measure_table_widths.py */
    th, td { padding: 0.5rem 0.45rem; }
    /* Header labels drive most residual table width on phones — let them wrap.
       Wrapping alone stops at the longest WORD, though, and that is what was
       left: measured per column, the widest thing in the column was the <th>
       itself in three of the four remaining clipped tables — "GENERATION" at
       123px against 73px of data beneath it, "CRITERIA" at 92px, "CURRENT" at
       93px. A one-word header cannot wrap, so it sets a floor the data never
       asked for.

       Uppercase plus 0.05em tracking is what makes those words expensive: the
       tracking alone adds ~5%, and at --fs-sm each is carrying 14px glyphs to
       label a column of 12px numbers. Dropping to --fs-xs and easing the
       tracking on phones only costs nothing legible — the label is still bold,
       uppercase and coloured — and it is one rule rather than four bespoke
       header rewrites.
           COLS=1 python scripts/measure_table_widths.py */
    th { white-space: normal; font-size: var(--fs-xs); letter-spacing: 0.02em; }
    /* `break-word`, NOT `anywhere` — the difference is the whole rule.
       Both break a word that genuinely cannot fit its line. Only `anywhere`
       also lets an intra-word break count toward the cell's MIN-CONTENT width,
       which is what lets the table squeeze a column narrower than its longest
       word and split it for no reason but to save a few pixels.

       That is what it was doing. Measured at 360px against the corpus, the
       `anywhere` default split seven ordinary values mid-word — "Autopilot",
       "SR22T", "(2002–2024)", "N512AM", "$1,150,000" — and the last two are
       exactly the tail number and the money that the exemption list below was
       written to protect. They slipped through because that list is an
       allow-list of classes, and those two cells carry none of them.

       An allow-list cannot be the mechanism here: it has to be re-derived every
       time a table is added, and a cell that is forgotten fails silently and
       ugly. Flipping the default inverts the burden — mid-word breaking is now
       opted INTO, by the two column types that genuinely need it (below), and
       every other cell is safe without anyone having to notice it.

       Cost, measured: two tables gain 3-4px and cross 360px. Both are handled
       by the opt-in, and tests/test_responsive_layouts.py pins the result. */
    td { overflow-wrap: break-word; }
    /* The opt-in, and it currently has exactly ONE user: the email column on
       /access. Reach for it only when a cell holds a single long token with NO
       break opportunity in it at all — no space, no hyphen, no slash — so
       refusing to break means the token itself pushes the column off the phone.
       An email is the textbook case; a 40-character trust name is not, because
       it has spaces and wraps on its own.

       Resist applying it to a column that merely looks long. Both /diagnostics
       ("generation") and /outreach (registrant names) were given it on first
       pass and then measured: neither table needed it, and both read visibly
       worse with it ("genera/tion", "BENEMA/RYD"). /outreach is already a
       deliberate horizontal-scroll keep at 360px, so the 24px it bought changed
       nothing a reader can perceive.

       Apply it to the COLUMN, not the table — the point is that the rest of the
       row keeps its words whole. */
    td.break-anywhere { overflow-wrap: anywhere; }
    /* Kept from the C6 follow-up, and still stronger than the new default:
       `overflow-wrap: normal` forbids breaking inside a word at all, where
       `break-word` still breaks one that cannot fit. `white-space: nowrap`
       additionally forbade breaking at SPACES, which is a different and much
       more expensive promise: it made every multi-word header ("Avg
       Calibrated", "Est Hrs/90d") an unbreakable run and pushed five tables past
       the viewport on their own. */
    td .row-link, td.text-right, th.text-right, td.nowrap { white-space: normal; overflow-wrap: normal; }
    table { font-size: var(--fs-sm); }
    /* Section headings fit the phone width. */
    h2 { font-size: var(--fs-lg); }
    /* Two-up breakdown cards (By Generation / By Model) stack full-width so the
       Count column isn't clipped. */
    .dash-breakdown { grid-template-columns: 1fr; }
    /* Touch ergonomics: comfortable minimum hit area for the small controls
       that concentrate in table rows, and a 12px floor on the tiny-font
       accents so nothing needs pinch-zoom to read. */
    .btn-sm { min-height: 40px; display: inline-flex; align-items: center; }
    summary { min-height: 40px; display: flex; align-items: center; gap: 0.4rem; }
    .text-sm { font-size: var(--fs-xs) !important; }
    small, .text-xs { font-size: var(--fs-xs) !important; }
    .wb-table th, .wb-table td { padding: 0.35rem 0.4rem; font-size: var(--fs-sm); white-space: normal; }
    /* Nav collapses to a hamburger: brand + toggle on one row, links stack
       full-width below and only show when the toggle is checked. */
    /* 2rem of dead space under the navbar costs a fifth of a phone screen
       before any content — the page title reads as the separator on its own. */
    .navbar { margin-bottom: 1rem; }
    h1 { font-size: var(--fs-xl); margin-bottom: 0.6rem; }
    /* Row 1: brand · avatar · hamburger. Links collapse to a full-width row 2.
       `order` drives the visual layout; DOM order (checkbox before .nav-links)
       is preserved so the CSS-only toggle selector still works. */
    .user-dropdown { min-width: 240px; }
    /* Stat tiles: 2-3 compact tiles per row on phones instead of one giant
       full-width card each (which pushed a handful of counts down a long scroll). */
    .cards { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.6rem; margin-bottom: 1rem; }
    .cards .card { padding: 0.75rem 0.85rem; }
    .cards .card-value { font-size: var(--fs-xl); }
    .cards .card-label { font-size: var(--fs-xs); }
    .flex-between { flex-direction: column; gap: 0.5rem; align-items: flex-start; }

    /* Convert table to cards on mobile */
    .mobile-cards thead { display: none; }
    .mobile-cards tbody tr {
        display: block;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        margin-bottom: 0.75rem;
        padding: 1rem;
    }
    .mobile-cards tbody td {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        align-items: baseline;
        gap: 0.15rem 0.75rem;
        padding: 0.3rem 0;
        border-bottom: none;
    }
    .mobile-cards tbody td::before {
        content: attr(data-label);
        font-weight: 600;
        font-size: var(--fs-xs);
        color: var(--text-secondary);
        white-space: nowrap;          /* keep "Engagement" on one line */
    }
    /* A cell whose value is an action (button/form) puts it on its own full-width
       line under the label, instead of overlapping the label when it can't fit. */
    .mobile-cards tbody td .btn,
    .mobile-cards tbody td form {
        flex: 1 0 100%;
    }
    .mobile-cards tbody td .btn {
        width: 100%;
        box-sizing: border-box;
        justify-content: center;
        text-align: center;
    }

    /* Stacked primary actions (portal home): full-width, no overlap. */
    .stacked-actions { display: flex; flex-direction: column; gap: 0.5rem; }
    .stacked-actions .btn { width: 100%; box-sizing: border-box; text-align: center; }
}

/* Global error toast (HTMX failures) */
.toast {
    position: fixed;
    bottom: 1rem;
    left: 50%;
    transform: translateX(-50%);
    max-width: calc(100% - 2rem);
    background: var(--danger);
    color: #fff;
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
    z-index: 1000;
    display: flex;
    gap: 0.75rem;
    align-items: center;
}
.toast[hidden] { display: none; }
.toast button {
    background: rgba(255, 255, 255, 0.2);
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.5);
    border-radius: 4px;
    padding: 0.25rem 0.6rem;
    cursor: pointer;
    min-height: 32px;
}

/* Text utilities */
.text-secondary { color: var(--text-secondary); }
.text-sm { font-size: var(--fs-sm); }

/* Spinner animation */
.spinner {
    display: inline-block;
    width: 0.65em;
    height: 0.65em;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    animation: spin 0.75s linear infinite;
    vertical-align: middle;
    margin-right: 0.25em;
}
@keyframes spin {
    to { transform: rotate(360deg); }
}

/* Scan log output */
.log-output {
    background: #1e1e2e;
    color: #cdd6f4;
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    padding: 1rem;
    border-radius: var(--radius);
    max-height: 300px;
    overflow-y: auto;
    margin-top: 0.5rem;
    white-space: pre-wrap;
    word-break: break-word;
    line-height: 1.5;
}

/* Settings: collapsible groups — the page is an index of sections, each one
   click away; deep links (#monthly-cost-assumptions) auto-open via JS. */
details.settings-group > summary { cursor: pointer; padding: 0.25rem 0; }
details.settings-group > summary h2 { display: inline-block; margin: 0; vertical-align: middle; }
details.settings-group > summary:hover h2 { color: var(--primary); }
details.settings-group[open] > summary { margin-bottom: 0.75rem; }
.filters-plain { margin-bottom: 0; background: transparent; border: none; padding: 0; }
.cell-plain-r { border: none; padding: 0.25rem 1rem 0.25rem 0; }
.cell-plain { border: none; padding: 0.25rem 0; }
.cell-tight { border: none; padding: 0.15rem 0; }
.input-locked { background: var(--bg-secondary); color: var(--text-secondary); cursor: not-allowed; }

/* Collapsible issue/report groups (diagnostics) */
details.issue-group { margin-bottom: 0.75rem; }
details.issue-group > summary {
    cursor: pointer;
    font-weight: 600;
    padding: 0.5rem 0.25rem;
    list-style-position: inside;
}
details.issue-group > summary:hover { color: var(--primary); }
details.issue-group > summary .section-heading { font-size: var(--fs-lg); }
details.issue-group > table,
details.issue-group > div { margin-top: 0.5rem; }

/* Compare page: pin the label column while the aircraft columns scroll
   horizontally. The global `table { overflow: hidden }` (border-radius
   clipping) silently defeats sticky cells — the table itself becomes the
   sticky containing block and it never scrolls. That was why the earlier
   sticky attempt "did not pin"; this table opts out of the clipping. */
.compare-table { overflow: visible; }
.compare-table td:first-child,
.compare-table th:first-child {
    position: sticky;
    left: 0;
    background: var(--surface);
    z-index: 1;
    border-right: 1px solid var(--border);
}

/* Row link: the real <a> inside a clickable table row — keyboard/screen-reader
   navigable where the tr onclick is not. */
.row-link { color: inherit; text-decoration: none; font-weight: 600; }
.row-link:hover { color: var(--primary); }

/* The expanded flight-detail row on engine-results. Its 1rem side padding
   was inline, and on a phone it was the last 9px keeping all three panels off
   the screen — the tables inside sit in a cell inside another table, so the
   cell's own padding is charged twice over. */
.flight-detail-cell { padding: 0.75rem 1rem; background: var(--hover-bg); }
@media (max-width: 768px) { .flight-detail-cell { padding: 0.6rem 0.35rem; } }

/* The contribution review form (operator engagement page). Its 230px
   min-width was inline, and on a phone it held the whole table 215px past the
   screen — a floor set for a desktop column, applied at every width. The
   controls wrap fine without it. */
.contribution-review { display: flex; flex-direction: column; gap: 0.3rem; min-width: 230px; }
@media (max-width: 768px) { .contribution-review { min-width: 0; } }

/* Empty state — shown in place of a chart/table when there is no data yet */
.empty-state {
    color: var(--text-secondary);
    text-align: center;
    padding: 2rem 1rem;
    font-size: var(--fs-base);
}

/* Notification banner */
.notification-banner {
    background: #fef3c7;
    border-bottom: 1px solid #f59e0b;
    padding: 0.75rem 1.5rem;
    display: flex;
    align-items: center;
    gap: 1rem;
    font-size: var(--fs-sm);
}
[data-theme="dark"] .notification-banner {
    background: #451a03;
    border-color: #92400e;
}
/* Success variant — confirmations (watchlist add/remove) read green, not amber. */
.notification-banner-success {
    background: #dcfce7;
    border-color: #22c55e;
}
[data-theme="dark"] .notification-banner-success {
    background: #052e16;
    border-color: #166534;
}
/* On phones the flex row squeezed the message to one word per line beside the
   buttons; let the message take the full width and the buttons wrap below. */
@media (max-width: 768px) {
    .notification-banner { flex-wrap: wrap; row-gap: 0.5rem; }
    .notification-banner > span { flex: 1 1 100%; }
}

/* Filter banner (dashboard saved search) */
.filter-banner {
    padding: 0.5rem 1rem;
    margin-bottom: 1rem;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: #f0f9ff;
    border-left: 3px solid #2563eb;
    border-radius: 4px;
    color: #1e293b;
}
[data-theme="dark"] .filter-banner {
    background: #1e3a5f;
    border-left-color: #60a5fa;
    color: #e2e8f0;
}

/* HTMX loading indicator */
.htmx-indicator { display: none; opacity: 0; transition: opacity 0.2s ease-in; }
.htmx-request .htmx-indicator { display: inline; opacity: 1; }
.htmx-request.htmx-indicator { display: inline; opacity: 1; }

/* Progress bar */
.progress-bar-container {
    width: 100%;
    height: 6px;
    background: var(--border);
    border-radius: 3px;
    overflow: hidden;
}
.progress-bar {
    height: 100%;
    background: var(--primary);
    border-radius: 3px;
    transition: width 0.5s ease;
    min-width: 2px;
}

/* Sparkline */
.sparkline {
    display: inline-flex;
    align-items: flex-end;
    gap: 2px;
    height: 20px;
}
.sparkline-bar {
    width: 6px;
    min-height: 2px;
    background: var(--primary);
    border-radius: 1px;
    opacity: 0.7;
}
.sparkline-bar:last-child {
    opacity: 1;
}
[data-theme="dark"] .sparkline-bar {
    background: #60a5fa;
}

/* HTMX swap transitions */
#listings-table-container { transition: opacity 0.15s ease-in-out; }
#listings-table-container.htmx-swapping { opacity: 0.5; }
#scan-history { transition: opacity 0.15s ease-in-out; }
#scan-history.htmx-swapping { opacity: 0.5; }

/* ─── Engine Analysis: Tabs ─────────────────────────────────────────── */
.tab-nav {
    display: flex;
    gap: 0;
    border-bottom: 2px solid var(--border);
    margin-bottom: 1.5rem;
    overflow-x: auto;
}
.tab-btn {
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    padding: 0.6rem 1.25rem;
    font-size: var(--fs-sm);
    font-weight: 500;
    color: var(--text-secondary);
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s, border-color 0.15s;
}
.tab-btn:hover { color: var(--primary); }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab-content { display: none; }
.tab-content.active { display: block; }

/* ─── Engine Analysis: Upload Dropzone ──────────────────────────────── */
.upload-dropzone {
    border: 2px dashed var(--border);
    border-radius: var(--radius);
    padding: 3rem 2rem;
    text-align: center;
    cursor: pointer;
    transition: border-color 0.2s, background 0.2s;
    background: var(--surface);
}
.upload-dropzone:hover { border-color: var(--primary); }
.upload-dropzone.drag-active {
    border-color: var(--primary);
    background: rgba(37, 99, 235, 0.05);
}
[data-theme="dark"] .upload-dropzone.drag-active {
    background: rgba(59, 130, 246, 0.1);
}

/* ─── Engine Analysis: Finding Cards ────────────────────────────────── */
.finding-card { position: relative; }
.finding-critical { border-left: 4px solid #dc2626; }
.finding-caution { border-left: 4px solid #d97706; }
.finding-info { border-left: 4px solid #2563eb; }
[data-theme="dark"] .finding-critical { border-left-color: #ef4444; }
[data-theme="dark"] .finding-caution { border-left-color: #f59e0b; }
[data-theme="dark"] .finding-info { border-left-color: #3b82f6; }

/* ─── Engine Analysis: Threshold Cell Coloring ──────────────────────── */
.cell-critical { background: #fee2e2; color: #991b1b; font-weight: 600; }
.cell-caution { background: #ffedd5; color: #9a3412; font-weight: 600; }
.cell-normal { background: #dcfce7; color: #166534; }
[data-theme="dark"] .cell-critical { background: #450a0a; color: #fca5a5; }
[data-theme="dark"] .cell-caution { background: #431407; color: #fdba74; }
[data-theme="dark"] .cell-normal { background: #052e16; color: #86efac; }

/* ─── Engine Analysis: Parameter Range Bars ─────────────────────────── */
.param-ranges-table td { font-size: var(--fs-sm); }
.range-bar-cell { padding: 0.5rem 0.75rem !important; }
.range-bar { position: relative; height: 16px; background: var(--bg); border-radius: 3px; overflow: hidden; border: 1px solid var(--border); }
.range-zone { position: absolute; top: 0; height: 100%; opacity: 0.25; }
.range-zone-normal { background: #22c55e; left: 0; }
.range-zone-caution { background: #f97316; }
.range-zone-critical { background: #ef4444; }
.range-data-bar { position: absolute; top: 3px; height: 10px; background: var(--primary); border-radius: 2px; opacity: 0.7; min-width: 2px; }
.range-mean-marker { position: absolute; top: 1px; width: 2px; height: 14px; background: var(--text); border-radius: 1px; }
.param-ref-link { color: var(--text-secondary); font-size: var(--fs-xs); text-decoration: none; margin-left: 0.25rem; opacity: 0.6; }
.param-ref-link:hover { opacity: 1; }
.sparkline-cell { padding: 0.25rem 0.5rem !important; vertical-align: middle; }
.sparkline-canvas { display: block; width: 120px; height: 30px; image-rendering: pixelated; }
.badge-green { background: #dcfce7; color: #166534; }
[data-theme="dark"] .badge-green { background: #052e16; color: #86efac; }

/* ─── Engine Analysis: Inline Finding Charts ────────────────────────── */
.finding-charts-panel { display: none; margin-top: 0.75rem; }
.finding-charts-panel.active { display: block; }
.finding-charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.finding-chart-item { background: var(--bg); border-radius: var(--radius); padding: 0.75rem; }
.finding-chart-item h3 { font-size: var(--fs-sm); color: var(--text-secondary); margin-bottom: 0.25rem; }
@media (max-width: 768px) { .finding-charts-grid { grid-template-columns: 1fr; } }

/* ─── Engine Analysis: Flight Row Finding Highlights ────────────────── */
.flight-row-critical { border-left: 3px solid #dc2626; }
.flight-row-caution { border-left: 3px solid #d97706; }
.flight-row-info { border-left: 3px solid #2563eb; }
[data-theme="dark"] .flight-row-critical { border-left-color: #ef4444; }
[data-theme="dark"] .flight-row-caution { border-left-color: #f59e0b; }
[data-theme="dark"] .flight-row-info { border-left-color: #3b82f6; }

/* ─── Engine Analysis: Clickable Finding Message ────────────────────── */
.finding-message-clickable { cursor: pointer; }
.finding-message-clickable:hover { opacity: 0.85; }
.finding-toggle-icon { font-size: var(--fs-xs); color: var(--text-secondary); margin-right: 0.25rem; transition: transform 0.15s; }
.finding-charts-panel.active + .finding-toggle-icon,
.finding-card.charts-open .finding-toggle-icon { transform: rotate(90deg); }

/* ─── Engine Analysis: Drill-Down Interactions ──────────────────────── */
.drill-target { cursor: pointer; }
.drill-target:hover { opacity: 0.85; }
.drill-highlight {
    animation: drillFlash 1.5s ease-out;
}
@keyframes drillFlash {
    0% { box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.5); border-radius: var(--radius); }
    100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); }
}
[data-theme="dark"] .drill-highlight {
    animation: drillFlashDark 1.5s ease-out;
}
@keyframes drillFlashDark {
    0% { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); border-radius: var(--radius); }
    100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}

/* Risk assessment card backgrounds */
.ra-bg-critical { background: #fef2f2; }
.ra-bg-high { background: #fff7ed; }
.ra-bg-moderate-high { background: #fffbeb; }
.ra-bg-moderate { background: #fefce8; }
.ra-bg-low { background: #f0fdf4; }
[data-theme="dark"] .ra-bg-critical { background: #450a0a; }
[data-theme="dark"] .ra-bg-high { background: #431407; }
[data-theme="dark"] .ra-bg-moderate-high { background: #422006; }
[data-theme="dark"] .ra-bg-moderate { background: #422006; }
[data-theme="dark"] .ra-bg-low { background: #052e16; }

/* HTMX inline response fragments — dark mode */
.ra-success { background: #f0fdf4; border: 1px solid #86efac; color: #166534; }
.ra-success p { color: var(--text); }
.ra-error { background: #fef2f2; border: 1px solid #fca5a5; color: #991b1b; }
.ra-loading { background: #eff6ff; border: 1px solid #93c5fd; }
.ra-loading-title { color: #1e40af; }
[data-theme="dark"] .ra-success { background: #052e16; border-color: #166534; color: #86efac; }
[data-theme="dark"] .ra-error { background: #450a0a; border-color: #7f1d1d; color: #fca5a5; }
[data-theme="dark"] .ra-error a { color: #60a5fa; }
[data-theme="dark"] .ra-loading { background: #1e293b; border-color: #334155; }
[data-theme="dark"] .ra-loading-title { color: #93c5fd; }

/* Utility: secondary/muted text */
.muted { color: var(--text-secondary); }

/* Navbar release badge (env · version) — subtle monospace pill */
.version-badge {
    font-family: var(--font-mono);
    font-size: var(--fs-xs);
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-secondary);
}
.version-badge:hover { color: var(--primary); border-color: var(--primary); }

/* Navbar signed-in identity chip (AUTH_MODE=google only) */
.nav-user { display: inline-flex; align-items: center; gap: 0.5rem; }
.nav-user-name {
    font-size: var(--fs-sm);
    color: var(--text-secondary);
    max-width: 14rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.nav-logout { font-size: var(--fs-sm); }

/* ---------------------------------------------------------------------------
   Public market table ("By Generation") — shared by the landing page and
   Market Pulse via templates/_generations_table.html. Lives here (not inline in
   one page) so both surfaces render an identical table and can't drift.

   Stays a REAL table at every width — it must never stack into one tile per
   generation, which is what made the mobile view unusably tall.
   --------------------------------------------------------------------------- */
.pulse-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.pulse-table th, .pulse-table td {
    padding: 0.55rem 0.5rem;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
    text-align: right;
}
.pulse-table th:first-child, .pulse-table td:first-child { text-align: left; }
.pulse-table thead th {
    font-size: var(--fs-xs);
    font-weight: 600;
    color: var(--text-secondary);
    border-bottom-width: 2px;
}
.pulse-table tbody td { font-size: var(--fs-base); }
.pulse-table .num { color: var(--text-secondary); }
/* Generation column: no header label (the badge identifies it), shrunk to badge
   width so the freed space goes to the price columns. */
.pulse-table .pulse-gen-col { width: 1%; white-space: nowrap; }
@media (max-width: 520px) {
    .pulse-table th, .pulse-table td { padding: 0.5rem 0.3rem; }
    .pulse-table thead th { font-size: var(--fs-xs); }
    .pulse-table tbody td { font-size: var(--fs-sm); }
}
/* 30-day direction: up is bad for buyers (danger), down is buyer-friendly. */
.pulse-dir-up { color: var(--danger); }
.pulse-dir-down { color: var(--success); }
.pulse-dir-flat { color: var(--text-secondary); }


/* ─── Reduced motion ────────────────────────────────────────────────────
   Honour the reader's OS setting (WCAG 2.3.3). Deliberately NOT the blanket
   `animation: none !important` sweep that gets pasted in for this: the spinner
   is the only signal that a scan is running, and freezing it turns "working"
   into "hung". It slows to a crawl instead — still moving, no longer spinning
   in someone's peripheral vision.

   The drill-flash highlight has no such job; its whole content is the motion,
   and the element it points at is already scrolled to. That one goes.

   Chart.js is handled in charts.js, which reads the same media query — CSS
   cannot reach animations drawn into a <canvas>. */
@media (prefers-reduced-motion: reduce) {
    .spinner { animation-duration: 3s; }
    .drill-highlight { animation: none; }
    *, *::before, *::after {
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
    }
}

/* ─── Filters / secondary-form disclosure ───────────────────────────────
   Shared by /listings, /faa/owners and /faa/registration-changes, and by the
   document-upload block on /engine-analysis. Open on desktop with the summary
   hidden (the panel is just there); on a phone it collapses behind a real
   "Filters" button, because those panels were putting the first row of DATA at
   663px and 582px on a 360x640 screen — the page opened on nothing but
   controls. filters_disclosure.js does the opening and closing.

   Lived in listings.css until three pages needed it. A shared rule in a
   page-specific sheet is a rule the other pages silently do without. */
.filters-wrap > summary { display: none; }
@media (min-width: 769px) {
    .filters-wrap > .filters { display: flex !important; }
    /* ...except where the panel is big enough that its height is the problem.
       /listings carries 28 filter-groups (the next largest panel has 7), which
       is ~325px of controls before the first row of data on a 900px screen —
       the same complaint that put the mobile collapse here, at a size the
       other three panels never reach. `open` is decided server-side from the
       active-filter count, so a panel with filters set never hides them. */
    .filters-wrap.filters-collapsible > summary { display: inline-flex; }
    .filters-wrap.filters-collapsible:not([open]) > .filters { display: none !important; }
}
.filters-wrap.filters-collapsible > summary {
    align-items: center; gap: 0.4rem; list-style: none; cursor: pointer;
    margin-bottom: 0.5rem; padding: 0.45rem 0.8rem; min-height: 40px;
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface); color: var(--text); font-weight: 600;
}
.filters-wrap.filters-collapsible > summary::-webkit-details-marker { display: none; }
.filters-wrap.filters-collapsible > summary::before { content: "\2699"; color: var(--text-secondary); }

/* A range is one control with two ends, so it reads as one. */
.range-pair { display: flex; align-items: center; gap: 0.3rem; }
.range-sep { color: var(--text-secondary); }
@media (max-width: 768px) {
    .filters-wrap > summary {
        display: inline-flex; align-items: center; gap: 0.4rem;
        list-style: none; cursor: pointer; margin-bottom: 0.5rem;
        padding: 0.45rem 0.8rem; min-height: 40px;
        border: 1px solid var(--border); border-radius: var(--radius);
        background: var(--surface); font-weight: 600;
    }
    .filters-wrap > summary::-webkit-details-marker { display: none; }
    .filters-wrap > summary::before { content: "\2699"; }
}

/* ─── Drill-down controls inside table cells ────────────────────────────
   The engine-results tables drill from a header, a cell or a row into the
   chart behind the number. That was a `data-action` on the <th>/<td>/<tr>
   itself: mouse-only, and unfixable with tabindex — <tr role="button"> is not
   valid ARIA, and sixty rows in the tab order announce nothing useful.

   So the control is a real <button> INSIDE the cell, and this rule makes it
   render exactly like the text it replaced: `all: unset` strips the UA chrome,
   and font/colour inherit so a cell-caution or severity colour still reaches
   it. The row-level click stays as a mouse convenience, the same arrangement
   as a clickable row that contains a link — the keyboard path is the control,
   not the row.
       python scripts/measure_a11y.py */
.drill-btn {
    all: unset;
    display: inline;
    font: inherit;
    color: inherit;
    cursor: pointer;
}
.drill-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
    border-radius: 3px;
}

/* Screen-reader-only. `display:none` and `visibility:hidden` both remove the
   text from the accessibility tree as well as the page, which is the opposite
   of what is wanted here — this hides it visually and keeps it announceable. */
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}
