/* =========================================================================
   Sova dashboard
   The design system of ai-book-processor (tokens, buttons, theme toggle, flag
   picker, sign-in card), adapted to an operational dashboard. Colour is driven
   entirely by custom properties, so light/dark is one attribute on <html>.
   The one motion motif is a ping's ripple: the live dot, a newly arrived row,
   and the empty log waiting for one.
   ========================================================================= */

/* ---------- Tokens ------------------------------------------------------ */

:root {
    color-scheme: light;

    --bg: #f5f6fa;
    --bg-elevated: #ffffff;
    --bg-sunken: #eceef5;
    --bg-inset: #f8f9fc;
    /* The surface a control moves to under the pointer: darker on light, lighter on dark. */
    --bg-hover: #eceef5;

    --surface-border: #e0e3ee;
    --surface-border-strong: #c9cee0;

    --text: #171a24;
    --text-muted: #5c6379;
    /* The quietest text that still passes 4.5:1 on --bg-inset: column headings, milliseconds, placeholders. */
    --text-subtle: #646b82;

    --accent: #4f46e5;
    --accent-hover: #4338ca;
    --accent-soft: #eeecfe;
    --accent-contrast: #ffffff;

    --success: #0f8a5f;
    --success-soft: #e2f5ec;
    --warning: #b4690e;
    --warning-soft: #fdf1dd;
    /* Text set on the soft tints: the base hues fall under 4.5:1 there on the light theme. */
    --success-ink: #0b6b49;
    --warning-ink: #8a4f08;
    --danger: #c8342d;
    --danger-soft: #fdeaea;
    /* A destructive button: the danger hue as a fill, its text and its hover. */
    --danger-hover: #ad2a24;
    --danger-contrast: #ffffff;
    /* A button that loads afresh, such as Refresh: a green deep enough for white text at 4.5:1, its hover and its text. */
    --success-fill: #0b7a53;
    --success-hover: #08633f;
    --success-contrast: #ffffff;
    --info: #1a6fb5;
    --info-soft: #e6f1fb;

    /* What a modal dialog dims the page behind it with. */
    --scrim: rgb(16 20 40 / 42%);

    /* Charts: every series a colour of its own, its line solid (DESIGN.md, "The Series-Colour Rule"). Each keeps 3:1 on
       --bg-elevated. */
    --chart-inside-temperature: var(--accent);
    --chart-outside-temperature: #0b7f9a;
    --chart-inside-humidity: #2b8a3e;
    --chart-outside-humidity: #a87a00;
    --chart-inside-pressure: #c92a6b;
    --chart-outside-pressure: #d9480f;

    /* The idle Flasher's bolt: a yellow of its own, apart from the amber that means staged. 3:1 on --bg-inset. */
    --bolt: #b58900;

    /* Code in the Flasher's review (highlight.js's classes): each keeps 4.5:1 on --bg-sunken. */
    --code-keyword: var(--accent);
    --code-builtin: #a21caf;
    --code-string: #1a7340;
    --code-number: #b45309;
    --code-title: #1d4ed8;
    --code-meta: #9a3412;

    --shadow-sm: 0 1px 2px rgb(16 20 40 / 6%), 0 1px 3px rgb(16 20 40 / 4%);
    --shadow-md: 0 4px 12px rgb(16 20 40 / 8%), 0 2px 4px rgb(16 20 40 / 4%);
    --shadow-lg: 0 18px 40px rgb(16 20 40 / 12%), 0 4px 10px rgb(16 20 40 / 6%);

    --radius-sm: 8px;
    --radius: 14px;
    --radius-lg: 22px;
    --radius-pill: 999px;

    --font: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
    --font-mono: "Cascadia Code", "Consolas", ui-monospace, "SF Mono", Menlo, monospace;

    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --speed-fast: 140ms;
    --speed: 240ms;
    --speed-slow: 420ms;

    /* Flag chips: only the colours that would collide with a surface are tokens. */
    --flag-ring: rgb(16 20 40 / 22%);
    --flag-lift: 0 1px 3px rgb(16 20 40 / 30%);

    /* The sticky header's height, which the log's sticky column headings sit under. */
    --bar-height: 3.75rem;
    --gutter: clamp(1rem, 4vw, 2.25rem);
}

[data-theme="dark"] {
    color-scheme: dark;

    --bg: #0e1017;
    --bg-elevated: #171a23;
    --bg-sunken: #0a0c12;
    --bg-inset: #1d212c;
    --bg-hover: #232838;

    --surface-border: #272c3a;
    --surface-border-strong: #3a4155;

    --text: #eef0f7;
    --text-muted: #a2a9bd;
    --text-subtle: #8a92a8;

    --accent: #8b83ff;
    --accent-hover: #a099ff;
    --accent-soft: #232145;
    --accent-contrast: #14121f;

    --success: #4ad9a0;
    --success-soft: #102b22;
    --warning: #f0b354;
    --warning-soft: #2e2312;
    --success-ink: var(--success);
    --warning-ink: var(--warning);
    --danger: #ff7b73;
    --danger-soft: #331a1a;
    --danger-hover: #ff958e;
    --danger-contrast: #1f0d0c;
    --success-fill: var(--success);
    --success-hover: #74e4b7;
    --success-contrast: #06261a;
    --info: #6fb6f2;
    --info-soft: #12252f;

    --scrim: rgb(0 0 0 / 62%);

    --chart-outside-temperature: #4cc3dc;
    --chart-inside-humidity: #5cc87a;
    --chart-outside-humidity: #f0c04a;
    --chart-inside-pressure: #f27aa8;
    --chart-outside-pressure: #ff9a52;

    --bolt: #f5c842;

    --code-builtin: #e879f9;
    --code-string: #6ccb8a;
    --code-number: #f5a96b;
    --code-title: #7cb7ff;
    --code-meta: #fdba74;

    --shadow-sm: 0 1px 2px rgb(0 0 0 / 40%);
    --shadow-md: 0 4px 14px rgb(0 0 0 / 45%);
    --shadow-lg: 0 20px 44px rgb(0 0 0 / 55%);

    --flag-ring: rgb(255 255 255 / 26%);
    --flag-lift: 0 1px 3px rgb(0 0 0 / 65%);
}

/* ---------- Reset ------------------------------------------------------- */

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

html,
body {
    margin: 0;
    padding: 0;
}

body {
    min-height: 100dvh;
    font-family: var(--font);
    font-size: 15px;
    line-height: 1.55;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--speed) var(--ease-out), color var(--speed) var(--ease-out);
}

/* Set by app.js for two frames while the theme flips, so the page recolours in one step. */
.theme-switching,
.theme-switching * {
    transition: none !important;
}

h1, h2, h3 {
    margin: 0 0 0.4em;
    line-height: 1.25;
    font-weight: 600;
    letter-spacing: -0.011em;
    text-wrap: balance;
}

h1 { font-size: clamp(1.5rem, 1.2rem + 1.2vw, 1.85rem); }
h2 { font-size: 1.15rem; }

p { margin: 0 0 1em; }

/* Zero specificity, so a component's own colour always wins over the link colour. */
:where(a) {
    color: var(--accent);
    text-decoration: none;
    transition: color var(--speed-fast) var(--ease-out);
}

@media (hover: hover) { :where(a:hover) { color: var(--accent-hover); } }

/* Line-art icons. The flags opt out with stroke: none. */
svg {
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

/* FocusOnNavigate moves focus to each page's heading for screen readers; it isn't something to click. */
[tabindex="-1"]:focus { outline: none; }

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ---------- App shell --------------------------------------------------- */

.app-shell {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
}

.app-bar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 1rem;
    min-height: var(--bar-height);
    padding: max(0.55rem, env(safe-area-inset-top)) max(var(--gutter), env(safe-area-inset-right))
             0.55rem max(var(--gutter), env(safe-area-inset-left));
    background: color-mix(in srgb, var(--bg-elevated) 84%, transparent);
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--surface-border);
    transition: background-color var(--speed) var(--ease-out), border-color var(--speed) var(--ease-out);
}

.brand {
    display: flex;
    flex: none;
    align-items: center;
    gap: 0.65rem;
    color: var(--text);
}

@media (hover: hover) { .brand:hover { color: var(--text); } }

/* The radius matches rx="118" on favicon.svg's 512 viewBox, so the shadow follows the tile's corner. */
.brand__mark {
    width: 36px;
    height: 36px;
    flex: none;
    border-radius: 23%;
    box-shadow: var(--shadow-sm);
    transition: transform var(--speed) var(--ease-out-expo);
}

@media (hover: hover) { .brand:hover .brand__mark { transform: rotate(-6deg) scale(1.06); } }

.brand__text { display: flex; flex-direction: column; min-width: 0; line-height: 1.15; }
.brand__text strong { font-size: 0.98rem; }
.brand__text small { font-size: 0.72rem; color: var(--text-subtle); white-space: nowrap; }

/* When the bar runs short, as with the page links beside a long feed state, the account chip's name gives way. */
.app-bar__actions {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
    margin-left: auto;
}

.app-main {
    flex: 1;
    width: 100%;
    max-width: 1080px;
    margin: 0 auto;
    padding: clamp(1.25rem, 3vw, 2.5rem) max(var(--gutter), env(safe-area-inset-right))
             max(3rem, env(safe-area-inset-bottom)) max(var(--gutter), env(safe-area-inset-left));
}

@media (max-width: 480px) {
    .brand__text small { display: none; }
    .app-bar { gap: 0.6rem; }
    .app-bar__actions { gap: 0.4rem; }
}

/* The smallest phones have room for the controls or the name, and the mark already says which app this is. */
@media (max-width: 359px) {
    .brand__text { display: none; }
}

/* ---------- Navigation -------------------------------------------------- */

.app-nav {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: 0.5rem;
}

.app-nav__link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.25rem;
    padding: 0.3rem 0.85rem;
    border-radius: var(--radius-pill);
    font-size: 0.9rem;
    font-weight: 550;
    color: var(--text-muted);
    white-space: nowrap;
    transition: color var(--speed-fast) var(--ease-out), background-color var(--speed-fast) var(--ease-out);
}

@media (hover: hover) { .app-nav__link:hover { color: var(--text); background: var(--bg-hover); } }

/* The page you're on: the accent, as the selected language is. */
.app-nav__link[aria-current="page"],
.app-nav__link[aria-current="page"]:hover {
    color: var(--accent);
    background: var(--accent-soft);
    font-weight: 600;
}

/* Narrow screens: no room beside the controls, so the pages take a second row and share its width. The sticky
   column headings sit under the taller bar. */
@media (max-width: 720px) {
    .app-shell--nav { --bar-height: 6.5rem; }

    .app-shell--nav .app-bar {
        flex-wrap: wrap;
        align-content: center;
        row-gap: 0.5rem;
    }

    .app-nav {
        order: 3;
        width: 100%;
        margin-left: 0;
    }

    .app-nav__link { flex: 1; }
}

/* A phone: the four pages share its width, so they're padded less. */
@media (max-width: 480px) {
    .app-nav__link { padding-inline: 0.5rem; }
}

/* ---------- Pages ------------------------------------------------------- */

.page {
    animation: page-in var(--speed-slow) var(--ease-out) both;
}

@keyframes page-in {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}

.page__header {
    margin-bottom: 1.5rem;
}

.page__title-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    margin-bottom: 0.35rem;
}

.page__title-row h1 { margin: 0; }

.page__lead {
    max-width: 62ch;
    margin: 0;
    color: var(--text-muted);
}

/* ---------- Splash, spinner, error strip -------------------------------- */

.app-splash {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    min-height: 70dvh;
    color: var(--text-muted);
}

.app-splash p { margin: 0; }

.app-splash__mark {
    width: 56px;
    height: 56px;
    border-radius: 23%;
    box-shadow: var(--shadow-md);
    animation: float 2.6s var(--ease-out) infinite;
}

