*, *::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); }
/* AMEstimate mark + value. The MARK keeps the wordmark's two tokens (ink +
   coral) so the estimate is recognizably ours. The VALUE is neutral ink,
   deliberately not the coral: a red-toned number reads as "bad" wherever it
   appears (every AMEstimate on the dashboard looked like a warning), and
   colour that means nothing trains the reader to ignore colour that does.
   Status lives ONLY in the gap badge beside it — green = priced to clear,
   blue = within the negotiation margin, amber/red = above market — which
   also carries the sign and a title, so colour is never the only signal
   (WCAG 1.4.1). */
.am-mark { font-weight: 700; letter-spacing: -0.01em; white-space: nowrap; }
.am-value { color: var(--text); font-weight: 700; white-space: nowrap; font-variant-numeric: tabular-nums; }
.badge-am { background: transparent; color: var(--brand-metrics); border: 1px solid var(--brand-metrics); }
/* A badge that may wrap: for cells that list several (a phone-width table
   cell cannot hold a nowrap badge longer than itself). */
.badge-wrap { white-space: normal; }
a.card-link { display: block; color: inherit; text-decoration: none; }
a.card-link:hover { border-color: var(--brand-metrics); }
.card-accent { border-color: var(--brand-metrics); }
.row-total { font-weight: 700; border-top: 2px solid var(--border); }
.report-head { align-items: baseline; }
.grid-fit { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }
/* 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. Narrower than the full row, the operator bar moves
   links into "More" (see "Operator nav tiers") and the portal bar collapses to
   its hamburger (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; }
/* Sign-out is a POST form (audit L-3), so the portal nav's "Sign out" is a
   <button> in a <form>. display:contents keeps the button a flex item of
   .nav-links; the reset leaves .nav-link's size and weight alone (no `font`
   shorthand), and the collapsed nav's padding/min-height still win on
   specificity. */
.nav-signout { display: contents; }
.nav-link-button { background: none; border: 0; padding: 0; font-family: inherit; line-height: inherit; cursor: pointer; }
/* 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;
}

/* ─── Operator nav tiers ────────────────────────────────────────────────
   Ten links, eleven with AI Review on. Measured at the 16px root, the full
   row needed 1,190px — 1,274px with AI Review — while it carried an Activity
   link; retiring that page (v0.1.229) took ~66px off both, and the thresholds
   below were kept, so each band now has that much more slack. The row used
   to become a hamburger below 1,200px: every tablet and any laptop in split view got a
   menu instead of links, and with AI Review on, a 1,210px window pushed the
   account menu off the right edge (the row was wider than the breakpoint
   allowed for).

   It now gives up links a tier at a time instead of all at once:

     width ≥ 76em           the full row, as before
     55em ≤ width < 76em    Dashboard … Scans, then "More" (Diagnostics …)
     48em < width < 55em    Dashboard … Saved Searches, then "More"
     width ≤ 48em           the hamburger, as before

   No link costs a click it did not already cost: every width that shows
   "More" used to show only the hamburger. With AI Review on, 76em–81em keeps
   the full row by tightening the gap rather than dropping a link.

   IN em, NOT px, on purpose. The row is text sized in rem, so its width
   follows the reader's default font size, and so does an em media query (in
   a media query, em is the browser default, not the page's) — a px
   breakpoint is correct only at 16px. Each threshold is the measured row plus
   a 17px classic scrollbar (media queries count it, the row cannot use it)
   and some slack; tests/test_responsive_layouts.py sweeps the bands.

   Markup (base.html): tier 3 lives inside <div class="nav-more"> after the
   More button. By default — the full row, and the hamburger — the wrapper and
   the menu are display:contents and the button display:none, so those links
   are ordinary flex items of .nav-links in the desktop order. Tier 2 is in the
   markup twice, inline and as a .nav-t2-copy in the menu; exactly one of them
   is ever displayed, and a display:none link is out of the tab order and the
   accessibility tree. chrome.js only flips aria-expanded; the CSS below
   ignores it wherever there is no menu. */
.nav-more, .nav-more-menu { display: contents; }
/* Three classes deep on purpose: both are .nav-links .nav-link too, and the
   hamburger's `.nav-links .nav-link { display: flex }` would otherwise put
   the button and the copies back into the open phone menu. */
