/* digital-sandbox — application stylesheet.
 *
 * Served from /static/app.css (see routes::mod::app_css), embedded into the binary at compile time
 * via include_str! exactly like the vendored htmx files next to it -- consistent with this
 * project's local-first stance and with `app` shipping as a single self-contained binary. No build
 * step, no preprocessor, no CDN: this file is the whole design system.
 *
 * Structured as:
 *   1. Design tokens (light + dark)
 *   2. Base / reset
 *   3. Layout shell (header, nav, main)
 *   4. Typography
 *   5. Forms and controls
 *   6. Lists, cards, disclosure (details/summary)
 *   7. Tables
 *   8. Dashboard widgets
 *   9. Utilities and page-specific bits
 *
 * Mobile-first throughout: the base rules target a phone, and the handful of `min-width` media
 * queries progressively widen things for tablet/desktop. There is deliberately no hamburger menu
 * or any other JS-driven navigation state -- the nav simply wraps, which cannot break, get stuck
 * open, or trap focus.
 */

/* ==========================================================================
   1. Design tokens
   ========================================================================== */

:root {
    /* Spacing scale -- a 4px base, so every gap/padding in this file is one of these rather than
       an arbitrary one-off value. */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 14px;
    --radius-pill: 999px;

    --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

    /* Light palette (the default). Every colour the rest of this file uses is one of these, so a
       theme change is entirely contained to these two blocks. */
    --bg: #f5f7fa;
    --bg-gradient: radial-gradient(1200px 600px at 20% -10%, #e8eefc 0%, transparent 60%);
    --surface: #ffffff;
    --surface-2: #f1f4f8;
    --surface-3: #e7ecf3;
    --border: #dbe1ea;
    --border-strong: #c3ccd9;
    --text: #171c24;
    --text-muted: #5a6673;
    --text-faint: #8793a1;

    --accent: #2563eb;
    --accent-hover: #1d4ed8;
    --accent-soft: #e5edff;
    --accent-contrast: #ffffff;

    --danger: #d33b3b;
    --danger-hover: #b52d2d;
    --danger-soft: #fdeaea;
    --success: #16874a;
    --success-soft: #e3f5eb;
    --warning: #a96a00;

    --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05);
    --shadow: 0 1px 3px rgba(16, 24, 40, 0.08), 0 1px 2px rgba(16, 24, 40, 0.04);
    --shadow-lg: 0 10px 24px rgba(16, 24, 40, 0.10), 0 2px 6px rgba(16, 24, 40, 0.05);

    /* Log/terminal output stays dark in both themes -- a scrolling console is conventionally dark,
       and it reads as "raw machine output" rather than app chrome, which is exactly right here. */
    --console-bg: #10141a;
    --console-text: #d7dee8;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #0e1116;
        --bg-gradient: radial-gradient(1200px 600px at 20% -10%, #16203a 0%, transparent 60%);
        --surface: #161b22;
        --surface-2: #1c232c;
        --surface-3: #232c37;
        --border: #2a333f;
        --border-strong: #3a4653;
        --text: #e6eaf0;
        --text-muted: #9aa6b4;
        --text-faint: #71808f;

        --accent: #5b9bff;
        --accent-hover: #7cb0ff;
        --accent-soft: #16233c;
        --accent-contrast: #0b1220;

        --danger: #f2706b;
        --danger-hover: #f68b87;
        --danger-soft: #2c1719;
        --success: #4ec98a;
        --success-soft: #12251c;
        --warning: #e0a253;

        --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
        --shadow: 0 1px 3px rgba(0, 0, 0, 0.45), 0 1px 2px rgba(0, 0, 0, 0.3);
        --shadow-lg: 0 10px 28px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.35);

        --console-bg: #0a0d12;
        --console-text: #cbd5e1;
    }
}

/* ==========================================================================
   2. Base / reset
   ========================================================================== */

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

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

body {
    margin: 0;
    font-family: var(--font-sans);
    /* 16px minimum on the body: anything smaller makes iOS Safari zoom on focusing an input. */
    font-size: 16px;
    line-height: 1.55;
    color: var(--text);
    background-color: var(--bg);
    background-image: var(--bg-gradient);
    background-attachment: fixed;
    -webkit-font-smoothing: antialiased;
}

/* Never let a long id, URL, or unbroken token force the whole page to scroll sideways -- this app
   renders a lot of raw uuids and device addresses. */
body,
li,
p,
td,
th,
dd,
summary {
    overflow-wrap: break-word;
}

img,
svg,
video {
    max-width: 100%;
    height: auto;
}

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

/* Respect a user's reduced-motion preference -- every transition here is decorative. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ==========================================================================
   3. Layout shell
   ========================================================================== */

.site-header {
    position: sticky;
    top: 0;
    z-index: 20;
    background: color-mix(in srgb, var(--surface) 88%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--border);
}

/* color-mix isn't universal yet -- an opaque surface is the fallback, which looks correct, just
   without the translucency. */
@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .site-header {
        background: var(--surface);
    }
}

.site-header-inner {
    max-width: 1200px;
    margin: 0 auto;
    padding: var(--space-3) var(--space-4);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-4);
}

.brand {
    font-weight: 650;
    letter-spacing: -0.01em;
    font-size: 1.0625rem;
    color: var(--text);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex-shrink: 0;
}

.brand::before {
    /* A small "live" dot -- purely decorative, but it reads as a status/telemetry app at a glance. */
    content: "";
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--success);
    box-shadow: 0 0 0 3px var(--success-soft);
}

.site-nav {
    display: flex;
    /* One line, always. If the links genuinely can't fit they scroll sideways rather than wrapping
       the header to a second and third row -- a header whose height changes with the window is
       worse than one you occasionally swipe, and on a phone this turns three stacked rows of links
       into a single swipeable strip. */
    flex-wrap: nowrap;
    overflow-x: auto;
    /* Without this a flex item refuses to shrink below its content, so `overflow-x` would never
       engage and the links would push the actions off the end instead. */
    min-width: 0;
    scrollbar-width: none;
    align-items: center;
    gap: var(--space-1);
    flex: 1 1 100%;
    order: 3;
}

.site-nav::-webkit-scrollbar {
    display: none;
}

.site-nav a {
    display: inline-block;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-pill);
    color: var(--text-muted);
    text-decoration: none;
    font-size: 0.9375rem;
    font-weight: 500;
    white-space: nowrap;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.site-nav a:hover {
    background: var(--surface-2);
    color: var(--text);
}

/* Set by the small script in layout.html from location.pathname -- see there for why this isn't
   passed down from Rust instead. */
.site-nav a.is-active {
    background: var(--accent-soft);
    color: var(--accent);
    font-weight: 600;
}

.site-actions {
    margin-left: auto;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* Never squeezed to make room for the nav: the nav is the part that scrolls. */
    flex-shrink: 0;
    gap: var(--space-2);
    font-size: 0.875rem;
    color: var(--text-muted);
}

.site-user {
    white-space: nowrap;
}

