/*
 * check-opencloud-security - the "Halo" design system
 *
 * Hand-written, self-hosted, no framework. Everything the page needs is in
 * this file, the small scripts next to it and the fonts under
 * `/static/fonts/`, which is what makes `default-src 'self'` an honest
 * header rather than an aspiration. DESIGN.md in the repository root is the
 * long form of everything below.
 *
 * The idea: a lens, not a page. A scan looks *through* this interface at
 * somebody else's server, so the interface behaves like optical glass -
 * panes of frost floating a few millimetres above a slow aurora, hairline
 * edges catching the light along one side, and nothing painted that does not
 * carry a fact. Where the old design was warm paper, this one is cold glass:
 * fewer lines, more air, one hue.
 *
 * One hue, and it earns its place. The page is otherwise achromatic, so iris
 * means *interaction* everywhere it appears - the focused field, the primary
 * action, the current tab, the running step. Status colour appears only
 * inside a chip or a dial, where it is a reading rather than decoration.
 *
 * Two schemes, one instrument, and neither is the other dimmed. Light is
 * porcelain: cool white panes over a pale iris haze, near-black ink. Dark is
 * obsidian: the haze deepens to a nebula, the panes become smoked glass, the
 * iris turns luminous and takes dark ink on top of it, because a glowing
 * violet under white text is the one unreadable thing a dark page can do.
 * Every colour is a token here and in the `prefers-color-scheme: dark` block,
 * so no rule further down ever names one.
 *
 * Three voices, all self-hosted (SIL OFL 1.1, licenses beside the files):
 * Space Grotesk sets the display type - a geometric grotesque with enough
 * character in its g, k and question mark that a headline is recognisably
 * this project and not a template; Inter carries body text, because it
 * disappears; JetBrains Mono carries everything that is *data* - labels,
 * counters, addresses, findings, code - so that a machine-read fact never
 * wears the same face as a sentence a person wrote.
 *
 * Motion is meant to be felt rather than seen: the aurora drifts on a
 * half-minute cycle, blocks arrive with a short rise out of a blur, the
 * focused command bar lights a halo, the dial sweeps to its rating. All of
 * it dies with `prefers-reduced-motion`, which the reset below enforces on
 * every animation and transition in this file.
 */

/* ------------------------------------------------------------------ tokens */
:root {
    color-scheme: light dark;

    /*
     * Three tones per status, and the split is what keeps the page bright
     * without becoming unreadable: `--x` is the graphic tone, used for the
     * dial, markers and rules where nothing has to be read at 13px; `--x-soft`
     * is the tint behind it; `--x-ink` is the one that carries text on that
     * tint. Every `--x-ink` on its `--x-soft` clears WCAG AA (4.5:1).
     */

    --ink: #0d0f17;
    --ink-soft: #474e61;
    --ink-faint: #61687b;
    --paper: #eceff7;
    --card: rgba(255, 255, 255, .62);
    --card-solid: #ffffff;
    --line: rgba(13, 15, 23, .1);
    --line-strong: rgba(13, 15, 23, .26);

    /*
     * The glass. A pane is not a slab: it blurs and over-saturates whatever
     * drifts behind it, carries a lit edge along its top-left and fades that
     * edge out before the opposite corner, the way a real bevel catches one
     * light source. `--card-solid` is the same surface with the frost turned
     * off, for the few places something must be opaque - the dial's centre,
     * a focused field, a floating menu.
     */
    --glass-blur: blur(22px) saturate(1.7);
    --glass-hi: rgba(255, 255, 255, .8);
    --glass-edge: linear-gradient(145deg,
        rgba(255, 255, 255, .95),
        rgba(255, 255, 255, .25) 34%,
        rgba(91, 75, 255, .16) 68%,
        rgba(177, 58, 212, .12));

    --brand: #5b4bff;
    --brand-deep: #4132d6;
    --brand-soft: #e6e3ff;
    /*
     * The action. The primary button is the one place the hue is allowed to
     * become a gradient: iris through violet, with the nebula's magenta
     * arriving past the button's own edge so it is felt as heat rather than
     * seen as a second colour. `--accent` is that gradient's midpoint, used
     * anywhere a flat fill has to agree with it.
     */
    --accent: #6b3bf0;
    --accent-ink: #55299f;
    --cta: linear-gradient(135deg, #5b4bff 0%, #6f3bf1 52%, #b13ad4 128%);
    --cta-hover: linear-gradient(135deg, #4a39f5 0%, #5f2ce2 52%, #9d2cc0 128%);
    --on-accent: #ffffff;
    --on-brand: #ffffff;

    --good: #0f7a53;
    --good-soft: #d8f1e5;
    --good-ink: #075f3e;
    --fair: #a75a00;
    --fair-soft: #fbe9cd;
    --fair-ink: #7a4100;
    --bad: #cc2b3d;
    --bad-soft: #fbdfe3;
    --bad-ink: #971f2e;
    --info: #1e6fd9;
    --info-soft: #dfe9fd;
    --info-ink: #17539f;

    --brand-glow: rgba(91, 75, 255, .22);
    --brand-glow-strong: rgba(91, 75, 255, .4);

    /*
     * The aurora: three soft masses of the one hue, sitting far behind
     * everything and drifting on a half-minute cycle. It is the only thing
     * on the page allowed to be purely decorative, which is why it is also
     * the faintest. The grain is a monochrome noise tile baked into an SVG,
     * so a large flat surface has a texture rather than a banded gradient.
     */
    --sky:
        radial-gradient(52rem 34rem at 14% 4%, rgba(91, 75, 255, .26), transparent 62%),
        radial-gradient(44rem 30rem at 88% 10%, rgba(177, 58, 212, .18), transparent 60%),
        radial-gradient(60rem 40rem at 50% 104%, rgba(23, 195, 178, .12), transparent 64%);
    --grain:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='.035'/%3E%3C/svg%3E");
    /* The horizon: one hairline of the hue where the aurora meets the fold,
       drawn across the top of the document under the header. */
    --horizon: linear-gradient(90deg, transparent, rgba(91, 75, 255, .5), rgba(177, 58, 212, .35), transparent);

    /*
     * The sticky header is a pane in its own right - translucent, blurring
     * whatever scrolls under it - so it needs colours of its own rather than
     * the opaque surface tokens.
     */
    --header-bg: rgba(236, 239, 247, .66);
    --header-line: rgba(13, 15, 23, .09);

    /*
     * A field sits *inside* a pane, so copying the pane's own colour made it
     * invisible - worst of all in dark mode, where field and card blurred
     * into one slab. Fields get an inset tint of their own and a border a
     * step stronger than the hairline around them.
     */
    --field-bg: rgba(255, 255, 255, .72);
    --field-border: rgba(13, 15, 23, .28);

    /* Code blocks are a terminal: dark in both schemes, always. Inline code
       only borrows a tint of the surrounding surface. */
    --code-bg: #101320;
    --code-ink: #dfe3f2;
    --tint: rgba(13, 15, 23, .06);

    /* Minimalism with corners this soft needs the scale to be obvious: a
       control, a pane, a stage. */
    --radius-sm: 10px;
    --radius: 16px;
    --radius-lg: 22px;
    --radius-xl: 28px;

    --shadow-sm: 0 1px 2px rgba(13, 15, 23, .05);
    --shadow: 0 1px 2px rgba(13, 15, 23, .04), 0 18px 40px -24px rgba(13, 15, 23, .35);
    --shadow-lg: 0 2px 4px rgba(13, 15, 23, .05), 0 40px 80px -40px rgba(13, 15, 23, .45);

    --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto,
        "Helvetica Neue", "Noto Sans", Arial, sans-serif;
    --font-display: "Space Grotesk", "Inter", system-ui, -apple-system,
        "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo,
        Consolas, "Liberation Mono", monospace;

    /* One easing for everything that moves under a pointer, and one for
       everything that arrives on its own. */
    --ease: cubic-bezier(.32, .72, 0, 1);
    --ease-out: cubic-bezier(.22, 1, .36, 1);

    --measure: 66ch;
    --page: 1180px;
}

/*
 * Night, twice over.
 *
 * The scheme still follows the operating system: with nothing stored, the
 * media query below is the only thing that decides, exactly as it always
 * did. `theme.js` writes `data-theme` on the root element when - and only
 * when - a visitor has asked for one explicitly, so the two blocks are the
 * same tokens under two questions: "does the system say night, and has
 * nobody overruled it" and "did somebody ask for night". A token added to
 * one and forgotten in the other is a token that half the readers see in
 * the wrong colour, so they are kept adjacent and edited together.
 */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --ink: #eaedf7;
        --ink-soft: #a6aec5;
        --ink-faint: #7b839a;
        --paper: #07080e;
        --card: rgba(22, 25, 36, .6);
        --card-solid: #141722;
        --line: rgba(234, 237, 247, .11);
        --line-strong: rgba(234, 237, 247, .26);

        --glass-blur: blur(24px) saturate(1.5);
        --glass-hi: rgba(255, 255, 255, .1);
        --glass-edge: linear-gradient(145deg,
            rgba(255, 255, 255, .22),
            rgba(255, 255, 255, .04) 34%,
            rgba(168, 157, 255, .16) 68%,
            rgba(226, 133, 255, .1));

        --brand: #a89dff;
        --brand-deep: #c8c1ff;
        --brand-soft: #201c46;
        /*
         * At night the hue is the light source rather than the ink, so the
         * action reverses: a luminous violet field carrying near-black text.
         * White on this gradient is the one unreadable thing a dark page can
         * put in front of somebody.
         */
        --accent: #b3a6ff;
        --accent-ink: #cfc7ff;
        --cta: linear-gradient(135deg, #a89dff 0%, #bb9bff 52%, #e58ff0 128%);
        --cta-hover: linear-gradient(135deg, #bcb2ff 0%, #cbb0ff 52%, #f0a5f8 128%);
        --on-accent: #0a0b14;
        --on-brand: #0a0b14;

        --good: #34d399;
        --good-soft: #0c2a20;
        --good-ink: #6ee7b7;
        --fair: #fbbf24;
        --fair-soft: #2b2110;
        --fair-ink: #fcd34d;
        --bad: #fb7185;
        --bad-soft: #2f131a;
        --bad-ink: #fda4af;
        --info: #38bdf8;
        --info-soft: #0d2436;
        --info-ink: #7dd3fc;

        --brand-glow: rgba(168, 157, 255, .26);
        --brand-glow-strong: rgba(168, 157, 255, .45);

        /* Night: the same three masses, deeper and further apart, so the
           panes have something worth blurring. */
        --sky:
            radial-gradient(54rem 36rem at 12% 2%, rgba(109, 92, 255, .46), transparent 62%),
            radial-gradient(46rem 32rem at 90% 8%, rgba(196, 74, 232, .3), transparent 60%),
            radial-gradient(64rem 44rem at 52% 108%, rgba(23, 195, 178, .18), transparent 64%);
        --horizon: linear-gradient(90deg, transparent, rgba(168, 157, 255, .55), rgba(226, 133, 255, .4), transparent);

        --header-bg: rgba(7, 8, 14, .62);
        --header-line: rgba(234, 237, 247, .09);

        /* Darker than the pane around them, and with a border that actually
           shows against it - the unreadable dark-mode field was both. */
        --field-bg: rgba(9, 10, 18, .72);
        --field-border: rgba(234, 237, 247, .3);

        --code-bg: #0a0c14;
        --code-ink: #dde2f2;
        --tint: rgba(234, 237, 247, .1);

        --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
        --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 20px 44px -26px rgba(0, 0, 0, .9);
        --shadow-lg: 0 2px 4px rgba(0, 0, 0, .5), 0 44px 90px -44px rgba(0, 0, 0, .95);
    }
}

:root[data-theme="dark"] {
    --ink: #eaedf7;
    --ink-soft: #a6aec5;
    --ink-faint: #7b839a;
    --paper: #07080e;
    --card: rgba(22, 25, 36, .6);
    --card-solid: #141722;
    --line: rgba(234, 237, 247, .11);
    --line-strong: rgba(234, 237, 247, .26);

    --glass-blur: blur(24px) saturate(1.5);
    --glass-hi: rgba(255, 255, 255, .1);
    --glass-edge: linear-gradient(145deg,
        rgba(255, 255, 255, .22),
        rgba(255, 255, 255, .04) 34%,
        rgba(168, 157, 255, .16) 68%,
        rgba(226, 133, 255, .1));

    --brand: #a89dff;
    --brand-deep: #c8c1ff;
    --brand-soft: #201c46;
    /*
     * At night the hue is the light source rather than the ink, so the
     * action reverses: a luminous violet field carrying near-black text.
     * White on this gradient is the one unreadable thing a dark page can
     * put in front of somebody.
     */
    --accent: #b3a6ff;
    --accent-ink: #cfc7ff;
    --cta: linear-gradient(135deg, #a89dff 0%, #bb9bff 52%, #e58ff0 128%);
    --cta-hover: linear-gradient(135deg, #bcb2ff 0%, #cbb0ff 52%, #f0a5f8 128%);
    --on-accent: #0a0b14;
    --on-brand: #0a0b14;

    --good: #34d399;
    --good-soft: #0c2a20;
    --good-ink: #6ee7b7;
    --fair: #fbbf24;
    --fair-soft: #2b2110;
    --fair-ink: #fcd34d;
    --bad: #fb7185;
    --bad-soft: #2f131a;
    --bad-ink: #fda4af;
    --info: #38bdf8;
    --info-soft: #0d2436;
    --info-ink: #7dd3fc;

    --brand-glow: rgba(168, 157, 255, .26);
    --brand-glow-strong: rgba(168, 157, 255, .45);

    /* Night: the same three masses, deeper and further apart, so the
       panes have something worth blurring. */
    --sky:
        radial-gradient(54rem 36rem at 12% 2%, rgba(109, 92, 255, .46), transparent 62%),
        radial-gradient(46rem 32rem at 90% 8%, rgba(196, 74, 232, .3), transparent 60%),
        radial-gradient(64rem 44rem at 52% 108%, rgba(23, 195, 178, .18), transparent 64%);
    --horizon: linear-gradient(90deg, transparent, rgba(168, 157, 255, .55), rgba(226, 133, 255, .4), transparent);

    --header-bg: rgba(7, 8, 14, .62);
    --header-line: rgba(234, 237, 247, .09);

    /* Darker than the pane around them, and with a border that actually
       shows against it - the unreadable dark-mode field was both. */
    --field-bg: rgba(9, 10, 18, .72);
    --field-border: rgba(234, 237, 247, .3);

    --code-bg: #0a0c14;
    --code-ink: #dde2f2;
    --tint: rgba(234, 237, 247, .1);

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5);
    --shadow: 0 1px 2px rgba(0, 0, 0, .5), 0 20px 44px -26px rgba(0, 0, 0, .9);
    --shadow-lg: 0 2px 4px rgba(0, 0, 0, .5), 0 44px 90px -44px rgba(0, 0, 0, .95);
}

/* An explicit choice narrows `color-scheme` from "either" to the one that was
   chosen, so the browser's own furniture - scrollbars, spinners, the
   caret - follows the page instead of the operating system. With nothing
   chosen the token stays `light dark` and the system decides, which is the
   state every first visit is in. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

/* ------------------------------------------------------------------- fonts */
/*
 * All three faces are served from `/static/fonts/` (SIL OFL 1.1, licenses
 * beside the files) and none of them is fetched from anywhere else - there
 * is no font service in this design and there never will be, because a font
 * request is a request, and this page is handed the address of somebody's
 * server.
 *
 * Space Grotesk and JetBrains Mono are subset to the Latin range the
 * interface actually speaks, which is what keeps three families lighter than
 * the two that came before. `font-display: swap` keeps the system stack on
 * screen until the real face lands, so a slow connection never holds the
 * text hostage, and the fallback stacks are metric-close enough that the
 * swap does not throw the layout.
 */
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/static/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
    font-family: "Inter";
    font-style: italic;
    font-weight: 400;
    font-display: swap;
    src: url("/static/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/static/fonts/Inter-Medium.woff2") format("woff2");
}
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 600;
    font-display: swap;
    src: url("/static/fonts/Inter-SemiBold.woff2") format("woff2");
}
@font-face {
    font-family: "Inter";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/static/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
    font-family: "Space Grotesk";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/static/fonts/SpaceGrotesk-Medium.woff2") format("woff2");
}
@font-face {
    font-family: "Space Grotesk";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/static/fonts/SpaceGrotesk-Bold.woff2") format("woff2");
}
@font-face {
    font-family: "JetBrains Mono";
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    src: url("/static/fonts/JetBrainsMono-Regular.woff2") format("woff2");
}
@font-face {
    font-family: "JetBrains Mono";
    font-style: normal;
    font-weight: 500;
    font-display: swap;
    src: url("/static/fonts/JetBrainsMono-Medium.woff2") format("woff2");
}
@font-face {
    font-family: "JetBrains Mono";
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url("/static/fonts/JetBrainsMono-Bold.woff2") format("woff2");
}

/* ------------------------------------------------------------------- reset */
*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
        animation-duration: .001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .001ms !important;
    }
}

