/* ---------------------------------------------------------------------------
   Design tokens
   --------------------------------------------------------------------------- */

:root {
    --bg: #f6f7f9;
    --surface: #ffffff;
    --surface-hover: #f2f4f7;
    --text: #16191d;
    --muted: #6b7280;
    --border: #e3e6ea;
    --accent: #0b6bcb;

    /* Lighting is the domain, so "on" reads warm rather than as a generic success green. */
    --on: #a97400;
    --on-bg: #fdf6e3;
    --on-border: #e8d18a;

    --danger: #b3261e;
    --danger-bg: #fdecea;
    --danger-border: #f5c2c0;

    --live: #1e7e34;
    --live-bg: #e6f4ea;

    --radius: 12px;
    --radius-sm: 8px;
    --shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 4%);

    --sidebar-w: 232px;
    --tabbar-h: 60px;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #0f1115;
        --surface: #171a20;
        --surface-hover: #1e222a;
        --text: #e6e8eb;
        --muted: #9099a6;
        --border: #272c35;
        --accent: #63a4ff;

        --on: #f0c36d;
        --on-bg: #2a2317;
        --on-border: #5c4a22;

        --danger: #f2b8b5;
        --danger-bg: #2d1a19;
        --danger-border: #5c2b28;

        --live: #7ee2a8;
        --live-bg: #14291d;

        --shadow: 0 1px 2px rgb(0 0 0 / 40%);
    }
}

/* ---------------------------------------------------------------------------
   Base
   --------------------------------------------------------------------------- */

*, *::before, *::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
}

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    font-size: 16px;
    line-height: 1.5;
}

h1, h2 {
    margin: 0;
    font-weight: 650;
    letter-spacing: -0.01em;
}

h1 {
    font-size: 1.4rem;
}

h1:focus {
    outline: none;
}

button {
    font: inherit;
    color: inherit;
}

:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   App shell: sidebar on desktop, bottom tab bar on mobile
   --------------------------------------------------------------------------- */

.shell {
    min-height: 100dvh;
}

.brand {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    text-decoration: none;
    color: var(--text);
}

.nav-links {
    display: flex;
    gap: 0.25rem;
}

.nav-link {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.6rem 0.75rem;
    border-radius: var(--radius-sm);
    color: var(--muted);
    text-decoration: none;
    font-weight: 550;
}

.nav-link:hover {
    background: var(--surface-hover);
    color: var(--text);
}

.nav-link.active {
    background: var(--surface-hover);
    color: var(--accent);
}

.nav-link svg {
    width: 20px;
    height: 20px;
    flex: none;
}

.app-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
}

.app-header-actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* The user-menu items (Sign out, etc.) are styled by the controls library — but through CSS
   isolation, so those scoped styles only reach elements the library renders itself. The
   Settings and Users links this app injects as the menu's child content fall outside that
   scope and would otherwise render as bare inline blue links. Restyle them here to match.
   The popup is always light (the library hardcodes a white background), so these colours are
   the library's own rather than theme variables, which would go invisible in dark mode. */
.agi-user-item {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0.6rem;
    border-radius: 6px;
    color: #0f172a;
    text-decoration: none;
    font-size: 0.875rem;
}

.agi-user-item:hover {
    background: #f1f5f9;
}

.content {
    padding: 1rem;
    max-width: 1100px;
}