.site-user strong {
    color: var(--text);
    font-weight: 600;
}

.badge-admin {
    display: inline-block;
    margin-left: var(--space-1);
    padding: 1px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--accent-soft);
    color: var(--accent);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* The one wrapper layout.html puts around every page's content block. `nav.html` is included
   *inside* that block (each template starts with it), so the sticky header is a child of this
   wrapper rather than a sibling -- which rules out the usual "full-width header, then a separate
   max-width <main>" markup. This is the full-bleed grid pattern instead: a centre column holds the
   content at a readable measure, and the header alone opts into spanning all three columns, so it
   still reaches the viewport edges without needing to escape its container. */
.app {
    display: grid;
    grid-template-columns: 1fr min(1200px, 100% - 2rem) 1fr;
    align-content: start;
    min-height: 100vh;
    padding-bottom: var(--space-7);
}

.app > * {
    grid-column: 2;
}

.app > .site-header {
    grid-column: 1 / -1;
}

/* Breathing room below the header, expressed as the header's own bottom margin rather than a top
   margin on "whatever comes next" -- a `.site-header + *` rule (0,2,0) loses to any heading rule
   carrying its own element name (`h1:first-of-type` is 0,2,1), so the gap would silently vanish on
   exactly the pages that start with a heading, which is all of them. Note these are grid items, so
   adjacent margins do not collapse; this is the whole gap. */
.app > .site-header {
    margin-bottom: var(--space-5);
}

/* login/2FA pages don't include nav.html at all, so there's no header margin to space them off the
   top of the viewport. */
.app > :first-child:not(.site-header) {
    margin-top: var(--space-5);
}

@media (min-width: 900px) {
    .site-header-inner {
        flex-wrap: nowrap;
        padding: var(--space-3) var(--space-5);
        /* The header shares the page's 1200px column (it's a child of the `.app` grid), so the row
           has a fixed budget no matter how wide the monitor is -- the gaps are worth reclaiming. */
        gap: var(--space-2) var(--space-3);
    }

    .site-nav {
        flex: 1 1 auto;
        order: 0;
        gap: 2px;
    }

    /* Tighter link padding on desktop, where nine links plus the brand and the account actions all
       have to share one 1200px row. */
    .site-nav a {
        padding: var(--space-2) var(--space-2);
    }

    .site-actions {
        flex-wrap: nowrap;
    }

    /* The account buttons are secondary to the nav, so they're the ones that give up width. */
    .site-actions button {
        padding: var(--space-1) var(--space-2);
        min-height: 34px;
        font-size: 0.8125rem;
    }

    .app {
        grid-template-columns: 1fr min(1200px, 100% - 3rem) 1fr;
    }

    .app > .site-header {
        margin-bottom: var(--space-6);
    }
}

/* ==========================================================================
   4. Typography
   ========================================================================== */

h1,
h2,
h3,
h4 {
    line-height: 1.25;
    letter-spacing: -0.015em;
    color: var(--text);
    margin: 0 0 var(--space-3);
    font-weight: 640;
}

h1 {
    font-size: 1.75rem;
    margin-bottom: var(--space-4);
}

h2 {
    font-size: 1.3125rem;
    margin-top: var(--space-6);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
}

h3 {
    font-size: 1.0625rem;
    margin-top: var(--space-5);
}

h4 {
    font-size: 0.9375rem;
    margin-top: var(--space-4);
}

/* A heading that happens to be the first thing after the header shouldn't stack its own top margin
   on top of the header's bottom margin -- `h2`/`h3` carry a generous top margin for separating
   sections *within* a page, which is the wrong amount when there's nothing above them to separate
   from. (`h1` needs no equivalent: its own top margin is already 0.) */
.app > .site-header + h2,
.app > .site-header + h3 {
    margin-top: 0;
}

@media (min-width: 700px) {
    h1 {
        font-size: 2rem;
    }

    h2 {
        font-size: 1.5rem;
    }
}

p {
    margin: 0 0 var(--space-4);
}

a {
    color: var(--accent);
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
}

a:hover {
    color: var(--accent-hover);
}

small {
    font-size: 0.8125rem;
    color: var(--text-muted);
}

code {
    font-family: var(--font-mono);
    font-size: 0.875em;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: 0.1em 0.35em;
}

pre {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    line-height: 1.5;
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3);
    overflow-x: auto;
}

hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin: var(--space-5) 0;
}

/* Status messages. `.error` is already emitted by every template's error slot; `.success` by the
   Workloads page's restore confirmation. */
.error,
.success {
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius);
    border: 1px solid;
    font-size: 0.9375rem;
}

.error {
    background: var(--danger-soft);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
    color: var(--danger);
}

.success {
    background: var(--success-soft);
    border-color: color-mix(in srgb, var(--success) 35%, transparent);
    color: var(--success);
}

@supports not (background: color-mix(in srgb, red 50%, blue)) {
    .error {
        border-color: var(--danger);
    }

    .success {
        border-color: var(--success);
    }
}

/* ==========================================================================
   5. Forms and controls
   ========================================================================== */

/* A standalone form (the "Add …" forms at the bottom of most pages) reads as a card. Forms nested
   inside a list row are the inline-action kind instead and are handled further down. */
.app > form,
fieldset > form,
details > form {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    align-items: stretch;
}

.app > form {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow-sm);
    margin-bottom: var(--space-5);
}

/* The `hidden` attribute means "this element is not relevant", and browsers implement it in their
   *user-agent* stylesheet as `[hidden] { display: none }` -- the weakest source there is. Any author
   rule setting `display` on a matching element silently outranks it, and every script here that
   hides something by setting `.hidden` then does nothing at all: no error, no warning, the element
   simply stays on screen.

   That has now happened four times -- the dashboard card picker, the Scripts page's point browser,
   the DDNS provider groups, and the drivers' conditional `shown_when` fields, the last of which
   nobody had noticed. Three were fixed one at a time with their own `[hidden]` rule, which is
   exactly why the fourth was still broken. `!important` is right for this: it is not overriding a
   decision, it is restoring the one the platform already made. The specific guards elsewhere in
   this file are now redundant, and harmless.
   Selectors that set `display` and catch these elements: `details > form > div`, `fieldset > div`,
   `.app > form > div`, `.manage-list > li`, `.addable-list > li`, `.linear-fields`. */
[hidden] {
    display: none !important;
}

label {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    font-size: 0.875rem;
    font-weight: 550;
    color: var(--text);
}

/* A checkbox label is the one case that must stay a horizontal row -- the control belongs beside
   its text, not above it. */
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
    flex-direction: row;
    align-items: center;
    gap: var(--space-2);
    font-weight: 500;
}

input,
select,
textarea,
button {
    font: inherit;
    color: inherit;
}

input[type="text"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="email"],
input[type="url"],
select,
textarea {
    width: 100%;
    max-width: 100%;
    padding: var(--space-2) var(--space-3);
    /* 16px keeps iOS Safari from zooming the viewport when a field takes focus. */
    font-size: 16px;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
    min-height: 44px; /* comfortable touch target */
}