body {
    margin: 0;
    font-family: var(--font);
    font-size: 17px;
    line-height: 1.65;
    color: var(--ink);
    background: var(--paper);
    background-image: var(--grain);
    background-repeat: repeat;
    background-attachment: fixed;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    /* A URL, a header name or a config key has no space to break at, and on a
       phone one of them is wider than the screen. */
    overflow-wrap: break-word;
}

/* Nothing may push the document wider than the viewport. */
img, svg { max-width: 100%; }

/*
 * The aurora. One fixed layer behind the document, over-sized so that its
 * slow drift never uncovers an edge, and inert to both the pointer and the
 * screen reader because it has nothing to say. It is the only thing here
 * that moves without being asked, and it is the first thing reduced motion
 * takes away.
 */
body::before {
    content: "";
    position: fixed;
    inset: -20vmax;
    z-index: -2;
    pointer-events: none;
    background-image: var(--sky);
    background-repeat: no-repeat;
    animation: aurora 42s var(--ease) infinite alternate;
}
@keyframes aurora {
    from { transform: translate3d(-2%, -1%, 0) scale(1); }
    to { transform: translate3d(2%, 2%, 0) scale(1.08); }
}
@media (prefers-reduced-motion: reduce) {
    body::before { animation: none; }
}

/*
 * The lattice. A faint grid, fixed behind the panes and masked so it
 * dissolves before the first fold ends: enough structure for the glass to
 * refract, not enough to read as wallpaper.
 */
body::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image:
        linear-gradient(to right, var(--line) 1px, transparent 1px),
        linear-gradient(to bottom, var(--line) 1px, transparent 1px);
    background-size: 72px 72px;
    opacity: .38;
    -webkit-mask-image: radial-gradient(48rem 36rem at 50% -6rem, #000, transparent 78%);
    mask-image: radial-gradient(48rem 36rem at 50% -6rem, #000, transparent 78%);
}

/* -------------------------------------------------------------- typography */
/*
 * Display type is set the way a title card is: large, tight and optically
 * corrected. Space Grotesk has generous sidebearings, so headlines get
 * negative tracking that grows with the size - without it a 3.8rem headline
 * reads as a row of separate words rather than one line.
 */
h1, h2, h3, h4 {
    font-family: var(--font-display);
    font-weight: 500;
    line-height: 1.1;
    letter-spacing: -.022em;
    margin: 0 0 .5em;
    text-wrap: balance;
}
h1 { font-size: clamp(2.45rem, 1.6rem + 3.4vw, 4rem); line-height: 1; letter-spacing: -.038em; }
h2 { font-size: clamp(1.45rem, 1.15rem + 1.2vw, 1.95rem); letter-spacing: -.028em; }
h3 { font-size: 1.1rem; letter-spacing: -.018em; }
p { margin: 0 0 1em; }
/*
 * A link is underlined by an animated rule rather than by the browser's:
 * it grows from the left on hover, which is the same gesture the navigation
 * makes, so the whole page agrees about what "this is interactive" looks
 * like. Underline-offset keeps the rule clear of the descenders.
 */
a {
    color: var(--brand-deep);
    text-decoration-color: color-mix(in srgb, var(--brand) 40%, transparent);
    text-underline-offset: 4px;
    text-decoration-thickness: 1px;
    transition: color .16s var(--ease), text-decoration-color .16s var(--ease);
}
a:hover { color: var(--brand); text-decoration-color: var(--brand); }

code { font-family: var(--mono); font-size: .92em; font-feature-settings: "liga" 0; }
/* Inline code gets a tint of the surface it sits on; a block of code is a
   terminal and undoes this below. */
code {
    background: var(--tint);
    border-radius: 5px;
    padding: .1em .4em;
    font-size: .84em;
}
pre code { background: transparent; padding: 0; border-radius: 0; font-size: inherit; }

::selection { background: var(--brand); color: var(--on-brand); }

::placeholder { color: var(--ink-faint); opacity: 1; }

/*
 * One focus ring for the whole page, and it is the hue: a solid stroke plus
 * a soft halo, so it survives on glass, on a tint and on the dark code
 * blocks alike. Nothing here ever removes an outline without putting a
 * stronger signal in its place.
 */
:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 3px;
    border-radius: 4px;
}

.visually-hidden {
    position: absolute; width: 1px; height: 1px;
    padding: 0; margin: -1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
    position: absolute; left: 1rem; top: -4rem;
    background: var(--card-solid); color: var(--ink);
    padding: .6rem 1rem; border-radius: var(--radius-sm);
    box-shadow: var(--shadow); z-index: 40;
    transition: top .18s ease;
}
.skip-link:focus { top: 1rem; }

/* Anchored headings land below the sticky header, not underneath it. */
[id] { scroll-margin-top: 5.5rem; }

/*
 * The machine voice. A kicker is the small monospace line that names a
 * section before the display headline speaks; its leading rule is drawn in
 * the hue and is the signature this design repeats - on a section head, on
 * the command bar, on a card that carries a reading.
 */
.kicker {
    display: flex; align-items: center; gap: .7rem;
    margin: 0 0 .85rem;
    font-family: var(--mono);
    font-size: .7rem;
    font-weight: 500;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--ink-faint);
}
.kicker::before {
    content: "";
    width: 1.75rem; height: 2px; border-radius: 2px;
    background: linear-gradient(90deg, var(--brand), transparent);
}

/*
 * The display face has no italic, so the phrase a headline turns on is
 * marked in the hue and lifted out of the ink instead - a gradient that runs
 * warm across the words, upright, at the same weight.
 */
.swash {
    font-style: normal;
    background: var(--cta);
    -webkit-background-clip: text;
    background-clip: text;
    color: var(--brand-deep);
}
@supports (-webkit-text-fill-color: transparent) {
    .swash { -webkit-text-fill-color: transparent; }
}

/* -------------------------------------------------------------- the motion */
/*
 * One arrival, used everywhere: a short rise out of a blur, as if the pane
 * were settling into focus. The hero's copy gets it line by line and the
 * mobile menu reuses it for its links. `backwards` fill so the element is
 * visible before the delay, and no fill after, so a hover transform is never
 * fighting a finished animation for the same property.
 */
@keyframes rise {
    from { opacity: 0; transform: translateY(14px); filter: blur(6px); }
    to { opacity: 1; transform: none; filter: blur(0); }
}

/*
 * Below the fold the arrival is reveal.js's job: it marks the document, and
 * only then do [data-reveal] blocks start hidden - a browser that never runs
 * the script simply shows everything, which is the same contract nav.js
 * keeps with the header.
 */
[data-reveal-root="on"] [data-reveal] {
    opacity: 0;
    transform: translateY(18px);
    filter: blur(6px);
    transition: opacity .6s var(--ease-out), transform .6s var(--ease-out),
        filter .6s var(--ease-out);
}
[data-reveal-root="on"] [data-reveal][data-revealed="true"] {
    opacity: 1;
    transform: none;
    filter: blur(0);
}
[data-reveal-root="on"] [data-reveal="2"] { transition-delay: .07s; }
[data-reveal-root="on"] [data-reveal="3"] { transition-delay: .14s; }

/*
 * The hand-off from waiting to reading. scan.js marks the document a beat
 * before it reloads, and the page leaves the way its blocks arrive - a
 * short fall into blur instead of a hard cut. Reduced-motion readers never
 * get the attribute, so for them the reload stays as immediate as it always
 * was.
 */
[data-exit="true"] main {
    opacity: 0;
    transform: translateY(10px);
    filter: blur(4px);
    transition: opacity .28s ease, transform .28s ease, filter .28s ease;
}

/* ------------------------------------------------------------------ layout */
.shell { width: min(100% - 2.5rem, var(--page)); margin-inline: auto; }

/*
 * The header is the first pane: it rides along while scrolling, blurring
 * whatever passes beneath it, and carries the horizon - one hairline of the
 * hue along its bottom edge, brightest in the middle, which is the only
 * ornament above the fold.
 */
.site-header {
    position: sticky;
    top: 0;
    z-index: 30;
    padding: .75rem 0;
    background: var(--header-bg);
    -webkit-backdrop-filter: blur(22px) saturate(1.7);
    backdrop-filter: blur(22px) saturate(1.7);
    border-bottom: 1px solid var(--header-line);
}
.site-header::after {
    content: "";
    position: absolute; left: 0; right: 0; bottom: -1px;
    height: 1px;
    background: var(--horizon);
    opacity: .7;
    pointer-events: none;
}
.site-header .shell {
    width: min(100% - 2.5rem, 1440px);
    display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.brand {
    display: inline-flex; align-items: center; gap: .7rem;
    font-family: var(--font-display);
    font-weight: 500; font-size: 1.05rem; letter-spacing: -.028em;
    text-decoration: none; color: var(--ink);
    white-space: nowrap;
}
.brand:hover { color: var(--ink); }
/* The mark sits in its own lit disc, and the disc lights up on hover - the
   only thing in the header that is not type. */
.brand img {
    width: 34px; height: 34px;
    padding: 3px;
    border-radius: 12px;
    border: 1px solid var(--line);
    background: var(--card);
    box-shadow: inset 0 1px 0 var(--glass-hi);
    transition: box-shadow .2s var(--ease), border-color .2s var(--ease),
        transform .2s var(--ease);
}
.brand:hover img {
    border-color: color-mix(in srgb, var(--brand) 45%, transparent);
    box-shadow: inset 0 1px 0 var(--glass-hi), 0 0 0 4px var(--brand-glow);
    transform: translateY(-1px);
}

/*
 * A tab is a pill that fills with the hue's faintest tint on hover and
 * carries a short rule under the current page. Two signals for one state:
 * the tint for the pointer, the rule for the eye scanning the row.
 */
.site-nav {
    display: flex;
    gap: .1rem;
    align-items: center;
    min-width: 0;
    font-size: .93rem;
    white-space: nowrap;
}
.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
.header-search {
    position: relative;
    width: 8rem;
    flex: 0 0 auto;
}
.header-search svg {
    position: absolute;
    left: .65rem; top: 50%;
    width: 15px; height: 15px;
    color: var(--ink-faint);
    transform: translateY(-50%);
    pointer-events: none;
}
.header-search input {
    width: 100%;
    min-height: 2.15rem;
    padding: .42rem .65rem .42rem 2rem;
    border: 1px solid var(--field-border);
    border-radius: 999px;
    background: var(--field-bg);
    color: var(--ink);
    font: inherit;
    font-size: .86rem;
    transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.header-search input:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-glow);
}
/*
 * The language switcher. A select and a button, because it has to work with
 * scripting blocked; lang.js marks the document and the button then steps
 * out of the way, since changing the select already submits the form.
 */