/* Mobile: sticky top bar, a scrolling content area, and a bottom tab bar under the thumb. */
@media (max-width: 767px) {
    /* Column layout so the content scrolls in its own area between the header and the tabs. The
       scrollbar then belongs to that area and stops above the tabs — rather than the whole page
       scrolling under a fixed bar, where the scrollbar ran the full height and down across the
       tabs. */
    .shell {
        display: flex;
        flex-direction: column;
        height: 100dvh;
        overflow: hidden;
    }

    main {
        order: 1;
        flex: 1;
        /* Without this the flex item won't shrink below its content, so nothing scrolls. */
        min-height: 0;
        overflow-y: auto;
    }

    .app-header {
        position: sticky;
        top: 0;
        z-index: 10;
        padding: 0.85rem 1rem;
        background: var(--surface);
        border-bottom: 1px solid var(--border);
    }

    /* A flex item at the bottom of the column now, not fixed over the content, so the scroll
       area ends cleanly above it and the scrollbar never reaches the tabs. */
    .sidebar-nav {
        order: 2;
        flex: none;
        padding-bottom: env(safe-area-inset-bottom);
        background: var(--surface);
        border-top: 1px solid var(--border);
    }

    .sidebar-nav .brand {
        display: none;
    }

    .nav-links {
        height: var(--tabbar-h);
        gap: 0;
    }

    .nav-link {
        /* min-width: 0 matters: without it a tab is floored at its label's width and six of
           them shove the bar wider than the phone, cutting the last ones off. */
        flex: 1 1 0;
        min-width: 0;
        flex-direction: column;
        justify-content: center;
        gap: 0.15rem;
        padding: 0.6rem 0.25rem;
        border-radius: 0;
        font-size: 0.66rem;
        font-weight: 600;
        text-align: center;
        white-space: nowrap;
    }

    .nav-link.active {
        background: transparent;
    }

    /* The tab bar no longer overlaps the content, so no big offset is needed — just a little
       breathing room at the end of the scroll. */
    .content {
        padding-bottom: 1.5rem;
    }
}

/* Desktop: persistent sidebar. */
@media (min-width: 768px) {
    .shell {
        display: grid;
        grid-template-columns: var(--sidebar-w) 1fr;
    }

    .sidebar-nav {
        position: sticky;
        top: 0;
        align-self: start;
        height: 100dvh;
        padding: 1.25rem 0.75rem;
        background: var(--surface);
        border-right: 1px solid var(--border);
    }

    .sidebar-nav .brand {
        padding: 0 0.75rem;
        margin-bottom: 1.5rem;
        font-size: 1.05rem;
    }

    .nav-links {
        flex-direction: column;
    }

    .app-header {
        padding: 1.75rem 2rem 0;
    }

    .content {
        padding: 1.25rem 2rem 2rem;
    }
}

/* ---------------------------------------------------------------------------
   Cards
   --------------------------------------------------------------------------- */

.section {
    margin-bottom: 2rem;
}

.section-title {
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin-bottom: 0.6rem;
}

.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 0.6rem;
}