.nav-links .nav-more .nav-more-btn, .nav-links .nav-more-menu .nav-t2-copy { display: none; }
@media (48em < width < 76em) {
    .nav-more { display: block; position: relative; }
    .nav-links .nav-more .nav-more-btn { display: inline; }
    .nav-more-btn::after { content: " \25BE" / ""; white-space: pre; }
    .nav-more-menu {
        display: none;
        position: absolute;
        right: 0;
        top: calc(100% + 0.75rem);
        z-index: 60;
        flex-direction: column;
        min-width: 12rem;
        padding: 0.35rem;
        background: var(--surface);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
    }
    .nav-more-btn[aria-expanded="true"] + .nav-more-menu { display: flex; }
    .nav-more-menu .nav-link { display: flex; align-items: center; min-height: 40px; padding: 0.5rem 0.6rem; border-radius: 6px; }
    .nav-more-menu .nav-link:hover { background: var(--hover-bg); }
    /* The page you are on is behind "More": say so on the button. */
    .nav-more-current-t3 { color: var(--primary-text); font-weight: 700; }
}
@media (48em < width < 55em) {
    .nav-links > .nav-t2 { display: none; }
    .nav-links .nav-more-menu .nav-t2-copy { display: flex; }
    .nav-more-current-t2 { color: var(--primary-text); font-weight: 700; }
}
@media (76em <= width < 81em) {
    .nav-links.nav-long { column-gap: 0.5rem; }
}

/* ─── Navbar collapse ───────────────────────────────────────────────────
   The hamburger. Two copies of one body, because the two bars need it at
   different widths and CSS cannot share a rule body between media queries:
   the operator bar (.navbar-tiered) only on a phone, since its tiers keep a
   row above that; the portal bar below 1,200px, where its eight items (about
   1,040px) stop fitting. Keep the bodies in step. `:where()` adds no
   specificity, so the landing page's .nav-public override below still wins.

   That override moves with the portal block, and the 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. */
@media (width <= 48em) {
    :where(.navbar-tiered) .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. */
    :where(.navbar-tiered) .nav-user-menu { order: 1; }
    :where(.navbar-tiered) .nav-toggle-btn { display: block; order: 2; }
    :where(.navbar-tiered) .nav-links {
        order: 3;
        display: none;
        flex-basis: 100%;
        flex-direction: column;
        align-items: stretch;
        gap: 0.25rem;
        margin-top: 0.5rem;
    }
    :where(.navbar-tiered) .nav-toggle:checked ~ .nav-links { display: flex; }
    :where(.navbar-tiered) .nav-links .nav-link, :where(.navbar-tiered) .nav-links .theme-toggle {
        padding: 0.6rem 0.5rem;
        border-radius: var(--radius);
        min-height: 44px;
        display: flex;
        align-items: center;
    }
    :where(.navbar-tiered) .nav-links .nav-link[aria-current="page"] { background: var(--hover-bg); }
}
@media (max-width: 1199px) {
    :where(.navbar:not(.navbar-tiered)) .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. */
    :where(.navbar:not(.navbar-tiered)) .nav-user-menu { order: 1; }
    :where(.navbar:not(.navbar-tiered)) .nav-toggle-btn { display: block; order: 2; }
    :where(.navbar:not(.navbar-tiered)) .nav-links {
        order: 3;
        display: none;
        flex-basis: 100%;
        flex-direction: column;
        align-items: stretch;
        gap: 0.25rem;
        margin-top: 0.5rem;
    }
    :where(.navbar:not(.navbar-tiered)) .nav-toggle:checked ~ .nav-links { display: flex; }
    :where(.navbar:not(.navbar-tiered)) .nav-links .nav-link, :where(.navbar:not(.navbar-tiered)) .nav-links .theme-toggle {
        padding: 0.6rem 0.5rem;
        border-radius: var(--radius);
        min-height: 44px;
        display: flex;
        align-items: center;
    }
    :where(.navbar:not(.navbar-tiered)) .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; }
}

/* Visible keyboard focus for all interactive elements (a11y). */
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; }
/* The Sign out item is a <button> in a POST form (audit L-3); the form adds no spacing. */
.user-dropdown-form { margin: 0; }
.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; }