textarea {
    min-height: 6rem;
    font-family: var(--font-mono);
    font-size: 0.875rem;
    line-height: 1.5;
    resize: vertical;
}

input::placeholder,
textarea::placeholder {
    color: var(--text-faint);
}

input:focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

input[type="checkbox"],
input[type="radio"] {
    width: 1.15rem;
    height: 1.15rem;
    min-height: 0;
    accent-color: var(--accent);
    flex-shrink: 0;
    margin: 0;
}

input[type="range"] {
    width: 100%;
    accent-color: var(--accent);
    min-height: 44px;
}

input[type="file"] {
    width: 100%;
    font-size: 0.875rem;
    padding: var(--space-2);
    background: var(--surface-2);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
}

select {
    /* Room for the native chevron, plus our own so it looks deliberate rather than platform-random. */
    appearance: none;
    -webkit-appearance: none;
    padding-right: var(--space-6);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7684' d='M6 8.5 1.5 4h9z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right var(--space-3) center;
    background-size: 12px;
}

button,
.button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    min-height: 44px;
    font-size: 0.9375rem;
    font-weight: 560;
    line-height: 1.2;
    text-decoration: none;
    white-space: nowrap;
    background: var(--accent);
    color: var(--accent-contrast);
    border: 1px solid transparent;
    border-radius: var(--radius);
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.05s ease;
}

button:hover,
.button:hover {
    background: var(--accent-hover);
    color: var(--accent-contrast);
}

button:active,
.button:active {
    transform: translateY(1px);
}

button:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

/* Inside a list row, a Delete/Stop/Reset action shouldn't shout louder than the primary create
   buttons -- these become quiet secondary buttons that only reveal their intent on hover. */
li button,
.site-actions button,
details:not(.workload-logs) li button {
    background: var(--surface);
    color: var(--text);
    border-color: var(--border-strong);
    font-weight: 500;
}

li button:hover,
.site-actions button:hover {
    background: var(--surface-2);
    color: var(--text);
    border-color: var(--border-strong);
}

/* Destructive actions, matched by their own label text so no template markup has to change. */
li form[action$="/delete"] button:hover,
.site-actions form[action="/logout"] button:hover {
    background: var(--danger-soft);
    border-color: var(--danger);
    color: var(--danger);
}

fieldset {
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    margin: 0 0 var(--space-4);
    background: var(--surface);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-width: 0; /* a flex/grid child fieldset otherwise refuses to shrink */
}

legend {
    padding: 0 var(--space-2);
    font-weight: 640;
    font-size: 0.9375rem;
    color: var(--text);
}

/* The generated driver/workload forms wrap each field in a bare <div>. */
fieldset > div,
details > form > div,
.app > form > div {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

/* Must follow the rule above: a `display` of anything but `none` overrides the `hidden` attribute,
   so without this the DDNS form's provider-specific field groups would both show at once regardless
   of which one the script picked. */
[data-ddns-fields][hidden] {
    display: none;
}

/* The per-field help text under a generated form field. */
fieldset > div > small,
details > form > div > small,
.app > form > div > small {
    color: var(--text-muted);
    line-height: 1.45;
}

@media (min-width: 700px) {
    /* On a wide screen an "Add …" card's own submit shouldn't stretch edge to edge. */
    .app > form > button[type="submit"] {
        align-self: flex-start;
        min-width: 10rem;
    }
}

/* ==========================================================================
   6. Lists, cards, disclosure
   ========================================================================== */

/* Every top-level list in this app is a list of *records* (devices, workloads, users, areas,
   dashboards), each with inline action forms after the label text. Making the row a wrapping flex
   container is what makes those forms lay out well -- including the ones whose markup hardcodes
   `style="display:inline"` (several are generated in Rust, with tests asserting on that exact
   string): a flex item's display value is blockified, so those inline styles simply stop mattering
   here instead of having to be removed. */
.app > ul,
.app > div > ul,
fieldset ul {
    list-style: none;
    margin: 0 0 var(--space-5);
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.app > ul > li,
.app > div > ul > li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow-sm);
}

.app > ul > li > strong:first-child {
    font-size: 1.0625rem;
    font-weight: 640;
    letter-spacing: -0.01em;
}

/* An inline action form inside a row: its own controls sit side by side. */
li form {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
}

li form input[type="text"],
li form input[type="password"],
li form input[type="number"] {
    width: auto;
    min-width: 9rem;
    flex: 1 1 9rem;
}

/* Block-level children of a row (a nested point list, a disclosure, an explanatory paragraph) each
   take their own full-width line rather than trying to sit in the action row. */
.app > ul > li > ul,
.app > ul > li > details,
.app > ul > li > p,
.app > div > ul > li > ul,
.app > div > ul > li > details,
.app > div > ul > li > p {
    flex-basis: 100%;
    width: 100%;
    margin: 0;
    min-width: 0;
}

/* The nested list inside a device row: its live points. Reads as a compact data table, not another
   stack of cards. */
.app > ul > li > ul,
.app > div > ul > li > ul {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin-top: var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-2);
}

.app > ul > li > ul > li,
.app > div > ul > li > ul > li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    font-size: 0.875rem;
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

.app > ul > li > ul > li + li,
.app > div > ul > li > ul > li + li {
    border-top: 1px solid var(--border);
}

/* A live point value (the SSE-swapped span) -- monospaced so digits don't jitter as it updates. */
.app > ul > li > ul > li [sse-swap],
.app > div > ul > li > ul > li [sse-swap] {
    font-family: var(--font-mono);
    font-weight: 600;
    color: var(--accent);
    background: var(--accent-soft);
    padding: 0.05em 0.4em;
    border-radius: var(--radius-sm);
}

/* Disclosure ("Add a device", "Logs", "Edit settings"). */
details {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-3) var(--space-4);
    margin-bottom: var(--space-4);
}

details[open] {
    box-shadow: var(--shadow-sm);
}

summary {
    cursor: pointer;
    font-weight: 580;
    padding: var(--space-1) 0;
    list-style: none;
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 32px;
    color: var(--text);
}

summary::-webkit-details-marker {
    display: none;
}

summary::before {
    content: "";
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform 0.15s ease;
    flex-shrink: 0;
    opacity: 0.6;
}

details[open] > summary::before {
    transform: rotate(90deg);
}

details[open] > summary {
    margin-bottom: var(--space-3);
    padding-bottom: var(--space-3);
    border-bottom: 1px solid var(--border);
}

/* A disclosure nested in a row is a sub-panel, not another card floating on a card. */
li > details {
    background: var(--surface-2);
    margin-top: var(--space-2);
    margin-bottom: 0;
}

details ul {
    margin-bottom: 0;
}

/* Container log output. The class is already on the element; this replaces what used to be a long
   hardcoded inline `style` attribute on every one of them. */