@keyframes float {
    0%, 100% { transform: translateY(0) rotate(0deg); }
    50%      { transform: translateY(-8px) rotate(-4deg); }
}

.spinner {
    display: inline-block;
    flex: none;
    width: 22px;
    height: 22px;
    border: 2.5px solid color-mix(in srgb, var(--accent) 25%, transparent);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 700ms linear infinite;
}

.spinner--sm { width: 14px; height: 14px; border-width: 2px; }

/* On a primary button, whose fill is the accent. */
.spinner--inverse {
    border-color: color-mix(in srgb, var(--accent-contrast) 35%, transparent);
    border-top-color: var(--accent-contrast);
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Blazor shows this by setting display: block. */
#blazor-error-ui {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 100;
    display: none;
    padding: 0.8rem max(var(--gutter), env(safe-area-inset-right)) max(0.8rem, env(safe-area-inset-bottom))
             max(var(--gutter), env(safe-area-inset-left));
    color: var(--danger);
    background: var(--danger-soft);
    border-top: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
    box-shadow: var(--shadow-lg);
}

#blazor-error-ui .reload {
    margin-left: 0.75rem;
    font-weight: 600;
    color: inherit;
    text-decoration: underline;
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: 0.55rem;
    right: max(var(--gutter), env(safe-area-inset-right));
    padding: 0 0.3rem;
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
}

/* ---------- Cards and notices ------------------------------------------- */

.card {
    background: var(--bg-elevated);
    border: 1px solid var(--surface-border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    transition: background-color var(--speed) var(--ease-out), border-color var(--speed) var(--ease-out);
}

.alert {
    margin: 1rem 0 0;
    padding: 0.7rem 0.9rem;
    border-radius: var(--radius-sm);
    font-size: 0.88rem;
}

.alert--error { color: var(--danger); background: var(--danger-soft); }
.alert--info { color: var(--info); background: var(--info-soft); }

.notice {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    margin-bottom: 1rem;
    padding: 0.6rem 0.6rem 0.6rem 1rem;
    border-radius: var(--radius-sm);
    font-size: 0.88rem;
    color: var(--warning-ink);
    background: var(--warning-soft);
}

.notice .btn { color: inherit; }

/* ---------- Buttons ----------------------------------------------------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    min-height: 2.5rem;
    padding: 0.55rem 1.1rem;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    font: inherit;
    font-size: 0.9rem;
    font-weight: 550;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color var(--speed-fast) var(--ease-out),
                border-color var(--speed-fast) var(--ease-out),
                color var(--speed-fast) var(--ease-out),
                transform var(--speed-fast) var(--ease-out),
                box-shadow var(--speed-fast) var(--ease-out);
}

.btn:active:not(:disabled) { transform: translateY(1px) scale(0.99); }

.btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.btn svg { width: 18px; height: 18px; flex: none; }

.btn--primary {
    color: var(--accent-contrast);
    background: var(--accent);
    box-shadow: 0 1px 2px rgb(16 20 40 / 18%);
}

@media (hover: hover) {
    .btn--primary:hover:not(:disabled) {
        background: var(--accent-hover);
        box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 38%, transparent);
    }
}

.btn--ghost {
    color: var(--text-muted);
    background: transparent;
}

@media (hover: hover) {
    .btn--ghost:hover:not(:disabled) {
        color: var(--text);
        background: var(--bg-hover);
    }
}

/* A destructive action, such as unstaging code: the danger hue as a fill, so it can't pass for the primary. */
.btn--danger {
    color: var(--danger-contrast);
    background: var(--danger);
    box-shadow: 0 1px 2px rgb(16 20 40 / 18%);
}

@media (hover: hover) {
    .btn--danger:hover:not(:disabled) {
        background: var(--danger-hover);
        box-shadow: 0 4px 14px color-mix(in srgb, var(--danger) 38%, transparent);
    }
}

.btn--danger:focus-visible { outline-color: var(--danger); }

/* Loading afresh, such as the filter bars' Refresh: green, so it stands out from the filters around it. */
.btn--refresh {
    color: var(--success-contrast);
    background: var(--success-fill);
    box-shadow: 0 1px 2px rgb(16 20 40 / 18%);
}

@media (hover: hover) {
    .btn--refresh:hover:not(:disabled) {
        background: var(--success-hover);
        box-shadow: 0 4px 14px color-mix(in srgb, var(--success-fill) 38%, transparent);
    }
}

.btn--refresh:focus-visible { outline: 2px solid var(--success-fill); outline-offset: 2px; }

.btn--sm { min-height: 2rem; padding: 0.3rem 0.7rem; font-size: 0.82rem; }
.btn--lg { min-height: 2.9rem; padding: 0.7rem 1.5rem; font-size: 0.98rem; }
.btn--block { width: 100%; }

/* ---------- Theme toggle ------------------------------------------------ */

.theme-toggle {
    padding: 0;
    background: none;
    border: none;
    cursor: pointer;
    line-height: 0;
}

/*
    As tall as the language chip beside it, 2.25rem. The knob sits 3px inside the hairline, and the track is two knobs
    plus those gaps wide, so the knob travels its own width and each icon centres where the knob isn't.
*/
.theme-toggle__track {
    --knob: calc(2.25rem - 8px);
    position: relative;
    display: block;
    width: calc(2 * var(--knob) + 8px);
    height: 2.25rem;
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    border: 1px solid var(--surface-border-strong);
    transition: background-color var(--speed) var(--ease-out), border-color var(--speed) var(--ease-out);
}

@media (hover: hover) { .theme-toggle:hover .theme-toggle__track { border-color: var(--accent); } }

.theme-toggle__knob {
    position: absolute;
    top: 3px;
    left: 3px;
    width: var(--knob);
    height: var(--knob);
    border-radius: 50%;
    background: var(--bg-elevated);
    box-shadow: var(--shadow-sm);
    transition: transform var(--speed) var(--ease-out-expo), background-color var(--speed) var(--ease-out);
}

.theme-toggle--dark .theme-toggle__knob {
    transform: translateX(var(--knob));
    background: var(--accent);
}

.theme-toggle__icon {
    position: absolute;
    top: 50%;
    width: 16px;
    height: 16px;
    stroke-width: 1.9;
    transform: translateY(-50%);
    transition: opacity var(--speed) var(--ease-out), transform var(--speed) var(--ease-out);
}

.theme-toggle__icon--sun {
    right: calc(var(--knob) / 2 - 5px);
    color: var(--warning);
}

.theme-toggle__icon--moon {
    left: calc(var(--knob) / 2 - 5px);
    color: var(--text-subtle);
    opacity: 0;
    transform: translateY(-50%) rotate(-40deg);
}

.theme-toggle--dark .theme-toggle__icon--sun {
    opacity: 0;
    transform: translateY(-50%) rotate(40deg);
}

.theme-toggle--dark .theme-toggle__icon--moon {
    opacity: 1;
    color: var(--accent);
    transform: translateY(-50%) rotate(0deg);
}

/* ---------- Language picker --------------------------------------------- */

/*
    `stroke: none` is load-bearing: stroke is inherited, so the global svg rule would otherwise outline
    every band of every flag in the text colour.
*/
.flag-icon {
    display: block;
    flex: none;
    width: 22px;
    height: 22px;
    stroke: none;
    filter: drop-shadow(var(--flag-lift));
}

/* Keeps the white half of the Czech flag from dissolving into a light surface. */
.flag-icon__ring {
    fill: none;
    stroke: var(--flag-ring);
    stroke-width: 1;
}

.picker { position: relative; }

.picker__button {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    min-height: 2.25rem;
    padding: 0.3rem 0.5rem 0.3rem 0.4rem;
    font: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--text);
    background: var(--bg-inset);
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: border-color var(--speed-fast) var(--ease-out),
                box-shadow var(--speed-fast) var(--ease-out),
                background-color var(--speed) var(--ease-out);
}

@media (hover: hover) { .picker__button:hover { border-color: var(--accent); } }

.picker__button:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

.picker__chevron {
    width: 14px;
    height: 14px;
    flex: none;
    stroke-width: 2.2;
    color: var(--text-subtle);
    transition: transform var(--speed) var(--ease-out);
}

.picker--open .picker__chevron { transform: rotate(180deg); }

/* Dismisses the menu on any outside click without an interop listener. */
.picker__backdrop {
    position: fixed;
    inset: 0;
    z-index: 40;
}

.picker__menu {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    z-index: 41;
    min-width: 11rem;
    margin: 0;
    padding: 0.3rem;
    list-style: none;
    border-radius: var(--radius);
    border: 1px solid var(--surface-border-strong);
    background: var(--bg-elevated);
    box-shadow: var(--shadow-lg);
    transform-origin: top right;
    animation: menu-in var(--speed) var(--ease-out);
}

@keyframes menu-in {
    from { opacity: 0; transform: translateY(-6px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}

.picker__option {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    min-height: 2.5rem;
    padding: 0.45rem 0.6rem;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
    transition: background-color var(--speed-fast) var(--ease-out);
}

@media (hover: hover) { .picker__option:hover { background: var(--bg-hover); } }

.picker__option--selected {
    color: var(--accent);
    font-weight: 600;
}

.picker__name { flex: 1; }

.picker__check {
    width: 15px;
    height: 15px;
    flex: none;
    stroke-width: 2.4;
}

/* ---------- Account menu ------------------------------------------------ */

.account { position: relative; min-width: 0; }

/*
    The language chip's shape, the user's initials where its flag is; the name beside them where there's room. A button
    sizes to its content, so it fills .account, which the bar can narrow: the name then ellipsizes.
*/
.account__button {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    max-width: 16rem;
    min-height: 2.25rem;
    padding: 0.2rem 0.5rem 0.2rem 0.2rem;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--text);
    background: var(--bg-inset);
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-pill);
    cursor: pointer;
    transition: border-color var(--speed-fast) var(--ease-out),
                box-shadow var(--speed-fast) var(--ease-out),
                background-color var(--speed) var(--ease-out);
}

@media (hover: hover) { .account__button:hover { border-color: var(--accent); } }

.account__button:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

.account--open .account__button .picker__chevron { transform: rotate(180deg); }

.account__label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 640px) {
    .account__label { display: none; }
}

.avatar {
    display: inline-grid;
    place-items: center;
    flex: none;
    width: 1.75rem;
    height: 1.75rem;
    border-radius: 50%;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    line-height: 1;
    color: var(--accent);
    background: var(--accent-soft);
}

.avatar svg { width: 16px; height: 16px; }
.avatar--large { width: 2.6rem; height: 2.6rem; font-size: 0.9rem; }
.avatar--large svg { width: 22px; height: 22px; }

.account__panel {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    z-index: 41;
    width: min(20rem, calc(100vw - 2 * var(--gutter)));
    padding: 0.4rem;
    border-radius: var(--radius);
    border: 1px solid var(--surface-border-strong);
    background: var(--bg-elevated);
    box-shadow: var(--shadow-lg);
    transform-origin: top right;
    animation: menu-in var(--speed) var(--ease-out);
}

.account__header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.55rem 0.6rem 0.8rem;
}

.account__who {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}

.account__who strong { font-size: 1.15rem; font-weight: 600; letter-spacing: -0.011em; overflow-wrap: anywhere; }
.account__who small { font-size: 0.75rem; color: var(--text-subtle); overflow-wrap: anywhere; }

.account__settings {
    padding: 0.3rem 0;
    border-block: 1px solid var(--surface-border);
}

.account__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    min-height: 2.9rem;
    padding: 0.2rem 0.6rem;
    font-size: 0.9rem;
}

.account__row-label { color: var(--text-muted); }