/* Expandable summary tiles — one per item where a page used to print every
   item's full detail in a column (Scans → each feed's activity analysis,
   Engine Analysis → each tail's documents). Closed, a tile is its summary;
   open, it spans the whole row and shows the detail under it. Native
   <details>: no script, and keyboard and screen-reader users get a real
   disclosure control. The padding lives on the summary and the body, not the
   card, so the whole summary is the hit area. */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 0.75rem; align-items: start; margin-bottom: 1rem; }
details.card.tile { padding: 0; }
details.tile[open] { grid-column: 1 / -1; }
/* display:block — the phone touch rule makes every <summary> a flex row. */
details.tile > summary { display: block; list-style: none; cursor: pointer; padding: 0.85rem 1rem; border-radius: var(--radius); }
details.tile > summary::-webkit-details-marker { display: none; }
details.tile > summary:hover .tile-title { color: var(--primary-text); }
.tile-head { display: flex; justify-content: space-between; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.tile-title { margin: 0; font-size: var(--fs-base); font-weight: 600; }
.tile-stats { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; margin-top: 0.4rem; font-size: var(--fs-sm); color: var(--text-secondary); }
.tile-stats strong { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.tile-toggle { display: block; margin-top: 0.4rem; font-size: var(--fs-xs); color: var(--primary-text); }
.tile-toggle::after { content: "Details \25BE"; }
details.tile[open] .tile-toggle::after { content: "Hide \25B4"; }
.tile-body { padding: 0 1rem 1rem; }
.tile-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: 0.5rem 1rem; margin-bottom: 1rem; }
.tile-panels { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.tile-panels > * { flex: 1 1 18rem; min-width: 0; }

/* 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); }
/* One control height across a filter row. The row aligns its groups on their
   bottom edge, so a native select, a number input and a multi-select toggle —
   each a slightly different height — put their LABELS at three different
   heights too. */
.filters .filter-group :is(input:not([type="checkbox"]), select),
.filters .multi-select-toggle { height: var(--filter-control-h, 2.25rem); box-sizing: border-box; }
/* The row's buttons, as one group the height of the fields beside it (three
   groups under blank labels sat lower and shorter; .btn-primary has no border,
   so it was a further 2px shorter than its outline neighbours). */
.filter-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: flex-end; }
.filter-actions .btn { height: var(--filter-control-h, 2.25rem); box-sizing: border-box; border: 1px solid transparent; }
.filter-actions .btn-outline { border-color: var(--border); }
/* A filter away from its default (listings.js applyFilterState): the label
   takes the accent and a leading dot, the control a 2px accent edge — colour is
   never the only signal (the dot and the edge are shape; a visually hidden
   "(changed)" reaches a screen reader). A faceted filter with one choice left
   and still at its default is hidden while "Hide filters with one choice" is on. */
.filters .filter-group.is-modified > label { color: var(--accent-blue-text); font-weight: 700; }
.filters .filter-group.is-modified > label::before { content: "\2022\00a0"; }
.filters .filter-group.is-modified :is(input:not([type="checkbox"]):not([type="hidden"]), select),
.filters .filter-group.is-modified .multi-select-toggle { border-color: var(--accent-blue-text); box-shadow: inset 0 0 0 1px var(--accent-blue-text); }
.filters .filter-group.filter-no-choice { display: none; }
.filter-hide-toggle { display: flex; align-items: center; gap: 0.35rem; font-size: var(--fs-xs); color: var(--text-secondary); align-self: flex-end; }
/* 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); }
/* A control is never wider than the box it sits in. A <select> sizes itself to
   its LONGEST option, so a list of long saved-search names made the dashboard's
   picker 454px wide on a 390px phone and pushed "Apply Default" off the screen
   entirely; a textarea's `cols` did the same on the engagement admin page. */
select, textarea { max-width: 100%; }
/* The sheet's own close button (row popovers, below): on a desktop the popover
   opens in place and its summary sits right above it. */
/* Listing detail "Share PDF": a dropdown under its button on a desktop (it
   sits in the header's action row, where opening in place would push every
   other action aside); on a phone the .row-popover sheet rule below applies. */
.share-pdf { position: relative; display: inline-block; }
.share-pdf > summary { list-style: none; }
.share-pdf > summary::-webkit-details-marker { display: none; }
.share-pdf[open] > .share-pdf-body {
    position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 60;
    width: min(26rem, 90vw); background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 1rem; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.18);
    text-align: left; white-space: normal;
}
.share-pdf-choice { border: none; margin: 0 0 0.5rem; padding: 0; }
.share-pdf-choice legend { margin-bottom: 0.5rem; padding: 0; }
.share-pdf-option { display: flex; gap: 0.5rem; align-items: flex-start; margin-bottom: 0.5rem; font-size: var(--fs-sm); }
.share-pdf-option input { margin-top: 0.2rem; }
/* Outranks the phone sheet's "every field full-width" rule, which is for text
   fields and would stretch these radios across the sheet. */