.workload-log-output {
    max-height: 18rem;
    overflow: auto;
    white-space: pre-wrap;
    word-break: break-word;
    background: var(--console-bg);
    color: var(--console-text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--space-3);
    margin: 0;
    font-size: 0.75rem;
    line-height: 1.5;
    /* Keeps a long log from being a page-length scroll trap on a phone. */
    overscroll-behavior: contain;
}

/* ==========================================================================
   7. Tables
   ========================================================================== */

table {
    width: 100%;
    border-collapse: collapse;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    overflow: hidden;
    margin-bottom: var(--space-5);
    font-size: 0.9375rem;
}

th,
td {
    text-align: left;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border);
}

thead th {
    background: var(--surface-2);
    font-size: 0.8125rem;
    font-weight: 640;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}

tbody tr:last-child td {
    border-bottom: 0;
}

tbody tr:hover td {
    background: var(--surface-2);
}

/* ==========================================================================
   8. Dashboard widgets
   ========================================================================== */

.widget-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
    gap: var(--space-4);
    margin-bottom: var(--space-5);
}

.widget-cell {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}

.widget {
    /* Every widget kind's primary readout resolves to this one size, so a gauge, a thermometer and
       a plain digit display sitting in the same card read as one family rather than three. The
       *base* is set by context (normal vs. kiosk below); the *scale* is the per-widget "Text size"
       setting, inherited from the wrapper `data-text-size` in dashboard.html. Kinds must never set
       `font-size` on their value element directly, or they drop back out of lockstep. */
    --widget-value-base: 1.5rem;
    --widget-unit-base: 0.9375rem;
    --widget-value-size: calc(var(--widget-value-base) * var(--widget-value-scale, 1));
    --widget-unit-size: calc(var(--widget-unit-base) * var(--widget-value-scale, 1));

    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    height: 100%;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow);
    transition: box-shadow 0.18s ease, transform 0.18s ease;
}

.widget:hover {
    box-shadow: var(--shadow-lg);
    transform: translateY(-1px);
}

.widget h3 {
    margin: 0;
    font-size: 0.8125rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.widget form {
    margin: 0;
    display: flex;
    align-items: center;
}

.widget.toggle form {
    justify-content: flex-start;
}

/* The big number on a gauge -- tabular figures so a changing value doesn't shift width. */
.gauge-value {
    margin: 0;
    font-size: var(--widget-value-size);
    font-weight: 660;
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

/* ---- Input vs output --------------------------------------------------------------------
   Every rendered widget carries `data-direction`, derived from its kind (see
   `crate::widgets::WidgetDirection`). On a wall display the useful question is "can I touch this,
   or is it just telling me something" -- an input gets an accent edge so controls are findable at
   a glance without reading each tile. */

.widget[data-direction="input"] {
    border-left: 3px solid var(--accent);
}

/* The direction badge on the manage page. */
.tag-input {
    background: var(--accent-soft);
    color: var(--accent);
}

.tag-output {
    background: var(--surface-3);
    color: var(--text-muted);
}

/* Inside a grouped card the widgets are flattened rows, so an accent *edge* would cut across the
   card's own border -- mark the control side of the row instead. */
.widget-card-items .widget[data-direction="input"] {
    border-left: 0;
    padding-left: var(--space-3);
    border-inline-start: 3px solid var(--accent);
}

/* ---- Digits widget ------------------------------------------------------------------------
   Deliberately the most minimal readout: the number carries the tile, everything else recedes. */

.digits-value {
    margin: 0;
    font-size: var(--widget-value-size);
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.03em;
    color: var(--text);
    /* Tabular figures so a changing reading doesn't shift width on every update. */
    font-variant-numeric: tabular-nums;
    display: flex;
    align-items: baseline;
    gap: var(--space-1);
}

.digits-unit {
    font-size: var(--widget-unit-size);
    font-weight: 600;
    letter-spacing: 0;
    color: var(--text-muted);
}

/* ---- Thermometer widget -------------------------------------------------------------------
   A reading shown in context: where it sits between the widget's configured min and max, drawn as
   a scale -- label and reading on the top line, the bar full-width beneath it, and the range
   marked under the bar's two ends the way a real thermometer is.

   One horizontal layout, not two. The earlier "vertical tube standing alone / horizontal bar in a
   card" split produced a run of layout bugs, and since a widget is only ever displayed inside a
   card, there was never a second orientation to serve. */

.thermometer-scale {
    position: relative;
    width: 100%;
    height: 1.125rem;
    border-radius: var(--radius-pill);
    background: var(--surface-3);
    border: 1px solid var(--border);
    overflow: hidden;
}

.thermometer-fill {
    /* Solid, not a gradient: at bar height a multi-stop gradient reads as a progress bar, and it
       implied a colour scale the reading never actually followed. */
    width: var(--thermometer-percent, 0%);
    height: 100%;
    background: var(--accent);
    border-radius: var(--radius-pill);
    transition: width 0.5s ease;
}

/* No reading yet is 0% fill, same as "genuinely at the bottom of the scale" -- the muted tube makes
   the difference visible instead of showing a confident-looking empty thermometer. */
.thermometer-scale:not(.has-reading) {
    opacity: 0.45;
}

/* The range, marked under each end of the bar. */
.thermometer-ticks {
    display: flex;
    justify-content: space-between;
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
}

body.is-kiosk .thermometer-scale {
    height: 1.75rem;
}

/* The thermometer is the one kind needing more than a single row: the bar and its ticks each take
   a full line beneath the label/reading line. `.widget-card-items .widget` sets
   `flex-direction: row`, so wrapping here stacks those bands rather than columnising them. */
.widget-card-items .widget.thermometer {
    flex-wrap: wrap;
    align-items: baseline;
}

.widget-card-items .widget.thermometer > .thermometer-scale,
.widget-card-items .widget.thermometer > .thermometer-ticks {
    flex: 1 1 100%;
}

/* A toggle widget's checkbox becomes a real switch -- a bare 16px checkbox is a poor touch target
   and reads as a form field rather than a control you operate. */
.widget.toggle input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    position: relative;
    width: 3.25rem;
    height: 1.85rem;
    border-radius: var(--radius-pill);
    background: var(--surface-3);
    border: 1px solid var(--border-strong);
    cursor: pointer;
    transition: background-color 0.18s ease, border-color 0.18s ease;
}

.widget.toggle input[type="checkbox"]::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 3px;
    transform: translateY(-50%);
    width: 1.35rem;
    height: 1.35rem;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: var(--shadow);
    transition: left 0.18s ease;
}

.widget.toggle input[type="checkbox"]:checked {
    background: var(--accent);
    border-color: var(--accent);
}

.widget.toggle input[type="checkbox"]:checked::after {
    left: calc(100% - 1.35rem - 3px);
}

.widget-area {
    color: var(--text-faint);
    font-size: 0.75rem;
    padding-left: var(--space-1);
}

/* ==========================================================================
   9. Utilities and page-specific bits
   ========================================================================== */