.language-switcher {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    flex: 0 0 auto;
}
.language-switcher > svg {
    width: 15px; height: 15px;
    color: var(--ink-faint);
    flex: 0 0 auto;
}
.language-switcher select {
    min-height: 2.15rem;
    padding: .3rem 1.6rem .3rem .6rem;
    border: 1px solid var(--field-border);
    border-radius: 999px;
    background: var(--field-bg);
    color: var(--ink-soft);
    font: inherit;
    font-size: .86rem;
    cursor: pointer;
    /* The arrow is drawn here rather than fetched: one less request, and a
       data: URL is all the CSP allows for an image anyway. */
    -webkit-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23808697' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .45rem center;
    background-size: 13px 13px;
    transition: border-color .18s var(--ease), box-shadow .18s var(--ease);
}
.language-switcher select:hover { color: var(--ink); }
.language-switcher select:focus-visible {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-glow);
}
.language-apply {
    min-height: 2.15rem;
    padding: .3rem .7rem;
    border: 1px solid var(--field-border);
    border-radius: 999px;
    background: var(--field-bg);
    color: var(--ink-soft);
    font: inherit;
    font-size: .82rem;
    cursor: pointer;
}
.language-apply:hover { color: var(--ink); border-color: var(--brand); }
[data-language="enhanced"] .language-apply { display: none; }

.site-nav a {
    position: relative;
    color: var(--ink-soft); text-decoration: none;
    padding: .45rem .7rem;
    border-radius: 999px;
    transition: color .16s var(--ease), background .16s var(--ease);
}
.site-nav a::after {
    content: "";
    position: absolute; left: .7rem; right: .7rem; bottom: .1rem;
    height: 2px; border-radius: 2px;
    background: var(--brand);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform .22s var(--ease);
}
.site-nav a:hover { color: var(--ink); background: var(--tint); }
.site-nav a:hover::after { transform: scaleX(1); }
.site-nav a[aria-current="page"] { color: var(--ink); font-weight: 600; }
.site-nav a[aria-current="page"]::after { transform: scaleX(1); }

/*
 * The menu button. Hidden until nav.js marks the document as enhanced, and
 * even then only on the widths where the row of links does not fit. The two
 * icons share one spot: the close icon is pinned over the open icon and the
 * swap is a cross-fade with a quarter turn, because a `display` swap cannot
 * be animated.
 */
.nav-toggle {
    display: none;
    position: relative;
    align-items: center;
    gap: .45rem;
    font: inherit;
    font-size: .9rem;
    font-weight: 600;
    color: var(--ink-soft);
    background: var(--card-solid);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: .45rem .85rem;
    cursor: pointer;
    transition: color .15s ease, border-color .15s ease;
}
.nav-toggle:hover { color: var(--brand-deep); border-color: var(--brand); }
.nav-toggle svg {
    width: 20px; height: 20px;
    transition: opacity .18s ease, transform .18s ease;
}
.nav-toggle .icon-close {
    position: absolute; left: .85rem; top: 50%; margin-top: -10px;
    opacity: 0; transform: rotate(-90deg) scale(.7);
}
.nav-toggle[aria-expanded="true"] .icon-open { opacity: 0; transform: rotate(90deg) scale(.7); }
.nav-toggle[aria-expanded="true"] .icon-close { opacity: 1; transform: none; }

/*
 * The scheme switch: the same hairline-button idiom as .nav-toggle, round,
 * and carrying one icon rather than a word - the scheme it would switch *to*,
 * which is the only thing pressing it does.
 *
 * Hidden until theme.js marks the document, the way the collapsed menu is
 * hidden until nav.js marks it. Without scripting there is nothing this
 * button could do that the operating system is not already doing, so a reader
 * without it is never shown a control that cannot work.
 *
 * Which icon shows is decided in CSS, by the same two questions the tokens
 * ask - the system's preference, and the visitor's override - so the button
 * is never briefly wrong while a script works out what to draw.
 */
.theme-toggle {
    display: none;
    align-items: center; justify-content: center;
    width: 38px; height: 38px;
    color: var(--ink-soft);
    background: var(--card-solid);
    border: 1px solid var(--line);
    border-radius: 999px;
    cursor: pointer;
    transition: color .15s ease, border-color .15s ease;
}
[data-theme-ready] .theme-toggle { display: inline-flex; }
.theme-toggle:hover { color: var(--brand-deep); border-color: var(--brand); }
.theme-toggle svg { width: 18px; height: 18px; }
/* Day is the base state, so the moon - the scheme a press would move to -
   is the icon on offer. */
.theme-toggle .icon-sun { display: none; }
.theme-toggle .icon-moon { display: block; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-toggle .icon-sun { display: block; }
    :root:not([data-theme="light"]) .theme-toggle .icon-moon { display: none; }
}
:root[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: none; }

/*
 * Same hairline-button idiom as .nav-toggle, pinned instead of inline.
 * back-to-top.js is what removes the `hidden` attribute past one viewport of
 * scroll; there is no entrance animation to reduce, on purpose - motion here
 * stays inside the five movements DESIGN.md allows, so this only ever swaps
 * `display` the way the collapsed nav panel already does.
 */
.back-to-top {
    position: fixed;
    right: 1.15rem;
    bottom: 1.15rem;
    z-index: 20;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    color: var(--ink-soft);
    background: var(--card-solid);
    border: 1px solid var(--line);
    border-radius: 999px;
    text-decoration: none;
    transition: color .15s var(--ease), border-color .15s var(--ease);
}
.back-to-top[hidden] { display: none; }
.back-to-top:hover, .back-to-top:focus-visible { color: var(--brand-deep); border-color: var(--brand); }
.back-to-top svg { width: 20px; height: 20px; }
@media (max-width: 620px) {
    .back-to-top { right: .75rem; bottom: .75rem; width: 2.5rem; height: 2.5rem; }
}

main { flex: 1 0 auto; padding-bottom: 4.5rem; }

.search-panel {
    max-width: 56rem;
    margin: 0 auto;
    padding: clamp(1.4rem, 3vw, 2.2rem);
}
.search-page-form label {
    display: block;
    margin-bottom: .65rem;
    font-family: var(--font-display);
    font-weight: 600;
}
.search-page-field {
    display: flex;
    gap: .7rem;
}
.search-page-field input {
    flex: 1 1 auto;
    min-width: 0;
    padding: .9rem 1rem;
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    background: var(--field-bg);
    color: var(--ink);
    font: inherit;
}
.search-page-field input:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
}
.search-results {
    display: grid;
    gap: .65rem;
    margin: 1.35rem 0 0;
    padding: 0;
    list-style: none;
}
.search-results a {
    display: grid;
    gap: .3rem;
    padding: 1rem 1.1rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--tint);
    color: var(--ink);
    text-decoration: none;
    transition: border-color .18s var(--ease), transform .18s var(--ease);
}
.search-results a:hover {
    border-color: var(--brand);
    transform: translateY(-1px);
}
.search-results span { color: var(--ink-soft); }
/* A result from the operator area, which only an operator is ever served.
   Named on the result rather than left to the path, so a reader can tell at
   a glance which answers are not public ones. */
.search-result-scope {
    justify-self: start;
    padding: .1rem .5rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: .68rem;
    letter-spacing: .04em;
    text-transform: uppercase;
}

/* ----------------------------------------------------------------- compare
 * Two scans side by side. The two grades are the same tokens the dial on a
 * result page uses, at a size that lets them be read as a pair rather than
 * as one headline: the reader is here for the difference between them, and
 * the lists below carry it.
 */
.compare-panel {
    max-width: 56rem;
    margin: 0 auto;
    padding: clamp(1.4rem, 3vw, 2.2rem);
}
.compare-form {
    display: grid;
    gap: .9rem;
    align-items: end;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
}
.compare-field label {
    display: block;
    margin-bottom: .5rem;
    font-family: var(--font-display);
    font-weight: 600;
}
.compare-field input {
    width: 100%;
    min-width: 0;
    padding: .9rem 1rem;
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    background: var(--field-bg);
    color: var(--ink);
    font: inherit;
    font-family: var(--mono);
    font-size: .88rem;
}
.compare-field input:focus {
    outline: none;
    border-color: var(--brand);
    box-shadow: 0 0 0 4px var(--brand-glow);
}
.compare-error { border-left: 3px solid var(--bad); }
.compare-verdict { margin: .2rem 0 .6rem; }
.compare-verdict[data-verdict="improved"] { color: var(--good-ink); }
.compare-verdict[data-verdict="regressed"] { color: var(--bad-ink); }
.compare-sides {
    display: grid;
    gap: 1.2rem;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    margin-top: 1.5rem;
}
.compare-side {
    padding: 1.1rem 1.2rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--tint);
}
.compare-grade { margin: .3rem 0 .8rem; }
.compare-grade .grade {
    font-family: var(--font-display);
    font-size: 2.4rem;
    font-weight: 700;
    letter-spacing: -.04em;
    color: var(--good-ink);
}
.compare-grade[data-tone="fair"] .grade { color: var(--fair-ink); }
.compare-grade[data-tone="bad"] .grade { color: var(--bad-ink); }
.compare-facts { display: grid; gap: .3rem 1rem; grid-template-columns: auto 1fr; margin: 0; }
.compare-facts dt { color: var(--ink-faint); font-size: .82rem; }
.compare-facts dd { margin: 0; font-size: .9rem; }
.compare-changes { width: 100%; border-collapse: collapse; }
.compare-changes th, .compare-changes td {
    padding: .55rem .7rem;
    border-bottom: 1px solid var(--line);
    text-align: left;
    font-size: .92rem;
    vertical-align: top;
}
.compare-changes th { color: var(--ink-faint); font-size: .82rem; font-weight: 600; }

/* The upload form. The file control is the browser's own - restyling it means
 * hiding it behind a label, and a hidden input is a control a keyboard and a
 * screen reader reach differently from the one everybody else is using. Only
 * the box around it is ours.
 */
.compare-field input[type="file"] {
    padding: .7rem 1rem;
    font-family: inherit;
    font-size: .9rem;
    cursor: pointer;
}
.compare-field input[type="file"]::file-selector-button {
    margin-right: .8rem;
    padding: .45rem .9rem;
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    background: var(--tint);
    color: var(--ink);
    font: inherit;
    font-size: .85rem;
    cursor: pointer;
}
/* Neutral, deliberately: where the earlier side came from is provenance, not
 * a verdict, and a coloured edge here would read as one.
 */
.compare-source { border-left: 3px solid var(--line); }