.share-pdf .row-popover-body .share-pdf-option input[type="radio"] { width: auto; flex: none; }
@media (max-width: 768px) { .share-pdf[open] > .share-pdf-body { width: auto; top: auto; } }
.row-popover-close { display: none; }
@media (max-width: 768px) { .row-popover-close { display: inline-flex; } }

/* 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; flex: none; }
/* Faceted options (listings): count on the right, matches first; the rest sit
   below a divider, dimmed but still selectable. */
.multi-select-dropdown .ms-text { flex: 1 1 auto; }
.multi-select-dropdown .ms-count { margin-left: auto; padding-left: 0.75rem; color: var(--text-secondary); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
/* "Only": tick just this option. Shown on hover / keyboard focus (and always
   on touch screens, where there is no hover), so the list stays quiet. */
.multi-select-dropdown .ms-only { margin-left: 0.5rem; padding: 0 0.3rem; border: 0; background: none;
    color: var(--accent-blue-text); font: inherit; font-size: var(--fs-xs); cursor: pointer; visibility: hidden; }
.multi-select-dropdown .ms-option:hover .ms-only,
.multi-select-dropdown .ms-option:focus-within .ms-only { visibility: visible; }
.multi-select-dropdown .ms-only:hover, .multi-select-dropdown .ms-only:focus-visible { text-decoration: underline; }
@media (hover: none) { .multi-select-dropdown .ms-only { visibility: visible; } }
.multi-select-dropdown .ms-divider { border-top: 1px solid var(--border); margin: 0.25rem 0 0.1rem; padding: 0.35rem 0.75rem 0.15rem; font-size: var(--fs-2xs); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
.multi-select-dropdown .ms-none .ms-text { color: var(--text-secondary); }

/* 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;
    /* 1px less than the visual padding on every side: the border below is part
       of every .btn now, so solid buttons keep their old size and outline ones
       lose the 2px they used to have over them (growing the solids instead
       pushed Scans' "Queue for desktop client" off a 360px screen). */
    padding: calc(0.5rem - 1px) calc(1rem - 1px);
    /* Every .btn is the same box whatever its element and fill: an <a> inherits
       the page's font and 1.5 line-height while a <button> gets the browser's own
       font and a tighter one, and a borderless solid button sat 2px shorter than
       an outline one beside it — so a row mixing links, form buttons and fills
       (Saved Searches' actions, the listing header) was three heights. A
       transparent border keeps the solid fills' size equal to the outlines'. */
    border: 1px solid transparent;
    border-radius: 6px;
    font-family: inherit;
    font-size: var(--fs-sm);
    font-weight: 500;
    line-height: 1.25;
    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: calc(0.35rem - 1px) calc(0.75rem - 1px); font-size: var(--fs-xs); }
/* Kept as a no-op alias: .btn now sets font-family and line-height itself, so
   a <button class="btn"> and an <a class="btn"> already match (it was the opt-in
   fix for auth_denied.html's Sign out, audit L-3). */
.btn-inherit-font { font-family: inherit; }

/* Detail page */
.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 1rem;
    margin-bottom: 2rem;
}
/* A grid item's min-width defaults to its content's, so a table that could not
   shrink widened its column past the track and ran under the next card. */