.account__item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    min-height: 2.9rem;
    margin-top: 0.3rem;
    padding: 0.5rem 0.6rem;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    text-align: left;
    cursor: pointer;
    transition: background-color var(--speed-fast) var(--ease-out);
}

.account__item svg { width: 20px; height: 20px; flex: none; color: var(--text-muted); }
.account__item:disabled { cursor: progress; }

@media (hover: hover) { .account__item:hover:not(:disabled) { background: var(--bg-hover); } }

.account__item--app svg { color: var(--accent); }

.account__item-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: 1.3;
}

.account__item-text small { font-size: 0.75rem; color: var(--text-subtle); }
.account__item-text small.account__note--failed { color: var(--danger); }

/* Inline in the caption's line, so the line is as tall with it as with the details that replace it. */
.account__item-text .account__skeleton {
    display: inline-block;
    width: 7rem;
    height: 0.6rem;
    vertical-align: middle;
}

/* No APK to offer: greyed out as a disabled link button is, the caption saying so. */
.account__item--unavailable:disabled { color: var(--text-subtle); cursor: not-allowed; }
.account__item--unavailable svg { color: var(--text-subtle); }

.account__divider {
    margin: 0.3rem 0.2rem 0;
    border: none;
    border-top: 1px solid var(--surface-border);
}

/* Signing out ends the session on this browser: the danger hue, as the language's alarms are. */
.account__item--danger,
.account__item--danger svg { color: var(--danger); font-weight: 550; }

@media (hover: hover) { .account__item--danger:hover:not(:disabled) { background: var(--danger-soft); } }

/* ---------- Sign-in ----------------------------------------------------- */

.auth-wrap {
    display: grid;
    place-items: center;
    min-height: calc(100dvh - var(--bar-height) - 6rem);
}

/* On a phone the card sits near the top, where the on-screen keyboard leaves it in view. */
@media (max-width: 560px) {
    .auth-wrap {
        min-height: 0;
        place-items: start center;
    }
}

.auth-card {
    width: min(460px, 100%);
    padding: clamp(1.5rem, 5vw, 2.1rem);
    border-radius: var(--radius-lg);
    border: 1px solid var(--surface-border);
    background: var(--bg-elevated);
    box-shadow: var(--shadow-lg);
    animation: auth-in var(--speed-slow) var(--ease-out-expo) both;
}

@keyframes auth-in {
    from { opacity: 0; transform: translateY(16px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}

.auth-card__lead { margin: 0; color: var(--text-muted); }

.auth-form {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-top: 1.5rem;
}

.auth-form .btn { margin-top: 1rem; }

.field__label {
    font-size: 0.85rem;
    font-weight: 600;
}

.input-with-action { position: relative; }

.input {
    width: 100%;
    min-height: 2.9rem;
    padding: 0.65rem 0.9rem;
    font: inherit;
    /* 16px or more, or iOS zooms the page in when the box gets focus. */
    font-size: 1rem;
    color: var(--text);
    background: var(--bg-inset);
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-sm);
    transition: border-color var(--speed-fast) var(--ease-out), box-shadow var(--speed-fast) var(--ease-out),
                background-color var(--speed) var(--ease-out);
}

.input::placeholder { color: var(--text-subtle); }

@media (hover: hover) { .input:hover { border-color: var(--text-subtle); } }

.input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}

/* A secret is read character by character, so it's set in the monospace face, where l, 1 and I differ. */
.input--secret {
    padding-right: 3.25rem;
    font-family: var(--font-mono);
    letter-spacing: 0.02em;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.input--secret::placeholder { font-family: var(--font); letter-spacing: normal; }

/* Edge draws its own reveal eye inside a password box, which would sit beside ours. */
.input--secret::-ms-reveal,
.input--secret::-ms-clear { display: none; }

.input-action {
    position: absolute;
    top: 50%;
    right: 0.3rem;
    display: grid;
    place-items: center;
    width: 2.4rem;
    height: 2.4rem;
    padding: 0;
    border: none;
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    background: none;
    cursor: pointer;
    transform: translateY(-50%);
    transition: color var(--speed-fast) var(--ease-out), background-color var(--speed-fast) var(--ease-out);
}

@media (hover: hover) { .input-action:hover { color: var(--text); background: var(--bg-hover); } }
.input-action svg { width: 20px; height: 20px; }

.auth-card__note {
    display: flex;
    gap: 0.6rem;
    margin: 1.4rem 0 0;
    padding-top: 1.1rem;
    border-top: 1px solid var(--surface-border);
    font-size: 0.82rem;
    color: var(--text-muted);
}

.auth-card__note svg {
    width: 17px;
    height: 17px;
    flex: none;
    margin-top: 0.1rem;
    color: var(--success);
}

/* ---------- Live feed status -------------------------------------------- */

.feed-status {
    --tone: var(--text-subtle);
    --tone-ink: var(--text-subtle);
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    flex: none;
    padding: 0.35rem 0.8rem;
    border-radius: var(--radius-pill);
    border: 1px solid color-mix(in srgb, var(--tone) 35%, transparent);
    background: color-mix(in srgb, var(--tone) 10%, var(--bg-elevated));
    color: var(--tone-ink);
    font-size: 0.82rem;
    font-weight: 600;
    white-space: nowrap;
    transition: color var(--speed) var(--ease-out), background-color var(--speed) var(--ease-out),
                border-color var(--speed) var(--ease-out);
}

.feed-status--live { --tone: var(--success); --tone-ink: var(--success-ink); }
.feed-status--connecting,
.feed-status--reconnecting { --tone: var(--warning); --tone-ink: var(--warning-ink); }
/* Danger passes 4.5:1 on its own tint in both themes, so it is its own ink. */
.feed-status--offline { --tone: var(--danger); --tone-ink: var(--danger); }

.feed-status-slot { display: inline-flex; flex: none; }

/* Offline, the pill is a button that retries at once. */
button.feed-status {
    font: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
}

@media (hover: hover) {
    button.feed-status:hover { background: color-mix(in srgb, var(--tone) 18%, var(--bg-elevated)); }
}

button.feed-status:focus-visible {
    outline: 2px solid var(--tone);
    outline-offset: 2px;
}

.feed-status__retry {
    width: 14px;
    height: 14px;
    stroke-width: 2.2;
}

.feed-status .spinner {
    border-color: color-mix(in srgb, var(--tone) 25%, transparent);
    border-top-color: var(--tone);
}

.feed-status__dot {
    position: relative;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--tone);
}

/* In the app bar, as tall as the account chip beside it. */
.app-bar .feed-status { min-height: 2.25rem; }

/* On a phone, the dot (or the spinner) alone, in a circle: the tooltip and screen readers still have the word. */
@media (max-width: 480px) {
    .app-bar .feed-status {
        justify-content: center;
        gap: 0.35rem;
        min-width: 2.25rem;
        padding-inline: 0.6rem;
    }

    .app-bar .feed-status__label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

/* The ripple: what a ping looks like. Only while live. */
.feed-status--live .feed-status__dot::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 2px solid var(--tone);
    animation: ripple 2.4s var(--ease-out) infinite;
}

@keyframes ripple {
    from { transform: scale(1); opacity: 0.8; }
    to   { transform: scale(3.2); opacity: 0; }
}

/* ---------- Ping log ---------------------------------------------------- */

/* clip rather than hidden: it rounds the table's corners without becoming a scroll container,
   so the column headings can still stick under the app bar. */
.ping-log { overflow: clip; }

.ping-log__toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.55rem 0.6rem 0.55rem 1.1rem;
    border-bottom: 1px solid var(--surface-border);
}

.ping-log__summary {
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

.ping-log__footnote {
    margin: 0;
    padding: 0.7rem 1.1rem;
    font-size: 0.8rem;
    color: var(--text-subtle);
    border-top: 1px solid var(--surface-border);
}

.ping-table {
    width: 100%;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}

.ping-table th {
    position: sticky;
    top: var(--bar-height);
    z-index: 1;
    padding: 0.55rem 1.1rem;
    font-size: 0.75rem;
    font-weight: 600;
    text-align: left;
    color: var(--text-subtle);
    background: var(--bg-inset);
    border-bottom: 1px solid var(--surface-border);
}

.ping-table td {
    padding: 0.6rem 1.1rem;
    font-size: 0.9rem;
    border-bottom: 1px solid var(--surface-border);
}

.ping-table tbody tr:last-child td { border-bottom: none; }

.ping-table tbody tr {
    transition: background-color var(--speed-fast) var(--ease-out);
}

@media (hover: hover) { .ping-table tbody tr:hover { background: var(--bg-inset); } }

.ping-table__ms { color: var(--text-subtle); }

/* Desktop: time and device shrink to their content, so a row reads as one line rather than three islands. */
.ping-table td:not(.ping-table__id):not(.ping-table__wide),
.ping-table th:not(.ping-table__id):not(.ping-table__wide) {
    width: 1%;
    padding-right: 3rem;
    white-space: nowrap;
}

/* Ping IDs are data to compare character by character, so they're set in the monospace face, with its ligatures off:
   Cascadia Code would otherwise draw a hyphen as a minus. */
.ping-table td.ping-table__id {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.ping-table td.ping-table__id { color: var(--text-muted); }

/*
    A ping that arrived while the page was open lands with the ripple, the same one the live dot makes, and the
    row's tint settles into the log.
*/
.ping-table td:first-child { position: relative; }

.ping-row--new {
    animation: ping-arrive 1.8s var(--ease-out);
}

.ping-row--new td:first-child::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 0.4rem;
    width: 6px;
    height: 6px;
    margin-top: -3px;
    border-radius: 50%;
    background: var(--accent);
    animation: ping-arrive-dot 1.8s var(--ease-out) forwards;
}

@keyframes ping-arrive {
    from { background-color: var(--accent-soft); }
    to   { background-color: transparent; }
}

@keyframes ping-arrive-dot {
    0%   { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent); }
    55%  { opacity: 1; box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 0%, transparent); }
    100% { opacity: 0; box-shadow: 0 0 0 6px transparent; }
}

/* Phones: narrower insets and cells. */
@media (max-width: 480px) {
    .ping-table th,
    .ping-table td { padding-inline: 0.75rem; }
    .ping-log__toolbar { padding-left: 0.75rem; }
    .ping-table td { font-size: 0.86rem; }
    .ping-row--new td:first-child::before { left: 0.2rem; }

    .ping-table td:not(.ping-table__id):not(.ping-table__wide),
    .ping-table th:not(.ping-table__id):not(.ping-table__wide) {
        width: auto;
        padding-right: 0.75rem;
    }

    .ping-table .ping-table__id {
        text-align: right;
        vertical-align: top;
    }

    .ping-table td.ping-table__id { font-size: 0.8rem; }
}

/* ---------- Devices ----------------------------------------------------- */

/* A card per device, two to a row on a desk and one on a phone: large enough to read its state at a glance. */
.device-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
    gap: 1rem;
}

.device-card {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 1rem 1.1rem;
    transition: border-color var(--speed-fast) var(--ease-out), box-shadow var(--speed-fast) var(--ease-out),
                transform var(--speed-fast) var(--ease-out), background-color var(--speed) var(--ease-out);
}

/* The ID's link covers the card, so all of it opens the device, while the link's own text stays just the ID. */
.device-card__link {
    color: inherit;
}

.device-card__link::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: var(--radius);
}

.device-card__link:focus-visible { outline: none; }