/* The intro/help paragraph directly under a page's h1. */
.app > h1 + p > small,
.app > p > small {
    line-height: 1.5;
}

/* The area tree's depth indent (settings_areas.html sets margin-left inline per node) still needs
   the row to start at its indent rather than stretch full width. */
.app > ul > li[style*="margin-left"] {
    scroll-margin-top: var(--space-6);
}

/* Pair-a-device / driver-instance sections keep their forms from stretching absurdly wide. */
li form input[name="code"] {
    max-width: 18rem;
}

/* The "Choose point…" picker on the widget-management page -- a native <dialog>, opened by its own
   inline `showModal()` handler in the template. Sized to be usable on a phone (nearly full-screen)
   without becoming a full-bleed sheet on desktop. */
dialog {
    width: min(34rem, calc(100vw - 2rem));
    max-height: min(32rem, calc(100vh - 4rem));
    padding: var(--space-4);
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    overflow: auto;
}

dialog::backdrop {
    background: rgba(10, 14, 20, 0.55);
    backdrop-filter: blur(2px);
}

dialog > * + * {
    margin-top: var(--space-3);
}

/* The live search results inside the picker: `routes::dashboards` renders these as a plain
   <ul><li><button>…</button></li></ul>, so they need list styling of their own (the card rules
   above only reach a page's own top-level lists). */
dialog ul,
[id$="_results"] ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    max-height: 18rem;
    overflow-y: auto;
}

dialog li,
[id$="_results"] li {
    display: block;
    padding: 0;
    background: none;
    border: 0;
}

/* Each result is a full-width, left-aligned target rather than a centred pill -- it's a row in a
   list of choices, not a call to action. */
dialog li button,
[id$="_results"] li button {
    width: 100%;
    justify-content: flex-start;
    text-align: left;
    font-weight: 500;
    background: var(--surface-2);
    color: var(--text);
    border-color: transparent;
}

dialog li button:hover,
[id$="_results"] li button:hover {
    background: var(--accent-soft);
    color: var(--accent);
}

/* Set by `routes::dashboards::binding_status_for` -- flags a widget bound to a point that no longer
   exists, which is otherwise invisible until the live dashboard silently shows "--" forever. */
.binding-status {
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    color: var(--text-muted);
}

/* --------------------------------------------------------------------------
   Devices page: each device is an accordion
   -------------------------------------------------------------------------- */

/* The row's <li> is only a container here -- the <details> inside it carries the card, so the
   summary's click target spans the whole row rather than leaving dead space around it. */
.device-list > li {
    display: block;
    padding: 0;
    border: 0;
    background: none;
    box-shadow: none;
}

details.device {
    margin: 0;
    padding: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}

details.device > summary {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-4);
    margin: 0;
    border: 0;
}

details.device[open] > summary {
    border-bottom: 1px solid var(--border);
}

details.device > summary:hover {
    background: var(--surface-2);
}

.device-name {
    font-size: 1.0625rem;
    font-weight: 640;
    letter-spacing: -0.01em;
}

/* The driver kind, as a small pill rather than the bare `[zigbee2mqtt]` bracket notation. */
.tag {
    display: inline-block;
    padding: 1px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--surface-3);
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
}

/* "Last reading" -- the at-a-glance "is this device alive" answer. Quiet when it's reporting
   normally; called out only when it has gone silent, since that's the state worth noticing. */
.device-reading {
    display: inline-block;
    padding: 1px var(--space-2);
    border-radius: var(--radius-pill);
    background: var(--success-soft);
    color: var(--success);
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
}

.device-reading.is-silent {
    background: var(--danger-soft);
    color: var(--danger);
}

.point-age {
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* Area and point count sit at the far end of the summary, away from the identity fields. */
.device-area {
    margin-left: auto;
    font-size: 0.8125rem;
    color: var(--text-muted);
    white-space: nowrap;
}

.device-point-count {
    white-space: nowrap;
}

.device-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
}

.device-external-id {
    font-family: var(--font-mono);
    color: var(--text-faint);
}

.point-list {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 0;
    margin: 0;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-2);
}

.point-list > li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    font-size: 0.875rem;
}

.point-list > li + li {
    border-top: 1px solid var(--border);
}

/* Live point value -- monospaced so digits don't jitter as it updates over SSE. */
.point-list [sse-swap] {
    font-family: var(--font-mono);
    font-weight: 600;
    color: var(--accent);
    background: var(--accent-soft);
    padding: 0.05em 0.4em;
    border-radius: var(--radius-sm);
}

.device-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.device-actions form {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
}

.device-actions input[type="text"],
.device-actions select {
    width: auto;
    min-width: 9rem;
    flex: 1 1 9rem;
}

/* A quieter button style for page-level controls like "Expand all" that aren't the primary action. */
button.secondary {
    background: var(--surface);
    color: var(--text);
    border-color: var(--border-strong);
    font-weight: 500;
}

button.secondary:hover {
    background: var(--surface-2);
    color: var(--text);
}

@media (max-width: 560px) {
    /* On a narrow screen the area label shouldn't be pushed to its own far-right corner. */
    .device-area {
        margin-left: 0;
    }
}

/* --------------------------------------------------------------------------
   Dashboard: room grouping, glance header, and kiosk ("display") mode
   -------------------------------------------------------------------------- */

.dashboard-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3) var(--space-4);
    margin-bottom: var(--space-5);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}

.dashboard-title {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
}

.dashboard-title h1 {
    margin: 0;
}

/* The live indicator: a coloured dot plus a word, so it reads at a glance from across a room and
   still says precisely what it means up close. `data-state` is set from the htmx SSE lifecycle
   events -- see dashboard.html. */
.live-dot {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap;
}

.live-dot-mark {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--text-faint);
    flex-shrink: 0;
}

.live-dot[data-state="live"] {
    background: var(--success-soft);
    color: var(--success);
}

.live-dot[data-state="live"] .live-dot-mark {
    background: var(--success);
    animation: live-pulse 2.4s ease-in-out infinite;
}

.live-dot[data-state="down"] {
    background: var(--danger-soft);
    color: var(--danger);
}

.live-dot[data-state="down"] .live-dot-mark {
    background: var(--danger);
}

@keyframes live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

.dashboard-clock {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    line-height: 1.15;
    margin-left: auto;
}

.clock-time {
    font-size: 1.75rem;
    font-weight: 660;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

.clock-date {
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.dashboard-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

/* Switching between dashboards from the display itself -- kiosk mode hides the main nav, so this
   is the only way across without leaving the screen. */
.dashboard-switch {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    padding: var(--space-1);
    background: var(--surface-2);
    border-radius: var(--radius-pill);
}

.dashboard-switch a {
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-pill);
    font-size: 0.875rem;
    font-weight: 550;
    color: var(--text-muted);
    text-decoration: none;
    white-space: nowrap;
}

.dashboard-switch a:hover {
    background: var(--surface);
    color: var(--text);
}

.dashboard-switch a.is-current {
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-sm);
}