.detail-grid > * { min-width: 0; }
/* AMEstimate breakdown: a fixed figure column, labels take the rest (the
   figure column used to take most of a half-width card, so each label wrapped
   a word a line). The range and the ask gap wrap inside it; labels and badges
   may break anywhere rather than push the table past its card. */
table.am-breakdown { table-layout: fixed; }
table.am-breakdown col.am-fig { width: 6.5em; }
table.am-breakdown td { padding-left: 0.6rem; padding-right: 0.6rem; }
table.am-breakdown td:last-child { white-space: nowrap; }
table.am-breakdown td:first-child { overflow-wrap: anywhere; }
table.am-breakdown td.am-wrap { white-space: normal; }
.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; }
/* A page header's action group (title left, controls right) wraps and may
   shrink rather than run past the screen edge. <main> scrolls sideways as a
   last resort (.container is overflow-x:auto), so a group that did not fit
   never overflowed the DOCUMENT — the layout suite passed — while /dashboard
   panned 377px on a phone and 142px at 820px (iPad portrait), "Apply Default"
   out of sight. When the group fits, neither property changes anything. */
.flex-between > .flex { flex-wrap: wrap; min-width: 0; justify-content: flex-end; }
.flex-between > .flex > select { min-width: 0; }
.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; }
.mr-xs { margin-right: 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; }
.min-w-0 { min-width: 0; }
.max-w-full { max-width: 100%; }
.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%; }
/* Fixed-width numeric/tail inputs (a form's own sizing, not a layout). */
.inp-xs { width: 6rem; } .inp-sm { width: 7rem; } .inp-md { width: 8rem; } .inp-lg { width: 10rem; }
.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.

   The query is the exact complement of .col-collapse's (max-width: 768px),
   not min-width: 769px: at a fractional CSS width between the two (display
   scaling or zoom — 1.1 × a 767px window is 768.18px) neither rule matched and
   a pair of alternative labels rendered together, "Ownership TransferXfer". */
@media not all and (max-width: 768px) { .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; }
/* A data table's own horizontal scroller. <main> scrolls sideways as a last
   resort (.container is overflow-x:auto), so a table wider than the screen
   never overflowed the DOCUMENT — the layout suite passed — while the whole
   page, headings and all, panned with it, and main's scrollbar sat at the
   bottom of the content. At 820px (iPad portrait) that was a dozen pages, the
   customer portal home among them. Wrapped, only the table scrolls, and its
   scrollbar sits right under it. `.table-container` is the same wrapper under
   the name /faa/registration-changes already used; neither had any CSS. */
.table-wrap, .table-container { 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); }
/* One scrollbar, not two. The listings table container used to carry an
   inline max-height:calc(100vh - 12rem) + overflow-y:auto — a second vertical
   scroller nested in a page that also scrolled (the 12rem guess for the chrome
   above it was wrong the moment the filter panel opened), and the header row
   scrolled away inside it. Now listings.js sizes the container to exactly the
   viewport space below it, so the document never overflows and the table
   region is THE scroller — which is also what lets the header stick:
   position:sticky resolves against the nearest scroll container, and the
   `table { overflow:hidden }` used for rounded corners would otherwise capture
   it, hence the override. */
.table-scroller { overflow: auto; }
.table-scroller table { overflow: visible; }
.table-scroller thead th {
    position: sticky; top: 0; z-index: 2;
    background: var(--th-bg);
    box-shadow: 0 1px 0 var(--border);
}
/* AMEstimate hover card (listings table). The trigger is a real button styled
   as its text (.drill-btn); the card is position:fixed and placed by
   listings.js, because an absolutely positioned card inside the scrolling
   table container would be clipped at its edges. */
.am-hover { cursor: help; white-space: nowrap; }
.am-hover:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 3px; }
/* Phones: the gap badge stacks under the value so the column is no wider
   than the VI badge it replaced (side by side it pushed the table 28px past
   a 360px screen). The row is photo-height already, so two lines fit. */