.device-card:has(.device-card__link:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

@media (hover: hover) {
    .device-card:hover {
        border-color: var(--surface-border-strong);
        box-shadow: var(--shadow-md);
        transform: translateY(-1px);
    }

    .device-card:hover .device-card__link { color: var(--accent); }
}

.device-card:active { transform: none; }

/* The title on the left, the state on the right, centred on the name's line; on a narrow card the state wraps under the ID. */
.device-card__top {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 0.5rem 0.75rem;
    margin-bottom: 1rem;
}

/* Device IDs are data to compare character by character, as in the logs; a name a user gave it is text, in the sans. */
.device-card__id {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: 1.3rem;
    font-weight: 650;
    overflow-wrap: anywhere;
}

.device-card__id .device-card__link { min-width: 0; }

/* Whether it's online, before the name: a dot centred on the name's first line (1.3rem at 1.25), over the card's link,
   so its tooltip, when the device was last seen, shows. */
.device-card__id .presence {
    position: relative;
    z-index: 1;
    flex: none;
    height: 1.625rem;
}

.device-card__id .presence__dot {
    width: 10px;
    height: 10px;
}

.device-card__id--id {
    font-family: var(--font-mono);
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

/* As tall as the name's line (1.3rem at 1.25), so the badges sit level with the name. */
.device-card__state {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.75rem;
    min-height: 1.625rem;
}

/* The newest event when it's an alarm or lost data, in Alarm Red as in the logs; the badge already shows a locking. */
.device-card__alarm {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0.9rem 0 0;
    font-size: 0.88rem;
}

.device-card__alarm-text {
    color: var(--danger);
    font-weight: 550;
}

.device-card__alarm time {
    flex: none;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-subtle);
}

.device-card--skeleton {
    gap: 0.9rem;
    min-height: 12rem;
}

.device-card--skeleton .skeleton--device { height: 1.3rem; }

.device-grid__problem {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    margin-top: 1rem;
}

.device-grid__problem .alert { margin: 0; }

/* How a device's presence shows: a dot and a word, quieter than the lock badge, or on a card the dot alone. Green
   online; amber lagging, still connected but overdue, the dot still filled; red offline, the dot a ring, so connected or
   not reads without colour. */
.presence {
    --tone: var(--warning);
    --tone-ink: var(--warning-ink);
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--tone-ink);
    white-space: nowrap;
}

.presence--online { --tone: var(--success); --tone-ink: var(--success-ink); }
.presence--lagging { --tone: var(--warning); --tone-ink: var(--warning-ink); }
.presence--offline { --tone: var(--danger); --tone-ink: var(--danger); }

.presence__dot {
    position: relative;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--tone);
}

.presence--offline .presence__dot {
    background: transparent;
    box-shadow: inset 0 0 0 2px var(--tone);
}

/* The server heard from it again: the dot swells a beat and sends out one ripple, the live dot's, once. */
.presence__dot--pulse {
    animation: presence-beat 0.6s var(--ease-out);
}

.presence__dot--pulse::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 2px solid var(--tone);
    opacity: 0;
    animation: ripple 1.8s var(--ease-out);
}

@keyframes presence-beat {
    40% { transform: scale(1.4); }
}

.badge {
    display: inline-flex;
    align-items: center;
    padding: 0.15rem 0.6rem;
    border-radius: var(--radius-pill);
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
}

.badge--locked {
    background: var(--success-soft);
    color: var(--success-ink);
}

.badge--unlocked {
    background: var(--bg-sunken);
    color: var(--text-muted);
}

/* Firmware staged for a device, on its card: the staged Flasher's amber, as its page shows it. */
.badge--staged {
    background: var(--warning-soft);
    color: var(--warning-ink);
}

/* A reading at a glance: each value large, its detail under it. */
.metrics {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0.75rem 1rem;
    margin: 0;
}

/* A device's page: its reading's tiles and its own, five in a row, three under 900px, two on a phone. */
.metrics--wide { grid-template-columns: repeat(5, minmax(0, 1fr)); }

.metric {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.metric dt {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-subtle);
}

.metric dd {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    margin: 0;
}