.card {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    min-height: 66px;
    padding: 0.75rem 0.9rem;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

.card.is-on {
    border-color: var(--on-border);
    background: var(--on-bg);
}

.card.is-unavailable {
    opacity: 0.55;
}

.card-text {
    flex: 1;
    min-width: 0;
}

.card-name {
    font-weight: 550;
    /* Device names are user-defined and can be long; never let one shove the control off-card. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.card-status {
    font-size: 0.8rem;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* A scene name sits beside its verification badge; the name ellipsizes, the badge never does. */
.scene-card-name {
    display: flex;
    align-items: center;
    gap: 0.35rem;
}

.scene-card-name .scene-name-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The little mark that a scene flip took. Verified fades in and back out on its own; the
   others hold until the state moves on. */
.scene-check {
    flex: none;
    font-size: 0.85rem;
    font-weight: 700;
    line-height: 1;
}

.scene-check.is-ok {
    color: var(--on, #2e7d32);
    animation: scene-check-flash 2.5s ease forwards;
}

.scene-check.is-failed {
    color: var(--danger, #c62828);
    /* A round alert badge so it reads as "look here", not as part of the name. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.05rem;
    height: 1.05rem;
    border-radius: 50%;
    background: var(--danger-bg, rgba(198, 40, 40, 0.14));
    cursor: help;
}

.scene-check.is-checking {
    color: var(--muted);
    opacity: 0.7;
}

@keyframes scene-check-flash {
    0% { opacity: 0; transform: scale(0.6); }
    18% { opacity: 1; transform: scale(1); }
    75% { opacity: 1; }
    100% { opacity: 0; transform: scale(1); }
}

/* Respect a reduced-motion preference: no grow/shrink, just a gentle fade. */
@media (prefers-reduced-motion: reduce) {
    .scene-check.is-ok {
        animation: scene-check-fade 2.5s ease forwards;
    }

    @keyframes scene-check-fade {
        0% { opacity: 0; }
        18% { opacity: 1; }
        75% { opacity: 1; }
        100% { opacity: 0; }
    }
}

.card.is-on .card-status {
    color: var(--on);
    font-weight: 600;
}

/* ---------------------------------------------------------------------------
   Automations
   --------------------------------------------------------------------------- */

.automation-card {
    flex-direction: column;
    align-items: stretch;
    gap: 0.7rem;
}

.automation-head {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

/* One automation is a day's schedule, so its steps read as a timeline down the card. */
.step-list {
    display: flex;
    flex-direction: column;
}

.step-row {
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    padding: 0.5rem 0;
    border-top: 1px solid var(--border);
}

.step-time {
    flex: none;
    width: 74px;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* The real clock time under a "sunset" label — quieter than the label, and free to wrap so
   "tomorrow 9:45 pm" doesn't overflow the narrow column. */
.step-time-real {
    margin-top: 0.15rem;
    font-size: 0.72rem;
    font-weight: 500;
    color: var(--muted);
    white-space: normal;
}

.step-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.step-run {
    flex: none;
    min-height: 32px;
    padding: 0.2rem 0.6rem;
    font-size: 0.75rem;
}

.step-editor {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    padding: 0.75rem;
    margin-bottom: 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
}

.step-editor-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* The reorder pair sits at the left of the head; Remove is pushed to the far right below. */
.step-move {
    flex: none;
    display: flex;
    gap: 0.2rem;
}

.step-move-btn {
    min-width: 30px;
    min-height: 30px;
    padding: 0.1rem 0.3rem;
    font-size: 0.95rem;
    line-height: 1;
}

.step-remove {
    flex: none;
    margin-left: auto;
    min-height: 32px;
    padding: 0.2rem 0.6rem;
    font-size: 0.78rem;
    white-space: nowrap;
}

.step-time-input {
    max-width: 150px;
    font-weight: 650;
}

.step-when {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    min-width: 0;
}

.step-trigger {
    max-width: 150px;
    font-weight: 650;
}

.step-offset {
    display: flex;
    align-items: center;
    gap: 0.35rem;
}

.step-offset .input {
    width: 80px;
}

/* The step's time column holds words now ("sunset −1h"), not just a clock. */
.step-time {
    width: 96px;
    white-space: normal;
}

/* The step editor needs more width than a login-sized form: three columns of dropdowns
   don't read at 460px.
   .form.form-wide, not .form-wide: the base .form rule is defined later in this file, so at
   equal specificity it would win and this would silently do nothing. */
.form.form-wide {
    max-width: 780px;
}

.automation-actions-list,
.day-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.automation-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.chip {
    padding: 0.2rem 0.55rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface-hover);
    font-size: 0.78rem;
    font-weight: 550;
}

.chip-warn {
    border-color: var(--danger-border);
    background: var(--danger-bg);
    color: var(--danger);
}

.day-btn {
    min-width: 46px;
    padding: 0.35rem 0.4rem;
    font-size: 0.78rem;
}

.day-btn.is-current {
    border-color: var(--accent);
    background: var(--accent);
    color: #fff;
}

.action-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.3rem 0;
}

.action-remove {
    min-height: 30px;
    padding: 0.2rem 0.5rem;
    font-size: 0.75rem;
}

/* Three editable columns — what / do / to — plus the remove button.
   Each action is its own row with identical columns, so they line up across rows without
   one action's cells ever being able to flow into another's. */
.action-row {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 1.3fr) minmax(0, 1fr) auto;
    gap: 0.4rem;
    align-items: center;
    margin-bottom: 0.35rem;
}

.action-cell {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.action-cell .input {
    width: 100%;
    min-width: 0;
}

/* The headings row does the labelling on wide screens, so the per-cell labels are
   redundant there. */
.action-head {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
    margin-bottom: 0.1rem;
}

.action-cell-label {
    display: none;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
}

/* On/off takes no argument; hold the cell so the columns keep lining up. */
.action-none {
    color: var(--muted);
    text-align: center;
}

/* A degree value reads as a temperature, not a bare number. */
.degrees {
    position: relative;
    display: block;
}

.degrees .input {
    padding-right: 1.5rem;
}

.degrees-unit {
    position: absolute;
    top: 50%;
    right: 0.6rem;
    transform: translateY(-50%);
    color: var(--muted);
    pointer-events: none;
}

/* Square, so it reads as an icon rather than stretching to fill its grid cell. */
.action-remove {
    justify-self: end;
    width: 34px;
    min-width: 34px;
    height: 34px;
    min-height: 34px;
    padding: 0;
    line-height: 1;
}

.action-add {
    align-self: flex-start;
    font-size: 0.78rem;
    min-height: 32px;
    padding: 0.2rem 0.6rem;
    margin-top: 0.2rem;
}

/* On a phone four columns don't fit. Give the target its own full-width row, put "do"
   and "to" beneath it, and label each control — the shared headings can't reach down
   here, and an unlabelled "Turn on" says nothing about what it turns on. */
@media (max-width: 640px) {
    .action-head {
        display: none;
    }

    .action-row {
        /* minmax(0, …), not 1fr: a bare 1fr means minmax(auto, 1fr), and that auto floors the
           column at its content's min-content width. A <select> is as wide as its longest
           option, so "Bed Wing Thermostat - Main" would refuse to shrink and shove the whole
           page sideways. */
        grid-template-columns: minmax(0, 1fr) minmax(0, 96px) auto;
        gap: 0.4rem 0.5rem;
        padding: 0.6rem;
        margin-bottom: 0.5rem;
        border: 1px solid var(--border);
        border-radius: var(--radius-sm);
        background: var(--surface-hover);
        align-items: end;
    }

    .action-row > .action-cell:first-child {
        grid-column: 1 / -1;
    }

    .action-cell-label {
        display: block;
    }
}

/* ---------------------------------------------------------------------------
   Thermostats
   --------------------------------------------------------------------------- */

/* Thermostat cards carry three rows of controls, so they need more room than a
   switch card's 250px track. */
.grid-wide {
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}

.thermostat-card {
    flex-direction: column;
    align-items: stretch;
    gap: 0.9rem;
}

.thermo-head {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.thermo-reading {
    font-size: 1.6rem;
    font-weight: 650;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.thermo-setpoints {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
}

/* Label above, stepper below. The stepper is a single bordered pill — a button inside a
   bordered box gave two parallel borders that read as an overlap. */
.setpoint {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.setpoint-label {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--muted);
}

.setpoint-heat .setpoint-label {
    color: var(--on);
}

.setpoint-cool .setpoint-label {
    color: var(--accent);
}

.setpoint-row {
    display: flex;
    align-items: stretch;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    /* Clip the borderless buttons to the rounded corners. */
    overflow: hidden;
}

.setpoint-value {
    flex: 1;
    align-self: center;
    text-align: center;
    font-size: 1.05rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* The − and + carry no border of their own; a single divider separates them from the value. */
.setpoint-btn {
    flex: none;
    min-width: 42px;
    min-height: 40px;
    padding: 0;
    border: none;
    border-radius: 0;
    background: transparent;
    color: var(--text);
    font-size: 1.2rem;
    line-height: 1;
}

.setpoint-btn:first-child {
    border-right: 1px solid var(--border);
}

.setpoint-btn:last-child {
    border-left: 1px solid var(--border);
}

.setpoint-btn:hover:not(:disabled) {
    background: var(--surface-hover);
    border-color: var(--border);
}

.thermo-modes {
    display: flex;
    gap: 0.35rem;
}

.btn-mode {
    flex: 1;
    min-width: 0;
}

.btn-mode.is-current {
    border-color: var(--accent);
    background: var(--accent);
    color: #fff;
}

.note {
    padding: 0.75rem 0;
    font-size: 0.82rem;
    /* A full-width line runs to ~150 characters on a wide screen, which nobody reads. */
    max-width: 72ch;
}

/* ---------------------------------------------------------------------------
   Sprinklers
   --------------------------------------------------------------------------- */

/* Water reads blue rather than the warm "on" used for lighting. */
.card.is-watering {
    border-color: var(--accent);
    background: color-mix(in srgb, var(--accent) 8%, var(--surface));
}

.card.is-watering .card-status {
    color: var(--accent);
    font-weight: 600;
}

.card.is-watering .lock-icon {
    background: var(--accent);
    color: #fff;
}

.zone-card {
    flex-direction: column;
    align-items: stretch;
    gap: 0.7rem;
}

.zone-head {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
}

/* The zone number as a badge rather than a "1." prefix on the name: it stops the number
   eating the width the name needs, and keeps names left-aligned with each other. */
.zone-number {
    flex: none;
    display: grid;
    place-items: center;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--surface-hover);
    color: var(--muted);
    font-size: 0.72rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.card.is-watering .zone-number {
    background: var(--accent);
    color: #fff;
}

.zone-controls {
    display: flex;
    gap: 0.5rem;
    /* Sit the pair at the right rather than letting the button stretch across the card. */
    justify-content: flex-end;
}

.zone-minutes {
    flex: none;
    /* Wide enough for "10 min" beside the dropdown chevron — 92px was clipping it to "10 …". */
    width: 116px;
    padding: 0.35rem 0.4rem;
    font-size: 0.85rem;
}

.zone-water {
    flex: none;
    min-width: 108px;
}

/* ---------------------------------------------------------------------------
   Pool
   --------------------------------------------------------------------------- */

/* The heater firing reads warm — the same amber lighting uses for "on" — rather than the
   blue that means water elsewhere. Mirrors the sprinklers' is-watering treatment. */
.card.is-heating {
    border-color: var(--on-border);
    background: var(--on-bg);
}

.card.is-heating .card-status {
    color: var(--on);
    font-weight: 600;
}

/* A small flame beside "Heating", shown only while the heater fires. */
.heat-icon {
    display: inline-flex;
    vertical-align: -0.15em;
    margin-right: 0.3rem;
    color: var(--on);
}

.heat-icon svg {
    width: 15px;
    height: 15px;
}

/* ---------------------------------------------------------------------------
   Locks (status only)
   --------------------------------------------------------------------------- */

/* No control on the card, so it shouldn't invite a tap. */
.card.is-readonly {
    cursor: default;
}

.lock-icon {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    border-radius: 50%;
    background: var(--surface-hover);
    color: var(--muted);
}

.lock-icon svg {
    width: 20px;
    height: 20px;
}

.card.is-unlocked {
    border-color: var(--danger-border);
    background: var(--danger-bg);
}

.card.is-unlocked .lock-icon {
    background: var(--danger);
    color: var(--danger-bg);
}

.card.is-unlocked .card-status {
    color: var(--danger);
    font-weight: 650;
}

.card.is-locked .lock-icon {
    background: var(--live-bg);
    color: var(--live);
}

.card.is-locked .card-status {
    color: var(--live);
    font-weight: 600;
}

/* A lock that hasn't reported is not "fine" — it just isn't known. Keep it visibly
   distinct from locked rather than letting it read as the quiet default. */
.card.is-unknown {
    border-style: dashed;
}

.card.is-unknown .card-status {
    font-style: italic;
}

/* ---------------------------------------------------------------------------
   Dimmers
   --------------------------------------------------------------------------- */

.device-card {
    flex-direction: column;
    align-items: stretch;
    gap: 0.6rem;
}

.device-head {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    min-width: 0;
}

.dim-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.dim-value {
    flex: none;
    width: 40px;
    text-align: right;
    font-size: 0.8rem;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}

.card.is-on .dim-value {
    color: var(--on);
    font-weight: 600;
}

.dim-slider {
    flex: 1;
    min-width: 0;
    height: 22px;
    margin: 0;
    background: none;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
}

.dim-slider:disabled {
    cursor: default;
    opacity: 0.5;
}

/* Track and thumb have to be styled per-engine; there is no shared shorthand. */
.dim-slider::-webkit-slider-runnable-track {
    height: 6px;
    border-radius: 999px;
    background: var(--border);
}

.dim-slider::-moz-range-track {
    height: 6px;
    border-radius: 999px;
    background: var(--border);
}

.dim-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    /* 20px: a finger's target on a wall tablet, not a mouse's. */
    width: 20px;
    height: 20px;
    margin-top: -7px;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 1px 2px rgb(0 0 0 / 25%);
}

.dim-slider::-moz-range-thumb {
    width: 20px;
    height: 20px;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 1px 2px rgb(0 0 0 / 25%);
}

.card.is-on .dim-slider::-webkit-slider-thumb {
    border-color: var(--on);
    background: var(--on);
}

.card.is-on .dim-slider::-moz-range-thumb {
    border-color: var(--on);
    background: var(--on);
}

/* The thumb is what's dragged, so that's what the focus ring belongs on — the
   default ring lands on the whole track and reads as the row being selected. */
.dim-slider:focus {
    outline: none;
}

.dim-slider:focus-visible::-webkit-slider-thumb {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.dim-slider:focus-visible::-moz-range-thumb {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   Switch (devices)
   --------------------------------------------------------------------------- */

.switch {
    flex: none;
    width: 52px;
    height: 32px;
    padding: 3px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface-hover);
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease;
}

.switch[aria-checked="true"] {
    background: var(--on);
    border-color: var(--on);
}

.switch:disabled {
    cursor: default;
    opacity: 0.5;
}

.switch-thumb {
    display: block;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgb(0 0 0 / 25%);
    transition: transform 120ms ease;
}

.switch[aria-checked="true"] .switch-thumb {
    transform: translateX(20px);
}

/* ---------------------------------------------------------------------------
   Buttons (scenes)
   --------------------------------------------------------------------------- */

.btn-group {
    display: flex;
    flex: none;
    gap: 0.35rem;
}

.btn {
    min-width: 52px;
    min-height: 38px;
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    font-weight: 600;
    font-size: 0.85rem;
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}

.btn:hover:not(:disabled) {
    background: var(--surface-hover);
    border-color: var(--muted);
}

.btn:disabled {
    cursor: default;
    opacity: 0.5;
}

.btn-on:hover:not(:disabled) {
    border-color: var(--on);
    color: var(--on);
}

.btn-primary {
    border-color: var(--accent);
    background: var(--accent);
    color: #fff;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
}

.btn-primary:hover:not(:disabled) {
    background: var(--accent);
    filter: brightness(1.08);
}

.btn-danger:hover:not(:disabled) {
    border-color: var(--danger);
    color: var(--danger);
}

/* A link styled as a button still needs the button's box. */
a.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    text-decoration: none;
    color: inherit;
}

/* ---------------------------------------------------------------------------
   Forms (admin)
   --------------------------------------------------------------------------- */

.form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    max-width: 460px;
}

.field {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.field-inline {
    flex-direction: row;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.field-label {
    font-size: 0.8rem;
    font-weight: 650;
    color: var(--muted);
}

.field-hint {
    font-size: 0.78rem;
    color: var(--muted);
}

.field-inline .field-hint {
    flex-basis: 100%;
}

.input {
    padding: 0.55rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
}

.input:disabled {
    opacity: 0.6;
}

/* Native selects, dressed to match the text inputs: the browser's default chevron removed and
   a consistent one drawn in its place. Applies to every dropdown in the app, not just
   Settings — thermostat mode, automation actions, sprinkler minutes. */
select.input {
    appearance: none;
    -webkit-appearance: none;
    padding-right: 2.1rem;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.6rem center;
    background-size: 1.1rem;
    cursor: pointer;
    /* A long zone name on a narrow screen ends in "…" rather than being hard-cut at the edge. */
    text-overflow: ellipsis;
}

select.input:hover:not(:disabled) {
    border-color: var(--muted);
}

/* The chevron is a background image, so its colour can't come from a CSS variable — the muted
   grey is too dark on a dark surface, so swap in a lighter one. */
@media (prefers-color-scheme: dark) {
    select.input {
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239099a6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    }
}

.form-actions {
    display: flex;
    gap: 0.5rem;
    margin-top: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
}

/* A checkbox and its label on one line — the notification toggles on Settings. */
.check-row {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    font-size: 0.85rem;
    cursor: pointer;
}

.check-row input {
    /* Nudge the box onto the cap height of the first line of label text. */
    margin-top: 0.15rem;
    flex: none;
}

/* A saved/failed note beside an action button (the test-email result). */
.field-hint.is-ok {
    color: var(--on, #2e7d32);
    font-weight: 600;
}

.field-hint.is-error {
    color: var(--danger, #c62828);
    font-weight: 600;
}

/* PIN gate over the Locks page. Themed with the app's own variables — unlike the shared modal,
   which is fixed white — so it reads correctly in dark mode too. */
.pin-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    z-index: 1000;
    /* No double-tap zoom anywhere in the gate, not just on the keys. */
    touch-action: manipulation;
}

.pin-dialog {
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg, 0 10px 40px rgba(0, 0, 0, 0.35));
    padding: 1.5rem;
    width: 100%;
    max-width: 340px;
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
}

.pin-title {
    margin: 0;
    font-size: 1.05rem;
    font-weight: 650;
}

.pin-hint {
    margin: 0;
    font-size: 0.85rem;
    color: var(--muted);
}

/* Masked progress: one filled dot per digit pressed, over a baseline of empty slots. */
.pin-dots {
    display: flex;
    justify-content: center;
    gap: 0.65rem;
    min-height: 0.9rem;
    margin: 0.2rem 0;
}

.pin-dot {
    width: 0.8rem;
    height: 0.8rem;
    box-sizing: border-box;
    border: 2px solid var(--muted);
    border-radius: 50%;
    transition: background-color 0.1s ease, border-color 0.1s ease;
}

.pin-dot.is-filled {
    background: var(--text);
    border-color: var(--text);
}

/* Reserve the error line's height even when empty, so the keypad doesn't jump when a wrong
   PIN message appears or clears. */
.pin-error {
    min-height: 1.1rem;
    text-align: center;
    font-size: 0.82rem;
    color: var(--danger, #c62828);
    font-weight: 600;
}

.pin-error.is-empty {
    visibility: hidden;
}

/* The keypad itself: three columns of round keys, 1–9 then ⌫ 0 ✓. */
.pin-pad {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.7rem;
    width: 100%;
    max-width: 300px;
    margin: 0 auto;
}

.pin-key {
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
    font-weight: 500;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 50%;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    /* Disables the double-tap-to-zoom gesture, so hammering the same digit twice doesn't zoom. */
    touch-action: manipulation;
    transition: transform 0.05s ease;
}

.pin-key svg {
    width: 1.6rem;
    height: 1.6rem;
}

/* Hover tint only where there's a real pointer: on touch, :hover sticks to the last-tapped key
   and reads as a persisted highlight, which is exactly what we don't want. */
@media (hover: hover) {
    .pin-key:hover:not(:disabled) {
        background: var(--surface-hover);
    }
}

/* Quick press feedback that releases with the finger — never left showing on the last key. */
.pin-key:active:not(:disabled) {
    transform: scale(0.92);
    background: var(--surface-hover);
}

/* A mouse/touch press must not leave a focus ring on the key; keyboard focus still shows one. */
.pin-key:focus {
    outline: none;
}

.pin-key:focus-visible {
    outline: 2px solid var(--on, #2e7d32);
    outline-offset: 2px;
}

.pin-key:disabled {
    opacity: 0.4;
    cursor: default;
}

.pin-key-util {
    color: var(--muted);
    font-size: 1.3rem;
}

.pin-key-enter {
    color: var(--on, #2e7d32);
    border-color: var(--on, #2e7d32);
    font-size: 1.6rem;
}

.pin-cancel {
    align-self: center;
    margin-top: 0.2rem;
}

/* On a phone the gate takes the whole screen: a thumb-sized target for the code and the
   actions, rather than a small card floating in a dimmed page. */
@media (max-width: 560px) {
    .pin-overlay {
        padding: 0;
    }

    .pin-dialog {
        max-width: none;
        height: 100%;
        border: none;
        border-radius: 0;
        box-shadow: none;
        /* Room above for the keyboard to push into, and clearance for the home-bar inset. */
        padding: 2rem 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
        gap: 1rem;
    }

    .pin-title {
        font-size: 1.4rem;
    }

    .pin-hint {
        font-size: 0.95rem;
    }

    /* Sit the keypad in the lower half of the screen, where a thumb reaches, and give the keys
       and dots more room on a big touch target. */
    .pin-pad {
        margin-top: auto;
        max-width: 340px;
        gap: 0.9rem;
    }

    .pin-key {
        font-size: 1.7rem;
    }

    .pin-dots {
        gap: 0.8rem;
    }

    .pin-dot {
        width: 0.95rem;
        height: 0.95rem;
    }

    .pin-cancel {
        margin-top: 1rem;
        padding: 0.8rem 1.5rem;
    }
}

/* The per-area permission matrix on the edit-user page. */
.perm-matrix {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.perm-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 160px);
    align-items: center;
    gap: 0.6rem;
}

.perm-area {
    font-weight: 550;
}

/* The location picker on the Settings page. */
.location-map {
    height: 320px;
    width: 100%;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    /* Tiles are square; clip them to the rounded corners. */
    overflow: hidden;
    /* A visible box before the tiles arrive, so a slow load isn't a blank gap. */
    background: var(--surface-hover);
    /* Leaflet gives its panes and controls high stacking values; a local context keeps them
       from sitting over the nav or a dropdown elsewhere on the page. */
    position: relative;
    z-index: 0;
}

/* Latitude and longitude side by side, stacking on a narrow screen. */
.coord-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.6rem;
    margin-top: 0.6rem;
}

.coord-field {
    margin: 0;
}

@media (max-width: 480px) {
    .coord-row {
        grid-template-columns: 1fr;
    }
}

/* ---------------------------------------------------------------------------
   Badges & messages
   --------------------------------------------------------------------------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.55rem;
    border-radius: 999px;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
}

.badge-live {
    background: var(--live-bg);
    color: var(--live);
}

.badge-offline {
    background: var(--danger-bg);
    color: var(--danger);
}

.badge .dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
}

.alert {
    padding: 0.6rem 0.8rem;
    margin-bottom: 1rem;
    background: var(--danger-bg);
    border: 1px solid var(--danger-border);
    border-radius: var(--radius-sm);
    color: var(--danger);
}

/* A confirmation, rather than an error. Reuses the "live" green already in the palette. */
.alert-ok {
    background: var(--live-bg);
    border-color: var(--live);
    color: var(--live);
}

.empty {
    color: var(--muted);
    padding: 2rem 0;
}

@media (prefers-reduced-motion: reduce) {
    * {
        transition: none !important;
    }
}

/* ---------------------------------------------------------------------------
   Blazor framework UI
   --------------------------------------------------------------------------- */

#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1000;
    padding: 0.7rem 1.25rem;
    background: #ffe7a8;
    color: #16191d;
    box-shadow: 0 -1px 4px rgb(0 0 0 / 20%);
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: 0.6rem;
    right: 0.9rem;
    cursor: pointer;
}

.blazor-error-boundary {
    padding: 1rem;
    background: #b32121;
    color: #fff;
}

.blazor-error-boundary::after {
    content: "An error has occurred.";
}