@media (max-width: 42rem) {
    .compare-form { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------------ footer */
.site-footer {
    border-top: 1px solid var(--line);
    padding: 2.2rem 0 2.6rem;
    color: var(--ink-faint);
    font-size: .88rem;
}
.footer-grid {
    display: grid; gap: 1.4rem 2.5rem;
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr) auto;
    align-items: start;
}
.footer-grid p { margin: 0; }
.footer-note strong { color: var(--ink-soft); font-weight: 600; }
.footer-nav { display: grid; gap: .35rem; align-content: start; }
.footer-nav a { color: var(--ink-faint); text-decoration: none; width: fit-content; }
.footer-nav a:hover { color: var(--brand-deep); text-decoration: underline; }

/* Which build answered you. Small, but the first thing worth knowing when a
   result looks wrong. */
.version-badge {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .35rem .7rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-family: var(--mono);
    font-size: .76rem;
    letter-spacing: .04em;
    color: var(--ink-soft);
    text-decoration: none;
    transition: border-color .15s ease, color .15s ease, transform .15s ease;
}
.version-badge svg { width: 14px; height: 14px; }
.version-badge:hover,
.version-badge:focus-visible { border-color: var(--accent); color: var(--accent-ink); }
.version-badge:hover { transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
    .version-badge { transition: none; }
    .version-badge:hover { transform: none; }
}

/* The trademark notice: present on every page, never competing for attention. */
.site-footer .legal {
    margin: 1.6rem 0 0;
    padding-top: 1rem;
    border-top: 1px solid var(--line);
    font-size: .8rem;
    line-height: 1.55;
    opacity: .85;
}

/* ------------------------------------------------------------------- cards */
/*
 * The pane. Frosted glass with a lit edge: the border is a gradient rather
 * than a colour, painted into the border box while the surface itself is
 * painted into the padding box, so one hairline can be bright at the top
 * left and gone by the bottom right. That single trick is what makes the
 * page read as glass instead of as rounded rectangles.
 */
.card {
    position: relative;
    background:
        linear-gradient(var(--card), var(--card)) padding-box,
        var(--glass-edge) border-box;
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid transparent;
    border-radius: var(--radius-lg);
    box-shadow: inset 0 1px 0 var(--glass-hi), var(--shadow);
    padding: 1.85rem;
}
.card + .card { margin-top: 1.25rem; }
.card h2 { margin-bottom: .35rem; }
.card > .lede { color: var(--ink-soft); margin-bottom: 1.2rem; }

/*
 * The halo. A pane that carries the page's one action - the scan form, the
 * verdict - is lit from behind by a soft field of the hue rather than framed
 * by a border. It is drawn as a shadow, not as an element, because a pane
 * that frosts what is behind it also frosts anything it would have stacked
 * underneath itself; a shadow paints outside the box and stays out of the
 * blur. It breathes on a nine-second cycle, which is slow enough to notice
 * only if you look for it.
 */
.brackets {
    position: relative;
    box-shadow: inset 0 1px 0 var(--glass-hi), var(--shadow-lg),
        0 0 0 1px var(--brand-glow), 0 24px 70px -34px var(--brand-glow-strong);
    animation: halo 9s var(--ease) infinite alternate;
}
@keyframes halo {
    from {
        box-shadow: inset 0 1px 0 var(--glass-hi), var(--shadow-lg),
            0 0 0 1px var(--brand-glow), 0 18px 50px -34px var(--brand-glow);
    }
    to {
        box-shadow: inset 0 1px 0 var(--glass-hi), var(--shadow-lg),
            0 0 0 1px var(--brand-glow-strong), 0 34px 90px -30px var(--brand-glow-strong);
    }
}
@media (prefers-reduced-motion: reduce) {
    .brackets { animation: none; }
}

.grid { display: grid; gap: 1.25rem; }
/* The gap already separates the cards: without this, the stacked-card
   rhythm above taxes every card but the first, and the row stops sharing
   one height. */
.grid > .card + .card { margin-top: 0; }
@media (min-width: 800px) {
    .grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* -------------------------------------------------------------------- hero */
/*
 * The hero is one gesture: a headline that states the question, the
 * instrument beside it, and then the whole width of the page given over to
 * the single field this service exists for. Nothing competes with the bar -
 * the track and format selects sit below it, quieter and smaller, because
 * they are refinements of an answer nobody has typed yet.
 */
.hero { padding: 3.5rem 0 1rem; }
.hero-grid { display: grid; gap: 2rem 3rem; align-items: center; }
@media (min-width: 1000px) {
    .hero-grid { grid-template-columns: minmax(0, 1.15fr) minmax(0, .85fr); }
}

/* The copy arrives line by line; the artwork follows last and then stays
   still - the emblem inside it moves on its own. */
.hero-copy > * { animation: rise .6s var(--ease-out) backwards; }
.hero-copy > *:nth-child(2) { animation-delay: .07s; }
.hero-copy > *:nth-child(3) { animation-delay: .14s; }

.hero .lede {
    font-size: 1.16rem; color: var(--ink-soft);
    max-width: var(--measure);
    margin-bottom: 0;
}

.hero .eyebrow {
    display: inline-flex; align-items: center; gap: .55rem;
    background: var(--card);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    border: 1px solid var(--line);
    border-radius: 999px; padding: .4rem .95rem;
    font-family: var(--mono);
    font-size: .7rem; font-weight: 500; letter-spacing: .16em; text-transform: uppercase;
    color: var(--ink-soft);
    margin-bottom: 1.4rem;
}
/* The live dot: the service is up, and it says so once a second. */
.hero .eyebrow::before {
    content: "";
    width: .5rem; height: .5rem; border-radius: 50%;
    background: var(--brand);
    box-shadow: 0 0 0 0 var(--brand-glow);
    animation: pulse 2.4s ease-in-out infinite;
}

.hero h1 { margin-bottom: 1rem; }

.hero-art {
    display: block; width: min(100%, 440px); margin: 0 0 0 auto;
    /* Inline SVG, so the box comes from the viewBox rather than from
       width/height attributes - which would have fought the width above. */
    height: auto; aspect-ratio: 480 / 300;
    animation: rise .6s var(--ease-out) .28s backwards;
}
@media (max-width: 999px) {
    .hero-art { margin-inline: 0; width: min(100%, 380px); }
}

/*
 * -------------------------------------------------------- the instrument
 *
 * The hero drawing, which used to be `<img src="hero.svg">` with its own
 * palette and its own `prefers-color-scheme` block inside the file. That
 * held until the scheme became something a visitor can *choose*: an <img>
 * is a separate document and cannot see the `data-theme` written on this
 * page's root element, so the drawing answered the operating system while
 * everything around it answered the toggle. Pressing the switch left a
 * daylight instrument on a midnight page, and the other way round.
 *
 * Inlined into the markup it is part of this document, so it reads these
 * tokens and follows all three states the rest of the page follows. That
 * also ends the second palette it was carrying: the statuses below are the
 * page's own `--good`/`--fair`/`--info`/`--bad`, the ink is `--ink`, the
 * centre is `--card-solid`. They were duplicated by hand before and had
 * already drifted apart in the light scheme.
 *
 * Three colours are genuinely the instrument's own - the magenta it sweeps
 * in, the lit top of the shield, and the static - so those are the only
 * ones declared here, in the same two-block shape the tokens at the top of
 * this file use, and edited together for the same reason.
 *
 * The animations need no reduced-motion branch of their own: the reset near
 * the top of this file already stops every animation in it.
 */
.hero-art {
    --ha-accent: #b13ad4;
    --ha-shield-top: #7a6bff;
    --ha-spark: #5b4bff;
}
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .hero-art {
        --ha-accent: #e58ff0;
        --ha-shield-top: #cfc7ff;
        --ha-spark: #cfc7ff;
    }
}
:root[data-theme="dark"] .hero-art {
    --ha-accent: #e58ff0;
    --ha-shield-top: #cfc7ff;
    --ha-spark: #cfc7ff;
}

.ha-brand { stop-color: var(--brand); }
.ha-accent { stop-color: var(--ha-accent); }
.ha-shield-top { stop-color: var(--ha-shield-top); }
.ha-shield-bottom { stop-color: var(--brand); }
.ha-hair { stroke: var(--ink); }
.ha-faint { stroke: var(--ink-faint); }
.ha-core { fill: var(--card-solid); }
.ha-good { fill: var(--good); }
.ha-fair { fill: var(--fair); }
.ha-info { fill: var(--info); }
.ha-bad { fill: var(--bad); }
.ha-spark { fill: var(--ha-spark); }
.ha-sweep-stop { stop-color: var(--ha-accent); }
/* The findings are punched out of the centre disc rather than outlined. */
.ha-markers { stroke: var(--card-solid); }
.ha-pulse-ring { stroke: var(--ha-accent); opacity: .5; }

.ha-sweep {
    animation: ha-spin 8s linear infinite;
    transform-origin: 240px 150px;
}
@keyframes ha-spin { to { transform: rotate(360deg); } }

.ha-pulse-ring { animation: ha-ring-out 3.4s ease-out infinite; }
@keyframes ha-ring-out {
    from { r: 64; opacity: .5; }
    to { r: 112; opacity: 0; }
}

.ha-spark { animation: ha-twinkle 4.6s ease-in-out infinite; }
.ha-spark-2 { animation-duration: 5.8s; animation-delay: 1.4s; }
@keyframes ha-twinkle {
    0%, 100% { opacity: .2; }
    50% { opacity: .8; }
}

/* The form is the third beat of the hero and arrives after the artwork. */
.hero .scan-form { margin-top: 2.4rem; animation: rise .6s var(--ease-out) .35s backwards; }

/* -------------------------------------------------------------------- form */
.scan-form { margin: 0; }

/*
 * The head of the form: what it is on the left, what it costs on the right.
 * A hairline rather than the old tear line - this is glass now, and glass
 * does not perforate.
 */
.ticket-head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
    margin: 0 0 1.15rem;
    padding-bottom: .85rem;
    border-bottom: 1px solid var(--line);
}
.ticket-head .kicker { margin: 0; }
.ticket-head .hint { margin: 0; font-family: var(--mono); font-size: .74rem; letter-spacing: .04em; }

/*
 * The command bar. One field, the width of the page, tall enough to be the
 * first thing a pointer finds and set in the mono face because what goes in
 * it is an address rather than a sentence. The mark on the left is the
 * instrument's reticle; the button is welded to the right so that the
 * gesture is a single object - type, then commit - instead of a field and a
 * distant control. Focus lights the whole bar rather than drawing a ring
 * around it.
 */
.command {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .55rem .55rem .55rem 1.15rem;
    border-radius: var(--radius);
    background:
        linear-gradient(var(--field-bg), var(--field-bg)) padding-box,
        var(--glass-edge) border-box;
    border: 1px solid transparent;
    box-shadow: inset 0 1px 3px rgba(13, 15, 23, .07), 0 1px 0 var(--glass-hi);
    transition: box-shadow .22s var(--ease), transform .22s var(--ease),
        background .22s var(--ease);
}
.command:hover { box-shadow: inset 0 1px 3px rgba(13, 15, 23, .07), 0 0 0 4px var(--brand-glow); }
/* `:focus-within` on the bar, `outline: none` on the field: the ring moves
   out to the object the eye actually sees, and it is stronger than the one
   it replaces. */
.command:focus-within {
    background:
        linear-gradient(var(--card-solid), var(--card-solid)) padding-box,
        var(--cta) border-box;
    box-shadow: 0 0 0 5px var(--brand-glow), 0 22px 50px -30px var(--brand-glow-strong);
    transform: translateY(-1px);
}
.command-mark {
    flex: none;
    display: grid; place-items: center;
    width: 26px; height: 26px;
    color: var(--ink-faint);
    transition: color .22s var(--ease), transform .5s var(--ease);
}
.command-mark svg { width: 24px; height: 24px; }
.command:focus-within .command-mark { color: var(--brand); transform: rotate(90deg); }

/*
 * An address the browser can already tell is not one. `:user-invalid` fires
 * only after the visitor has typed and left, never on a field they have not
 * reached yet, so the bar cannot greet anybody in red. A browser without it
 * loses nothing: the server refuses the same submission with a sentence.
 */
.command:has(.command-input:user-invalid) {
    background:
        linear-gradient(var(--card-solid), var(--card-solid)) padding-box,
        linear-gradient(145deg, var(--bad), var(--bad)) border-box;
    box-shadow: 0 0 0 4px var(--bad-soft);
}
.command:has(.command-input:user-invalid) .command-mark { color: var(--bad); }
/*
 * And the sentence that says what is wrong with it. The red bar alone was
 * colour carrying meaning on its own, which this design does not do
 * anywhere else: a severity is a word as well as a tint. It appears under
 * the same condition the bar reddens - typed, then left - so nobody is
 * corrected before they have finished.
 */
.field-error {
    display: none;
    margin: .55rem 0 0;
    font-size: .85rem;
    color: var(--bad-ink);
}
.command:has(.command-input:user-invalid) + .field-error { display: block; }

/* The field itself is stripped bare - the bar around it is the control. */
input[type="text"].command-input {
    flex: 1 1 auto;
    min-width: 0;
    width: auto;
    border: 0;
    background: transparent;
    box-shadow: none;
    padding: .75rem 0;
    font-family: var(--mono);
    font-size: clamp(1rem, .92rem + .4vw, 1.2rem);
    letter-spacing: -.01em;
}
input[type="text"].command-input:hover,
input[type="text"].command-input:focus {
    border: 0; background: transparent; box-shadow: none; outline: none;
}
.command .btn { flex: none; }
@media (max-width: 620px) {
    /* On a phone the bar becomes a stack: the field on its row, the button
       the full width of the thumb that reaches it. */
    .command { flex-wrap: wrap; padding: .85rem; gap: .5rem; }
    .command-mark { order: 1; }
    .command-input { order: 2; flex: 1 1 auto; }
    .command .btn { order: 3; width: 100%; margin-top: .35rem; }
}

.field-row { display: flex; gap: .95rem; flex-wrap: wrap; }
.field-row .field { flex: 1 1 22rem; }

/* Every label is machine voice: small, tracked, upper case - so a field
   reads as an instrument setting and never as prose. */
label {
    display: block;
    font-family: var(--mono);
    font-weight: 500; font-size: .72rem;
    letter-spacing: .14em; text-transform: uppercase;
    color: var(--ink-faint);
    margin-bottom: .45rem;
}
.hint { color: var(--ink-faint); font-size: .87rem; margin: .4rem 0 0; }

input[type="text"], select {
    width: 100%;
    font: inherit;
    color: var(--ink);
    background: var(--field-bg);
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    padding: .75rem .9rem;
    box-shadow: inset 0 1px 3px rgba(13, 15, 23, .05);
    transition: border-color .18s var(--ease), box-shadow .18s var(--ease),
        background .18s var(--ease);
}
/* A select carries its own mark rather than the platform's grey wedge - the
   chevron is drawn in the hairline colour and the native arrow is dropped. */