.metric__value {
    font-size: 1.15rem;
    font-weight: 600;
    letter-spacing: -0.011em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.metric__detail {
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

.metric__none {
    font-size: 0.85rem;
    color: var(--text-subtle);
}

.metrics__empty {
    margin: 0;
    font-size: 0.88rem;
    color: var(--text-muted);
}

/* After the metrics: a battery's state overrides the detail's grey. */
.battery { color: var(--text-muted); }
.battery--full { color: var(--success-ink); }
.battery--flat { color: var(--danger); font-weight: 550; }

@media (max-width: 900px) {
    .metrics--wide { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
    .metrics--wide { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* A device's page: back to the list, its ID as the title, its state under it, then its reading and its log. */
.back-link {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-bottom: 0.9rem;
    font-size: 0.88rem;
    font-weight: 550;
    color: var(--text-muted);
}

.back-link svg {
    width: 16px;
    height: 16px;
    stroke-width: 2;
}

@media (hover: hover) { .back-link:hover { color: var(--accent); } }

/* The title is the device's name, or else its ID, set in the monospace as IDs are everywhere. */
.device-title {
    overflow-wrap: anywhere;
}

.device-title--id {
    font-family: var(--font-mono);
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

/* The title with its pencil, which edits the name in place; the Flasher keeps to the right of the header. */
.device-heading {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
}

.device-heading__rename {
    flex: none;
    width: 2.25rem;
    min-height: 2.25rem;
    padding: 0;
    color: var(--text-subtle);
}

/* A named device's ID, under its name: data, in the monospace, and quiet. */
.device-id {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 0.82rem;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
    color: var(--text-subtle);
    overflow-wrap: anywhere;
}

/* Renaming: the field and its buttons stand in for the title, the buttons wrapping under the field when narrow. */
.rename-form {
    flex: 1 1 22rem;
    min-width: 0;
    max-width: 36rem;
}

.rename-form__row {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.input.rename-form__input {
    flex: 1 1 14rem;
    width: auto;
    min-width: 0;
    min-height: 2.5rem;
    padding: 0.45rem 0.8rem;
}

.rename-form__actions {
    display: flex;
    gap: 0.5rem;
}

.rename-form__note {
    margin: 0.45rem 0 0;
    font-size: 0.82rem;
}

.rename-form__hint { color: var(--text-subtle); }

.rename-form .alert { margin-top: 0.5rem; }

/* While renaming, the title row holds the field and its note; the Flasher beside it lines up with the field (below). */
.page__title-row:has(.rename-form) { align-items: flex-start; }

.device-state {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    margin-top: 0.6rem;
}

.device-state__seen {
    font-size: 0.82rem;
    color: var(--text-subtle);
}

/*
    A device's header: the title and its details on the left; the Flasher on the right, on the title's line. On a phone
    it goes under the details.
*/
.device-header {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "title aside" "details aside";
    column-gap: 1rem;
    align-items: start;
}

.device-header__title { grid-area: title; }
.device-header__details { grid-area: details; min-width: 0; }

/* As tall as the rename pencil, or as the field while renaming, so the Flasher sits on the title's line. */
.device-header__aside {
    grid-area: aside;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    min-height: 2.25rem;
}

.device-header:has(.rename-form) .device-header__aside { min-height: 2.5rem; }

/* A device the server has no secret for has no Flasher. */
.device-header__aside:empty { display: none; }

@media (max-width: 560px) {
    .device-header {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "title" "details" "aside";
    }

    .device-header__aside,
    .device-header:has(.rename-form) .device-header__aside {
        justify-content: flex-start;
        min-height: 0;
        margin-top: 0.85rem;
    }
}

.device-status {
    margin-bottom: 1.5rem;
    padding: 1rem 1.1rem;
}

/* ---------- Test devices ------------------------------------------------ */

/* The test devices, after the others: a caption on a hairline that runs to the edge, so no test device passes for one in the field. */
.device-section { margin-top: 2rem; }

.device-section:first-child { margin-top: 0; }

.device-section__heading {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 0 0 1rem;
    color: var(--text-muted);
    font-size: 0.85rem;
    font-weight: 600;
}

.device-section__heading::after {
    content: "";
    flex: 1 1 auto;
    height: 1px;
    background: var(--surface-border-strong);
}

/*
    A switch: a track with a knob, the theme toggle's at a smaller size (1.5rem tall, the knob 3px inside the hairline),
    and its label beside it. On, the track fills indigo and the knob slides its own width.
*/
.switch {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    min-height: 2.25rem;
    padding: 0 0.35rem;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.85rem;
    font-weight: 550;
    cursor: pointer;
}

.switch[aria-busy="true"] { cursor: progress; }

.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.switch__track {
    --knob: calc(1.5rem - 8px);
    position: relative;
    flex: none;
    width: calc(2 * var(--knob) + 8px);
    height: 1.5rem;
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
    transition: background-color var(--speed) var(--ease-out), border-color var(--speed) var(--ease-out);
}

.switch__knob {
    position: absolute;
    top: 3px;
    left: 3px;
    width: var(--knob);
    height: var(--knob);
    border-radius: 50%;
    background: var(--bg-elevated);
    box-shadow: var(--shadow-sm);
    transition: transform var(--speed) var(--ease-out-expo);
}

.switch[aria-checked="true"] { color: var(--text); }

.switch[aria-checked="true"] .switch__track {
    border-color: var(--accent);
    background: var(--accent);
}

.switch[aria-checked="true"] .switch__knob { transform: translateX(var(--knob)); }

@media (hover: hover) {
    .switch:hover { color: var(--text); }
    .switch:hover .switch__track { border-color: var(--accent); }
}

/* ---------- Device details ---------------------------------------------- */

/* What the device reported, shut at first: a header row that opens it, with the test switch on its right. */
.device-details { margin-bottom: 1.5rem; }

.device-details__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem 1rem;
    padding: 0.4rem 0.6rem 0.4rem 0.5rem;
}

.device-details__heading {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 600;
}

.device-details__toggle {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 2.25rem;
    padding: 0 0.6rem 0 0.4rem;
    border: none;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    cursor: pointer;
}

.device-details__toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (hover: hover) { .device-details__toggle:hover { background: var(--bg-hover); } }

.device-details__chevron {
    width: 18px;
    height: 18px;
    color: var(--text-subtle);
    transition: transform var(--speed) var(--ease-out);
}

.device-details__toggle[aria-expanded="true"] .device-details__chevron { transform: rotate(90deg); }

.device-details__alert { margin: 0 1.1rem 0.8rem; }

.device-details__body {
    padding: 0.9rem 1.1rem 1rem;
    border-top: 1px solid var(--surface-border);
}

.device-details__body[hidden] { display: none; }

.device-details__reported,
.device-details__none {
    margin: 0;
    color: var(--text-subtle);
    font-size: 0.82rem;
}

.device-details__none { color: var(--text-muted); font-size: 0.9rem; }

/* A column per group, as many as fit at 17rem or more. */
.device-details__groups {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
    gap: 1.1rem 2rem;
    margin-top: 0.9rem;
}

.device-details__group-heading {
    margin: 0 0 0.45rem;
    color: var(--text);
    font-size: 0.85rem;
    font-weight: 600;
}

/* The labels' column one width in every group, so values line up when the groups stack on a phone. */
.facts.device-details__facts {
    grid-template-columns: 8rem minmax(0, 1fr);
    margin: 0;
    font-size: 0.88rem;
}

.facts.device-details__facts dt { white-space: normal; }

/* On a phone the values need the room more: a 20-digit ICCID stays on one line, and a long label wraps instead. */
@media (max-width: 480px) {
    .facts.device-details__facts { grid-template-columns: 6.25rem minmax(0, 1fr); column-gap: 0.75rem; }
}

/* Forgetting it: under a hairline, at the end, as a quiet button in Alarm Red; the dialog has the red fill. */
.device-details__footer {
    display: flex;
    justify-content: flex-end;
    margin-top: 1.1rem;
    padding-top: 0.8rem;
    border-top: 1px solid var(--surface-border);
}

.btn--ghost-danger { color: var(--danger); }

@media (hover: hover) {
    .btn--ghost.btn--ghost-danger:hover:not(:disabled) {
        color: var(--danger);
        background: var(--danger-soft);
    }
}

.btn--ghost-danger:focus-visible { outline-color: var(--danger); }

/* ---------- Flasher ----------------------------------------------------- */

/*
    A pill in the live pill's type and padding. Idle it's a control on Inset with a strong hairline; staged it takes
    the amber tone, failed the red, tinted as the live pill is, since then it reports state.
*/
.flasher {
    position: relative;
    display: inline-flex;
    flex: none;
}

.flasher__button {
    --tone: var(--accent);
    --tone-ink: var(--text-muted);
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.35rem 0.8rem 0.35rem 0.65rem;
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-pill);
    background: var(--bg-inset);
    color: var(--tone-ink);
    font: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: color var(--speed-fast) var(--ease-out), background-color var(--speed-fast) var(--ease-out),
                border-color var(--speed-fast) var(--ease-out);
}

.flasher__icon {
    width: 15px;
    height: 15px;
    flex: none;
    stroke-width: 1.9;
}

.flasher--idle .flasher__icon { color: var(--bolt); }

@media (hover: hover) {
    .flasher--idle .flasher__button:hover { border-color: var(--accent); color: var(--text); }
}

.flasher__button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.flasher--staged .flasher__button,
.flasher--failed .flasher__button {
    border-color: color-mix(in srgb, var(--tone) 40%, transparent);
    background: color-mix(in srgb, var(--tone) 12%, var(--bg-elevated));
}

.flasher--staged .flasher__button { --tone: var(--warning); --tone-ink: var(--warning-ink); }
/* Danger passes 4.5:1 on its own tint in both themes, so it is its own ink. */
.flasher--failed .flasher__button { --tone: var(--danger); --tone-ink: var(--danger); }

/*
    Staged, the pill ripples in its tone, over and over, until the device runs the firmware or it's unstaged. It clips
    the fill behind its text to its shape; the ripple, a shadow, is outside it.
*/
.flasher--staged .flasher__button {
    position: relative;
    overflow: hidden;
    isolation: isolate;
    animation: flasher-ripple 2.4s var(--ease-out) infinite;
}

@keyframes flasher-ripple {
    from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone) 55%, transparent); }
    to   { box-shadow: 0 0 0 10px color-mix(in srgb, var(--tone) 0%, transparent); }
}

@media (hover: hover) {
    .flasher--staged .flasher__button:hover,
    .flasher--failed .flasher__button:hover { background: color-mix(in srgb, var(--tone) 20%, var(--bg-elevated)); }
}

.flasher--staged .flasher__button:focus-visible,
.flasher--failed .flasher__button:focus-visible { outline-color: var(--tone); }

/* Where staged firmware has got to, after a hairline: upload pending, uploading and how much, or applying. */
.flasher__stage {
    padding-left: 0.45rem;
    border-left: 1px solid color-mix(in srgb, var(--tone) 40%, transparent);
    font-variant-numeric: tabular-nums;
}

/*
    While it uploads, the pill fills up from the left behind its text, easing from one step to the next (the server tells
    every tenth). Its scale is set through the CSSOM (sova.flasher.fill). Opaque, so hover doesn't deepen it: the ink
    keeps 4.5:1 on it in both themes. Gone once it's uploaded.
*/
.flasher__fill {
    position: absolute;
    inset: 0;
    z-index: -1;
    background: color-mix(in srgb, var(--tone) 28%, var(--bg-elevated));
    transform: scaleX(0);
    transform-origin: left;
    transition: transform 1s var(--ease-out);
    pointer-events: none;
}

/*
    The tooltip: a small card under the pill, aligned to its right edge, with a nub pointing up at it. It shows on
    hover after a beat, and on keyboard focus; touch has the dialogs instead.
*/
.flasher__tip {
    position: absolute;
    top: calc(100% + 0.6rem);
    right: 0;
    z-index: 30;
    width: max-content;
    min-width: 17rem;
    max-width: min(23rem, calc(100vw - 2 * var(--gutter)));
    padding: 0.85rem 1rem 0.8rem;
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius);
    background: var(--bg-elevated);
    box-shadow: var(--shadow-lg);
    color: var(--text-muted);
    font-size: 0.85rem;
    line-height: 1.5;
    white-space: normal;
    text-align: left;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity var(--speed-fast) var(--ease-out), transform var(--speed-fast) var(--ease-out),
                visibility 0s linear var(--speed-fast);
}

.flasher__tip::before {
    content: "";
    position: absolute;
    top: -6px;
    right: 1.35rem;
    width: 10px;
    height: 10px;
    background: inherit;
    border-top: 1px solid var(--surface-border-strong);
    border-left: 1px solid var(--surface-border-strong);
    transform: rotate(45deg);
}

@media (hover: hover) {
    .flasher:hover .flasher__tip {
        opacity: 1;
        visibility: visible;
        transform: none;
        transition-delay: 200ms, 200ms, 0s;
    }
}

.flasher:has(:focus-visible) .flasher__tip {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition-delay: 0s;
}

.flasher__tip p { margin: 0; }

.flasher__tip-title {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-bottom: 0.3rem !important;
    color: var(--text);
    font-size: 0.9rem;
    font-weight: 600;
}

.flasher--staged .flasher__tip-title { color: var(--warning-ink); }
.flasher--failed .flasher__tip-title { color: var(--danger); }

.flasher__dot {
    width: 8px;
    height: 8px;
    flex: none;
    border-radius: 50%;
    background: currentColor;
}

.flasher--staged .flasher__dot { background: var(--warning); }

.flasher__tip-lead { color: var(--text-muted); }

.flasher__progress {
    width: 100%;
    height: 0.35rem;
    margin-top: 0.55rem;
    border: 0;
    border-radius: var(--radius-pill);
    overflow: hidden;
    background: var(--bg-sunken);
    accent-color: var(--warning);
}

.flasher__progress::-webkit-progress-bar { background: var(--bg-sunken); }
.flasher__progress::-webkit-progress-value { background: var(--warning); }
.flasher__progress::-moz-progress-bar { background: var(--warning); }

.flasher__tip-hint {
    margin-top: 0.75rem !important;
    padding-top: 0.6rem;
    border-top: 1px solid var(--surface-border);
    color: var(--text-subtle);
    font-size: 0.8rem;
}

/* On a phone the pill starts a row under the details: the tooltip spans the row, so it never runs off the screen. */
@media (max-width: 560px) {
    .device-header__aside { position: relative; }
    .flasher { position: static; }

    .flasher__tip {
        left: 0;
        right: 0;
        width: auto;
        min-width: 0;
        max-width: none;
    }

    .flasher__tip::before { display: none; }
}

/* Facts as label: value, in the Flasher's tooltip and dialogs. IDs in the monospace. */
.facts {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.3rem 0.9rem;
    margin: 0.7rem 0 0;
}

.facts dt {
    color: var(--text-subtle);
    font-size: 0.78rem;
    font-weight: 600;
    line-height: 1.6;
    white-space: nowrap;
}

.facts dd {
    min-width: 0;
    margin: 0;
    color: var(--text);
    overflow-wrap: anywhere;
    font-variant-numeric: tabular-nums;
}

.facts code {
    font-family: var(--font-mono);
    font-size: 0.82rem;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.facts__sub {
    display: block;
    color: var(--text-subtle);
    font-size: 0.8rem;
}

.facts__files { font-family: var(--font-mono); font-variant-ligatures: none; font-feature-settings: "calt" 0; }

/* ---------- Dialogs ----------------------------------------------------- */

/* The browser's modal <dialog>: a lifted card over a scrim, rising in as the sign-in card does. */
.dialog {
    width: min(32rem, calc(100vw - 2rem));
    max-height: calc(100dvh - 2rem);
    padding: 0;
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-lg);
    background: var(--bg-elevated);
    color: var(--text);
    box-shadow: var(--shadow-lg);
    overflow: auto;
}

.dialog[open] { animation: dialog-in var(--speed-slow) var(--ease-out-expo); }

.dialog::backdrop {
    background: var(--scrim);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
}

@keyframes dialog-in {
    from { opacity: 0; transform: translateY(12px) scale(0.97); }
    to   { opacity: 1; transform: none; }
}

.dialog__body {
    display: grid;
    gap: 0.8rem;
    padding: 1.4rem 1.5rem 1.1rem;
    font-size: 0.9rem;
}

.dialog__body p { margin: 0; }
.dialog__body .facts { margin: 0; }

.dialog__title {
    margin: 0;
    font-size: 1.15rem;
}

.dialog__lead { color: var(--text-muted); }

.dialog__note {
    color: var(--text-subtle);
    font-size: 0.85rem;
}

/* What unstaging does: the warning that comes with a destructive action, ink on its tint. */
.dialog__warning {
    padding: 0.7rem 0.9rem;
    border-radius: var(--radius-sm);
    background: var(--warning-soft);
    color: var(--warning-ink);
    font-size: 0.88rem;
}

.dialog__alert { margin: 0; }
.dialog__alert ul { margin: 0.35rem 0 0; padding-left: 1.2rem; }

.dialog__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.9rem 1.5rem 1.2rem;
    border-top: 1px solid var(--surface-border);
}

@media (max-width: 480px) {
    .dialog__body { padding: 1.2rem 1.1rem 1rem; }
    .dialog__actions { padding: 0.8rem 1.1rem 1.1rem; }
    .dialog__actions .btn { flex: 1 1 auto; }
}

/*
    The Flasher's staging form: three numbered fields, the client's folder, the key and the review, each with its
    actions on its heading's line, what was chosen on Inset, and its checks under it, ticks in green and problems in red.
    A field's number turns into a tick once it's chosen and right, and red with what's wrong.
*/
.dialog--stage { width: min(52rem, calc(100vw - 2rem)); }

.stage-field {
    display: grid;
    gap: 0.55rem;
    min-width: 0;
}

.stage-field__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem 0.75rem;
}

.stage-field__label {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    margin: 0;
    font-size: 0.95rem;
    font-weight: 600;
}