/* An <a> that should look like a button (the "Manage widgets" link). */
.button-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-2) var(--space-4);
    min-height: 44px;
    border-radius: var(--radius);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    color: var(--text);
    font-size: 0.9375rem;
    font-weight: 550;
    text-decoration: none;
    white-space: nowrap;
}

.button-link:hover {
    background: var(--surface-2);
    color: var(--text);
}

.dashboard-rooms {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}

.room-name {
    margin: 0 0 var(--space-3);
    padding-bottom: var(--space-2);
    font-size: 0.8125rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
    border-bottom: 1px solid var(--border);
}

.dashboard-empty {
    color: var(--text-muted);
}

/* ---- Multi-widget cards -------------------------------------------------------------------
   Several widgets sharing a card label render on one surface (see `sandbox_core::Widget::card`).
   The card is the raised surface; the widgets inside it are flattened to plain rows so the result
   reads as one grouped control panel rather than cards nested inside a card. */

.widget-card {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    height: 100%;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
    box-shadow: var(--shadow);
}

.widget-card-title {
    margin: 0;
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--border);
    font-size: 0.8125rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.widget-card-items {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* Inside a card, each widget drops its own card chrome -- no nested border, shadow or padding. */
.widget-card-items .widget {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    height: auto;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: var(--space-2) 0;
}

.widget-card-items .widget:hover {
    box-shadow: none;
    transform: none;
}

.widget-card-items > div + div .widget {
    border-top: 1px solid var(--border);
}

/* A widget's title becomes its row label, so it sits beside the control rather than above it. */
.widget-card-items .widget h3 {
    font-size: 0.9375rem;
    font-weight: 550;
    text-transform: none;
    letter-spacing: 0;
    color: var(--text);
    flex: 1 1 auto;
    min-width: 0;
    /* The label is the one thing in the row that stays flush left; everything else goes right. */
    text-align: left;
}

.widget-card-items .gauge-value {
    flex-shrink: 0;
}

.widget-card-items .widget form {
    flex-shrink: 0;
}

/* A slider needs the row's full width to be usable, so that one wraps instead. */
.widget-card-items .widget.slider {
    flex-wrap: wrap;
}

.widget-card-items .widget.slider form {
    flex: 1 1 100%;
}

/* A grouped card holds more than a single tile's worth, so give it two columns where there's room
   -- `min()` keeps it from overflowing a single-column grid on a phone. */
@media (min-width: 700px) {
    .widget-cell-grouped {
        grid-column: span 2;
    }
}

body.is-kiosk .widget-card {
    padding: var(--space-5);
}

body.is-kiosk .widget-card-title {
    font-size: 1rem;
}

body.is-kiosk .widget-card-items .widget h3 {
    font-size: 1.25rem;
}

/* ---- Kiosk / display mode ----------------------------------------------------------------
   Toggled by adding `is-kiosk` to <body> (see dashboard.html). Everything here is about turning a
   general-purpose admin page into something readable and operable from across a room: app chrome
   goes away, and every control grows. */

body.is-kiosk .site-header,
body.is-kiosk .dashboard-tools .button-link {
    display: none;
}

body.is-kiosk .app {
    padding-bottom: var(--space-4);
}

body.is-kiosk .app > .site-header {
    margin-bottom: 0;
}

body.is-kiosk .dashboard {
    padding-top: var(--space-4);
}

/* Kiosk lifts the *base* only, so every kind grows together and each widget's own "Text size"
   choice still applies on top of it rather than being overridden by it. */
body.is-kiosk .widget {
    --widget-value-base: 2.25rem;
    --widget-unit-base: 1.25rem;
}

/* Bigger everything: a wall display is viewed from metres away, not centimetres. */
body.is-kiosk .dashboard-title h1 {
    font-size: 2.25rem;
}

body.is-kiosk .clock-time {
    font-size: 3rem;
}

body.is-kiosk .clock-date {
    font-size: 1rem;
}

body.is-kiosk .room-name {
    font-size: 1rem;
    letter-spacing: 0.06em;
}

body.is-kiosk .widget {
    padding: var(--space-5);
    gap: var(--space-3);
}

body.is-kiosk .widget h3 {
    font-size: 1rem;
    letter-spacing: 0.03em;
}

/* Controls scale up too -- a wall display is operated by fingertip, often in passing. */
body.is-kiosk .widget.toggle input[type="checkbox"] {
    width: 4.5rem;
    height: 2.5rem;
}

body.is-kiosk .widget.toggle input[type="checkbox"]::after {
    width: 1.9rem;
    height: 1.9rem;
}

body.is-kiosk .widget.toggle input[type="checkbox"]:checked::after {
    left: calc(100% - 1.9rem - 4px);
}

body.is-kiosk .widget.slider input[type="range"] {
    height: 2.5rem;
}

body.is-kiosk .widget-grid {
    gap: var(--space-5);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
}

@media (min-width: 700px) {
    .dashboard-bar {
        flex-wrap: nowrap;
    }

    /* Tools wrap to their own row on narrow screens; on wide ones they sit inline after the clock. */
    .dashboard-tools {
        flex-shrink: 0;
    }
}

@media (max-width: 699px) {
    /* On a phone the clock shouldn't shove the tools off the row -- give each its own line. */
    .dashboard-clock {
        margin-left: auto;
    }

    .dashboard-tools {
        flex-basis: 100%;
    }
}

@media (min-width: 700px) {
    .widget-grid {
        grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
    }
}

/* Print: drop the app chrome, keep the content. */
@media print {
    .site-header,
    form,
    button {
        display: none !important;
    }

    body {
        background: #fff;
        color: #000;
    }
}

/* --------------------------------------------------------------------------
   Dashboard manage page: the widgets and cards accordions
   -------------------------------------------------------------------------- */

/* Two top-level sections, each a disclosure holding a searchable list plus its own "Add" form.
   They're `open` by default -- this is a working page, not a reference one, so hiding the contents
   behind a click every visit would cost more than it saves. */
.manage-section {
    padding: 0;
    overflow: hidden;
}

.manage-section > summary {
    padding: var(--space-4);
    margin: 0;
    background: var(--surface-2);
    font-size: 1.0625rem;
}

.manage-section[open] > summary {
    border-bottom: 1px solid var(--border);
    margin-bottom: 0;
    padding-bottom: var(--space-4);
}

.manage-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-4);
}

.manage-filter {
    width: 100%;
}

.manage-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

.manage-list > li {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* Setting `display` on a list item defeats the browser's own `[hidden] { display: none }`: an
   author rule outranks the user-agent stylesheet, so `row.hidden = true` hides nothing and every
   filter box over one of these lists silently does nothing. Confirmed twice now -- first on the
   card picker, which got its own `.addable-list > li[hidden]` rule, and then again on the Scripts
   page's point browser, because that fix was written for one list instead of for the rule that
   causes it. This is that rule. */
.manage-list > li[hidden] {
    display: none;
}

/* Each row is three stacked bands -- identity, metadata, actions -- rather than one long
   horizontal run of controls. The old single-line layout put five inline forms in a row, which
   wrapped into an unreadable tangle as soon as a widget had a real name. */
.manage-row-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.manage-row-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    color: var(--text-muted);
}