select {
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    padding-right: 2.4rem;
    background-image:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9' fill='none' stroke='%23666d80' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m1 1 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right .95rem center;
}
input[type="text"]:hover, select:hover { border-color: var(--brand); }
/* The chevron is an image, so it cannot read a token: the night version is
   the same drawing in the night's faint ink. Both questions again - what the
   system says, and what the visitor asked for - because an image cannot be
   redefined once and inherited. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) select {
        background-image:
            url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9' fill='none' stroke='%237b839a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m1 1 6 6 6-6'/%3E%3C/svg%3E");
    }
}
:root[data-theme="dark"] select {
    background-image:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9' fill='none' stroke='%237b839a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m1 1 6 6 6-6'/%3E%3C/svg%3E");
}
input[type="text"]:focus, select:focus {
    border-color: var(--brand);
    background-color: var(--card-solid);
    box-shadow: 0 0 0 4px var(--brand-glow);
    outline: none;
}

/*
 * A control is a small pane: soft corners, a lit top edge, and a lift under
 * the pointer. Every button on the page shares this shell and differs only
 * in what fills it.
 */
.btn {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
    font: inherit; font-weight: 600; font-size: .98rem;
    letter-spacing: -.005em;
    border: 1px solid transparent; border-radius: var(--radius-sm);
    padding: .85rem 1.5rem;
    cursor: pointer;
    text-decoration: none;
    overflow: hidden;
    transition: transform .18s var(--ease), box-shadow .18s var(--ease),
        background .18s var(--ease), border-color .18s var(--ease), color .18s var(--ease);
}
.btn svg { width: 18px; height: 18px; }
/* `display: inline-flex` above outranks the user-agent `[hidden]` rule, which
   showed the script-only copy buttons to readers who could never use them. */
.btn[hidden] { display: none; }
/*
 * The primary action is the only gradient on the page: iris through violet,
 * with the nebula's magenta arriving past the button's own right edge, so
 * the fill has a direction without ever becoming a second colour. The
 * sheen - one soft highlight that crosses on hover - is why it reads as
 * glass rather than as a coloured rectangle.
 */
.btn-primary {
    color: var(--on-accent);
    background: var(--cta);
    box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, .28);
}
.btn-primary::after {
    content: "";
    position: absolute; inset: 0;
    background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, .34) 50%, transparent 70%);
    transform: translateX(-120%);
    transition: transform .7s var(--ease);
    pointer-events: none;
}
.btn-primary:hover {
    color: var(--on-accent);
    background: var(--cta-hover);
    transform: translateY(-2px);
    box-shadow: 0 14px 32px -12px var(--brand-glow-strong), inset 0 1px 0 rgba(255, 255, 255, .3);
}
.btn-primary:hover::after { transform: translateX(120%); }
.btn-primary:active { transform: translateY(0); }
.btn:disabled { opacity: .55; cursor: not-allowed; transform: none; }
.btn:disabled::after { display: none; }
.btn-quiet {
    background: var(--card);
    -webkit-backdrop-filter: var(--glass-blur);
    backdrop-filter: var(--glass-blur);
    color: var(--ink-soft);
    border-color: var(--line-strong);
}
.btn-quiet:hover {
    color: var(--brand-deep);
    border-color: var(--brand);
    transform: translateY(-2px);
    box-shadow: 0 12px 26px -16px var(--brand-glow-strong);
}
@media (prefers-reduced-motion: reduce) {
    .btn, .btn:hover { transform: none; }
    .btn-primary::after { display: none; }
}

/* waivers */
details.waivers {
    margin-top: 1.25rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: transparent;
    overflow: hidden;
}
details.waivers > summary {
    cursor: pointer; padding: .9rem 1.15rem; font-weight: 600; font-size: .95rem;
    list-style: none; display: flex; align-items: center; gap: .6rem;
    transition: color .16s var(--ease), background .16s var(--ease);
}
details.waivers > summary:hover { color: var(--brand-deep); background: var(--tint); }
details.waivers > summary::-webkit-details-marker { display: none; }
details.waivers > summary::after {
    content: ""; margin-left: auto;
    width: .55rem; height: .55rem;
    border-right: 2px solid var(--ink-faint);
    border-bottom: 2px solid var(--ink-faint);
    transform: rotate(45deg);
    transition: transform .18s ease;
}
details.waivers[open] > summary::after { transform: rotate(-135deg); }
.waiver-body { padding: 0 1.05rem 1.05rem; border-top: 1px solid var(--line); }
.waiver-group { margin-top: 1rem; }
.waiver-group h3 {
    font-family: var(--mono); font-size: .72rem; font-weight: 500;
    text-transform: uppercase; letter-spacing: .14em; color: var(--ink-faint);
}
.waiver-list { display: grid; gap: .5rem; }
@media (min-width: 720px) { .waiver-list { grid-template-columns: 1fr 1fr; } }
/*
 * The catalogue is long enough that reading it is the cost of using it, so
 * it can be narrowed to the check somebody already has in mind. Hidden until
 * waivers.js is there to do the narrowing: a search field that does not
 * search is worse than no search field.
 */
.waiver-search { display: none; margin-bottom: .9rem; }
[data-waiver-search] .waiver-search { display: block; }
/* The same field shell `input[type="text"]` wears, in the machine voice the
   identifiers it searches are set in. */
.waiver-search input[type="search"] {
    width: 100%;
    font: inherit;
    font-family: var(--mono); font-size: .88rem;
    color: var(--ink);
    background: var(--field-bg);
    border: 1px solid var(--field-border);
    border-radius: var(--radius-sm);
    padding: .6rem .8rem;
    box-shadow: inset 0 1px 3px rgba(13, 15, 23, .05);
    transition: border-color .18s var(--ease), box-shadow .18s var(--ease),
        background .18s var(--ease);
}
/* iOS Safari zooms into a focused field set below 16px and leaves the page
   zoomed, so on a phone the search takes the full size. */
@media (max-width: 640px) {
    .waiver-search input[type="search"] { font-size: 1rem; }
}
.waiver-search input[type="search"]:hover { border-color: var(--brand); }
.waiver-search input[type="search"]:focus {
    border-color: var(--brand);
    background-color: var(--card-solid);
    box-shadow: 0 0 0 4px var(--brand-glow);
    outline: none;
}
.waiver-empty { margin: .4rem 0 0; }
.waiver-group[hidden], .toggle[hidden] { display: none; }

.toggle {
    display: flex; gap: .7rem; align-items: flex-start;
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    padding: .7rem .85rem; cursor: pointer;
    transition: border-color .16s var(--ease), background .16s var(--ease),
        transform .16s var(--ease);
}
.toggle:hover { border-color: var(--brand); transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) { .toggle:hover { transform: none; } }
.toggle input { margin-top: .3rem; accent-color: var(--brand); width: 1.05rem; height: 1.05rem; }
.toggle strong { display: block; font-family: var(--mono); font-weight: 600; font-size: .85rem; }
.toggle span { display: block; color: var(--ink-faint); font-size: .82rem; line-height: 1.45; }
.toggle:has(input:checked) { border-color: var(--brand); background: var(--brand-soft); }

/* ---------------------------------------------------------- the assay strip */
/*
 * The promises, set as a ruled specification row rather than a cloud of
 * pills: one hairline box, four columns, each with its mark. It is the
 * landing page's signature element, and it reads the same on paper.
 */
.assurance {
    list-style: none; margin: 3rem 0 0; padding: 0;
    display: grid;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
}
.assurance li {
    display: flex; gap: .8rem; align-items: flex-start;
    padding: 1.15rem 1.25rem;
    border-top: 1px solid var(--line);
}
.assurance li:first-child { border-top: 0; }
.assurance svg { width: 19px; height: 19px; flex: none; margin-top: .15rem; color: var(--brand); }
.assurance strong { display: block; font-size: .92rem; font-weight: 600; letter-spacing: -.01em; }
.assurance span { display: block; color: var(--ink-faint); font-size: .82rem; line-height: 1.5; }
@media (min-width: 720px) {
    .assurance { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .assurance li:nth-child(-n + 2) { border-top: 0; }
    .assurance li:nth-child(even) { border-left: 1px solid var(--line); }
}
@media (min-width: 1060px) {
    .assurance { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .assurance li { border-top: 0; border-left: 1px solid var(--line); }
    .assurance li:first-child { border-left: 0; }
}

/* ------------------------------------------------------------------ badges */
.tag {
    display: inline-flex; align-items: center; gap: .35rem;
    border-radius: 6px; padding: .16rem .6rem;
    font-family: var(--mono);
    font-size: .7rem; font-weight: 500; text-transform: uppercase; letter-spacing: .1em;
}
.tag-critical { background: var(--bad-soft); color: var(--bad-ink); }
.tag-warning { background: var(--fair-soft); color: var(--fair-ink); }
.tag-info { background: var(--info-soft); color: var(--info-ink); }
.tag-good { background: var(--good-soft); color: var(--good-ink); }
.tag-neutral { background: var(--tint); color: var(--ink-soft); }

.alert {
    display: flex; gap: .7rem; align-items: flex-start;
    border-radius: var(--radius-sm);
    padding: .9rem 1.05rem; margin-bottom: 1.2rem;
    background: var(--bad-soft); color: var(--bad-ink);
    border: 1px solid var(--bad);
    font-size: .94rem;
}
.alert svg { width: 20px; height: 20px; flex: none; margin-top: .1rem; }

/* --------------------------------------------------------------- features */
/* A pane rises towards the pointer and its mark lights up with it - the
   whole card is the affordance, so nothing inside it needs a border. */
.feature { transition: transform .22s var(--ease), box-shadow .22s var(--ease); }
.feature:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg), 0 0 0 1px var(--brand-glow); }
.feature .icon {
    width: 46px; height: 46px; border-radius: 14px;
    display: grid; place-items: center; margin-bottom: 1rem;
    background: var(--brand-soft);
    color: var(--brand-deep);
    border: 1px solid var(--line);
    transition: transform .22s var(--ease), box-shadow .22s var(--ease);
}
.feature:hover .icon { transform: scale(1.06); box-shadow: 0 0 0 5px var(--brand-glow); }
.feature .icon svg { width: 22px; height: 22px; }
.feature p { color: var(--ink-soft); margin: 0; font-size: .95rem; }
@media (prefers-reduced-motion: reduce) {
    .feature, .feature:hover { transition: none; transform: none; }
}

.steps { counter-reset: step; list-style: none; padding: 0; margin: 0; display: grid; gap: 1.05rem; }
.steps li { display: flex; gap: .95rem; align-items: flex-start; }
.steps li::before {
    counter-increment: step; content: counter(step, decimal-leading-zero);
    flex: none; width: 36px; height: 36px; border-radius: 12px;
    display: grid; place-items: center;
    border: 1px solid var(--line);
    background: var(--brand-soft);
    color: var(--brand-deep);
    font-family: var(--mono); font-weight: 500; font-size: .8rem;
}

/* --------------------------------------------------- content sub-pages */
/* The landing page keeps the form; the explanations live on their own pages,
   each opening with this block and closing with the cross-links below. */
.page-head { padding: 2.4rem 0 .4rem; max-width: var(--measure); }
.page-head h1 { margin-bottom: .55rem; }
.page-head .lede { color: var(--ink-soft); font-size: 1.1rem; margin: 0; }

.page-nav-title { font-size: 1.05rem; margin-bottom: .9rem; }
.page-nav-list {
    list-style: none; padding: 0; margin: 0;
    display: grid; gap: 1rem;
}
@media (min-width: 800px) {
    .page-nav-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.page-nav-card {
    position: relative;
    display: grid; gap: .3rem; height: 100%;
    padding: 1.2rem 2.8rem 1.2rem 1.35rem;
    text-decoration: none; color: var(--ink);
    transition: transform .2s var(--ease), box-shadow .2s var(--ease);
}
.page-nav-card:hover { color: var(--ink); }
.page-nav-card strong { font-family: var(--font-display); font-size: 1.06rem; font-weight: 500; letter-spacing: -.028em; }
.page-nav-card span { color: var(--ink-soft); font-size: .92rem; }
/* The arrow is drawn, not typed, and it slides on hover. */
.page-nav-card::after {
    content: "";
    position: absolute; right: 1.15rem; top: 1.25rem;
    width: .6rem; height: .6rem;
    border-top: 2px solid var(--ink-faint);
    border-right: 2px solid var(--ink-faint);
    transform: rotate(45deg);
    transition: transform .18s ease, border-color .18s ease;
}
.page-nav-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--shadow-lg), 0 0 0 1px var(--brand-glow);
}
.page-nav-card:hover::after { border-color: var(--brand); transform: rotate(45deg) translate(2px, -2px); }
.page-nav-cta { border-color: var(--brand); }
.page-nav-cta::after { border-color: var(--brand); }

@media (prefers-reduced-motion: reduce) {
    .page-nav-card, .page-nav-card:hover { transition: none; transform: none; }
    .page-nav-card::after, .page-nav-card:hover::after { transition: none; transform: rotate(45deg); }
}

/* -------------------------------------------------------------- scan page */
.scan-head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; justify-content: space-between; }
.scan-head h1 { margin-bottom: .3rem; }
/* Rescan and "scan another instance" are the two actions a reader reaches
   for once a result is in, so they sit as one group: same gap and alignment
   as every other button row on the page. */