.stage-field__label code {
    font-family: var(--font-mono);
    font-size: 0.9rem;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.stage-field__step {
    display: inline-grid;
    place-items: center;
    width: 1.45rem;
    height: 1.45rem;
    flex: none;
    border-radius: 50%;
    background: var(--accent-soft);
    color: var(--accent);
    font-family: var(--font);
    font-size: 0.78rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    transition: background-color var(--speed) var(--ease-out), color var(--speed) var(--ease-out);
}

.stage-field__step svg { width: 14px; height: 14px; stroke-width: 2.6; }
.stage-field__step--done { background: var(--success-soft); color: var(--success-ink); }
.stage-field--error .stage-field__step,
.stage-field--missing .stage-field__step { background: var(--danger-soft); color: var(--danger); }

.stage-field__choose {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.stage-field__chooser { cursor: pointer; }
.stage-field__chooser svg { width: 16px; height: 16px; }
.stage-field__chooser:has(input:disabled) { opacity: 0.5; cursor: not-allowed; }

.stage-field__chooser:has(:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* A phone's browser can't choose a folder; nor can the Android app. The empty field says so instead. */
.stage-field__touch { display: none; }

@media (hover: none) and (pointer: coarse) {
    .stage-field__folder,
    .stage-field__desk { display: none; }
    .stage-field__touch { display: inline; }
}

/* An empty field: what goes in it, on a dashed hairline; red once Stage was pressed without it. */
.stage-field__empty {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
    padding: 0.75rem 0.9rem;
    border: 1px dashed var(--surface-border-strong);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    color: var(--text-subtle);
    font-size: 0.85rem;
}

.stage-field--missing .stage-field__empty {
    border-color: var(--danger);
    background: var(--danger-soft);
    color: var(--danger);
}

.stage-field__panel {
    display: grid;
    gap: 0.4rem;
    min-width: 0;
    padding: 0.65rem 0.8rem;
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    background: var(--bg-inset);
}

.stage-source {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    margin: 0;
    font-weight: 600;
}

.stage-source svg { width: 16px; height: 16px; flex: none; color: var(--text-subtle); }

.stage-source__name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

code.stage-source__name {
    font-family: var(--font-mono);
    font-size: 0.85rem;
    font-weight: 550;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.stage-source__meta {
    margin-left: auto;
    padding-left: 0.5rem;
    flex: none;
    color: var(--text-muted);
    font-size: 0.85rem;
    font-weight: 400;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.stage-files {
    display: grid;
    gap: 0.05rem;
    margin: 0;
    padding: 0;
    overflow: auto;
    list-style: none;
}

/* The key's select, as wide as the field allows up to a comfortable line. */
.stage-key { max-width: 26rem; }

.stage-field__aside {
    margin: 0;
    color: var(--text-subtle);
    font-size: 0.8rem;
}

/*
    The review: the .zip's name over two columns, its files on the left, each a button that shows it, and the file shown
    on the right. One column on a narrow screen, the files over the file.
*/
.stage-review {
    grid-template-columns: minmax(11rem, 14rem) minmax(0, 1fr);
    grid-template-areas: "source source" "files view";
    gap: 0.6rem 0.8rem;
}

.stage-review__source { grid-area: source; }
.stage-review__files { grid-area: files; align-content: start; max-height: 22rem; }
.stage-review__view { grid-area: view; display: grid; gap: 0.4rem; min-width: 0; align-content: start; }

@media (max-width: 640px) {
    .stage-review {
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas: "source" "files" "view";
    }

    .stage-review__files { max-height: 10rem; }
}

.stage-file {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "name size" "tag size";
    align-items: center;
    gap: 0 0.5rem;
    width: 100%;
    padding: 0.3rem 0.5rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.82rem;
    font-variant-numeric: tabular-nums;
    text-align: left;
    cursor: pointer;
    transition: background-color var(--speed-fast) var(--ease-out), color var(--speed-fast) var(--ease-out);
}

.stage-file code {
    grid-area: name;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.82rem;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.stage-file__tag { grid-area: tag; color: var(--text-subtle); font-size: 0.75rem; }
.stage-file__size { grid-area: size; white-space: nowrap; }

@media (hover: hover) { .stage-file:hover { background: var(--bg-hover); } }

.stage-file:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.stage-file[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); }
.stage-file[aria-pressed="true"] code { color: var(--accent); font-weight: 600; }

/*
    A file's text, read only (Components/CodeViewer.razor): line numbers in Mist beside the code, in Cascadia Code, on
    Sunken in a hairline box that scrolls both ways. highlight.js marks the code with classes, coloured by the code tokens.
*/
.code-view { margin: 0; min-width: 0; }

.code-view__scroll {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    max-height: 22rem;
    overflow: auto;
    border: 1px solid var(--surface-border);
    border-radius: var(--radius-sm);
    background: var(--bg-sunken);
    overscroll-behavior: contain;
}

.code-view__scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.code-view__lines,
.code-view__code {
    margin: 0;
    padding: 0.6rem 0;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    line-height: 1.55;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
    tab-size: 4;
}

.code-view__lines {
    position: sticky;
    left: 0;
    z-index: 1;
    padding-inline: 0.7rem 0.6rem;
    border-right: 1px solid var(--surface-border);
    background: var(--bg-sunken);
    color: var(--text-subtle);
    text-align: right;
    user-select: none;
}

.code-view__code { padding-inline: 0.8rem; color: var(--text); }
.code-view__code code { font: inherit; }

.hljs-keyword,
.hljs-operator { color: var(--code-keyword); }
.hljs-built_in,
.hljs-literal { color: var(--code-builtin); }
.hljs-string,
.hljs-regexp { color: var(--code-string); }
.hljs-subst { color: var(--text); }
.hljs-number { color: var(--code-number); }
.hljs-comment { color: var(--text-subtle); font-style: italic; }
.hljs-title,
.hljs-title.function_,
.hljs-title.class_ { color: var(--code-title); }
.hljs-params { color: var(--text); }
.hljs-meta { color: var(--code-meta); }
.hljs-doctag { font-weight: 600; }

/* A field's checks: a tick and what's right, or a cross and each thing wrong. */
.checks {
    display: grid;
    gap: 0.3rem;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: 0.85rem;
}

.check {
    display: flex;
    align-items: flex-start;
    gap: 0.45rem;
    min-width: 0;
}

.check svg {
    width: 16px;
    height: 16px;
    flex: none;
    margin-top: 0.2rem;
    stroke-width: 2.2;
}

.check span { min-width: 0; overflow-wrap: anywhere; }
.check--ok { color: var(--success-ink); }
.check--error { color: var(--danger); }
.check--note { color: var(--text-muted); }
.check--note svg { color: var(--info); stroke-width: 1.9; }

.device-pings { margin-top: 1.5rem; }

/* What the next page of the log loads on as it nears the viewport; out of the footer's flow. */
.history__footer { position: relative; }

.log-sentinel {
    position: absolute;
    top: 0;
    left: 0;
    width: 1px;
    height: 1px;
    pointer-events: none;
}

.notice--error {
    color: var(--danger);
    background: var(--danger-soft);
}

.ping-log__toolbar .ping-log__summary { margin-left: auto; }

.ping-log__title {
    margin: 0;
    font-size: 0.95rem;
    font-weight: 600;
}

/* An alarm, or data lost: the one thing in the log that asks for attention. */
.event-row--alarm td.ping-table__wide {
    color: var(--danger);
    font-weight: 550;
}

/* A firmware line's firmware, as the Flasher names it: data, in the monospace, quiet, and kept whole. */
.event-bundle {
    margin-left: 0.45rem;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    font-weight: 400;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
    color: var(--text-subtle);
    white-space: nowrap;
}

/* ---------- Logs and filters -------------------------------------------- */

.history__filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 0.75rem 1rem;
    padding: 0.85rem 0.6rem 0.85rem 1.1rem;
    border-bottom: 1px solid var(--surface-border);
}

/* A device's log: its title on the left, level with the fields, and what it shows on the right, wrapping beside it. */
.device-log__bar {
    display: flex;
    align-items: flex-end;
    gap: 0.75rem 1.5rem;
    padding: 0.85rem 1.1rem;
    border-bottom: 1px solid var(--surface-border);
}

.device-log__title {
    flex: none;
    line-height: 2.5rem;
}

.device-log__filters {
    display: flex;
    flex: 1 1 0;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: flex-end;
    gap: 0.75rem 1rem;
    min-width: 0;
}

.filter {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    min-width: 0;
}

.filter__label {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-subtle);
}

/* A native select, for its keyboard and phone pickers, dressed as the inputs are. */
.select {
    position: relative;
    display: block;
}

.select__control {
    appearance: none;
    width: 100%;
    min-width: 11rem;
    min-height: 2.5rem;
    padding: 0.4rem 2.2rem 0.4rem 0.8rem;
    font: inherit;
    /* 16px or more, or iOS zooms the page in when it gets focus. */
    font-size: 1rem;
    color: var(--text);
    background: var(--bg-inset);
    border: 1px solid var(--surface-border-strong);
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: border-color var(--speed-fast) var(--ease-out), box-shadow var(--speed-fast) var(--ease-out),
                background-color var(--speed) var(--ease-out);
}

@media (hover: hover) { .select__control:hover { border-color: var(--text-subtle); } }

.select__control:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}

.select__chevron {
    position: absolute;
    top: 50%;
    right: 0.75rem;
    width: 14px;
    height: 14px;
    stroke-width: 2.2;
    color: var(--text-subtle);
    transform: translateY(-50%);
    pointer-events: none;
}

/* Chromium's date field is taller than a line of text, so it takes less padding to be as tall as the selects: 2.5rem. */
.input.filter__time {
    width: auto;
    min-height: 2.5rem;
    padding: 0.3rem 0.7rem;
}

/* At the bar's right end, labelled, in the refresh green; as tall as the fields. */
.history__refresh {
    margin-left: auto;
    padding-block: 0.4rem;
    padding-inline: 0.95rem 1.1rem;
}

/* Days split the log: a quiet heading at the start of each, not sticky, as the column headings are. */
.history-table .history-day th {
    position: static;
    padding: 0.85rem 1.1rem 0.35rem;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--text-muted);
    background: var(--bg-elevated);
}

.history-table tr.history-day:hover { background: none; }

/* Each day is its own tbody; only the very last row goes without a divider, the footer drawing its own. */
.history-table tbody tr:last-child td { border-bottom: 1px solid var(--surface-border); }
.history-table tbody:last-child tr:last-child td { border-bottom: none; }

.history-time {
    display: inline-flex;
    align-items: baseline;
    gap: 0.45rem;
}

/* Received well after it was measured: the device was offline meanwhile. */
.history-late {
    padding: 0 0.45rem;
    border-radius: var(--radius-pill);
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--warning-ink);
    background: var(--warning-soft);
}

.reading__temp { font-weight: 550; }
/* Humidity and pressure move as one, onto a line of their own when the cell is narrow. */
.reading__rest { font-size: 0.85rem; color: var(--text-muted); white-space: nowrap; }
.reading__none { font-size: 0.85rem; color: var(--text-subtle); }

.history-table__compact { display: none; }

.reading-lines {
    display: grid;
    gap: 0.2rem;
    margin: 0;
}

.reading-lines div {
    display: grid;
    grid-template-columns: 4.25rem 1fr;
    gap: 0.5rem;
    align-items: baseline;
}

.reading-lines dt {
    font-size: 0.78rem;
    color: var(--text-muted);
}

.reading-lines dd { margin: 0; }

/* Loading: the shape of the log, still; the one loop the design allows is the ripple. */
.history-skeleton td { padding-block: 0.85rem; }

.skeleton {
    display: block;
    height: 0.7rem;
    border-radius: var(--radius-pill);
    background: var(--bg-sunken);
}

.skeleton--time { width: 4.25rem; }
.skeleton--device { width: 8.5rem; }
.skeleton--0 { width: 62%; }
.skeleton--1 { width: 44%; }
.skeleton--2 { width: 53%; }

.history__message {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    padding: clamp(2rem, 6vw, 3.5rem) 1.5rem;
    text-align: center;
}

.history__message .alert { margin: 0; }
.history__message h2 { margin: 0; }

.history__message--empty p {
    max-width: 44ch;
    margin: 0;
    color: var(--text-muted);
}

.history__footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    min-height: 3rem;
    padding: 0.5rem 0.6rem 0.5rem 1.1rem;
    border-top: 1px solid var(--surface-border);
}

.history__end {
    padding-right: 0.5rem;
    font-size: 0.8rem;
    color: var(--text-subtle);
}

.history__footer-error {
    margin-left: auto;
    font-size: 0.85rem;
    color: var(--danger);
}

/* Readings on narrower screens: the three values in one cell, a line each. */
@media (max-width: 760px) {
    .history-table--readings .history-table__value { display: none; }

    .history-table--readings .history-table__compact { display: table-cell; }

    .history-table--readings td:first-child,
    .history-table--readings th:first-child {
        padding-right: 1rem;
        vertical-align: top;
    }
}

/* Phones: the device, the widest choice, shares the first row with Refresh only, its arrow alone in a square (its label
   still names it to screen readers); the rest go two to a row. A device's log puts its filters under its title. */