@media (max-width: 768px) {
    .am-hover .badge { display: block; width: max-content; margin: .15rem 0 0; line-height: 1.2; }
}
.am-tip {
    position: fixed; z-index: 1100; width: min(340px, calc(100vw - 2rem));
    padding: .65rem .8rem; background: var(--surface); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: 0 10px 28px rgba(0, 0, 0, .22);
    font-size: var(--fs-sm); font-weight: 400; line-height: 1.4;
    white-space: normal; text-align: left; text-transform: none; letter-spacing: 0;
}
.am-tip-title { font-size: var(--fs-sm); margin-bottom: .35rem; }
.am-tip dl { display: grid; grid-template-columns: auto 1fr; gap: .2rem .7rem; margin: 0; }
.am-tip dt { color: var(--text-secondary); white-space: nowrap; }
.am-tip dd { margin: 0; font-variant-numeric: tabular-nums; }
.am-verdict { margin: .45rem 0 0; }

/* /am-estimate "How it is built": four short numbered steps in a responsive
   grid at secondary size, so the method reads as a legend rather than a wall
   of prose. The marker reuses the primary button tokens (contrast already
   measured in both themes). */
.am-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 1rem 1.75rem; margin: 0; padding: 0; list-style: none; counter-reset: am-step; font-size: var(--fs-sm); }
.am-steps li { counter-increment: am-step; position: relative; padding-left: 2rem; }
.am-steps li::before { content: counter(am-step); position: absolute; left: 0; top: .05rem; width: 1.4rem; height: 1.4rem; border-radius: 50%; background: var(--primary); color: var(--on-accent); font-weight: 700; font-size: var(--fs-xs); line-height: 1.4rem; text-align: center; }
.am-steps strong { display: block; margin-bottom: .2rem; }
.am-steps p { margin: 0; color: var(--text-secondary); line-height: 1.45; }
/* /am-estimate/tail equipment checkboxes: one wrapping row of labelled boxes. */
.lp-equip { border: 0; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: .3rem .9rem; align-items: center; flex-basis: 100%; }
.lp-equip legend { font-size: var(--fs-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: .04em; padding: 0; margin-bottom: .25rem; width: 100%; }
.lp-equip label { display: inline-flex; align-items: center; gap: .3rem; font-size: var(--fs-sm); white-space: nowrap; }

/* 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, .filters .filter-actions { 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; }
    /* A picker's list never runs past its column on a phone: long avionics
       names wrap instead, and the list may grow taller to compensate. */
    .filters .multi-select-dropdown { width: 100%; max-height: min(60vh, 22rem); }
    .filters .multi-select-dropdown label { white-space: normal; padding-top: 0.5rem; padding-bottom: 0.5rem; }
    /* 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; }
    /* Recent Activity: five short columns (Gen, Tail, Price, Date, Event) on a
       phone. At 0.45rem a side it was 9px wider than a 360px screen; 0.3rem
       fits it without the wrapper's sideways scroll. */
    table.activity-table th, table.activity-table td { padding-left: 0.3rem; padding-right: 0.3rem; }
    /* 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, .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); }
    /* ...except tiles that hold controls and tables rather than one figure
       (Scans → FAA Registry): two-up at 140px each, the Reparse button slid
       under the neighbouring tile and the manufacturer counts ran past the
       tile's edge. They take the full width on a phone. */
    .cards.cards-stack { grid-template-columns: 1fr; }
    .flex-between { flex-direction: column; gap: 0.5rem; align-items: flex-start; }
    /* A page header's action group wraps instead of running off the screen.
       <main> scrolls sideways (.container is overflow-x:auto), so an unwrapped
       group never overflowed the DOCUMENT — the layout suite passed — while the
       dashboard and listings pages panned 377px and 169px sideways and their
       last buttons sat out of reach. */
    .flex-between > .flex { max-width: 100%; justify-content: flex-start; }

    /* Row-action popovers (<details class="row-popover"> in a table cell: Rule
       out, Record response, Vouch). Their form is wider than the cell, and
       `table { overflow:hidden }` clips whatever spills out — on a phone that
       was the submit button. Open, the body becomes a sheet pinned to the
       bottom of the viewport instead: fixed positioning escapes the table's
       clip, and every field gets the full width. */
    .row-popover[open] > .row-popover-body {
        position: fixed; left: 0.75rem; right: 0.75rem; bottom: 0.75rem; z-index: 60;
        max-height: 75vh; overflow-y: auto; margin: 0; max-width: none;
        background: var(--surface); border: 1px solid var(--border);
        border-radius: var(--radius); padding: 1rem;
        box-shadow: 0 -4px 24px rgba(15, 23, 42, 0.25); text-align: left;
    }
    .row-popover-body input:not([type="checkbox"]):not([type="hidden"]) { width: 100%; }

    /* 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 h3 { display: inline-block; margin: 0 0.5rem 0 0; vertical-align: middle; }
details.settings-group > summary:hover h2,
details.settings-group > summary:hover h3 { 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 — the landing page's AMEstimate read ("Where listings
   clear", .pulse-estimate). Lives here rather than inline in landing.html so
   any future public table renders the same way.

   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; }
/* Model + the ask-vs-estimate word are labels, not figures: left-aligned. */
.pulse-table td:nth-child(2), .pulse-table th:nth-child(2) { text-align: left; }
.pulse-table .lp-gap { font-size: var(--fs-sm); color: var(--text-secondary); }
@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); }
}
/* The narrowest phones: six nowrap columns of banded prices came to ~22px
   more than a 360px screen's content box, so the landing page — the public
   front door — panned sideways. Tighter gutters, same type size. */