.scan-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
.scan-actions form { margin: 0; }
.scan-actions .btn[disabled] { opacity: .55; cursor: default; }
.scan-actions .btn[data-waiting] { font-variant-numeric: tabular-nums; }
/* The address that was scanned, set as a chip of the hue: it is the one
   fact the whole page hangs from, and it is a machine's word, not a
   person's. */
.scan-target {
    font-family: var(--mono); font-size: .95rem;
    background: var(--brand-soft); color: var(--brand-deep);
    border: 1px solid var(--brand-glow);
    border-radius: var(--radius-sm); padding: .3rem .65rem;
    word-break: break-all;
}
/* Which track the result was rated against. It changes what "end of life"
   means, so it belongs next to the address rather than in the details. */
.scan-track {
    display: inline-block;
    margin-left: .5rem;
    padding: .14rem .55rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-family: var(--mono);
    font-size: .74rem;
    letter-spacing: .05em;
    text-transform: capitalize;
    color: var(--ink-soft);
    vertical-align: middle;
}

.progress { display: grid; gap: 1.1rem; }
/*
 * The pipeline, drawn as an instrument: a node per stage, a hairline between
 * them, and the stage that is running breathes in the signal colour. The
 * labels are set in the dossier mono, small enough never to wrap into each
 * other on a desk; the phone reflow is at the bottom of this file.
 */
.progress-track { display: flex; gap: .5rem; }
.progress-track .step {
    position: relative;
    flex: 1;
    padding-top: 1.35rem;
    font-family: var(--mono);
    font-size: .72rem; font-weight: 500;
    letter-spacing: .14em; text-transform: uppercase;
    color: var(--ink-faint);
    transition: color .3s ease;
}
.progress-track .step::before {
    content: "";
    position: absolute; top: 0; left: 0;
    width: 11px; height: 11px; border-radius: 50%;
    background: var(--card-solid);
    border: 2px solid var(--line-strong);
    transition: border-color .3s ease, background .3s ease;
}
.progress-track .step::after {
    content: "";
    position: absolute; top: 5px; left: 19px; right: -3px;
    height: 1.5px;
    background: var(--line-strong);
    transition: background .3s ease;
}
.progress-track .step:last-child::after { display: none; }
.progress-track .step[data-state="active"] { color: var(--brand-deep); }
.progress-track .step[data-state="active"]::before {
    border-color: var(--brand);
    animation: node-breathe 1.6s ease-in-out infinite;
}
.progress-track .step[data-state="done"] { color: var(--good-ink); }
.progress-track .step[data-state="done"]::before { border-color: var(--good); background: var(--good); }
.progress-track .step[data-state="done"]::after { background: var(--good); }
.progress-track .step[data-state="failed"] { color: var(--bad-ink); }
.progress-track .step[data-state="failed"]::before { border-color: var(--bad); background: var(--bad); }
/* The active node breathes a halo; the label never moves. */
@keyframes node-breathe {
    0%, 100% { box-shadow: 0 0 0 0 var(--brand-glow-strong); }
    50% { box-shadow: 0 0 0 6px transparent; }
}

.pulse {
    display: inline-block; width: .6rem; height: .6rem; border-radius: 50%;
    background: var(--brand); margin-right: .5rem;
    box-shadow: 0 0 0 0 var(--brand-glow);
    animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
    0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 0 0 var(--brand-glow); }
    50% { opacity: .45; transform: scale(.8); box-shadow: 0 0 0 7px transparent; }
}

.queue-note {
    background: var(--info-soft); color: var(--info-ink);
    border-radius: var(--radius-sm); padding: .85rem 1.05rem;
    font-weight: 600;
}

/*
 * How long this has taken, and roughly how long it should. A wait with no
 * clock on it is the one that feels stuck: twenty seconds of nothing reads
 * as a page that has quietly died. The reading is data, so it wears the
 * machine voice and tabular figures - the sentence beside it does not move
 * while the seconds do.
 */
.progress-timing {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: .55rem;
    margin: 0;
}
.progress-elapsed {
    font-family: var(--mono); font-variant-numeric: tabular-nums;
    color: var(--ink-soft);
}

/*
 * The waiting beam: one highlight travelling a hairline, so a queued or
 * running scan reads as movement rather than as a page that forgot about
 * you. It lives only inside the progress card, which hides the moment there
 * is a result - and it is the first thing the reduced-motion query takes
 * away, because a traveller that never arrives is just a smudge.
 */
.progress-beam {
    height: 2px; border-radius: 2px;
    background: var(--line);
    overflow: hidden;
}
.progress-beam span {
    display: block; height: 100%; width: 32%;
    background: linear-gradient(90deg, transparent, var(--brand), transparent);
    animation: beam 1.7s ease-in-out infinite;
}
@keyframes beam {
    from { transform: translateX(-110%); }
    to { transform: translateX(330%); }
}
@media (prefers-reduced-motion: reduce) {
    .progress-beam { display: none; }
}

/* score */
.score {
    display: flex; gap: 2rem; align-items: center; flex-wrap: wrap;
}
/*
 * Registering --fill is what lets the dial sweep from zero to the rating
 * instead of snapping to it: an unregistered custom property cannot
 * interpolate, so this is the difference between an arc being drawn and a
 * jump cut. Where the registration is not understood the dial simply shows
 * its final value, which is the old behaviour and perfectly readable.
 */
@property --fill {
    syntax: "<number>";
    inherits: false;
    initial-value: 0;
}
/*
 * The dial: the rating as one arc in the verdict's colour, sweeping in from
 * zero, with the grade set large at the centre in the display face. The
 * verdict is the one place a status colour is allowed to glow - everywhere
 * else it stays inside a chip.
 */