@media (max-width: 560px) {
    .device-log__bar { flex-wrap: wrap; }
    .device-log__title { line-height: inherit; }
    .device-log__filters { flex-basis: 100%; justify-content: flex-start; }

    /* Side by side while their longest choices fit, each on a row of its own when not, rather than cut short. */
    .device-log__filters .filter { flex: 1 1 auto; }

    .filter { flex: 1 1 8.5rem; order: 2; }
    .history__filters > .filter:first-child { flex-basis: 12rem; order: 0; }

    .history__refresh {
        position: relative;
        flex: none;
        width: 2.5rem;
        padding-inline: 0;
        order: 1;
    }

    .history__refresh-label {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .filter:has(.filter__time) { flex-basis: 100%; }
    .select__control { min-width: 0; }
    .input.filter__time { width: 100%; }
}

@media (max-width: 480px) {
    .history__filters,
    .history__footer { padding-left: 0.75rem; }
    .device-log__bar { padding-inline: 0.75rem; }

    .history-table .history-day th { padding-inline: 0.75rem; }
    .reading-lines div { grid-template-columns: 3.75rem 1fr; }
}

/* ---------- Reports ----------------------------------------------------- */

/* The device and the range: a filter bar, as a card of its own over the report. The range is taller, its
   quick ranges centred under its dates, so the bar lines its fields up by their tops; Refresh ends the range's first
   row, level with the inputs, at the bar's end. */
.report-filters {
    align-items: flex-start;
    margin-bottom: 1rem;
}

.range-filter {
    display: grid;
    flex: 1 1 auto;
    grid-template-columns: auto auto 1fr;
    gap: 0.4rem 1rem;
}

.range-filter .history__refresh {
    align-self: end;
    justify-self: end;
}

/* While it loads it keeps its colour, its arrow spinning, rather than fading as a disabled button does. */
.history__refresh--busy svg { animation: spin 900ms linear infinite; }
.history__refresh--busy:disabled { opacity: 1; cursor: progress; }

/* The quick ranges: plain links, each setting the dates and loading the readings again. */
.range-presets {
    display: flex;
    grid-column: 1 / 3;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.2rem 1.25rem;
}

/* Too narrow for the range's grid: its parts join the bar's flow, lined up by their bottoms, as a filter bar's are, and the
   quick ranges take a row of their own after them. */
@media (max-width: 760px) {
    .report-filters { align-items: flex-end; }
    .range-filter { display: contents; }

    .range-presets {
        flex-basis: 100%;
        justify-content: flex-start;
        margin-top: -0.3rem;
    }
}

/* In hyperlink blue (Info Blue), not the accent: a row of them would put too much indigo beside the chart's. */
.link-button {
    padding: 0.1rem 0;
    font: inherit;
    font-size: 0.86rem;
    font-weight: 550;
    color: var(--info);
    background: none;
    border: none;
    border-radius: 2px;
    cursor: pointer;
    text-underline-offset: 3px;
}

@media (hover: hover) { .link-button:hover:not(:disabled) { text-decoration: underline; } }

.link-button:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.link-button:disabled {
    color: var(--text-subtle);
    cursor: not-allowed;
}

/* A report: its chart card, with the summary over the plot, then its data. While another range loads, the old one fades.
   Its one column never grows past the page, so what's too wide scrolls inside its card. */
.report {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 1rem;
    transition: opacity var(--speed) var(--ease-out);
}

.report--loading { opacity: 0.55; }

.report-card__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    padding: 0.8rem 0.6rem 0.6rem 1.1rem;
}

.report-card__heading {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0.75rem;
}

.report-card__title { margin: 0; }

.report-card__meta {
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

.report-card__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-left: auto;
}

.btn--square { width: 2rem; padding: 0; }

/* The summary, over the chart: a measure per column, under its caption. */
.report-summary {
    padding: 0.2rem 1.1rem 1rem;
    border-bottom: 1px solid var(--surface-border);
}

.stat-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 7.25rem), 1fr));
    gap: 1rem;
    margin: 0;
}

.stat {
    display: grid;
    align-content: start;
    gap: 0.3rem;
    min-width: 0;
}

.stat dt {
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-subtle);
}

.stat__line {
    display: grid;
    align-items: baseline;
    margin: 0;
}

.stat__value {
    font-size: 1.15rem;
    font-weight: 600;
    letter-spacing: -0.011em;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.stat__value--none { font-weight: 400; color: var(--text-subtle); }

.stat__detail {
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

.stat__detail.battery--full { color: var(--success-ink); }
.stat__detail.battery--flat { color: var(--danger); font-weight: 550; }

/* The temperature's summary: a column per measure and a row per sensor. Columns share the width; under 760px each keeps the
   room its values need and the table scrolls sideways, the sensors' names staying put. */
.climate-summary { overflow-x: auto; }

.climate-summary__table {
    width: 100%;
    table-layout: fixed;
    border-collapse: collapse;
}

/* The sensors' column stays put while the rest scrolls under it. */
.climate-summary__corner {
    position: sticky;
    left: 0;
    z-index: 1;
    width: 6rem;
    background: var(--bg-elevated);
}

.climate-summary__table thead th {
    padding: 0 1rem 0.3rem 0;
    font-size: 0.75rem;
    font-weight: 600;
    text-align: left;
    vertical-align: bottom;
    color: var(--text-subtle);
}

.climate-summary__row > * {
    padding: 0.45rem 1rem 0.45rem 0;
    vertical-align: baseline;
}

.climate-summary__row td > * { display: block; }
.climate-summary__row .stat__detail { margin-top: 0.15rem; }

/* A value with no detail under it, such as the average, is centred on its row, level with the sensor's name. */
.climate-summary__row > td.climate-summary__single { vertical-align: middle; }

.climate-summary__row > th {
    position: sticky;
    left: 0;
    z-index: 1;
    font-size: 0.8rem;
    font-weight: 600;
    text-align: left;
    /* Centred on the row, which spans a value and its detail. */
    vertical-align: middle;
    color: var(--text-muted);
    background: var(--bg-elevated);
}

@media (max-width: 760px) {
    .climate-summary__table { width: auto; table-layout: auto; }
    .climate-summary__corner { width: auto; }

    /* Each column as wide as its widest line, at least 5rem: the captions wrap, the values and details don't. */
    .climate-summary__row td > * { min-width: 5rem; }
    .climate-summary__row .stat__detail { white-space: nowrap; }
}

.report-note {
    margin: 0.9rem 0 0;
    font-size: 0.8rem;
    color: var(--text-subtle);
}

/* How long the battery spent in each state: a bar in the states' colours, the chart's bands as a share of time. */
.state-share {
    display: grid;
    gap: 0.45rem;
    margin-top: 1.1rem;
}

.state-share__title {
    margin: 0;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--text-subtle);
}

/* Not a line-art icon: its parts are fills, so it opts out of the icons' stroke. */
.state-bar {
    display: block;
    width: 100%;
    height: 0.6rem;
    border-radius: var(--radius-pill);
    overflow: hidden;
    background: var(--bg-sunken);
    stroke: none;
}

.state-bar__gap {
    stroke: var(--bg-elevated);
    stroke-width: 2px;
    stroke-linecap: butt;
    vector-effect: non-scaling-stroke;
}

.state-bar__part--full { fill: var(--success); }
.state-bar__part--normal { fill: var(--surface-border-strong); }
.state-bar__part--flat { fill: var(--danger); }

.state-share__key {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem 1.25rem;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: 0.82rem;
}

.state-share__key li {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.state-key {
    flex: none;
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 50%;
}

.state-key--full { background: var(--success); }
.state-key--normal { background: var(--surface-border-strong); }
.state-key--flat { background: var(--danger); }

.state-share__name { font-weight: 550; }

.state-share__value {
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* The chart: uPlot's own sheet (lib/uplot) lays it out; these dress it in the system's type and colours. */
.report-chart { padding: 0.9rem 0.6rem 0.8rem; }

.report-chart__plot { min-height: 240px; }

.report-chart__hint {
    margin: 0.5rem 0.5rem 0;
    font-size: 0.78rem;
    color: var(--text-subtle);
}

/* The mouse's hint, or a finger's where the screen is the only pointer. */
.report-chart__hint--touch { display: none; }

@media (hover: none) and (pointer: coarse) {
    .report-chart__hint--pointer { display: none; }
    .report-chart__hint--touch { display: block; }
}

.report-chart__status {
    font-size: 0.8rem;
    color: var(--text-muted);
}

.report-chart__status:empty { display: none; }
.report-chart__status--failed { color: var(--danger); }

.report-chart .uplot {
    width: auto;
    font-family: var(--font);
}

/* A finger sliding along the plot reads its values, and two zoom it (app.js); only a vertical swipe scrolls the page. */
.report-chart .u-over {
    touch-action: pan-y;
    -webkit-touch-callout: none;
}

/* Under the plot: the key, and docked at the plot's bottom right the chart's tools: Reset zoom while zoomed, then after a
   wider gap the switch for every series or none, then after a short gap the copy button; they stay there however many
   rows the key has. Narrow, they go over it. */
.report-chart__foot {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: start;
    gap: 0.25rem 1rem;
    margin-top: 0.4rem;
}

.report-chart__key { min-width: 0; }

.report-chart__tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem 0.75rem;
    padding-right: 0.25rem;
}

.report-chart__reset { margin-right: 0.5rem; }

@media (max-width: 760px) {
    .report-chart__foot { grid-template-columns: minmax(0, 1fr); }
    .report-chart__tools { grid-row: 1; justify-self: end; }
}

.report-chart .u-legend {
    overflow-x: auto;
    font-size: 0.82rem;
    text-align: left;
    color: var(--text-muted);
}

/* The key in rows, as the summary: the time on its own, then a row per sensor, headed by its name, its series in columns
   that line up, named by their quantity, each after a dot in its colour (app.js places them, and adds the dots). uPlot's
   markers are hidden rather than switched off, which would colour the labels. */
.report-chart .u-legend tbody {
    display: grid;
    justify-content: start;
    align-items: start;
    gap: 0.4rem 0.75rem;
}

.report-chart .u-legend tr { margin: 0; }
.report-chart .u-legend .u-marker { display: none; }

/* Without the marker to centre on, a label sits on the baseline, level with its row's heading, and so does its colon. */
.report-chart .u-legend .u-label,
.report-chart .u-legend .u-series th::after { vertical-align: baseline; }

.report-chart .u-legend .u-series > * { padding: 0.15rem 0.3rem; }
.report-chart .u-legend th { font-weight: 550; color: var(--text); border-radius: var(--radius-sm); }
.report-chart .u-legend .u-value { font-variant-numeric: tabular-nums; }

/* An entry's colour, as its line on the plot. */
.report-chart .u-legend .u-key-dot {
    display: inline-block;
    width: 0.6rem;
    height: 0.6rem;
    margin-right: 0.4rem;
    border-radius: 50%;
    vertical-align: middle;
    background: var(--key-color);
}

/* The heading centred on its row, which is two lines tall where each entry's value goes under its name. */
.report-chart .u-key-group {
    display: flex;
    align-items: center;
    align-self: stretch;
}

.report-chart .u-legend .u-key-group th {
    padding: 0.15rem 0.3rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-muted);
}

.report-chart .u-legend .u-key-group th::after { content: none; }

@media (max-width: 720px) {
    /* Each entry's name over its value, so a sensor's row fits the width. */
    .report-chart .u-legend--grouped .u-series:not(:first-child) th,
    .report-chart .u-legend--grouped .u-key-group th { display: block; }
    .report-chart .u-legend--grouped .u-series:not(:first-child) th::after { content: none; }
}

/* The key's entries switch their series, by keyboard too (app.js makes them buttons). */
.report-chart .u-legend th:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

@media (hover: hover) { .report-chart .u-legend th:hover .u-label { text-decoration: underline; text-underline-offset: 3px; } }

.report-chart .u-select { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.report-chart .u-hz .u-cursor-x { border-right-color: var(--text-subtle); }
.report-chart .u-hz .u-cursor-y { border-bottom-color: var(--text-subtle); }

/* Loading: the chart card's shape, still, with the spinner where the plot goes. */
.report-skeleton .stat-strip { padding: 0.2rem 1.1rem 1rem; }
.report-skeleton .stat { gap: 0.55rem; }

.report-skeleton__plot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    height: 320px;
    margin: 0 1.1rem 1.1rem;
    font-size: 0.88rem;
    color: var(--text-muted);
    background: var(--bg-inset);
    border-radius: var(--radius-sm);
}

/* The data: every reading, a page at a time, under headings that stick below the app bar, as the logs' do. */
.report-data { scroll-margin-top: calc(var(--bar-height) + 1rem); }

.report-table {
    width: 100%;
    border-collapse: collapse;
    font-variant-numeric: tabular-nums;
}

.report-table thead {
    position: sticky;
    top: var(--bar-height);
    z-index: 1;
}

.report-table th {
    padding: 0.5rem 1.1rem;
    font-size: 0.75rem;
    font-weight: 600;
    text-align: left;
    white-space: nowrap;
    color: var(--text-subtle);
    background: var(--bg-inset);
    border-bottom: 1px solid var(--surface-border);
}

.report-table thead tr:first-child th { border-top: 1px solid var(--surface-border); }
.report-table thead th[title] { cursor: help; }
.report-table .report-table__group { text-align: center; }

.report-table td {
    padding: 0.5rem 1.1rem;
    font-size: 0.9rem;
    white-space: nowrap;
    border-bottom: 1px solid var(--surface-border);
}

/* Columns as wide as their values, packed under their groups; a last, empty cell per row takes the room left. */
.report-table td,
.report-table th { width: 1%; }

.report-table tr::after {
    content: "";
    display: table-cell;
    border-bottom: 1px solid var(--surface-border);
}

.report-table thead tr::after { background: var(--bg-inset); }
.report-table thead tr:first-child::after { border-top: 1px solid var(--surface-border); }
.report-table .report-table__day::after { background: var(--bg-elevated); }

.report-table .report-table__number {
    padding-left: 2rem;
    text-align: right;
}

.report-table__none { color: var(--text-subtle); }

.report-table .report-table__day th {
    padding: 0.85rem 1.1rem 0.35rem;
    font-size: 0.78rem;
    color: var(--text-muted);
    background: var(--bg-elevated);
}

.report-table tbody:last-child tr:last-child td,
.report-table tbody:last-child tr:last-child::after { border-bottom: none; }

@media (hover: hover) { .report-table tbody tr:not(.report-table__day):hover { background: var(--bg-inset); } }

.report-pager__nav {
    display: flex;
    align-items: center;
    gap: 0.15rem;
}

.report-pager__page {
    padding: 0 0.5rem;
    font-size: 0.82rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--text-muted);
}