@media (max-width: 380px) {
    .pulse-table th, .pulse-table td { padding: 0.45rem 0.15rem; }
}


/* ─── 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); }

/* ─── Page-header secondary actions (listing detail's "More") ───────────
   The same disclosure shape as .filters-wrap, driven by the same script: on a
   desktop the <details> is open and its summary hidden, so the menu's buttons
   sit inline beside the primary action exactly as before; on a phone it is a
   "More" button whose menu drops down under it. */
.page-actions { display: inline-block; position: relative; }
.page-actions > summary { display: none; }
.page-actions-menu { display: inline-flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
@media (max-width: 768px) {
    .page-actions > summary { display: inline-flex; list-style: none; cursor: pointer; }
    .page-actions > summary::-webkit-details-marker { display: none; }
    .page-actions > summary::after { content: " \25BE" / ""; white-space: pre; }
    .page-actions[open] > .page-actions-menu {
        position: absolute; right: 0; top: calc(100% + 0.35rem); z-index: 30;
        display: flex; flex-direction: column; align-items: stretch; gap: 0.35rem;
        min-width: 13rem; padding: 0.5rem;
        background: var(--surface); border: 1px solid var(--border);
        border-radius: var(--radius); box-shadow: 0 6px 20px rgba(15, 23, 42, 0.18);
    }
    .page-actions-menu form { display: block; }
    .page-actions-menu .btn { width: 100%; justify-content: center; }
}

/* Listing detail's key facts: the numbers the page is opened for, first. */
.key-facts { margin-bottom: 1.25rem; }
.key-facts .card-value { font-size: var(--fs-2xl); line-height: 1.2; }
.key-facts-sub { display: flex; flex-wrap: wrap; gap: 0.25rem 0.5rem; align-items: center;
    margin-top: 0.35rem; font-size: var(--fs-xs); 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;
}

/* Guided question flows (portal/brief_form.html; /listings carries its own
   copy in listings.css): answer chips and option buttons that wrap. */
.guided-brief .guided-answers, .guided-brief .guided-options { list-style: none; padding: 0; margin: 0.5rem 0 0; display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
.guided-brief .guided-question { font-size: var(--fs-base); margin: 0.75rem 0 0; }

/* Release notes (/release-notes): one card per release, its notes grouped by
   area. The running release's card is the account menu's link target. */
.release-notes .release { margin-bottom: 1rem; }
.release-notes .release-title { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; margin: 0 0 0.25rem; font-size: var(--fs-lg, 1.125rem); }
.release-notes .release-date { font-size: var(--fs-sm); font-weight: 400; margin-left: auto; }
.release-notes .release-headline { margin: 0 0 0.5rem; font-weight: 600; }
.release-notes .release-area { margin: 0.75rem 0 0.25rem; font-size: var(--fs-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.03em; }
.release-notes .release-notes-list { margin: 0; padding-left: 1.25rem; }
.release-notes .release-notes-list li { margin: 0.2rem 0; }
.release-notes .release:target { outline: 2px solid var(--accent-blue-text); outline-offset: 2px; }