.manage-row-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border);
}

.manage-row-actions form {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
}

.manage-row-actions input[type="text"],
.manage-row-actions select {
    width: auto;
    min-width: 9rem;
    flex: 1 1 9rem;
}

/* A card's member widgets: a compact inset list, visually subordinate to the card row itself. */
.card-members {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--surface-2);
}

.card-members > li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    font-size: 0.875rem;
}

.card-members > li + li {
    border-top: 1px solid var(--border);
}

.card-members form {
    margin: 0 0 0 auto;
}

/* The "Add widget"/"Add card" disclosure at the foot of each section. */
.manage-add {
    margin: 0;
    background: var(--surface-2);
}

.manage-add > form {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

/* The linear-conversion operands, shown only for that conversion. A class rather than an id
   because every widget's edit form carries its own copy alongside the "Add widget" form's. */
.linear-fields {
    display: flex;
    flex-wrap: wrap;
    /* Compact: these three operands are one setting read together, so they sit as a tight group
       rather than three widely spaced fields. */
    gap: var(--space-2);
    padding: var(--space-2);
    margin: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* The parent form's own row gap would otherwise add to this box's padding, leaving a conspicuous
   gap under the last input. */
.linear-fields > label {
    gap: 2px;
}

/* Must follow the rule above: a `display` of anything but `none` overrides the `hidden` attribute,
   so without this the operands would show for every conversion. */
.linear-fields[hidden] {
    display: none;
}

.linear-fields > label {
    flex: 1 1 7rem;
    min-width: 0;
}

/* Flags a widget that exists but isn't in a card, and so isn't rendered on the dashboard at all. */
.tag-warn {
    background: var(--danger-soft);
    color: var(--danger);
}

/* The positive counterpart to `.tag-warn` -- a site's enabled/disabled state needs to be visible
   either way at a glance, not just flagged when it's the exceptional (disabled) one. */
.tag-ok {
    background: var(--success-soft);
    color: var(--success);
}

/* A whole `.manage-list` row collapsed behind its own disclosure: the summary carries just enough
   to pick the right site out of a list (name, domain, active/disabled) without opening it, same
   precedent as the Devices list's per-device `<details>`. The row's own `<li>` already draws the
   card (border/background/padding, see `.manage-list > li`), so this -- nested one level deeper --
   sheds the base `details` card styling the same way `.manage-edit` does, rather than drawing a
   second box inside the first. */
.site-row {
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
}

.site-row > summary {
    flex-wrap: wrap;
}

.site-row > .manage-row-meta {
    margin-top: var(--space-2);
}

.site-row > .manage-row-actions {
    margin-top: var(--space-2);
}

/* ---- Per-widget text size -------------------------------------------------------------------
   The "Text size" setting, applied to the SSE wrapper in dashboard.html so it survives a live
   update (SSE replaces the wrapper's *inner* HTML, never the wrapper itself). It sets only a
   multiplier, so a widget stays in proportion whether the dashboard is in kiosk mode or not.
   The default is deliberately absent rather than `1`: an unset widget inherits the shared base
   exactly, which is what "all default to the same size" means. */
[data-text-size="sm"] {
    --widget-value-scale: 0.75;
}

[data-text-size="lg"] {
    --widget-value-scale: 1.5;
}

[data-text-size="xl"] {
    --widget-value-scale: 2;
}

/* ---- Row alignment inside a card ------------------------------------------------------------
   Every row in a card reads label-left / value-right, so the readings form one clean column down
   the card instead of each landing wherever its own text happens to end. Without this the values
   are technically flush right but their *contents* are left-aligned inside boxes of differing
   widths, which reads as ragged -- the thing that's annoying to look at. */
.widget-card-items .widget > * {
    text-align: right;
}

/* These two are flex containers rather than plain text, so `text-align` alone doesn't move them. */
.widget-card-items .digits-value {
    justify-content: flex-end;
}

/* ---- Per-widget edit form ---------------------------------------------------------------------
   A stacked form, not one of the inline single-field mini-forms `.manage-row-actions` was built
   for, so it has to opt out of that sizing. `details > form` lays it out as a *column* flex
   container, which means the inherited `flex: 1 1 9rem` was being read on the main axis -- giving
   every select and text input a 9rem *height* basis and letting it grow, while number inputs (not
   covered by that selector) stayed normal. Hence dropdowns several times taller than the fields
   beside them. */
.manage-edit {
    /* Its own full-width band within the wrapping actions row, not a chip beside the Remove button. */
    flex: 1 1 100%;
    margin: 0;
    /* The base `details` rule styles every disclosure as a card -- border, tint, radius, padding.
       At this depth (a panel, inside a row, inside an accordion) that box reads as an indent even
       with the padding zeroed, which is what it still looked like on a narrow screen. So the whole
       card treatment comes off and this becomes a plain disclosure, flush with the row it belongs
       to; the row's own <li> padding is the only inset left. */
    padding: var(--space-2) 0 0;
    background: none;
    border: 0;
    border-radius: 0;
}

.manage-edit > summary {
    padding-left: 0;
}

/* `.manage-row-actions form` (0,1,1) outranks the base `details > form` (0,0,2), so without this
   the edit form was laid out as an inline-flex *row* that wrapped -- fields sharing lines at
   whatever width their content happened to want, which is what read as ragged indentation rather
   than a flat stack. These three rules put it back to a plain full-width column. */
.manage-row-actions .manage-edit > form {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-3);
    width: 100%;
}

.manage-row-actions .manage-edit label {
    width: 100%;
}

/* Not checkboxes/radios: `width: 100%` on one of those stretches its layout box to the full row,
   which leaves a `label:has(> input[type="checkbox"])`'s sibling text almost no room and wraps it
   into a near-vertical sliver -- exactly the "checkbox and its explanation aren't on the same
   line" bug this excludes them from causing. */
.manage-row-actions .manage-edit input:not([type="checkbox"]):not([type="radio"]),
.manage-row-actions .manage-edit select {
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
}

/* The device list's own controls, and the countdown shown while it polls for newly joined devices. */
.device-list-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
}

.device-autorefresh {
    color: var(--text-muted);
    font-size: 0.875rem;
}

.device-autorefresh[hidden] {
    display: none;
}

/* "When did this last report", under each widget in a card. Deliberately quiet: it's the answer to
   "is this reading stale", which only matters when you go looking for it. */
.widget-stamp {
    display: block;
    text-align: right;
    color: var(--text-faint);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
}

/* The "add widgets to this card" picker: a filterable, scrolling checkbox list rather than a
   <select>, since a house's worth of widgets outgrows a dropdown quickly. */
.manage-add-widgets {
    flex: 1 1 100%;
    margin: 0;
    padding: var(--space-2) 0 0;
    background: none;
    border: 0;
    border-radius: 0;
}