@media (max-width: 560px) {
    .report-card__header { padding-left: 0.75rem; }

    /* The quick ranges stay under the dates. */
    .range-presets { order: 3; }

    .report-summary { padding-inline: 0.75rem; }
    .report-chart { padding-inline: 0.25rem; }
    .report-skeleton__plot { height: 240px; margin-inline: 0.75rem; }

    /* A wide table scrolls sideways here, its headings no longer sticking: the price of keeping every column. */
    .report-data { overflow-x: auto; }
    .report-table thead { position: static; }

    .report-table th,
    .report-table td { padding-inline: 0.6rem; }

    .report-table .report-table__number { padding-left: 1rem; }

    .report-table td { font-size: 0.86rem; }
}

/* ---------- Keys -------------------------------------------------------- */

/* A new key: its name and the key side by side, the key's eye beside it and Generate over it; under a phone's width, one under the other. */
.key-new {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-bottom: 1.25rem;
    padding: 1rem 1.1rem;
}

.key-new__title {
    margin: 0;
    font-size: 0.9rem;
    font-weight: 600;
}

.key-new__fields {
    display: grid;
    grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
    gap: 0.75rem;
}

.key-new__field {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

/* A label, and for the key Generate at its end: as tall in both fields, so the fields line up side by side. */
.key-new__label-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    min-height: 2rem;
}

.input.key-new__input {
    min-height: 2.5rem;
    padding: 0.45rem 0.8rem;
}

/* The field and, while a generated key shows for a while, its countdown over the border. */
.key-new__secret { position: relative; }

.key-new__secret .input { display: block; }

/* A key is read character by character: the monospace, where l, 1 and I differ. */
.input.key-new__secret-input {
    font-family: var(--font-mono);
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
}

.input.key-new__secret-input::placeholder { font-family: var(--font); }

/* Masked as a password box would be, without being one, which password managers would fill (Keys.razor). */
.input.key-new__secret-input--masked { -webkit-text-security: disc; }

.key-new__generate { flex: none; }

.key-new__foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
}

.key-new__note {
    flex: 1 1 18rem;
    margin: 0;
    font-size: 0.85rem;
}

.key-new__hint { color: var(--text-subtle); }

.key-new__submit { flex: none; margin-left: auto; }

@media (max-width: 640px) {
    .key-new__fields { grid-template-columns: minmax(0, 1fr); }
}
.notice--info {
    color: var(--info);
    background: var(--info-soft);
}

/* The Keys page is a little wider than the others, for two columns of cards each showing a key of average length whole;
   its lead runs wider too, rather than squeezing into a corner of it. */
.app-main:has(.page--keys) { max-width: 1200px; }

.page--keys .page__lead { max-width: 96ch; }

.key-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.9rem;
}

@media (max-width: 820px) {
    .key-list { grid-template-columns: minmax(0, 1fr); }
}

.key-card {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    min-width: 0;
    padding: 1rem 1.1rem;
}

.key-card--skeleton { min-height: 6.5rem; }

.key-card__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem 0.75rem;
}

.key-card__name {
    margin: 0;
    min-width: 0;
    font-size: 1.15rem;
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* The name with its pencil, as a device's title has (Components/KeyName.razor); renaming, the form stands in for both. */
.key-name {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    min-width: 0;
}

.key-name__rename {
    flex: none;
    width: 2rem;
    min-height: 2rem;
    padding: 0;
    color: var(--text-subtle);
}

.key-name__form { flex: 1 1 22rem; }

.key-card__meta,
.key-card__devices {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.88rem;
}

.key-card__devices {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.25rem 0.75rem;
}

.key-card__label { color: var(--text-subtle); }

.key-card__device {
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* A device without a name goes by its ID: data, in the monospace. */
.key-card__device--id {
    font-family: var(--font-mono);
    font-weight: 400;
}

.key-card__alert { margin: 0; }

/* Cards side by side line up their keys and actions at the foot, whatever is above them. */
.key-card .secret-field { margin-top: auto; }

/* Copy at the start, Delete in Alarm Red at the end, as Forget device is. */
.key-card__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0.1rem -0.35rem 0;
}

.key-card__delete { margin-left: auto; }

/* ---------- Secrets ----------------------------------------------------- */

/*
    A secret behind a spoiler (Components/SecretField.razor), which the field itself shows and hides, with no button beside
    it: one box of fixed height, masked or shown, so revealing it moves nothing. Concealment is by not rendering it, never by
    blurring: the masked box holds no secret.
*/
.secret-field {
    position: relative;
    display: flex;
    min-width: 0;
}

.secret-field__veil,
.secret-field__value {
    flex: 1 1 auto;
    min-width: 0;
    height: 2.5rem;
    padding: 0 0.8rem;
    border-radius: var(--radius-sm);
}

/* The veil: a dashed box saying how to lift it, centred, as spoilers read anywhere. Still: a moving sheen read as loading. */
.secret-field__veil {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    overflow: hidden;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 550;
    color: var(--text-subtle);
    white-space: nowrap;
    cursor: pointer;
    background: var(--bg-sunken);
    border: 1px dashed var(--surface-border-strong);
    animation: secret-conceal var(--speed) var(--ease-out);
    transition: color var(--speed-fast) var(--ease-out), border-color var(--speed-fast) var(--ease-out),
                background-color var(--speed-fast) var(--ease-out);
}

@media (hover: hover) {
    .secret-field__veil:hover {
        color: var(--text-muted);
        border-color: var(--accent);
        background: color-mix(in srgb, var(--accent-soft) 55%, var(--bg-sunken));
    }
}

.secret-field__veil:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}

.secret-field[aria-busy="true"] .secret-field__veil { cursor: progress; }

.secret-field__eye {
    flex: none;
    width: 16px;
    height: 16px;
}

.secret-field__hint {
    overflow: hidden;
    text-overflow: ellipsis;
}

/* "Click to reveal", or on a touch screen "Tap to reveal", as the report chart's hint picks. */
.secret-field__hint--touch { display: none; }

@media (hover: none) and (pointer: coarse) {
    .secret-field__hint--pointer { display: none; }
    .secret-field__hint--touch { display: inline; }
}

/* Shown: read-only, in the monospace, where l, 1 and I differ; a long one scrolls in its box. Its text selects as any field's. */
.secret-field__value {
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.9rem;
    font-weight: 550;
    letter-spacing: 0.02em;
    font-variant-ligatures: none;
    font-feature-settings: "calt" 0;
    text-overflow: ellipsis;
    background: var(--bg-inset);
    border: 1px solid var(--surface-border);
    animation: secret-reveal var(--speed-slow) var(--ease-out);
}

.secret-field__value:focus-visible {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
}

@keyframes secret-reveal {
    from { opacity: 0; filter: blur(6px); }
    to   { opacity: 1; filter: blur(0); }
}

@keyframes secret-conceal {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/*
    A reveal's clock (Components/RevealCountdown.razor): a 2px line along the bottom edge of the field that shows the
    secret, over its border, which drains from right to left at an even pace, the time left, as a toast's timer does. Not
    round the whole outline: on a wide field that path is long, and judging how much of it is left is hard. The box clips
    the line to the field's rounded corners. Indigo, but softened: nothing turns to the accent outright, which would read
    as an alarm.
*/
.reveal-countdown {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: var(--radius-sm);
    pointer-events: none;
}

.reveal-countdown__line {
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 2px;
    background: color-mix(in srgb, var(--accent) 70%, transparent);
    transform-origin: left center;
    /* RevealCountdown sets the duration from its window, through the CSSOM; this is only until it has. */
    animation: reveal-countdown 20s linear forwards;
}

@keyframes reveal-countdown {
    from { transform: scaleX(1); }
    to   { transform: scaleX(0); }
}
/* ---------- Empty and not-found ----------------------------------------- */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: clamp(2.5rem, 8vw, 4.5rem) 1.5rem;
    text-align: center;
}

.empty-state h1,
.empty-state h2 { margin-top: 1.5rem; }

.empty-state p {
    max-width: 44ch;
    color: var(--text-muted);
}

.empty-state__glyph {
    font-size: 3.5rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1;
    color: var(--accent);
}

/* Waiting for the first ping: the same ripple as the live dot, at the scale of the page. */
.ripple {
    position: relative;
    display: grid;
    place-items: center;
    width: 5.5rem;
    height: 5.5rem;
}

.ripple::before {
    content: "";
    width: 0.85rem;
    height: 0.85rem;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 0 6px var(--accent-soft);
}

.ripple span {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    border: 1.5px solid var(--accent);
    opacity: 0;
    animation: ripple-wide 3.6s var(--ease-out) infinite;
}

.ripple span:nth-child(2) { animation-delay: 1.2s; }
.ripple span:nth-child(3) { animation-delay: 2.4s; }

@keyframes ripple-wide {
    from { transform: scale(0.2); opacity: 0.7; }
    to   { transform: scale(1); opacity: 0; }
}

/* ---------- Reduced motion ---------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 1ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 1ms !important;
    }

    .ripple span { opacity: 0.35; animation: none !important; }

    .reveal-countdown { display: none; }
    .ripple span:nth-child(1) { transform: scale(0.45); }
    .ripple span:nth-child(2) { transform: scale(0.72); }
    .ripple span:nth-child(3) { transform: scale(1); }
}