.score-dial {
    --dial: var(--good);
    --dial-ink: var(--good-ink);
    position: relative; flex: none;
    width: 168px; height: 168px; border-radius: 50%;
    display: grid; place-items: center;
    background: conic-gradient(var(--dial) calc(var(--fill) * 1%), var(--line) 0);
    box-shadow: 0 0 0 1px var(--line), 0 26px 60px -34px var(--dial);
    animation: dial .9s var(--ease-out) backwards,
        sweep 1.2s var(--ease-out) .15s backwards;
}
.score-dial::after {
    content: ""; position: absolute; inset: 12px;
    background: var(--card-solid); border-radius: 50%;
    box-shadow: inset 0 2px 10px rgba(13, 15, 23, .12);
}
.score-dial .grade {
    position: relative; z-index: 1;
    font-family: var(--font-display);
    font-size: 3.1rem; font-weight: 700; letter-spacing: -.05em; color: var(--dial-ink);
}
.score-dial[data-tone="good"] { --dial: var(--good); --dial-ink: var(--good-ink); }
.score-dial[data-tone="fair"] { --dial: var(--fair); --dial-ink: var(--fair-ink); }
.score-dial[data-tone="bad"] { --dial: var(--bad); --dial-ink: var(--bad-ink); }
@keyframes dial { from { transform: scale(.92); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes sweep { from { --fill: 0; } }

/*
 * The facts beside the grade, set as a ledger: hairline rows, the term in
 * small caps on the left and the reading on the right. It is the part of the
 * page that most wants to look like a report, so it does.
 */
.score-facts { flex: 1 1 20rem; }
.score-facts dl { display: grid; grid-template-columns: minmax(8.5rem, auto) 1fr; margin: 0; }
.score-facts dt, .score-facts dd { padding: .42rem 0; }
.score-facts dt {
    color: var(--ink-faint);
    font-family: var(--mono);
    font-size: .7rem;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: .13em;
    padding-right: 1rem;
    padding-top: .55rem;
}
.score-facts dd { margin: 0; font-weight: 500; font-size: .95rem; }
.score-facts dt:not(:first-of-type),
.score-facts dt:not(:first-of-type) + dd { border-top: 1px solid var(--line); }

/* A transport fact beside the grade. The colour is the scanner's own verdict
   on that check; the dot carries it for anybody who cannot see the hue. */
.tls-fact::before {
    content: ""; display: inline-block; width: .5rem; height: .5rem;
    border-radius: 50%; margin-right: .4rem; vertical-align: .05rem;
    background: var(--ink-faint);
}
.tls-fact[data-tone="good"]::before { background: var(--good); }
.tls-fact[data-tone="bad"]::before { background: var(--bad); }
.tls-fact[data-tone="bad"] { color: var(--bad-ink); }
.hint-inline { color: var(--ink-faint); font-weight: 400; font-size: .85rem; }

.counters { display: flex; flex-wrap: wrap; gap: .55rem; margin-top: 1.15rem; }
/* A reading, boxed: the number large in the mono face, its name beneath in
   the same voice, and a hairline that lifts towards the pointer. */
.counter {
    border-radius: var(--radius-sm); border: 1px solid var(--line);
    background: var(--tint);
    padding: .6rem .95rem; text-align: center; min-width: 5.5rem;
    transition: border-color .18s var(--ease), transform .18s var(--ease);
}
.counter:hover { border-color: var(--brand); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .counter:hover { transform: none; } }
.counter strong {
    display: block;
    font-family: var(--mono); font-weight: 500;
    font-size: 1.35rem; font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
/*
 * A counter that filters. The reading was always a hairline box that lifted
 * towards the pointer; pressing one now hides every finding of another
 * severity, so it becomes a button and says so - `aria-pressed` for the
 * reader who cannot see the tint, the severity's own colour for the reader
 * who can. A counter standing at zero is disabled rather than offered: a
 * filter that empties the list is a dead end nobody asked for.
 */
.counter[data-filter] {
    font: inherit;
    cursor: pointer;
    appearance: none; -webkit-appearance: none;
    color: inherit;
}
.counter:disabled { opacity: .5; cursor: not-allowed; }
.counter:disabled:hover { border-color: var(--line); transform: none; }
.counter[data-filter="critical"][aria-pressed="true"] { background: var(--bad-soft); border-color: var(--bad); }
.counter[data-filter="critical"][aria-pressed="true"] strong,
.counter[data-filter="critical"][aria-pressed="true"] span { color: var(--bad-ink); }
.counter[data-filter="warning"][aria-pressed="true"] { background: var(--fair-soft); border-color: var(--fair); }
.counter[data-filter="warning"][aria-pressed="true"] strong,
.counter[data-filter="warning"][aria-pressed="true"] span { color: var(--fair-ink); }
.counter[data-filter="info"][aria-pressed="true"] { background: var(--info-soft); border-color: var(--info); }
.counter[data-filter="info"][aria-pressed="true"] strong,
.counter[data-filter="info"][aria-pressed="true"] span { color: var(--info-ink); }
/* The sentence under the list saying what is being shown, and the way back
   to all of it. The control is a link rather than a second button: it undoes
   a choice, it does not make one. */
.filter-status {
    display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
    margin: 0 0 .9rem;
}
/* `display: flex` above outranks the user-agent `[hidden]` rule, which left
   an empty sentence and a "show all" link on the page before any filter. */
.filter-status[hidden] { display: none; }
.filter-clear {
    font: inherit; font-size: inherit;
    padding: 0; border: 0; background: none;
    color: var(--brand-deep);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}
.filter-clear:hover { color: var(--brand); }
.counter span {
    font-family: var(--mono);
    font-size: .68rem; color: var(--ink-faint);
    text-transform: uppercase; letter-spacing: .1em;
}

/* findings */
/* The single column may shrink below its content: a remediation naming
   OC_PASSWORD_POLICY_MIN_UPPERCASE_CHARACTERS has no natural break, and an
   implicit `auto` track sized to it pushed the whole list off a phone screen. */
.findings { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: .7rem; }
.finding {
    border: 1px solid var(--line); border-left: 3px solid var(--line);
    border-radius: var(--radius-sm); padding: .85rem 1.05rem;
    transition: border-color .15s ease, box-shadow .15s ease;
}
.finding:hover { border-color: var(--line-strong); box-shadow: var(--shadow-sm); }
.finding[data-tag="critical"] { border-left-color: var(--bad); }
.finding[data-tag="warning"] { border-left-color: var(--fair); }
.finding[data-tag="info"] { border-left-color: var(--info); }
.finding-head { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.finding-head code { font-size: .92rem; font-weight: 600; color: var(--brand-deep); }
.finding p { margin: .5rem 0 0; font-size: .93rem; color: var(--ink-soft); overflow-wrap: anywhere; }
.finding-head code { overflow-wrap: anywhere; }
.finding .fix { color: var(--ink); }
.finding .fix strong { color: var(--ink); }
/* The documentation / advisory link reads as a small action, in the same
   chip shape as the severity and category tags above it, rather than as a
   sentence of body text that happens to be blue. The arrow is the same
   affordance `.page-nav-card` uses for "this leads elsewhere", scaled down. */
.fix a {
    display: inline-flex; align-items: center; gap: .3rem;
    margin-left: .4rem;
    padding: .12rem .55rem;
    border-radius: 6px;
    background: var(--tint);
    color: var(--brand-deep);
    font-family: var(--mono);
    font-size: .72rem; font-weight: 500;
    text-transform: uppercase; letter-spacing: .08em;
    text-decoration: none;
    transition: background .16s var(--ease), color .16s var(--ease);
}
.fix a::after {
    content: "";
    width: .38rem; height: .38rem;
    border-top: 1.5px solid currentColor; border-right: 1.5px solid currentColor;
    transform: rotate(45deg);
    transition: transform .18s var(--ease);
}
.fix a:hover, .fix a:focus-visible { background: var(--brand-soft); color: var(--brand-deep); }
.fix a:hover::after, .fix a:focus-visible::after { transform: rotate(45deg) translate(1px, -1px); }
@media (prefers-reduced-motion: reduce) {
    .fix a:hover::after, .fix a:focus-visible::after { transform: rotate(45deg); }
}

/* An identifier that leads to its catalogue entry. It has to still read as
   the identifier - that is what a reader is scanning the list for - so the
   affordance is an underline that only fills in on hover, rather than link
   colour fighting the `code` colour beside it. */
.check-link { color: inherit; text-decoration: underline; text-decoration-style: dotted; }
.check-link:hover, .check-link:focus-visible { text-decoration-style: solid; }
.check-link code { cursor: pointer; }
.pill-list .check-link { text-underline-offset: 2px; }

/* Arriving from a result page, the entry that was clicked is the only reason
   the reader is on this page, so it says which one it is rather than leaving
   them to find it in a list of sixty. */
.finding:target {
    border-left-color: var(--brand); background: var(--brand-soft);
    scroll-margin-top: 5rem;
}

/* remediation plan - the findings list, numbered and with a predicted grade */
.plan { margin-top: .4rem; }
.plan-step[data-gain="no"] { border-left-color: var(--line); }
.plan-order {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 1.6rem; height: 1.6rem; border-radius: 50%;
    border: 1.5px solid var(--line-strong);
    font-family: var(--mono); font-size: .76rem; color: var(--ink-soft);
}
.plan-grade {
    margin-left: auto;
    font-family: var(--mono);
    font-size: .74rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .06em; color: var(--good-ink);
    background: var(--good-soft); border-radius: 4px; padding: .18rem .55rem;
}
.plan-step[data-gain="no"] .plan-grade {
    color: var(--ink-faint); background: transparent; border: 1px solid var(--line);
}

/* The rehearsal grades a release nobody has installed, so its colour is the
   tone of the rating that release would reach - not, as above, whether a
   step improved anything. */
.rehearsal-grade {
    margin-left: auto;
    font-family: var(--mono);
    font-size: .74rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .06em;
    border-radius: 4px; padding: .18rem .55rem;
    color: var(--bad-ink); background: var(--bad-soft);
}
.rehearsal-grade[data-tone="good"] { color: var(--good-ink); background: var(--good-soft); }
.rehearsal-grade[data-tone="fair"] { color: var(--fair-ink); background: var(--fair-soft); }

.facts { display: grid; grid-template-columns: auto 1fr; gap: .35rem 1rem; margin: .6rem 0 0; }
.facts dt { color: var(--ink-faint); font-size: .88rem; }
.facts dd { margin: 0; font-size: .95rem; overflow-wrap: anywhere; }
@media (max-width: 30rem) {
  .facts { grid-template-columns: 1fr; gap: 0 0; }
  .facts dd { margin: 0 0 .5rem; }
}

.pill-list { display: flex; flex-wrap: wrap; gap: .4rem; padding: 0; margin: .6rem 0 0; list-style: none; }
.pill-list li {
    font-family: var(--mono); font-size: .8rem;
    border: 1px solid var(--line);
    border-radius: 4px; padding: .2rem .6rem; color: var(--ink-soft);
}

.expiry {
    display: flex; gap: .6rem; align-items: center;
    color: var(--ink-faint); font-size: .88rem; margin-top: 1.4rem;
}
.expiry svg { width: 17px; height: 17px; }

.plain-list { margin: .6rem 0 0; padding-left: 1.1rem; color: var(--ink-soft); }
.plain-list li { margin-bottom: .5rem; line-height: 1.6; }
.plain-list strong { color: var(--ink); }

/* -------------------------------------------------------------- grade guide */
.grade-list {
    list-style: none;
    margin: 1.5rem 0 0;
    padding: 0;
    display: grid;
}
.grade-row {
    --grade: var(--bad);
    --grade-ink: var(--bad-ink);
    display: grid;
    grid-template-columns: 5rem minmax(0, 1fr);
    gap: 1.35rem;
    align-items: start;
    padding: 1.35rem 0;
    border-top: 1px solid var(--line);
}
.grade-row:first-child { border-top: 0; padding-top: .35rem; }
.grade-row[data-tone="good"] { --grade: var(--good); --grade-ink: var(--good-ink); }
.grade-row[data-tone="fair"] { --grade: var(--fair); --grade-ink: var(--fair-ink); }
.grade-mark {
    display: grid;
    place-items: center;
    width: 4.5rem;
    height: 4.5rem;
    border: 1px solid var(--grade);
    border-radius: 50%;
    color: var(--grade-ink);
    background: var(--card-solid);
    box-shadow: 0 18px 34px -24px var(--grade);
    font-family: var(--font-display);
    font-size: 1.65rem;
    font-weight: 700;
    letter-spacing: -.05em;
}
.grade-body h3 {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: .35rem 1rem;
    margin-bottom: .35rem;
    font-size: 1.25rem;
}
.grade-score {
    color: var(--ink-faint);
    font-family: var(--mono);
    font-size: .68rem;
    font-weight: 500;
    letter-spacing: .12em;
    text-transform: uppercase;
}
.grade-fix { margin: .55rem 0 0; color: var(--ink-soft); }
.grade-fix strong { color: var(--ink); }
.cap-list {
    list-style: none;
    margin: 1.25rem 0 1.4rem;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .65rem;
}
.cap-list li {
    display: flex;
    align-items: center;
    gap: .65rem;
    min-width: 0;
    padding: .75rem .85rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--tint);
}
.cap-arrow { color: var(--ink-faint); }
.cap-grade { white-space: nowrap; }
.cap-list .hint { margin: 0 0 0 auto; white-space: nowrap; }
@media (max-width: 720px) {
    .grade-row { grid-template-columns: 3.75rem minmax(0, 1fr); gap: .9rem; }
    .grade-mark { width: 3.5rem; height: 3.5rem; font-size: 1.35rem; }
    .cap-list { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------- CLI docs */
.docs-toc {
    display: flex;
    align-items: center;
    gap: .35rem;
    flex-wrap: wrap;
    padding: 1rem 1.15rem;
}
.docs-toc .kicker { margin: 0 auto 0 0; }
.docs-toc a {
    padding: .35rem .65rem;
    border-radius: 999px;
    color: var(--ink-soft);
    text-decoration: none;
    font-size: .86rem;
}
.docs-toc a:hover { color: var(--ink); background: var(--tint); }
.docs-toc-groups {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    gap: .75rem 1.25rem;
    flex-basis: 100%;
    margin-top: .35rem;
}
.docs-toc-group {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: .1rem;
    min-width: 0;
}
.docs-toc-label {
    margin: 0 0 .2rem .65rem;
    color: var(--ink-soft);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
}
.docs-command-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem;
}
.docs-command-grid > div {
    min-width: 0;
    padding: 1rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--tint);
}
.docs-command-grid .code-block { margin-bottom: 0; }
.option-list dl { margin: 1.2rem 0; }
.option-list dl > div {
    display: grid;
    grid-template-columns: minmax(12rem, .7fr) 1.3fr;
    gap: 1rem;
    padding: .65rem 0;
    border-top: 1px solid var(--line);
}
.option-list dt, .option-list dd { margin: 0; }
.option-list dd { color: var(--ink-soft); }
.precedence {
    list-style: none;
    margin: 1.25rem 0;
    padding: 0;
    counter-reset: precedence;
}
.precedence li {
    counter-increment: precedence;
    display: grid;
    grid-template-columns: 2rem minmax(9rem, .35fr) 1fr;
    gap: .75rem;
    align-items: baseline;
    padding: .65rem 0;
    border-top: 1px solid var(--line);
}
.precedence li::before {
    content: counter(precedence);
    color: var(--brand-deep);
    font-family: var(--mono);
    font-size: .75rem;
}
.precedence span { color: var(--ink-soft); }
.docs-guide-grid {
    list-style: none;
    margin: 1.25rem 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem;
}
.docs-guide-grid .page-nav-card { margin: 0; }
.docs-article { overflow: hidden; }
.docs-article h2 {
    margin-top: 2.25rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--line);
}
.docs-article h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.docs-article h3 { margin-top: 1.6rem; }
.docs-article h4 { margin-top: 1.25rem; }
.docs-article pre {
    max-width: 100%;
    overflow-x: auto;
    margin: 1rem 0;
    padding: 1rem 1.15rem;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--code-bg);
    color: var(--code-ink);
    font-family: var(--mono);
    font-size: .82rem;
    line-height: 1.65;
    white-space: pre;
}
.docs-article table {
    display: block;
    width: 100%;
    max-width: 100%;
    overflow-x: auto;
    border-collapse: collapse;
    margin: 1rem 0 1.5rem;
    font-size: .88rem;
}
.docs-article th, .docs-article td {
    min-width: 9rem;
    padding: .65rem .75rem;
    border: 1px solid var(--line);
    text-align: left;
    vertical-align: top;
}
.docs-article th {
    background: var(--tint);
    color: var(--ink);
    font-family: var(--mono);
    font-size: .7rem;
    letter-spacing: .08em;
    text-transform: uppercase;
}
.docs-article blockquote {
    margin: 1.25rem 0;
    padding: .8rem 1rem;
    border-left: 3px solid var(--brand);
    background: var(--brand-soft);
    color: var(--ink-soft);
}
.docs-article blockquote > :last-child { margin-bottom: 0; }
.docs-article li { margin-bottom: .35rem; }
.docs-article hr { border: 0; border-top: 1px solid var(--line); margin: 2rem 0; }
@media (max-width: 720px) {
    .docs-command-grid, .docs-guide-grid { grid-template-columns: 1fr; }
    .option-list dl > div { grid-template-columns: 1fr; gap: .25rem; }
    .precedence li { grid-template-columns: 1.5rem 1fr; }
    .precedence span { grid-column: 2; }
}

.export-list { margin: .6rem 0 0; padding: 0; list-style: none; }
.export-list li {
    display: flex; flex-wrap: wrap; align-items: center; gap: .7rem;
    padding: .6rem 0; border-bottom: 1px solid var(--line);
}
.export-list li:last-child { border-bottom: 0; }
.export-list .btn { min-width: 8.5rem; text-align: center; }
.export-list .hint { margin: 0; }

/* Sharing. The caution above the buttons carries the one thing a reader has
   to know before using them - the address is the credential - so it is toned
   rather than left as another grey hint nobody reads. */
.share-warning {
    color: var(--fair-ink); border-left: 3px solid var(--fair);
    padding: .5rem 0 .5rem .8rem; margin-top: .8rem;
}
/* The copy buttons report back in place. Colour alone would say nothing to a
   reader who cannot see it, so the label itself changes and this only tints
   what the label already says. */
.share-list .btn[data-share-state="done"] { color: var(--good-ink); border-color: var(--good); }
.share-list .btn[data-share-state="failed"] { color: var(--bad-ink); border-color: var(--bad); }
/* A report address is long and has no spaces, so it needs somewhere to break
   rather than widening the card on a phone. */
.share-fallback code { overflow-wrap: anywhere; }

/* A plain disclosure (the raw JSON on the result page): quiet, with the
   marker the browser supplies, unlike the waiver panel's custom chevron. */
details > summary { cursor: pointer; font-weight: 600; }
details > summary:hover { color: var(--brand-deep); }

pre.code-block {
    background: var(--code-bg); border: 1px solid transparent;
    border-radius: var(--radius-sm); padding: .95rem 1.1rem;
    overflow-x: auto; font-size: .84rem; line-height: 1.6;
    margin: 0 0 1rem;
}
pre.code-block code { font-family: var(--mono); color: var(--code-ink); }

pre.json {
    background: var(--code-bg); border: 1px solid transparent;
    border-radius: var(--radius-sm); padding: 1rem 1.1rem;
    overflow-x: auto; font-size: .84rem; line-height: 1.5;
    max-height: 26rem; color: var(--code-ink);
}

/* ----------------------------------------------------------- scan again */
/* Tabular figures on the countdown, so the sentence does not shift sideways
   every time the seconds tick over. */
#rescan-note { font-variant-numeric: tabular-nums; }

/* --------------------------------------------------- configuration fragment */
/* The picker is hidden until fragment.js reveals it. Without scripting every
   fragment stays on the page under its own heading, which is why the heading
   is styled to work in both arrangements. */
.flavour-picker {
    display: flex; flex-wrap: wrap; gap: .4rem;
    margin: 1rem 0 .2rem;
}
/* Rendered `hidden` for readers without scripting; `display: flex` above
   would otherwise show them four buttons that do nothing. */
.flavour-picker[hidden] { display: none; }
.flavour {
    font: inherit; font-size: .82rem; font-weight: 600;
    padding: .4rem .8rem; cursor: pointer;
    color: var(--ink-soft); background: var(--tint);
    border: 1px solid var(--line); border-radius: var(--radius-sm);
}
.flavour:hover { color: var(--brand-deep); }
.flavour[aria-pressed="true"] {
    color: var(--brand-deep); background: var(--card);
    border-color: var(--brand);
}
.fragment { margin-top: 1.1rem; }
/* With the picker showing, the heading would repeat the pressed button, so it
   goes; without scripting it is the only thing separating five blocks. */
[data-fragment-picked] .fragment-name { display: none; }
.fragment-name { font-size: .95rem; margin: 0 0 .4rem; }
pre.config {
    background: var(--code-bg); border: 1px solid transparent;
    border-radius: var(--radius-sm); padding: 1rem 1.1rem;
    overflow-x: auto; font-size: .84rem; line-height: 1.55;
    color: var(--code-ink); margin: .5rem 0 .7rem;
}
pre.config code { font-family: var(--mono); }

/* -------------------------------------------------------------- not found */
.empty { text-align: center; padding: 3rem 0; }
.empty .kicker { justify-content: center; }
.empty .kicker::before { display: none; }
.empty img { width: min(100%, 280px); margin-bottom: 1.5rem; }
.empty .lede { margin-inline: auto; max-width: var(--measure); }

/* ---------------------------------------------------------- small helpers */
/*
 * The content security policy has no 'unsafe-inline' for styles, so a
 * template cannot carry a style attribute. These are the handful of one-off
 * adjustments the markup would otherwise have made inline.
 */
.section-gap { margin-top: 1.25rem; }
.section-gap-lg { margin-top: 3rem; }
.flush { margin: 0; }
.hint-lead { margin-top: .9rem; }
.field-narrow { flex: 0 1 12rem; }

/*
 * The dial is drawn with a conic gradient whose sweep is the rating. The
 * rating is one of six integers, so each gets a rule rather than an inline
 * custom property the policy would refuse.
 */
.score-dial[data-rating="0"] { --fill: 4; }
.score-dial[data-rating="1"] { --fill: 20; }
.score-dial[data-rating="2"] { --fill: 40; }
.score-dial[data-rating="3"] { --fill: 60; }
.score-dial[data-rating="4"] { --fill: 80; }
.score-dial[data-rating="5"] { --fill: 100; }

/* ------------------------------------------------------------- small screens */
/*
 * Everything above is written for a desk. These two blocks are the phone, and
 * they are last so they win without a specificity trick.
 *
 * The header is the reason this section exists: a brand line and six links
 * across 390px used to run off the side of the screen, and the only way to
 * reach "About" was to scroll the page sideways.
 */
@media (max-width: 1360px) {
    .site-header .shell { flex-wrap: wrap; row-gap: .8rem; }
    /* A basis rather than `auto`: flex breaks a line before it shrinks an
       item, so a brand asking for its full width would push the button onto
       a row of its own instead of wrapping its own second line. */
    .brand { flex: 1 1 55%; min-width: 0; }
    .brand span {
        overflow: hidden;
        font-size: .95rem;
        text-overflow: ellipsis;
    }
    /* Without JavaScript: a second row that wraps, never a sideways scroll. */
    .site-nav { width: 100%; flex-wrap: wrap; gap: .2rem .5rem; }
    .header-search { width: 100%; margin-bottom: .35rem; }

    [data-nav="enhanced"] .nav-toggle { display: inline-flex; }
    [data-nav="enhanced"] .site-nav { display: none; }
    [data-nav="enhanced"] .site-nav[data-open="true"] {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: .15rem;
        padding: .4rem;
        background: var(--card);
        -webkit-backdrop-filter: var(--glass-blur);
        backdrop-filter: var(--glass-blur);
        border: 1px solid var(--line);
        border-radius: var(--radius);
        box-shadow: var(--shadow-lg);
        animation: menu-in .22s ease;
        transform-origin: top right;
    }
    /* The panel drops in, then its links arrive one after another - 40ms
       apart, felt rather than counted. */
    @keyframes menu-in {
        from { opacity: 0; transform: translateY(-6px) scale(.98); }
        to { opacity: 1; transform: none; }
    }
    [data-nav="enhanced"] .site-nav[data-open="true"] a {
        padding: .7rem 1rem;
        font-size: 1rem;
        border-radius: var(--radius-sm);
        animation: rise .3s ease backwards;
    }
    [data-nav="enhanced"] .site-nav[data-open="true"] .header-search {
        display: block;
        width: 100%;
    }
    [data-nav="enhanced"] .site-nav[data-open="true"] .language-switcher {
        display: flex;
        width: 100%;
        margin-top: .35rem;
    }
    [data-nav="enhanced"] .site-nav[data-open="true"] .language-switcher select {
        flex: 1 1 auto;
    }
    [data-nav="enhanced"] .site-nav[data-open="true"] a::after { display: none; }
    [data-nav="enhanced"] .site-nav[data-open="true"] a:nth-child(2) { animation-delay: .04s; }
    [data-nav="enhanced"] .site-nav[data-open="true"] a:nth-child(3) { animation-delay: .08s; }
    [data-nav="enhanced"] .site-nav[data-open="true"] a:nth-child(4) { animation-delay: .12s; }
    [data-nav="enhanced"] .site-nav[data-open="true"] a:nth-child(5) { animation-delay: .16s; }
    [data-nav="enhanced"] .site-nav[data-open="true"] a:nth-child(6) { animation-delay: .2s; }
    [data-nav="enhanced"] .site-nav[data-open="true"] a:nth-child(7) { animation-delay: .24s; }
}

@media (max-width: 640px) {
    .shell { width: min(100% - 1.6rem, var(--page)); }

    /*
     * On a phone the field comes first.
     *
     * The hero reads copy, artwork, field - which is the right order on a
     * wide screen, where the first two sit side by side and the field is
     * still the first thing under them. Stacked into one column it stops
     * being that order: the eyebrow, a two-line headline and the lede are
     * most of a phone screen on their own, so the one field this service
     * exists for ended up at the fold, and the visitor's first gesture was
     * a scroll looking for something they were already promised.
     *
     * So the column is reordered: `display: contents` dissolves the
     * two-column wrapper so that the copy, the form and the artwork are all
     * children of the same flex column, and the form is painted at the top.
     * The copy follows it, then the artwork at a size that looks deliberate
     * rather than stretched, then the promises. The form carries its own
     * kicker and hint, so what it is for arrives with it.
     *
     * Only the painting order changes. Nothing is hidden and the DOM is
     * untouched, so a screen reader still meets the page's heading before
     * its form, the tab order still runs as the markup is written, and a
     * reader without CSS gets the order the markup states.
     */
    .hero { display: flex; flex-direction: column; padding: .9rem 0 .5rem; }
    .hero-grid { display: contents; }
    /* The delay follows the new order, so the field is the first thing to
       arrive rather than rising into place under copy that is already there. */
    .hero .scan-form { order: 1; margin-top: 0; animation-delay: 0s; }
    .hero-copy { order: 2; margin-top: 2rem; }
    .hero-art {
        order: 3;
        width: min(100%, 300px);
        margin: 2rem auto 0;
        animation-delay: .35s;
    }
    /* The strip is a child of `.hero` too, and the items above are the only
       ones that name an order - its default `0` would sort it above all of
       them, so the promises would open the page above the field. It reads
       as a footnote to the instrument, so it goes after the artwork: last
       ordered, last placed. */
    .assurance { order: 4; margin-top: 2.25rem; }

    .card { padding: 1.15rem; }
    main { padding-bottom: 2.5rem; }

    /* A submit button the width of the thumb that reaches it. */
    .field-narrow { flex: 1 1 100%; }
    .export-list .btn { width: 100%; }
    .scan-actions { align-items: stretch; width: 100%; }
    .scan-actions form, .scan-actions .btn { width: 100%; }

    /* The ledger stacks: the term above the reading, one hairline per row. */
    .score { gap: 1.25rem; }
    .score-dial { width: 136px; height: 136px; }
    .score-dial .grade { font-size: 2.4rem; }
    .score-facts dl { grid-template-columns: 1fr; }
    .score-facts dt { padding-bottom: 0; border-top: 1px solid var(--line); }
    .score-facts dt:first-of-type { border-top: 0; }
    .score-facts dl > dd { padding-top: .1rem; border-top: 0; margin-bottom: .3rem; }

    .scan-track { margin-left: 0; margin-top: .4rem; }

    .footer-grid { grid-template-columns: 1fr; }
}

.build {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
}
/* --------------------------------------------------------------- on paper */
/*
 * A report is a document before it is a page: it gets printed for a change
 * record, saved to PDF for somebody who was not at the screen, and filed. The
 * screen design is glass over a moving aurora, which is exactly the wrong
 * thing to put on paper - so print gets its own reading of the same markup
 * rather than a second template.
 *
 * Three rules decide everything below.
 *
 * The tokens are forced back to daylight. This is not taste: `--ink` in the
 * dark scheme is near-white, and a reader who chose dark and pressed print
 * would otherwise be handed a sheet of blank paper with the ink the printer
 * refused to lay down. Paper has one scheme.
 *
 * Anything that only exists to be pressed goes. A menu, a search field, a
 * scheme switch, an export button, a copy-to-clipboard control and a rescan
 * form are all instructions to a browser, and on paper they are furniture
 * that survived a move. What stays is what says something: the grade, the
 * facts, the findings, the plan, and the notice at the foot of the page.
 *
 * Nothing that carries meaning is allowed to depend on colour reaching the
 * page, because most of the time it will not - a browser drops backgrounds
 * from a print unless it is told otherwise. Every severity is already a word
 * as well as a tint, so the words are what print; the tints are asked for
 * with `print-color-adjust` and are welcome if they arrive.
 */
@media print {
    /* Daylight, whatever the screen was set to. */
    :root,
    :root[data-theme="dark"],
    :root[data-theme="light"] {
        color-scheme: light;
        --ink: #0d0f17;
        --ink-soft: #33394a;
        --ink-faint: #4d5468;
        --paper: #ffffff;
        --card: #ffffff;
        --card-solid: #ffffff;
        --line: rgba(13, 15, 23, .28);
        --line-strong: rgba(13, 15, 23, .45);
        --tint: transparent;
        --glass-blur: none;
        --glass-hi: transparent;
        --glass-edge: none;
        --brand: #2f2599;
        --brand-deep: #241c76;
        --brand-soft: transparent;
        --brand-glow: transparent;
        --brand-glow-strong: transparent;
        --sky: none;
        --grain: none;
        --horizon: none;
        --shadow-sm: none;
        --shadow: none;
        --shadow-lg: none;
        --code-bg: #ffffff;
        --code-ink: #0d0f17;
    }

    @page { margin: 16mm; }

    body {
        font-size: 10.5pt;
        line-height: 1.45;
        background: #ffffff;
        background-image: none;
        min-height: 0;
        display: block;
    }
    /* The aurora and the lattice are fixed layers: on paper a fixed layer is
       a stamp on every sheet. */
    body::before, body::after { display: none; }

    /*
     * The chrome. Everything here is a control or a way to somewhere else,
     * and paper is neither.
     */
    .site-header,
    .skip-link,
    .back-to-top,
    .theme-toggle,
    .nav-toggle,
    .site-nav,
    .docs-toc,
    .footer-note,
    .footer-nav,
    .build,
    .progress-beam,
    .pulse,
    .hero-art,
    .scan-actions,
    .export-list,
    .filter-clear,
    .flavour-picker,
    [data-copy-fragment],
    [data-print="hide"] {
        display: none !important;
    }

    /* With the picker gone, the heading each fragment carries is the only
       thing left saying which file the block belongs in - so the rule that
       hides it as a repeat of the pressed button is lifted. */
    [data-fragment-picked] .fragment-name { display: block; }

    /* A pane becomes a ruled block: one hairline, no frost, no lift. */
    .card {
        background: #ffffff;
        -webkit-backdrop-filter: none;
        backdrop-filter: none;
        border: 1px solid var(--line);
        border-radius: 0;
        box-shadow: none;
        padding: 1rem 1.1rem;
        break-inside: auto;
    }
    .card + .card { margin-top: .8rem; }

    /* A finding split across a fold is two half-findings. The same goes for a
       reading, a step of the plan and a block of configuration. */
    .finding, .counter, .step, .toggle, pre, .waiver-group {
        break-inside: avoid;
    }
    h1, h2, h3 { break-after: avoid; }

    /*
     * The grade. The ring is a conic gradient and will usually not be
     * printed; the letter inside it is text and always will be, so the dial
     * is given an outline that does not depend on ink arriving.
     */
    .score-dial {
        width: 112px; height: 112px;
        border: 2px solid var(--ink);
        box-shadow: none;
        animation: none;
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }
    .score-dial::after { inset: 8px; box-shadow: none; }
    .score-dial .grade { font-size: 2.2rem; color: var(--ink); }

    /* A severity is a word first. The tint is asked for and not relied on. */
    .tag, .tls-fact {
        border: 1px solid var(--line);
        print-color-adjust: exact;
        -webkit-print-color-adjust: exact;
    }

    /* A counter is a reading again, not a control. */
    .counter { border: 1px solid var(--line); }
    .counter:disabled { opacity: 1; }

    /*
     * A link on paper is a dead end unless it says where it went. Only the
     * references inside a finding get their address printed - the
     * documentation and the advisory, which are the two somebody reads next
     * with the sheet in their hand.
     */
    .fix a { text-decoration: underline; background: none; padding: 0; }
    .fix a::after {
        content: " (" attr(href) ")";
        font-family: var(--mono);
        font-size: .78em;
        word-break: break-all;
    }

    /* The trademark and scope notices stay: a printed report stands on its
       own, and DESIGN.md says every surface that does carries them. */
    .site-footer { border-top: 1px solid var(--line); padding-top: .6rem; }
    .legal { font-size: .72rem; color: var(--ink-soft); }
}