.addable-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0;
    max-height: 16rem;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.addable-list > li {
    border-bottom: 1px solid var(--border);
}

.addable-list > li:last-child {
    border-bottom: 0;
}

.addable-list > li[hidden] {
    display: none;
}

/* The whole row is the hit target -- a bare checkbox is a poor one on a touch screen. */
.addable-list label {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    padding: var(--space-2) var(--space-3);
    cursor: pointer;
    min-height: 44px;
}

.addable-list label:hover {
    background: var(--surface-2);
}

.addable-list input[type="checkbox"] {
    width: auto;
    min-height: 0;
    flex-shrink: 0;
}

/* ---- Reordering a card's widgets --------------------------------------------------------------
   A drag handle plus up/down buttons. The handle is the only draggable part on purpose: dragging
   the whole row would fight text selection, and on a phone it would fight scrolling. */
.drag-handle {
    cursor: grab;
    color: var(--text-faint);
    letter-spacing: -0.2em;
    padding: 0 var(--space-2) 0 0;
    /* Stops the browser claiming the gesture as a scroll before the pointer handler sees it. */
    touch-action: none;
    user-select: none;
    flex-shrink: 0;
    /* A comfortable target on a touch screen -- the glyph itself is only a few pixels wide. */
    min-width: 1.5rem;
    min-height: 44px;
    display: flex;
    align-items: center;
}

.drag-handle:active {
    cursor: grabbing;
}

/* The row being dragged, so it's clear which one is moving. */
.card-members > li.is-dragging {
    background: var(--surface);
    box-shadow: var(--shadow);
    opacity: 0.9;
}

/* The name takes the slack so the buttons stay put as rows are dragged past each other. */
.card-member-name {
    flex: 1 1 auto;
    min-width: 0;
}

.card-members button[disabled] {
    opacity: 0.35;
    cursor: default;
}

/* A device's one save form: fields side by side on a wide row, stacked when there isn't room. */
.device-save {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2);
}

.device-save label {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    /* The name takes the slack; the area select keeps a usable minimum. */
    flex: 1 1 12rem;
    min-width: 0;
    font-size: 0.8125rem;
    color: var(--text-muted);
}

.device-save label:last-of-type {
    flex: 0 1 12rem;
}

/* The device save form's fields stack, so they must opt out of `.device-actions`' inline sizing --
   `flex: 1 1 9rem` inside a column container is read on the vertical axis, which made every input
   and select a 9rem-tall box. The same trap the widget edit form fell into. */
.device-actions .device-save input,
.device-actions .device-save select {
    flex: 0 0 auto;
    width: 100%;
    min-width: 0;
}

.device-actions form.device-save {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-end;
    width: 100%;
}

/* The bound-point field and its "Choose point…" button are one control: the field alone can't be
   typed into (it's readonly), so a button stacked underneath read as an unrelated action. */
.point-picker {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: 100%;
}

/* Beats `.manage-row-actions .manage-edit input { width: 100% }`, which would otherwise make the
   field claim the whole row and push the button out of it. */
.point-picker input[type="text"] {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
}

.point-picker button {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* A scannable QR needs a light quiet zone regardless of theme -- a dark-mode page would otherwise
   invert it and some scanners refuse that. */
.qr-code {
    display: inline-block;
    /* These pages put this box directly inside the `.app` grid, where `.app > *` makes it a grid
       item -- and a grid item stretches to fill its column no matter what `display` says, which is
       why the code was rendering across the full width. Both of these keep it at its own size. */
    justify-self: start;
    width: max-content;
    padding: var(--space-3);
    background: #fff;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    line-height: 0;
}

.qr-code svg {
    display: block;
    width: 240px;
    height: 240px;
}

.wireguard-config {
    white-space: pre-wrap;
    word-break: break-all;
}

/* The otpauth:// URI is long and has no spaces, so it needs an explicit break to stay in its box. */
.totp-uri {
    word-break: break-all;
}

/* A transient "lost contact, retrying" note above a log panel. It sits alongside the log rather
   than replacing it, so a blip never destroys output someone is reading. */
.log-status {
    margin: 0 0 var(--space-2);
    color: var(--danger);
    font-size: 0.8125rem;
}

.log-status[hidden] {
    display: none;
}

/* Stacked forms stretch every control to the full column, which is right for a long value and
   wrong for a short one -- an email box spanning a 1200px row reads as a mistake. Capping at a
   comfortable reading measure keeps the field a plausible size for what goes in it, while still
   shrinking on a narrow screen. */
.manage-add input[type="text"],
.manage-add input[type="email"],
.manage-add input[type="number"],
.manage-add input[type="password"],
.manage-add select,
.manage-edit input[type="text"],
.manage-edit input[type="email"],
.manage-edit input[type="number"],
.manage-edit select {
    max-width: 32rem;
}

/* Genuinely short values get a shorter box still, so the form reads as a set of fields rather than
   a stack of identical bars. */
.manage-add input[type="number"],
.manage-edit input[type="number"] {
    max-width: 10rem;
}

/* ---- Account menu -----------------------------------------------------------------------------
   The account controls spelled out inline cost roughly 300px of the header's 1200px row. With ten
   nav links for an admin, that was the difference between the last tabs being reachable and being
   scrolled off the end -- so they collapse behind the username. */
.account-menu {
    position: relative;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
}

.account-menu > summary {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    font-size: 0.875rem;
    font-weight: 500;
    white-space: nowrap;
    min-height: 34px;
}

.account-menu > summary:hover {
    background: var(--surface-3);
}

/* Overlaid rather than in flow: opening it must not change the header's height or shove the page
   down, which on a sticky header would move everything the moment you click. */
.account-menu[open] > summary {
    margin-bottom: 0;
    padding-bottom: var(--space-2);
    border-bottom: 0;
}

.account-menu-body {
    position: absolute;
    right: 0;
    top: calc(100% + var(--space-2));
    z-index: 30;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    min-width: 12rem;
    padding: var(--space-3);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

.account-menu-body .site-user {
    color: var(--text-muted);
    font-size: 0.8125rem;
}

.account-menu-body form {
    margin: 0;
}

.account-menu-body button {
    width: 100%;
}

/* The point browser on the Scripts page: dense, since its job is scanning a long list. */
.point-browser > li {
    padding: var(--space-2) var(--space-3);
    gap: var(--space-1);
}

.point-browser .manage-row-meta {
    align-items: center;
    gap: var(--space-2);
}

.point-id {
    color: var(--text-faint);
    font-size: 0.75rem;
    word-break: break-all;
}

.copy-point {
    font-size: 0.75rem;
    padding: var(--space-1) var(--space-2);
    min-height: 0;
    flex-shrink: 0;
}

/* A plain link inside the account menu, styled to sit with the buttons beside it. */
.account-menu-link {
    padding: var(--space-1) 0;
    font-size: 0.8125rem;
    color: var(--accent);
    text-decoration: none;
}

.account-menu-link:hover {
    text-decoration: underline;
}
