/* Shared logo height — used by both .statusBarLogo (dashboard header) and
   .authLogo (sign-in card) so the two stay in sync and a future resize is
   a one-line change instead of hunting down every spot the logo appears. */
:root{
    --brand-logo-height: 84px;

    /* Focus ring, shared by every control (see ACCESSIBILITY BASELINE
       below). Two colours because the ring is two-tone by necessity. */
    --focus-ring: #58a6ff;
    --focus-ring-separator: #0d1117;

    /* Minimum comfortable hit area (WCAG 2.5.8 Target Size, AA). */
    --tap-min: 44px;

    /* ---- Type scale (2026-09-17) ----
       The stylesheet had grown 20 distinct font sizes across 237
       declarations — 8.5, 9, 9.5, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
       20, 22, 24, 26, 28, 32, 36 — which is sprawl, not hierarchy: 13px and
       14px were doing the same job in different components, and nothing
       told you which to reach for next.

       These eight steps replace all of them. Every font-size in the file
       now resolves to one of these, so a size change happens here once
       rather than in fifty places, and there's no longer a judgement call
       about what "slightly smaller" means.

       Sizes were mapped to the nearest step, ties rounding up. The floor is
       11px deliberately: everything below it was a label somebody had to
       squint at. */
    --fs-micro:    11px;   /* dense labels, rail captions, chip text      */
    --fs-caption:  12px;   /* secondary/meta text under a value           */
    --fs-body:     14px;   /* default reading size, most UI text          */
    --fs-emphasis: 16px;   /* a body line that needs to lead              */
    --fs-subhead:  18px;   /* sub-section titles                          */
    --fs-heading:  22px;   /* panel + modal titles                        */
    --fs-display:  28px;   /* stat tiles, cockpit counts (mobile)         */
    --fs-hero:     36px;   /* the single biggest number on a surface      */
}

*{
    margin:0;
    padding:0;
    box-sizing:border-box;
    font-family:Segoe UI, Arial, sans-serif;
}

body{
    background:#0d1117;
    color:white;
    min-height:100vh;
    display:flex;
    flex-direction:column;
}


/* ==========================
   ACCESSIBILITY BASELINE
   Added 2026-09-17 — see wxsentinel-ui-professionalism-accessibility-review
   -2026-09-17.md. Global rules only; anything component-specific stays with
   its component further down.
========================== */

/* One focus ring for the whole app, replacing three differently-coloured
   hand-rolled rings and five bare `outline:none` rules that left keyboard
   users with nothing to follow.

   It's two-tone on purpose. No single colour clears WCAG 1.4.11's 3:1
   against every surface here, because controls sit on everything from
   #0d1117 up to the green primary button and the yellow severe-warning
   fill. So the dark separator hugs the control and carries the contrast on
   bright fills (9.00:1 on #2ecc71, 11.39:1 on #f1c40f), and the blue sits
   just outside it and carries the contrast on dark ones (5.51:1 on
   #232d3a through 7.49:1 on the page background). Whatever the control is
   sitting on, one of the two rings is doing the work.

   :focus-visible, not :focus, so a mouse click doesn't leave a ring
   behind — the browser decides, and it gets that right. */
:focus-visible{
    outline:2px solid var(--focus-ring);
    outline-offset:2px;
    box-shadow:0 0 0 2px var(--focus-ring-separator);
}

/* Chevron on the two disclosure buttons (Menu, Filter). aria-expanded tells
   a screen reader the panel's state; this is the same fact drawn, for
   everyone who isn't using one. aria-hidden on the svg so it isn't read
   twice. Rotates to point up when the panel is open. */
.disclosureChevron{
    flex:0 0 auto;
    transition:transform .15s ease;
}

@media (prefers-reduced-motion:reduce){
    .disclosureChevron{
        transition:none;
    }
}

[aria-expanded="true"] > .disclosureChevron{
    transform:rotate(180deg);
}

/* 44x44 minimum hit areas for the small icon controls, via a centred
   overlay rather than by growing the buttons themselves — the target
   grows, the icon stays exactly where it was, and nothing reflows.
   Each of these needs a positioning context, set alongside its own rule. */
.locationEditBtn::after,
.addLocationForm .addLocationCloseBtn::after,
.locationTagChipRemove::after,
.chatImagePreviewRemove::after,
.reorderDragHandle::after,
.reorderLocationDeleteBtn::after{
    content:"";
    position:absolute;
    top:50%;
    left:50%;
    transform:translate(-50%,-50%);
    width:var(--tap-min);
    height:var(--tap-min);
    /* Purely a hit area — never paints, never affects layout. */
    background:transparent;
    pointer-events:auto;
}


/* ==========================
   STATUS BAR
========================== */

#statusBar{
    background:#1b2430;
    display:flex;
    justify-content:space-between;
    align-items:center;
    padding:16px 24px;
    border-bottom:3px solid #2e3c4f;
    flex-wrap:wrap;
    gap:12px;
}

.statusBarLeft{
    display:flex;
    align-items:center;
    gap:20px;
    flex-wrap:wrap;
}

/* Replaced the plain "WxSentinel" text h1 with the real logo (2026-08-26).
   height (not width) is set so the lockup — shield + wordmark + tagline —
   scales proportionally at whatever width its own aspect ratio needs;
   width:auto keeps it from stretching/squashing. */
.statusBarLogo{
    height:var(--brand-logo-height);
    width:auto;
    display:block;
}

.statusBarActions{
    display:flex;
    gap:8px;
    flex-wrap:wrap;
}

/* REMOVED 2026-10-08: ".statusBarActions button" and its :hover.
   This was a descendant selector written when the header action row held
   several loose buttons. It does not any more — the only element left in
   .statusBarActions is .menuBar, i.e. the ☰ toggle plus the dropdown
   panel hanging off it — so the rule had stopped styling what it was
   written for and started reaching INTO the dropdown.

   That made it a live bug. ".statusBarActions button" is specificity
   (0,1,1); ".menuPanelItem--danger" is (0,1,0). So the descendant rule's
   "color:white" beat the modifier's red, and Sign Out in the ☰ menu — the
   only sign-out below 900px, where the nav rail is hidden — rendered
   white while the rail's identical button rendered red. Same two classes
   on both elements. The !important flags on .menuPanelItem's background,
   border and padding are the fossil of someone fighting this same rule
   and patching only the parts they could see.

   Nothing is lost by deleting it: .menuToggleBtn below declares the same
   background, colour, border, radius, padding, font-size, font-weight and
   cursor, and the same :hover fill, and it is the only button this rule
   was still legitimately reaching. .chatToggleBtn's comment further down
   still says it inherits its base look from here — stale since the
   2026-09-15 rail restyle moved it into the rail, where .navRailItem
   dresses it. Verified by measuring both buttons, not by reading. */

/* Menu button + dropdown — houses Map/History/Settings/Sign Out so the
   header's action row stays to one line (Add Location + this button) on
   more screen widths instead of wrapping to a second row. */
.menuBar{
    position:relative;
}

.menuToggleBtn{
    /* flex + gap so the disclosure chevron sits on the text baseline
       alongside "☰ Menu" (2026-09-17) — matches .filterToggleBtn, which
       was already laid out this way. */
    display:flex;
    align-items:center;
    gap:6px;
    background:#0d1117;
    color:white;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 14px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
}

.menuToggleBtn:hover{
    background:#2e3c4f;
}

.menuPanel{
    position:absolute;
    top:calc(100% + 8px);
    left:0;
    display:flex;
    flex-direction:column;
    background:#1b2430;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:6px;
    box-shadow:0 8px 24px rgba(0,0,0,.4);
    z-index:20;
    min-width:180px;
}

.menuPanel--hidden{
    display:none;
}

/* The three !important flags that used to sit on background, border and
   padding here were removed 2026-10-08. They existed to beat
   ".statusBarActions button", which is now deleted (see the note above it),
   so they were load-bearing for nothing. Verified by computing every item
   in this panel with and without them at 420px and 1100px: identical. They
   are worth removing rather than leaving: an !important on a base class
   silently defeats every modifier that tries to change the same property,
   which is the exact failure mode that hid Sign Out's red here. */
.menuPanelItem{
    display:flex;
    align-items:center;
    gap:10px;
    background:transparent;
    border:none;
    border-radius:6px;
    padding:10px 12px;
    font-size:var(--fs-body);
    font-weight:bold;
    color:white;
    text-align:left;
    cursor:pointer;
    width:100%;
}

.menuPanelItem svg{
    flex:0 0 auto;
}

/* !important dropped 2026-10-08 with the rest — it was there to beat
   ".statusBarActions button:hover" (0,2,1), now deleted. */
.menuPanelItem:hover{
    background:#2e3c4f;
}

/* Sign Out reads as its own thing at rest, not only on hover.

   Red TEXT AND ICON, no red fill. The filled-red treatment stays reserved for
   the genuinely destructive and irreversible — delete location, remove member
   — because on a severe-weather dashboard the one colour that means "stop and
   think" should not also mean "leave the page". Signing out is reversible by
   signing back in; it just should not sit visually identical to Settings and
   Account, which is what it did before 2026-10-06 (the --danger class existed
   but had a :hover rule and nothing else).

   The shade is #f26558, not the #e74c3c used everywhere else, and that is a
   contrast fix rather than a preference. Both surfaces this sits on — the
   account menu panel and the nav rail — are #1b2430, where #e74c3c measures
   4.10:1, under the 4.5:1 floor for body text. #f26558 measures 5.05:1 there
   and 4.72:1 on the #3a2226 hover fill. The old hover rule was already using
   #e74c3c on #3a2226 at 3.83:1, so this fixes a failure that predates the
   change. #e74c3c keeps its job as the destructive FILL colour, where it sits
   on #0d1117 at 4.95:1 and passes. Measured, not eyeballed. */
.menuPanelItem--danger{
    color:#f26558;
}

.menuPanelItem--danger:hover{
    background:#3a2226;
    color:#f26558;
}

.menuPanelDivider{
    height:1px;
    background:#2e3c4f;
    margin:6px 4px;
}

/* "N of N locations used" sits above Add Location, which is a plain
   .menuPanelItem now — same look as Map/History/Settings/Reorder/Sign Out
   below it. Full-width like every other item, so the label (own line above)
   and the button naturally stack rather than sitting side by side. */
.menuPanelAddLocationRow{
    display:flex;
    flex-wrap:wrap;
    gap:2px;
    padding-bottom:6px;
}

/* Moved 2026-09-05 out of the hamburger menu dropdown into the
   always-visible dashboardControls toolbar (next to Search) — this is
   account-state info ("3 of 4 locations used") worth seeing without an
   extra click. Unscoped now (was .menuPanelAddLocationRow
   .locationUsageLabel) since it no longer lives in that row. See
   wxsentinel-dashboard-layout-review.md.
   Renamed to .dashboardUsageLabel 2026-09-06 — this used to share the
   .locationUsageLabel class name with the unrelated Account-modal usage
   label (#memberUsageLabel, styled further down in "PLAN / LOCATION
   USAGE"), so both elements were silently picking up a cascade-merged mix
   of each other's font-size/weight/display/white-space/padding instead of
   either one's actual intended look. Kept the .locationUsageLabel--atLimit
   modifier class shared between the two — that one was never duplicated,
   just the base rule. */
.dashboardUsageLabel{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    white-space:nowrap;
    padding:0 12px;
}

#muteToggleBtn{
    font-size:var(--fs-emphasis);
    padding:8px 12px;
}

#muteToggleBtn.muteToggleBtn--muted{
    border-color:#e74c3c !important;
    color:#e74c3c !important;
}

/* ---- "Still chime for…" per-user mute override (Settings → Notifications)

   Muting silences everything by default; this list is how a user says
   "silence, except these". It only has an effect while muted, and the UI
   says so rather than leaving the user to find that out the hard way. */
.muteRow{
    display:flex;
    align-items:center;
    gap:12px;
    flex-wrap:wrap;
}

.muteStateLabel{
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

/* `margin-left:auto` removed 2026-10-08. It pushed this button to the far
   right of a full-width row, which measured 580px of empty space between
   the mute toggle and the control that modifies it — so the two read as two
   unrelated settings that happened to share a line. It now sits directly
   after the toggle and its state label, which is the whole association. */
.muteOverrideToggle{
    display:inline-flex;
    align-items:center;
    gap:7px;
    background:transparent;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 12px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
    min-height:var(--tap-min);
}

.muteOverrideToggle:hover{
    background:#1b2430;
}

.muteOverrideChevron{
    transition:transform .15s ease;
    flex-shrink:0;
}

.muteOverrideToggle--open .muteOverrideChevron{
    transform:rotate(180deg);
}

@media (prefers-reduced-motion:reduce){
    .muteOverrideChevron{
        transition:none;
    }
}

.muteOverrideCount{
    font-size:var(--fs-micro);
    font-weight:bold;
    color:#0d1117;
    background:#2ecc71;
    border-radius:9px;
    padding:2px 7px;
    font-variant-numeric:tabular-nums;
}

.muteOverrideBody{
    display:block;
    margin-top:14px;
    padding-top:14px;
    border-top:1px solid #2e3c4f;
}

/* The author `display:block` above beats the UA stylesheet's [hidden] rule
   no matter the specificity, so without this the panel is permanently open
   and the toggle appears to do nothing. Exactly what showed all four Admin
   panels at once on 2026-10-01 — same shape, different file. */
.muteOverrideBody[hidden]{
    display:none;
}

.muteOverrideHint{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    line-height:1.6;
    margin:0 0 14px;
    max-width:70ch;
}

.muteOverrideHint strong{
    color:#e8ebee;
}

/* Shown only while NOT muted, where the list is inert. Saying so costs one
   line and heads off the likeliest support question this feature can
   generate — "I checked Tornado Warning and nothing happens". */
.muteOverrideHint--idle{
    margin:10px 0 0;
    color:#6b7f95;
}

.muteOverrideHint--idle[hidden]{
    display:none;
}

.muteOverrideActions{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
}

.muteOverrideClear{
    background:transparent;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 12px;
    font-size:var(--fs-caption);
    cursor:pointer;
    min-height:var(--tap-min);
}

.muteOverrideClear:hover{
    background:#1b2430;
    color:#c5d2e0;
}

/* At phone width the toggle's margin-left:auto has nothing left to push
   against once the row wraps, so it lands flush under the mute button
   looking like a second, unrelated control. Give it the whole row. */
@media (max-width:480px){
    .muteOverrideToggle{
        margin-left:0;
        width:100%;
        justify-content:flex-start;
    }
}

.statusBox{
    display:flex;
    flex-direction:column;
    align-items:flex-end;
}

/* cursor:help on both halves of the readout — the tooltip explaining what
   "NWS Unreachable" means for the alerts on screen is no use to anyone who
   never finds out it is there, and a status badge is not somewhere people
   think to hover. The cursor is the only affordance that costs no layout.
   
   title= and not a styled popover: every other explain-on-hover in this app
   is a title (the whole nav rail, the dashboard toggle, Filter), and a
   second mechanism for the same job would be the odd one out. */
.connectionStatus,
.connectionFreshness{
    cursor:help;
}

.connectionStatus{
    display:flex;
    align-items:center;
    gap:6px;
    font-size:var(--fs-caption);
    font-weight:bold;
    text-transform:uppercase;
    letter-spacing:.5px;
    margin-bottom:6px;
}

.connectionStatus::before{
    content:"";
    width:8px;
    height:8px;
    border-radius:50%;
}

.connectionStatus--online{
    color:#2ecc71;
}

.connectionStatus--online::before{
    background:#2ecc71;
}

/* Partial data — some of the user's tracked states fetched, some didn't.
   Amber and static: it's a real problem worth noticing, but unlike the
   offline state it doesn't pulse for attention, because the alerts that DID
   load are still live and correct and shouldn't be undermined. Hover the
   badge for which states are missing (title set in setConnectionStatus). */
.connectionStatus--degraded{
    color:#f39c12;
    cursor:help;
}

.connectionStatus--degraded::before{
    background:#f39c12;
}

.connectionStatus--offline{
    color:#e74c3c;
    animation:connectionStatusPulse 1.5s ease-in-out infinite;
}

.connectionStatus--offline::before{
    background:#e74c3c;
}

/* Visually hidden but still announced by screen readers. Used by the
   #alertAnnouncer live region — clip-path over display:none deliberately,
   since display:none and visibility:hidden remove an element from the
   accessibility tree entirely, which would make the live region silent. */
.srOnly{
    position:absolute;
    width:1px;
    height:1px;
    margin:-1px;
    padding:0;
    overflow:hidden;
    clip:rect(0 0 0 0);
    clip-path:inset(50%);
    white-space:nowrap;
    border:0;
}

@keyframes connectionStatusPulse{
    0%, 100%{ opacity:1; }
    50%{ opacity:.4; }
}

#date{
    font-size:var(--fs-subhead);
    font-weight:600;
    color:#9fb3c8;
    margin-bottom:2px;
}

/* The clock leads the block and the freshness line sits under it (2026-10-07,
   was: Live badge, date, clock). Stepped up from --fs-display to --fs-hero:
   this is read from across a room on a wall display, and it was previously
   the same size as a Cockpit stat tile.
   
   tabular-nums so the seconds digit does not shove the rest of the line
   left and right once a second — at this size the jitter is very visible. */
#clock{
    font-size:var(--fs-hero);
    font-weight:bold;
    line-height:1.1;
    font-variant-numeric:tabular-nums;
}

/* "Live · updated 12s ago" — one line, badge and age together.
   
   The badge alone answers "is the connection up", which is not the question
   an operator has. They want to know whether what is on screen is current,
   and those two answers come apart exactly when it matters: a socket that is
   still open while the poll has been failing for four minutes reads as Live.
   The age is the honest number; the badge is the colour next to it. */
.statusLine{
    display:flex;
    align-items:center;
    gap:6px;
    margin:2px 0 4px;
}

/* aria-hidden, so it carries no meaning and no contrast requirement, but at
   #3f506a it measured 1.91:1 and read as a rendering artifact rather than as
   a separator. Lifted enough to look deliberate. */
.statusLineSep{
    color:#6b7f95;
}

.connectionFreshness{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    white-space:nowrap;
    font-variant-numeric:tabular-nums;
}

/* Amber once the data is older than a couple of poll cycles, red past a
   minute. Deliberately NOT tied to the connection badge: this is about the
   data, and it has to be able to disagree with the socket.
   #f1c40f is 11.4:1 on #0d1117, #e74c3c is 4.95:1 — both measured. */
.connectionFreshness--stale{
    color:#f1c40f;
}

.connectionFreshness--dead{
    color:#e74c3c;
    font-weight:bold;
}

/* ==========================
   DESKTOP STATUS READOUT (toolbar row, >=900px)

   The same clock/Live/freshness/date as #statusBar's .statusBox, for the
   width where #statusBar is hidden. It lived in the 76px nav rail until
   2026-10-07; the freshness string added with the status block did not fit
   there and clipped mid-word.

   One of the two is on screen at a time, never both — this one only at
   >=900px, where #statusBar is display:none; below that #statusBar is back
   and this is display:none. Two clocks disagreeing by a frame on one screen
   is worse than either.
========================== */

.dashboardStatusBox{
    display:none;
}

@media (min-width:900px){

    /* Clock BESIDE the two-line stack, not above it. Stacked (clock, then
       the Live line, then the date) the block runs ~70px tall and this
       toolbar row — ~38px, set by the buttons in it — grows by most of its
       own height again. Side by side, the clock at 36px and the stack at
       two 12px lines are both about button height, and the row does not
       grow at all. That was the condition for moving it here. */
    .dashboardStatusBox{
        display:flex;
        align-items:center;
        gap:10px;
        /* Not margin-left:auto — .filterBar's margin-right:auto already
           absorbs the free space in this row, and a second auto margin
           would split it rather than stack with the first. */
        padding-left:4px;
    }

    .dashboardClock{
        font-size:var(--fs-hero);
        font-weight:bold;
        line-height:1;
        color:white;
        white-space:nowrap;
        /* The seconds digit changes once a second at 36px. Without this,
           every other glyph in the string shifts with it. */
        font-variant-numeric:tabular-nums;
    }

    .dashboardStatusStack{
        display:flex;
        flex-direction:column;
        align-items:flex-start;
        gap:1px;
    }

    /* The shared .statusLine carries vertical margins for the header's
       stacked block; inside this one the flex gap does that job. */
    .dashboardStatusStack .statusLine{
        margin:0;
    }

    .dashboardStatusStack .connectionStatus{
        margin-bottom:0;
    }

    .dashboardDate{
        font-size:var(--fs-caption);
        font-weight:600;
        color:#9fb3c8;
        white-space:nowrap;
        font-variant-numeric:tabular-nums;
    }

}

/* Compact tier. Between 900 and 1280 the full block (372px with "Live",
   435px with the longest badge label) does not leave the rest of the row
   enough to stay on one line even with the search box fully squeezed, and
   a toolbar that grows a second row is the thing this layout was chosen to
   avoid. So the clock steps down to 18px and the readout folds onto one
   line beside it; the date goes, being the one piece of this block nobody
   is watching a weather dashboard for.
   
   Measured, not guessed: full block 372px, compact 266px, and the space
   this row has spare at 1100px is 252px with the search box at its 140px
   floor. */
@media (min-width:900px) and (max-width:1279px){

    .dashboardStatusBox{
        gap:8px;
    }

    .dashboardClock{
        font-size:var(--fs-subhead);
    }

    .dashboardStatusStack{
        flex-direction:row;
        align-items:center;
        gap:8px;
    }

    .dashboardDate{
        display:none;
    }

}

/* Mobile header collapse — the logo + wrapped action row + stacked
   date/clock block used to run to ~510px tall on a 390px-wide phone,
   eating well over half the screen before the first location card.
   --brand-logo-height already exists as a single override point (see its
   definition at the top of the file); the rest just tightens spacing and
   drops the date line, which duplicates information the clock already
   implies and isn't worth a whole row at this width. */
@media (max-width:640px){
    :root{
        --brand-logo-height: 40px;
    }

    /* Reworked 2026-09-17 (Josh: "move the time/Live indicator to the
       middle and the menu button on the right"). Was a plain 2-child flex
       row (.statusBarLeft — logo + menu button together — on the left,
       .statusBox on the right, via justify-content:space-between).
       #statusBar becomes a 3-column grid instead of a 2-child flex row so
       the middle column is genuinely centered regardless of how wide the
       logo/menu happen to be (a 2-item flex space-between can't do that —
       the middle only looks centered when both neighbors are equal
       width, which logo vs. a small text button never are). minmax(0,1fr)
       on the side columns (not plain 1fr) so they can shrink below their
       content's natural width on a very narrow phone instead of
       overflowing — same fix already used on the footer's own 1fr auto
       1fr legal-links row for the identical reason.
       .statusBarLeft is unwrapped via display:contents so its two
       children (the logo and the menu button/dropdown, previously grouped
       together on the left) become direct grid items here, individually
       placed instead of moving/staying bundled as a pair. */
    #statusBar{
        display:grid;
        grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);
        align-items:center;
        padding:10px 14px;
        gap:8px;
    }

    .statusBarLeft{
        display:contents;
    }

    .statusBarLogo{
        grid-column:1;
        justify-self:start;
    }

    .statusBarActions{
        grid-column:3;
        justify-self:end;
    }

    /* Scoped to the ☰ toggle itself 2026-10-08 (was ".statusBarActions
       button"). The old descendant form also shrank every item inside the
       dropdown to caption size on exactly the screens where the dropdown
       IS the navigation, which is backwards; panel items keep --fs-body
       now. See the removal note on .statusBarActions button above. */
    .menuToggleBtn{
        padding:6px 10px;
        font-size:var(--fs-caption);
    }

    /* The dropdown itself (#menuPanel) still anchors off .menuBar's own
       box (position:relative, untouched) — just right:0 instead of the
       desktop-left-side left:0 now that the button it hangs off sits at
       the right edge of a narrow phone screen; left:0 there would open it
       running off the right side of the viewport. */
    .menuBar .menuPanel{
        left:auto;
        right:0;
    }

    .statusBox{
        grid-column:2;
        justify-self:center;
        align-items:center;
    }

    .connectionStatus{
        font-size:var(--fs-micro);
        margin-bottom:2px;
    }

    #date{
        display:none;
    }

    #clock{
        font-size:var(--fs-emphasis);
    }
}


/* ==========================
   LEFT NAV RAIL (Phase 4, 2026-09-15)
   ==========================
   Replaces the ☰ dropdown's destinations at ≥900px (see .appRoot's
   row-layout rule inside the media query below). Hidden by default here
   (mobile-first) so the original single-column header/main/footer stack
   is completely untouched below 900px — the ☰ Menu button + .menuPanel
   dropdown (styled above, unchanged by this section) is the only nav
   there, exactly as it always has been.

   .navRailItem buttons are also given the existing .menuPanelItem class
   right in the markup (see index.html) rather than duplicating that
   button's icon+label/hover styling here — same look as every dropdown
   item, one shared source of truth for it. .navRailDivider likewise
   reuses .menuPanelDivider. Only the RAIL's own container/grouping rules
   are new. */
.navRail{
    display:none;
}

@media (min-width:900px){

    /* .appRoot's own flex-direction:row override for this breakpoint lives
       down in the AUTH SCREEN section, right after .appRoot's base rule —
       NOT here. .appRoot is also styled unconditionally down there
       (pre-dates this nav rail section, appears later in the file), and a
       same-specificity `.appRoot{ flex-direction:column }` positioned
       after this block in source order would silently win the cascade at
       every viewport width, cancelling this override entirely. Keeping
       the override next to the rule it overrides is what actually fixes
       that, not just moving the symptom around. See the bugfix note next
       to the base rule for how this was found. */

    /* Wraps everything that used to be .appRoot's only children
       (header/warningBanner/planLimitBanner/main/footer) so the rail can
       sit beside that whole stack as a fixed-width column rather than
       above it. Below 900px this is just an extra flex-column wrapper
       around the exact same children in the exact same order — visually
       a no-op, which is what keeps the <900px layout byte-for-byte what
       it was before this phase. */
    .appRootContent{
        display:flex;
        flex-direction:column;
        flex:1;
        min-width:0;
    }

    /* Slimmed icon rail with small captions underneath (2026-09-15, per the
       UI review mockup + follow-up ask for legible labels that don't widen
       the rail) — was a 200px icon+label ROW; .navRailItem now stacks icon
       over label in a COLUMN instead, so each button stays narrow but the
       label is still visible (small caption, not just a hover tooltip).
       No id, handler, or markup structure touched — visual only. */
    .navRail{
        display:flex;
        flex-direction:column;
        justify-content:space-between;
        align-items:center;
        flex:0 0 76px;
        width:76px;
        background:#1b2430;
        border-right:3px solid #2e3c4f;
        padding:16px 6px;
        gap:14px;

        /* Sticky, added 2026-10-01. The rail was the primary navigation and it
           scrolled away: on an account with enough locations to fill more than
           a screen, every destination — and Sign Out — ended up somewhere up
           the document rather than on it. It also undercut the docked Cockpit,
           which is position:fixed at left:76px beside a rail that was not
           staying there.

           position:sticky rather than fixed: the rail is a flex child of
           .appRoot and fixed would take it out of flow, collapsing the 76px
           column the content is laid out beside. Sticky keeps the column and
           pins the contents.

           align-self:flex-start is what makes it work at all — a flex child
           stretches to the container's full height by default, and an element
           already as tall as its scroll container has nothing to stick within.

           height:100vh, NOT max-height — and this is the whole reason the rail
           needs an explicit height at all. align-self:flex-start is what lets
           it stick, but it also stops the flex item stretching to the
           container's height, so with only a max-height the rail collapses to
           SHRINK-TO-CONTENT. justify-content:space-between then has no spare
           room to distribute, every item bunches at the top, and Sign Out and
           the avatar sit in the middle of the column with dead space beneath.
           Reported 2026-10-01 straight after this shipped.

           It is invisible on a viewport SHORTER than the rail's own content
           (~835px), because there the content height and the viewport height
           are the same thing — which is exactly the 800px-tall viewport the
           first version of the test used.

           100dvh second so it wins where supported: on a tablet or touch
           laptop at >=900px, 100vh is the viewport with the browser chrome
           hidden, which leaves the bottom of the rail under it.

           overflow-y so a viewport shorter than the rail's content can still
           reach Sign Out instead of clipping it. */
        position:sticky;
        top:0;
        align-self:flex-start;
        height:100vh;
        height:100dvh;
        overflow-y:auto;
        overscroll-behavior:contain;
    }

    .navRailTop{
        display:flex;
        flex-direction:column;
        align-items:center;
        gap:4px;
        width:100%;
    }

    /* .menuPanelItem is normally sized for a dropdown's own min-width:180px
       panel, laid out icon-then-label in a row — stood up into a column
       here instead (icon above label) so width:100% still fills the slim
       rail edge-to-edge without needing the row's full label width. */
    .navRailItem{
        width:100%;
        flex-direction:column;
        justify-content:center;
        gap:4px;
        padding:8px 2px !important;
        position:relative;
    }

    /* Small caption under the icon — kept legible (not a decorative
       micro-label) at 9.5px/600 weight, allowed to wrap to a second line
       for the two longer names ("Reorder Locations", "Add Location")
       rather than truncating or forcing the rail wider than 76px to fit
       them on one line. */
    .navRailItemLabel{
        display:block;
        font-size:var(--fs-micro);
        font-weight:600;
        line-height:1.15;
        white-space:normal;
        text-align:center;
        color:inherit;
    }

    /* #chatBtn is the one rail item with extra content (the unread dot) —
       normally DOM-order first, before the icon, which reads fine in the
       ☰ dropdown's row layout (dot, then icon, then text) but would sit
       oddly above the icon in this column layout. Pulled out of flow and
       pinned to the icon's top-right corner instead, so the column still
       reads as icon-then-label with the dot as a badge on the icon, not a
       third stacked row. Scoped to the rail only — #menuChatBtn in the
       ☰ dropdown keeps the original inline dot, unaffected. */
    .navRailItem .chatUnreadDot{
        position:absolute;
        top:9px;
        left:50%;
        margin-left:2px;
    }

    .navRailDivider{
        margin:6px 2px;
        width:100%;
    }

    /* The rail now covers everything the ☰ dropdown did at this width —
       hide the hamburger button + dropdown itself so there isn't a
       redundant second way to reach the same destinations. Team Chat and
       Cockpit moved into the rail too as of the 2026-09-15 restyle (see
       #chatBtn/#cockpitBtn in index.html) — #menuChatBtn/#menuCockpitBtn,
       inside this same dropdown, are their <900px fallback instead. */
    .menuBar{
        display:none;
    }

    /* Brand mark at the top of the rail — the real logo (images/
       ShadowLogo.png), shrunk to fit the column, rather than the earlier
       simplified shield-and-bolt glyph. The wordmark+tagline read as
       texture more than text at this width, but it's the same lockup the
       header uses rather than a second, different mark. No badge
       background this time (the earlier glyph's blue rounded square) —
       ShadowLogo.png is a transparent PNG with its own shadow/weight, and
       a background behind it doubled up oddly. */
    .navRailBrand{
        display:flex;
        align-items:center;
        justify-content:center;
        width:100%;
        margin:0 auto 4px;
        padding:0 4px;
    }

    .navRailBrandLogo{
        display:block;
        width:100%;
        height:auto;
    }

    /* "Board" (the dashboard itself) reads as active whenever nothing is
       covering it — see updateBoardRailActiveState() in script.js, called
       from the shared openModal()/closeModal() helpers so this stays
       correct for every modal without per-modal wiring. */
    .navRailItem--active{
        background:#0f75bc !important;
        color:white;
    }

    .navRailItem--active:hover{
        background:#0f75bc !important;
    }

    /* Signed-in-user avatar, bottom of the rail (2026-09-15 restyle) —
       single uppercase initial, same "first letter of the email" rule
       chatAvatarInitial() already uses for chat message avatars
       (updateNavRailAvatar() in script.js). Empty/blank (no initial, no
       visible circle content) whenever signed out — #navRail itself is
       only ever visible behind the signed-in #appRoot anyway, this is
       just defensive. */
    /* A button since 2026-10-01 (was a bare <div>), so it needs the reset the
       other rail controls get — a <button> brings its own background, border
       and font that the generic styling here never had to fight before. */
    .navRailAvatar{
        appearance:none;
        padding:0;
        cursor:pointer;
        display:flex;
        align-items:center;
        justify-content:center;
        width:32px;
        height:32px;
        margin:8px auto 0;
        border-radius:50%;
        background:#2e3c4f;
        border:1px solid #4a5b6f;
        color:#c5d2e0;
        font-size:var(--fs-body);
        font-weight:bold;
        flex-shrink:0;
    }

    .navRailAvatar:hover{
        background:#3a4a5e;
        border-color:#2ecc71;
        color:white;
    }

    /* The rail held the Live status + date/time from 2026-09-15 until
       2026-10-07, when they moved to .dashboardStatusBox in the toolbar row
       above the location grid. The rules that sized them are gone with
       them, including the 9px .connectionStatus exception that was the one
       font-size in this file deliberately off the type scale — it existed
       only because "CONNECTED" measures 86.4px at the scale's 11px floor
       and the rail is 76px wide. Nothing in the rail needs to be off the
       scale any more.
       
       If a clock ever comes back to the rail, that exception comes back
       with it; the rail is still 76px. */

}


/* ==========================
   PLAN LIMIT BANNER (Round 2 fix 2B)
========================== */

.planLimitBanner{
    background:#4a2f0a;
    border-bottom:2px solid #c9852a;
    color:#ffd9a0;
    padding:10px 20px;
    font-size:var(--fs-body);
    text-align:center;
}

.planLimitBanner--hidden{
    display:none;
}

/* ==========================
   WARNING BANNER
========================== */

.warningBanner{
    height:0;
    overflow:hidden;
    background:#1b2430;
    border-bottom:0 solid #2e3c4f;
    transition:height .3s ease, border-bottom-width .3s ease;
}

.warningBanner--active{
    height:55px;
    border-bottom-width:3px;
}

.warningBannerTrack{
    height:100%;
    overflow:hidden;
    white-space:nowrap;
    position:relative;
    display:flex;
    align-items:center;
}

.warningBannerContent{
    display:inline-block;
    white-space:nowrap;
    font-size:var(--fs-heading);
    font-weight:bold;
    padding-left:100%;
    animation:scrollWarning 20s linear 5;
}

.warningItem{
    /* Color is set inline from ALERT_LEVEL_COLORS by tickerColorForLevel()
       in js/script.js. There were 15 .warningItem--* rules here: a
       hand-maintained copy of a 30-entry map that lives in the model. They
       had drifted on four levels (Flash Flood and Flood were green against
       the model's cyan, Tropical Storm wore severe's yellow, Hurricane
       Force Wind wore hurricane's pink) and six watch levels had no rule at
       all. Deleted 2026-10-06 — a color that lives in the model does not
       get a second home in the stylesheet.

       This base rule is the fallback for a level the map does not know. */
    color:#c5d2e0;
}

@keyframes scrollWarning{
    from{ transform:translateX(0); }
    to{ transform:translateX(-100%); }
}


/* ==========================
   MAIN CONTENT
========================== */

#mainContent{
    flex:1;
    padding:24px;
    display:flex;
    flex-direction:column;
}


/* ==========================
   ADD LOCATION FORM
========================== */

.addLocationForm{
    position:relative;
    display:flex;
    flex-wrap:wrap;
    gap:12px;
    align-items:center;
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    padding:16px 26px 16px 16px;
    margin-bottom:24px;
}

.addLocationForm--hidden{
    display:none;
}

.addLocationForm .addLocationCloseBtn{
    position:absolute;
    top:4px;
    right:6px;
    width:16px;
    height:16px;
    display:flex;
    align-items:center;
    justify-content:center;
    background:none;
    border:none;
    border-radius:0;
    color:#c5d2e0;
    font-size:var(--fs-body);
    font-weight:normal;
    cursor:pointer;
    line-height:1;
    padding:0;
}

.addLocationForm .addLocationCloseBtn:hover{
    background:none;
    color:#e74c3c;
}

.addLocationForm input{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    color:white;
    font-size:var(--fs-emphasis);
}

#newLocationName{
    flex:2;
    min-width:200px;
}

#newLocationLat, #newLocationLon{
    flex:1;
    min-width:120px;
}

.addLocationModeToggle{
    display:flex;
    gap:6px;
}

/* Scoped under .addLocationForm — NOT decoration, and not for namespacing.
   `.addLocationForm button` further down this file (the rule that makes the
   submit button green) is specificity (0,1,1); these were (0,1,0) and lost to
   it outright. The result was that all three mode buttons rendered green and
   bold, so the toggle could never show which mode you were in — the only
   signal was which input row happened to be visible.

   Found 2026-10-01 while switching the default mode to Address, by rendering
   the form and looking at it; nothing about the markup or the JS was wrong.
   Same cascade bug as .locationCard--pstier-* (§1.1) and .createAccountForm
   input, and the sibling .addLocationCloseBtn rules above already carry this
   exact defence — these were simply never given it. */
.addLocationForm .addLocationModeBtn{
    background:#0d1117;
    border:1px solid #2e3c4f;
    color:#9fb3c8;
    border-radius:6px;
    padding:10px 14px;
    font-size:var(--fs-body);
    font-weight:normal;
    cursor:pointer;
}

.addLocationForm .addLocationModeBtn:hover{
    border-color:#2ecc71;
    color:#c5d2e0;
}

.addLocationForm .addLocationModeBtn--active{
    background:#2ecc71;
    color:#0d1117;
    border-color:#2ecc71;
    font-weight:bold;
}

.addLocationForm .addLocationModeBtn--active:hover{
    color:#0d1117;
}

.addLocationInputRow{
    display:flex;
    gap:12px;
    flex:2 1 260px;
}

.addLocationInputRow--hidden{
    display:none;
}

#newLocationAddress{
    flex:1;
    min-width:220px;
}

/* Optional coverage radius — shown for all three Add Location modes
   (coords/address/map), since it's independent of how the point itself
   was chosen. */
.addLocationRadiusRow{
    flex-basis:100%;
    align-items:center;
    gap:8px;
}

.addLocationRadiusRow label{
    font-size:var(--fs-body);
    color:#9fb3c8;
    white-space:nowrap;
}

#newLocationRadius{
    flex:0 1 200px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 10px;
    color:white;
    font-size:var(--fs-body);
}

/* "Pick on Map" mode — flex-basis:100% forces this row onto its own full
   line within the form's flex-wrap layout (name/mode-toggle/submit/error
   stay on their own lines around it) since a map needs real width and
   height to be usable, unlike the single-line lat/lon or address inputs. */
.addLocationMapRow{
    flex-basis:100%;
    flex-direction:column;
    align-items:stretch;
    gap:6px;
}

.addLocationMap{
    width:100%;
    height:280px;
    border-radius:8px;
}

.addLocationMapSelectedLabel{
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.addLocationForm button{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:10px 20px;
    font-size:var(--fs-emphasis);
    font-weight:bold;
    cursor:pointer;
}

.addLocationForm button:disabled{
    opacity:.6;
    cursor:default;
}

.addLocationError{
    color:#e74c3c;
    font-size:var(--fs-body);
    width:100%;
}

.emptyState{
    text-align:center;
    color:#9fb3c8;
    font-size:var(--fs-emphasis);
    padding:40px 20px;
}

/* Was `style.display = "none"` set from script.js. Moved onto a class so the
   two states inside it (first-run panel vs "nothing matched your filters")
   can be switched independently without three different places fighting over
   one inline style. */
.emptyState--hidden{
    display:none;
}

.emptyStateFiltered{
    margin:0;
}

.emptyStateFiltered--hidden,
.firstRun--hidden{
    display:none;
}

/* ==========================
   FIRST RUN — the board before it has ever had a location
   ========================== */

.firstRun{
    max-width:560px;
    margin:0 auto;
}

.firstRunIcon{
    width:44px;
    height:44px;
    border-radius:50%;
    background:#18212c;
    border:1px solid #2e3c4f;
    display:flex;
    align-items:center;
    justify-content:center;
    margin:0 auto 14px;
    color:#2ecc71;
}

.firstRunTitle{
    font-size:var(--fs-heading);
    margin:0 0 8px;
    color:white;
}

.firstRunBody{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin:0 auto 20px;
    max-width:46ch;
    line-height:1.55;
}

.firstRunActions{
    display:flex;
    gap:10px;
    justify-content:center;
    flex-wrap:wrap;
}

/* min-height honours the --tap-min target-size baseline; these are the two
   controls a brand-new account is most likely to press first. */
.firstRunPrimary{
    display:inline-flex;
    align-items:center;
    gap:8px;
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:12px 20px;
    font-size:var(--fs-emphasis);
    font-weight:bold;
    cursor:pointer;
    min-height:var(--tap-min);
}

.firstRunSecondary{
    background:transparent;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:12px 18px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
    min-height:var(--tap-min);
}

.firstRunSecondary:hover{
    background:#18212c;
    color:white;
}

.firstRunFoot{
    font-size:var(--fs-caption);
    color:#7c8da3;
    margin:18px 0 0;
}

/* ==========================
   SETUP TOUR — spotlight + coachmark
   ========================== */

/* The layer itself does not take pointer events: the whole point is that the
   control being pointed at stays clickable. Only the card does. */
.tourLayer{
    position:fixed;
    inset:0;
    z-index:4000;
    pointer-events:none;
}

.tourLayer--hidden{
    display:none;
}

/* The dim and the ring are ONE element. A separate full-screen overlay plus a
   separate highlight would need their geometry kept in sync on every scroll
   and resize; a 9999px spread shadow cuts the hole for free, and the box
   itself stays transparent so the real control shows through unaltered. */
.tourSpotlight{
    position:fixed;
    border-radius:8px;
    box-shadow:0 0 0 2px #2ecc71, 0 0 0 9999px rgba(5,8,12,.68);
    transition:top .18s ease, left .18s ease, width .18s ease, height .18s ease;
    pointer-events:none;
}

/* No target to point at (the step's element is missing, or the tour is
   introducing itself rather than highlighting something) — dim everything and
   ring nothing, rather than drawing a 2px green box in the corner. */
.tourSpotlight--none{
    box-shadow:0 0 0 9999px rgba(5,8,12,.68);
}

@media (prefers-reduced-motion:reduce){
    .tourSpotlight{
        transition:none;
    }
}

.tourCard{
    position:fixed;
    width:316px;
    max-width:calc(100vw - 32px);
    background:#18212c;
    border:1px solid #2e3c4f;
    border-radius:10px;
    padding:16px 18px;
    box-shadow:0 14px 40px rgba(0,0,0,.6);
    pointer-events:auto;
}

.tourCardArrow{
    position:absolute;
    width:12px;
    height:12px;
    background:#18212c;
    border-left:1px solid #2e3c4f;
    border-top:1px solid #2e3c4f;
    transform:rotate(45deg);
}

.tourCardArrow--hidden{
    display:none;
}

.tourTopRow{
    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:8px;
    margin-bottom:8px;
}

.tourKicker{
    font-size:var(--fs-micro);
    color:#2ecc71;
    font-weight:bold;
    letter-spacing:.05em;
    text-transform:uppercase;
}

.tourKicker--hidden,
.tourStepCount--hidden,
.tourAside--hidden,
.tourSecondary--hidden{
    display:none;
}

.tourStepCount{
    font-size:var(--fs-micro);
    color:#7c8da3;
    font-weight:bold;
    letter-spacing:.06em;
    text-transform:uppercase;
}

/* margin-right:auto so "Skip tour" sits hard right whether or not a kicker or
   step count is present on this step. */
.tourSkipLink{
    background:none;
    border:none;
    color:#7c8da3;
    font-size:var(--fs-micro);
    cursor:pointer;
    text-decoration:underline;
    padding:0;
    margin-left:auto;
}

.tourSkipLink:hover{
    color:white;
}

.tourTitle{
    font-size:var(--fs-subhead);
    margin:0 0 6px;
    color:white;
}

.tourBody{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    line-height:1.55;
    margin:0 0 10px;
}

.tourAside{
    font-size:var(--fs-micro);
    color:#7c8da3;
    line-height:1.5;
    margin:0 0 12px;
    border-left:2px solid #2e3c4f;
    padding-left:9px;
}

/* Amber, not red: a plan boundary is a fact about the account, not a fault.
   #f1c40f on #1f1a10 measures 10.1:1; the body text #cbd7e4 measures 11.6:1. */
.tourLock{
    font-size:var(--fs-micro);
    color:#cbd7e4;
    line-height:1.55;
    background:#1f1a10;
    border:1px solid #6b5420;
    border-radius:6px;
    padding:10px 12px;
    margin:0 0 12px;
}

.tourLock--hidden{
    display:none;
}

.tourLock strong{
    display:block;
    color:#f1c40f;
    margin-bottom:3px;
}

.tourLockLink{
    background:none;
    border:none;
    color:#58a6ff;
    font-size:var(--fs-micro);
    cursor:pointer;
    text-decoration:underline;
    padding:0;
    margin-top:5px;
}

.tourFoot{
    display:flex;
    gap:8px;
    justify-content:flex-end;
    align-items:center;
}

.tourGhost{
    background:transparent;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:9px 13px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
}

.tourGhost:hover{
    background:#18212c;
    color:white;
}

.tourPrimary{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:9px 15px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
}

.tourDots{
    display:flex;
    gap:5px;
    justify-content:center;
    margin-top:12px;
}

.tourDots--hidden{
    display:none;
}

.tourDot{
    width:5px;
    height:5px;
    border-radius:50%;
    background:#2e3c4f;
}

.tourDot--on{
    background:#2ecc71;
}

/* Phone: the card stops trying to sit beside anything and docks to the bottom
   of the screen. At 390px there is no "beside" — a 316px card anchored to a
   control would cover it. The spotlight still rings the real target above. */
@media (max-width:640px){

    .tourCard{
        left:16px !important;
        right:16px;
        top:auto !important;
        bottom:calc(16px + env(safe-area-inset-bottom, 0px));
        width:auto;
    }

    .tourCardArrow{
        display:none;
    }

}


/* ==========================
   DASHBOARD FILTERS & PAGINATION
========================== */

.dashboardControls{
    display:flex;
    flex-wrap:wrap;
    justify-content:flex-end;
    align-items:center;
    gap:8px;
    margin-bottom:16px;
}

/* Sticky from 2026-10-07, and only because the clock moved in here.
   
   The clock used to live in .navRail, which is position:sticky — it stayed
   on screen however far down the board you scrolled. Putting it in a row
   that scrolls away would have quietly taken that back, and "updated 12s
   ago" is worth exactly nothing if you cannot see it while you are reading
   the cards it describes. Search, Filter and the view toggle staying
   reachable is a bonus, not the reason.
   
   Needs the opaque background: without it the location cards scroll
   visibly underneath. z-index 100 clears the cards and sits well under the
   modals (1000) and the first-run tour layer (4000).
   
   >=900px only. Below that #statusBar holds the clock, this row holds no
   status readout, and a sticky toolbar would just eat phone screen. */
@media (min-width:900px){
    .dashboardControls{
        position:sticky;
        top:0;
        z-index:100;
        background:#0d1117;
        padding-block:8px;
    }
}

.dashboardViewToggle{
    /* No margin-right:auto of its own — .filterBar, the last item in this
       row's left group, carries the one auto margin that pins everything
       before it to the left edge. A second auto margin here would split
       the free space between the two rather than stack with it. */
    display:flex;
    border:1px solid #2e3c4f;
    border-radius:8px;
    overflow:hidden;
}

.dashboardViewToggleBtn{
    background:#0d1117;
    border:none;
    color:#9fb3c8;
    padding:8px 14px;
    font-size:var(--fs-body);
    cursor:pointer;
}

.dashboardViewToggleBtn + .dashboardViewToggleBtn{
    border-left:1px solid #2e3c4f;
}

.dashboardViewToggleBtn:hover{
    color:#c5d2e0;
}

.dashboardViewToggleBtn--active{
    background:#0f75bc;
    color:#ffffff;
    font-weight:bold;
}

.dashboardViewToggleBtn--active:hover{
    color:#ffffff;
}

/* Free-text location search. It carried this row's margin-right:auto until
   2026-10-07, when Filter moved to sit directly beside it — an auto margin
   on the search box pushes everything AFTER it right, which put Filter back
   at the far end the moment it was moved. The auto margin now lives on
   .filterBar, the last item in the left group, where it does the job the
   name suggests. */
/* Icon is a CSS background-image (inline SVG data URI) rather than an
   emoji character in the placeholder text — added 2026-09-05 alongside the
   rest of the icon-consistency pass, so it survives placeholder
   translation/clearing and renders identically across every OS instead of
   depending on the browser's native emoji font. */
.locationSearchInput{
    background-color:#161b22;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239fb3c8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M21 21l-4.3-4.3'/%3E%3C/svg%3E");
    background-repeat:no-repeat;
    background-position:12px center;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 12px 8px 34px;
    color:white;
    font-size:var(--fs-body);
    /* THE flexible item in this row, and the reason the row still holds one
       line after the status readout moved into it. It gives width back
       before anything else does and takes it back on a wide screen.

       The basis is the FLOOR, not the preferred width, and that is the
       whole trick: flex-wrap decides whether an item fits using its
       flex-basis, BEFORE any shrinking happens. A 220px basis — what this
       box used to be — wrapped the row at widths where a shrunk 220px box
       would have fitted fine. flex-grow is what makes it wide again when
       there is room (928px at 1920). Measured, after getting it wrong the
       first time.

       Together with the compact status tier below, this holds the row to
       one line from 900px up — the same width it wrapped at before any of
       this landed. */
    flex:1 1 130px;
    min-width:130px;
}

.locationSearchInput::placeholder{
    color:#9fb3c8;
}

.locationSearchInput:focus{
    /* Logo shield blue (#0f75bc), swapped in 2026-09-05 from a generic
       UI blue — see wxsentinel-color-scheme-proposal.md. */
    border-color:#0f75bc;
}

/* The border tint alone is the mouse-focus treatment. Keyboard focus keeps
   it AND gets the shared ring, because a 3.53:1 border change on its own
   isn't enough to track a caret by. */
.locationSearchInput:focus:not(:focus-visible){
    outline:none;
}

@media (max-width:640px){
    .locationSearchInput{
        min-width:0;
        flex:1 1 100%;
    }
}

.dashboardFilters{
    display:flex;
    align-items:center;
    gap:8px;
    flex-wrap:wrap;
}

.dashboardFilters label{
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.dashboardFilters select{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 10px;
    color:white;
    font-size:var(--fs-body);
}

.tagFilterGroup{
    display:flex;
    align-items:center;
    gap:8px;
}

.tagFilterGroup--hidden{
    display:none;
}

/* Filter button + popover — wraps the State/County/Tag selects above so
   the dashboard's top row stays uncluttered. The selects themselves keep
   their original ids/behavior; only how they're revealed changed. */
.filterBar{
    position:relative;
    /* The one auto margin in this row. Everything up to and including
       Filter sits left; the usage label and the status readout after it
       sit right. Moved here from .locationSearchInput on 2026-10-07 when
       Filter was placed beside Search — see the comment there. */
    margin-right:auto;
}

.filterToggleBtn{
    display:flex;
    align-items:center;
    gap:6px;
    background:#161b22;
    color:white;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 14px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
}

.filterToggleBtn:hover{
    background:#2e3c4f;
}

/* Hides the tag chips on every location card while the setting is off — a
   pure CSS switch driven by the body class, so it applies regardless of
   which cards are currently rendered (page nav, edits, etc.). The per-card
   "+ Tag" add control is intentionally unaffected.
   
   Driven from Settings -> Display since 2026-10-07; this was a "Hide Tags"
   button in the toolbar row until then, and .tagsVisibilityToggleBtn's
   rules went with it. */
body.hideLocationTags .locationTags{
    display:none;
}

/* Hidden unless the account has tags at all — see
   updateTagFeatureVisibility(). Same gate the toolbar button carried. */
.tagsVisibilitySettingsBlock--hidden{
    display:none;
}

.filterActiveBadge{
    background:#2ecc71;
    color:#0d1117;
    border-radius:999px;
    min-width:18px;
    height:18px;
    padding:0 5px;
    font-size:var(--fs-micro);
    font-weight:bold;
    display:flex;
    align-items:center;
    justify-content:center;
}

.filterActiveBadge--hidden{
    display:none;
}

.filterPanel{
    position:absolute;
    top:calc(100% + 8px);
    right:0;
    background:#1b2430;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:16px;
    box-shadow:0 8px 24px rgba(0,0,0,.4);
    z-index:20;
    min-width:280px;
}

.filterPanel--hidden{
    display:none;
}

/* Scoped to the panel so the History modal's own filter bar (which reuses
   .dashboardFilters/.tagFilterGroup for its wider row of filters) keeps
   its original horizontal layout untouched. */
.filterPanel .dashboardFilters{
    flex-direction:column;
    align-items:stretch;
}

.filterPanel .dashboardFilters label{
    margin-top:8px;
}

.filterPanel .dashboardFilters label:first-child{
    margin-top:0;
}

.filterPanel .dashboardFilters select{
    width:100%;
}

.filterPanel .tagFilterGroup{
    flex-direction:column;
    align-items:stretch;
    gap:0;
}

.filterClearBtn{
    margin-top:14px;
    width:100%;
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 10px;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    cursor:pointer;
}

.filterClearBtn:hover{
    background:#0d1117;
    color:#c5d2e0;
}

.paginationControls{
    display:flex;
    justify-content:center;
    align-items:center;
    gap:16px;
    margin-top:24px;
}

.paginationControls--hidden{
    display:none;
}

.paginationControls button{
    background:#0d1117;
    color:white;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 16px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
}

.paginationControls button:hover:not(:disabled){
    background:#2e3c4f;
}

.paginationControls button:disabled{
    opacity:.4;
    cursor:default;
}

#pageIndicator{
    font-size:var(--fs-body);
    color:#9fb3c8;
    font-weight:bold;
}


/* ==========================
   LOCATION GRID
========================== */

/* --card-scale drives how big the cards render, set by buildLocationCards()
   in script.js via the locationGrid--roomy/--spacious modifier classes
   based on how many cards are actually on the current page — fewer cards
   means fewer, bigger tracks (wider minmax floor) AND bigger text/padding
   (calc()'d off this variable throughout the card, see .locationName etc.
   below), so a lightly-populated page reads as "enlarged to fill the
   screen" rather than just left with empty space. auto-fit (not auto-fill)
   is what lets an incomplete last row's cards stretch to fill the leftover
   width instead of leaving a gap where more columns could've gone.
   var(--card-scale, 1) always falls back to 1 for anything outside
   #locationGrid that happens to reuse one of these classes (e.g. the
   Alert History table reuses .locationStatus for its type labels). */
#locationGrid{
    --card-scale: 1;
    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(calc(300px * var(--card-scale, 1)), 1fr));
    grid-auto-rows:minmax(calc(140px * var(--card-scale, 1)), 1fr);
    gap:calc(20px * var(--card-scale, 1));
    flex:1;
}

#locationGrid.locationGrid--roomy{
    --card-scale: 1.15;
}

#locationGrid.locationGrid--spacious{
    --card-scale: 1.35;
}

/* Card Density (Settings -> Display, added 2026-10-06).
   
   A multiplier on the SAME --card-scale the auto-sizing already uses, rather
   than a second set of card dimensions. The auto-sizing classes above widen
   cards when a board is sparse; density is the user saying how roomy their
   baseline is. Multiplying composes the two — a compact board with few
   locations still gets some of the roomy treatment, just less of it — where
   a competing rule would have one silently cancel the other depending on
   source order.
   
   Set on <body> by applyCardDensity() in script.js, so it survives every
   rebuild of the grid's own modifier classes. */
body.density--compact #locationGrid{
    --card-density: .85;
}

body.density--compact #locationGrid{
    grid-template-columns:repeat(auto-fit, minmax(calc(300px * var(--card-scale, 1) * var(--card-density, 1)), 1fr));
    grid-auto-rows:minmax(calc(140px * var(--card-scale, 1) * var(--card-density, 1)), 1fr);
    gap:calc(20px * var(--card-scale, 1) * var(--card-density, 1));
}

/* min-height is a flat px value on .locationCard, so it does not follow
   --card-scale and has to be brought down separately — otherwise compact
   cards keep a comfortable card's floor and nothing visibly tightens. */
body.density--compact .locationCard{
    min-height:148px;
}

/* A previously-undiscovered overflow source, caught by a regression test
   using a lightly-populated board (few locations): --card-scale enlarges
   cards up to 1.35x ("spacious") when there's room to spare on desktop,
   and #locationGrid's minmax() floor scales with it (300px * 1.35 =
   405px). A minmax() px minimum doesn't shrink for anything, unlike an
   auto/fr track, so on a 390px phone a "spacious" board overflowed
   regardless of the footer/header fixes above it in this file. Capped
   back to the base scale under this breakpoint — mobile always gets the
   compact card size no matter the location count, matching what every
   card already assumes at the base 300px width. Placed here (after the
   rules it overrides, same specificity) rather than in the header's
   @media block, so cascade order — not just specificity — puts it on
   top. */
@media (max-width:640px){
    #locationGrid.locationGrid--roomy,
    #locationGrid.locationGrid--spacious{
        --card-scale: 1;
    }
}

/* ==========================
   PRIORITY STACK (2026-09-14) — the alternate dashboard view. Locations
   are grouped into up to four severity tiers instead of one uniform grid;
   see buildLocationCardsPriorityStack() in script.js. #locationGrid stays
   display:grid with its existing repeat(auto-fit, minmax(...)) column
   rule completely untouched here on purpose — calculateLocationsPerPage()
   (script.js) reads that resolved column count to size pagination, and it
   doesn't care what the grid's actual children are. Each tier section
   just spans every resolved column via grid-column, so the auto-fit
   machinery keeps working identically for both view modes without a
   parallel measurement path.

   The ROW rule is a different story (mobile bug fix, 2026-09-15): base
   #locationGrid's grid-auto-rows is minmax(140px, 1fr), deliberately —
   Card Wall wants a sparse page of individual cards to stretch and fill
   the screen (--card-scale's "roomy"/"spacious" look). But every tier
   section here (grid-column: 1/-1 above) is itself ONE full-width row in
   that same implicit grid, so the exact same 1fr was forcing each tier —
   Life-Threatening, Warnings, Watches, Clear — to stretch to an equal
   share of the page's full height, however little content it actually
   had. On a phone that meant 200-400px of dead space between tiers
   (reported as the page looking broken/empty while scrolling). Overriding
   back to auto here — scoped to buildLocationCardsPriorityStack()'s own
   .locationGrid--priorityStack class, so Card Wall's stretch is
   untouched — sizes each tier's row to its own content instead.
   #locationGrid.locationGrid--priorityStack, not just
   .locationGrid--priorityStack — the base rule this overrides is an ID
   selector (#locationGrid), which beats a plain class regardless of
   source order; matches the #locationGrid.locationGrid--roomy/--spacious
   pattern already used above for the same reason. */
#locationGrid.locationGrid--priorityStack{
    grid-auto-rows:auto;
}

.priorityStackTier{
    grid-column: 1 / -1;
}

.priorityStackTier + .priorityStackTier{
    margin-top:8px;
}

.priorityStackTierHeading{
    font-size:var(--fs-body);
    font-weight:bold;
    letter-spacing:.04em;
    text-transform:uppercase;
    color:#9fb3c8;
    margin-bottom:10px;
    padding-bottom:6px;
    border-bottom:1px solid #2e3c4f;
}

.priorityStackTier--critical .priorityStackTierHeading{
    color:#ff8fa3;
    border-bottom-color:#5c2233;
}

.priorityStackTierGrid{
    display:grid;
    gap:16px;
}

/* Flat minmax minimums (no --card-scale multiplier) — deliberately, so
   these stay safe at any viewport width the same way the base
   #locationGrid rule already is: auto-fit collapses to one column on its
   own once two tracks plus gap would exceed the available width, with no
   separate mobile override needed (see the --card-scale bug fixed just
   above, which only happened because THAT minimum was being multiplied
   past what auto-fit could still protect against). */
.priorityStackTierGrid--critical{
    grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));
    gap:20px;
}

.priorityStackTierGrid--warning{
    grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));
}

.priorityStackTierGrid--watch{
    grid-template-columns:1fr;
    gap:8px;
}

/* Tiled, not stacked — buildClearLocationRow() rows are small (name +
   county/state, nothing else), so a single wide column would waste most of
   the row's width on every screen wider than a phone. auto-fill packs as
   many as fit per line instead, which matters more now that this holds
   EVERY clear location at once with no pagination cap (see computePages()'s
   priority-stack branch in script.js) — could be a couple, could be a few
   hundred. max-height is a generous fixed cap rather than a measured value
   purely so the collapse/expand transition below has a height to animate
   between; real content only ever fills part of it. */
.priorityStackTierGrid--clear{
    grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));
    gap:6px;
    overflow:hidden;
    max-height:20000px;
    transition:max-height .25s ease, opacity .2s ease, margin-top .2s ease;
    margin-top:10px;
}

.priorityStackTierGrid--clear.priorityStackTierGrid--collapsed{
    max-height:0;
    opacity:0;
    margin-top:0;
}

.priorityStackClearSummary{
    display:block;
    width:100%;
    text-align:left;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:12px 16px;
    color:#9fb3c8;
    font-size:var(--fs-body);
    cursor:pointer;
}

.priorityStackClearSummary:hover{
    border-color:#4a5b6f;
    color:#c5d2e0;
}

.priorityStackClearSummary:focus-visible{
    outline:2px solid var(--focus-ring);
    outline-offset:2px;
}

/* Life-Threatening tier — bigger, calmer, deliberately rare. Bumps the
   name/status text up rather than just the card's own padding, since
   those two lines are the entire point of giving this tier more room.

   Compound selector (.locationCard.locationCard--pstier-critical) rather
   than the single class, for the same reason the mobile override further
   down this file already does it: the base .locationCard{ min-height:176px;
   padding:calc(16px * ...) } rule lives at the TOP of the LOCATION CARDS
   section below, which is later in source order and equal specificity, so
   it won.

   Fixed 2026-09-28. Until then these two declarations had never applied at
   desktop width — measured at 1400px, a Life-Threatening card rendered at
   the base 176px/16px, identical to every other card, which defeated the
   entire premise of the tier. The mobile block below had already hit this
   exact trap and documented it; the desktop rules were never given the same
   treatment. If you add a third box-model rule for a tier, double the class
   there too. */
.locationCard.locationCard--pstier-critical{
    min-height:220px;
    padding:24px;
}

.locationCard--pstier-critical .locationName{
    font-size:var(--fs-heading);
}

.locationCard--pstier-critical .locationStatus{
    font-size:var(--fs-subhead);
}

/* Watches — compact horizontal rows instead of the full vertical card
   layout. Every element from buildOneLocationCard() is still present
   (renderLocationStatus() depends on that), just rearranged/hidden via
   CSS; nothing here is ever empty for this tier anyway (damage
   threat/detection/hazard/TOR-possible only ever populate for
   Tornado/Severe Thunderstorm Warnings), so hiding them is purely about
   not reserving layout space for content that was never coming. The
   Clear tier below uses its own much lighter buildClearLocationRow()
   markup instead (see .locationClearRow) rather than a cut-down
   buildOneLocationCard(), so it has no rules here.

   Compound selector here too, and for the same cascade reason as the
   critical tier above — flex-direction, min-height, text-align and padding
   were all losing to the base .locationCard rule further down the file, so
   a Watch rendered as a full-height vertical card instead of a compact
   row. The descendant rules below (.locationCounty, and the display:none
   block) were unaffected, since two classes already beat one. Fixed
   2026-09-28. */
.locationCard.locationCard--pstier-watch{
    flex-direction:row;
    align-items:center;
    justify-content:flex-start;
    flex-wrap:wrap;
    min-height:0;
    text-align:left;
    padding:10px 14px;
    gap:10px;
}

.locationCard--pstier-watch .locationCounty{
    color:#8a9bb3;
    font-size:var(--fs-caption);
}

/* COLUMNS, not an inline run (2026-10-13).
   =========================================
   These rows laid out as plain flex children, so every column started
   wherever the previous one happened to end. With four watches on screen the
   hazard label began at four different x positions — measured from Josh's
   screenshot: 196, 196, 216 and 230px — because the location names were "11",
   "15", "Test5" and "Tor Test". The eye reads that as four unrelated rows
   rather than one table, and the hazard is the thing you are scanning for.

   Fixed flex-basis per column rather than CSS grid: the row carries nine more
   children that are display:none for this tier, plus a hidden name-edit form,
   plus optional tags — all of which would need explicit grid placement or
   would land in the wrong track the moment one of them appears. Flex bases
   leave every one of them exactly as it was.

   clamp() rather than a fixed rem: these need to hold their column on a
   1920px board and still be readable at 900px, and a hard width does one or
   the other. The upper bounds are sized to the longest real content —
   "SEVERE T-STORM WATCH" and "TROPICAL STORM WATCH" are both 20 characters,
   which is what the status column is cut to fit. */

.locationCard--pstier-watch .locationName{
    flex:0 0 clamp(4.5rem, 12%, 10rem);
    min-width:0;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
}

.locationCard--pstier-watch .locationCounty{
    flex:0 0 clamp(6.5rem, 15%, 11rem);
    min-width:0;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
}

.locationCard--pstier-watch .locationStatus{
    /* Wide enough for the longest watch label at this tier's real font, which
       is --fs-heading (the base .locationStatus rule), not the caption size
       the phone breakpoint drops it to. Measured from the live board:
       "SEVERE T-STORM WATCH" renders 266px there, and "TROPICAL STORM WATCH"
       is the same 20 characters. The first attempt used 14rem (224px) and the
       label wrapped to two lines, which made every row taller than it had
       been — worse than the misalignment it was fixing.

       The cap multiplies by --card-scale because that is what scales the font
       in the first place: a denser board shrinks the text and the column has
       to shrink with it, or the columns stop being columns.

       nowrap is the guarantee. If content ever outgrows the basis it spills a
       little rather than reflowing the row to two lines. */
    flex:0 0 clamp(8.5rem, 22%, calc(18rem * var(--card-scale, 1)));
    min-width:0;
    white-space:nowrap;
}

.locationCard--pstier-watch .locationExpires{
    flex:0 1 auto;
    min-width:0;
}

/* Below this the columns stop helping: there is not enough width for four of
   them, and squeezing produces ellipses on names rather than alignment. Back
   to the inline run, which is what a phone wants anyway. */
@media (max-width:760px){
    .locationCard--pstier-watch .locationName,
    .locationCard--pstier-watch .locationCounty,
    .locationCard--pstier-watch .locationStatus{
        flex:0 1 auto;
        overflow:visible;
        text-overflow:clip;
    }
}

.locationCard--pstier-watch .locationDamageThreat,
.locationCard--pstier-watch .locationDetection,
.locationCard--pstier-watch .locationHazardInfo,
.locationCard--pstier-watch .locationTorPossible{
    display:none;
}

/* buildClearLocationRow()'s row — name + county/state, nothing else, tiled
   by .priorityStackTierGrid--clear above. Deliberately not a cut-down
   .locationCard: no edit button, no tags, no click target, since there's
   nothing behind any of that for a location the user already knows is
   clear. */
.locationClearRow{
    display:flex;
    flex-direction:column;
    gap:1px;
    padding:6px 10px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    overflow:hidden;
}

.locationClearRowName{
    font-size:var(--fs-caption);
    font-weight:600;
    color:#c5d2e0;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
}

.locationClearRowCounty{
    font-size:var(--fs-micro);
    color:#8a9bb3;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
}

/* Mobile density pass (2026-09-17): Josh asked to fit the active
   warnings/watches on screen with less scrolling, while keeping the
   Life-Threatening/Warnings/Watches tier structure intact — this trims
   card size/spacing per tier rather than collapsing or hiding anything,
   so every field that mattered on desktop is still visible, just smaller.
   Life-Threatening keeps noticeably more presence than Warnings (it's
   still the "bigger, calmer, deliberately rare" tier — see the desktop
   rule above), just not the full 220px/24px built for a wide 2-column
   desktop grid, which is a lot of height per card stacked single-column
   on a phone. */
@media (max-width:640px){
    .priorityStackTier + .priorityStackTier{
        margin-top:4px;
    }

    .priorityStackTierHeading{
        margin-bottom:6px;
        padding-bottom:4px;
    }

    .priorityStackTierGrid--critical,
    .priorityStackTierGrid--warning{
        gap:10px;
    }

    /* Compound selector (.locationCard.locationCard--pstier-critical), not
       just .locationCard--pstier-critical: the base .locationCard{
       min-height:176px } rule further down the file is equal specificity
       (one class) and would otherwise win on source order alone, which is
       exactly what silently no-op'd this min-height:0 the first time —
       confirmed via direct measurement (card height stayed 176px on a
       phone viewport) before this got shipped. Doubling the class in the
       selector bumps specificity so this wins regardless of where
       .locationCard is declared. Same reasoning applied to the warning and
       watch tier overrides just below. */
    .locationCard.locationCard--pstier-critical{
        min-height:0;
        padding:14px 16px;
    }

    .locationCard--pstier-critical .locationName{
        font-size:var(--fs-subhead);
        margin-bottom:3px;
    }

    .locationCard--pstier-critical .locationStatus{
        font-size:var(--fs-emphasis);
    }

    .locationCard.locationCard--pstier-warning{
        min-height:0;
        padding:12px 14px;
    }

    .locationCard--pstier-warning .locationName{
        font-size:var(--fs-emphasis);
        margin-bottom:2px;
    }

    .locationCard--pstier-warning .locationCounty{
        font-size:var(--fs-caption);
    }

    .locationCard--pstier-warning .locationStatus{
        font-size:var(--fs-body);
    }

    .locationCard--pstier-warning .locationExpires{
        font-size:var(--fs-caption);
        margin-top:3px;
    }

    /* Watches were already meant to be the compact tier — stacking them
       to a column on mobile (kept from the original mobile fix) actually
       worked against that, so this shrinks the stacked text down to
       something closer to the desktop row's own density instead of
       leaving it at full card-sized type. */
    .locationCard.locationCard--pstier-watch{
        min-height:0;
        flex-direction:column;
        align-items:flex-start;
        text-align:left;
        padding:8px 12px;
        gap:1px;
    }

    .locationCard--pstier-watch .locationName{
        font-size:var(--fs-body);
        margin-bottom:0;
    }

    .locationCard--pstier-watch .locationStatus{
        font-size:var(--fs-caption);
    }

    .locationCard--pstier-watch .locationExpires{
        font-size:var(--fs-micro);
        margin-top:2px;
    }
}

.locationCard{
    display:flex;
    flex-direction:column;
    align-items:center;
    /* Top-anchored (was vertical-center) — 2026-09-14 Card Wall alignment
       fix. Vertically centering meant a card with little content (a
       one-line "OK" location) and a card packed with secondary alerts/
       hazard detail landed their name/status at two different heights on
       the same row, which read as misaligned rather than just "different
       amount of content." min-height keeps short cards from looking too
       squat now that they're not being stretched to fill by centering. */
    justify-content:flex-start;
    min-height:176px;
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    padding:calc(16px * var(--card-scale, 1));
    text-align:center;
    position:relative;
    transition:border-color .3s ease, background .3s ease;
    /* Lets the "@container locationCard" rules below (secondary alerts,
       OTHER SIMULTANEOUS ALERTS section) key off this card's own rendered
       width rather than the viewport's — the same card can be 300px wide
       on a dense grid and 500px+ wide with only one or two locations on
       the page (auto-fit stretches it), and the inline secondary-alerts
       layout should only kick in once THIS card actually has the room. */
    container-name:locationCard;
    container-type:inline-size;
}

.locationCard--clickable{
    cursor:pointer;
}

.locationCard--clickable:hover{
    filter:brightness(1.15);
}

/* Added 2026-09-05 alongside the card's new tabindex/role — a keyboard
   user tabbing to a clickable card needs the same kind of visible signal
   a mouse user gets from the hover brightness above. Same blue as the
   search box's own focus ring (see wxsentinel-color-scheme-proposal.md).
   :focus-visible (not plain :focus) so a mouse click into the card
   doesn't leave a lingering ring — only actual keyboard focus does. */
.locationCard--clickable:focus-visible{
    outline:2px solid var(--focus-ring);
    outline-offset:3px;
}

.locationEditBtn{
    position:absolute;
    top:10px;
    right:10px;
    display:flex;
    align-items:center;
    justify-content:center;
    width:22px;
    height:22px;
    background:none;
    border:none;
    border-radius:4px;
    color:#8a9bb3;
    line-height:1;
    cursor:pointer;
}

.locationEditBtn:hover{
    color:#c5d2e0;
    background:rgba(255,255,255,.06);
}

.locationName--hidden{
    display:none;
}

.locationNameEdit{
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:8px;
    margin-bottom:6px;
}

.locationNameEdit--hidden{
    display:none;
}

.locationNameEditInput{
    width:100%;
    max-width:220px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 10px;
    color:white;
    font-size:calc(var(--fs-emphasis) * var(--card-scale, 1));
    text-align:center;
}

.locationRadiusEditLabel{
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

.locationRadiusEditInput{
    width:100%;
    max-width:220px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 10px;
    color:white;
    font-size:var(--fs-body);
    text-align:center;
}

.locationNameEditActions{
    display:flex;
    gap:8px;
}

.locationNameDeleteBtn, .locationNameOkBtn{
    border:none;
    border-radius:6px;
    padding:6px 14px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
}

.locationNameDeleteBtn{
    background:#3a1414;
    color:#e74c3c;
}

.locationNameDeleteBtn:hover{
    background:#4a1a1a;
}

.locationNameOkBtn{
    background:#2ecc71;
    color:#0d1117;
}

.locationNameOkBtn:hover{
    background:#27ae60;
}

@keyframes blinkTornado{
    0%, 49%{ background:#3a1414; border-color:#e74c3c; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#3a1414; border-color:#e74c3c; }
}

@keyframes blinkSevere{
    0%, 49%{ background:#3a3214; border-color:#f1c40f; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#3a3214; border-color:#f1c40f; }
}

/* Tropical Storm Warning's own blink, split from Severe 2026-09-05 — see
   wxsentinel-color-scheme-proposal.md. */
@keyframes blinkTropicalStorm{
    0%, 49%{ background:#143830; border-color:#1abc9c; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#143830; border-color:#1abc9c; }
}

@keyframes blinkHurricane{
    0%, 49%{ background:#3a1428; border-color:#ff4fa3; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#3a1428; border-color:#ff4fa3; }
}

@keyframes blinkFlashFlood{
    0%, 49%{ background:#0f333d; border-color:#29b6d6; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#0f333d; border-color:#29b6d6; }
}

@keyframes blinkBlizzard{
    0%, 49%{ background:#142a3a; border-color:#3498db; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#142a3a; border-color:#3498db; }
}

@keyframes blinkFlood{
    0%, 49%{ background:#0f333d; border-color:#29b6d6; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#0f333d; border-color:#29b6d6; }
}

@keyframes blinkWinterStorm{
    0%, 49%{ background:#251a3a; border-color:#9d76f8; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#251a3a; border-color:#9d76f8; }
}

@keyframes blinkExtremeCold{
    0%, 49%{ background:#0f2e33; border-color:#06b6d4; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#0f2e33; border-color:#06b6d4; }
}

@keyframes blinkExtremeHeat{
    0%, 49%{ background:#3a2814; border-color:#e67e22; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#3a2814; border-color:#e67e22; }
}

/* Added 2026-08-28 for the bulk alert-tier expansion. */

@keyframes blinkTornadoEmergency{
    0%, 49%{ background:#3a0f28; border-color:#ff1493; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#3a0f28; border-color:#ff1493; }
}

@keyframes blinkSpecialMarine{
    0%, 49%{ background:#241a35; border-color:#b19cd9; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#241a35; border-color:#b19cd9; }
}

@keyframes blinkExtremeWind{
    0%, 49%{ background:#24272b; border-color:#e8ebee; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#24272b; border-color:#e8ebee; }
}

@keyframes blinkHighWind{
    0%, 49%{ background:#24272b; border-color:#94a3b8; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#24272b; border-color:#94a3b8; }
}

@keyframes blinkStormSurge{
    0%, 49%{ background:#14283a; border-color:#74b9ff; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#14283a; border-color:#74b9ff; }
}

@keyframes blinkHurricaneForceWind{
    0%, 49%{ background:#26163a; border-color:#b16fce; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#26163a; border-color:#b16fce; }
}

@keyframes blinkRedFlag{
    0%, 49%{ background:#3a1f10; border-color:#ff6b35; }
    50%, 99%{ background:#161b22; border-color:#2e3c4f; }
    100%{ background:#3a1f10; border-color:#ff6b35; }
}

.locationCard--tornado-warning-blink{
    animation:blinkTornado 0.5s linear 16 forwards;
}

/* PDS Tornado Warning blinks in with the same red as a plain Tornado
   Warning — the distinguishing constant white border flash only starts
   once the blink-in finishes (see .locationCard--tornado-warning-pds
   below, no JS special-casing needed: renderLocationStatus() already
   applies whatever class matches status.level generically). */
.locationCard--tornado-warning-pds-blink{
    animation:blinkTornado 0.5s linear 16 forwards;
}

.locationCard--tornado-warning-emergency-blink{
    animation:blinkTornadoEmergency 0.5s linear 16 forwards;
}

.locationCard--severe-warning-blink{
    animation:blinkSevere 0.5s linear 16 forwards;
}

/* Split from Severe T-Storm Warning 2026-09-05 — Tropical Storm Warning
   gets its own teal identity so it no longer collides with the same
   yellow as Severe/Tornado Watch/Hurricane Watch/etc. See
   wxsentinel-color-scheme-proposal.md. */
.locationCard--tropical-storm-warning-blink{
    animation:blinkTropicalStorm 0.5s linear 16 forwards;
}

.locationCard--severe-warning-destructive-blink, .locationCard--severe-warning-considerable-blink{
    animation:blinkSevere 0.5s linear 16 forwards;
}

.locationCard--hurricane-warning-blink{
    animation:blinkHurricane 0.5s linear 16 forwards;
}

.locationCard--flash-flood-warning-blink{
    animation:blinkFlashFlood 0.5s linear 16 forwards;
}

.locationCard--flash-flood-warning-considerable-blink, .locationCard--flash-flood-warning-catastrophic-blink{
    animation:blinkFlashFlood 0.5s linear 16 forwards;
}

.locationCard--blizzard-warning-blink{
    animation:blinkBlizzard 0.5s linear 16 forwards;
}

.locationCard--special-marine-warning-blink{
    animation:blinkSpecialMarine 0.5s linear 16 forwards;
}

.locationCard--extreme-wind-warning-blink{
    animation:blinkExtremeWind 0.5s linear 16 forwards;
}

.locationCard--high-wind-warning-blink{
    animation:blinkHighWind 0.5s linear 16 forwards;
}

.locationCard--storm-surge-warning-blink{
    animation:blinkStormSurge 0.5s linear 16 forwards;
}

.locationCard--hurricane-force-wind-warning-blink{
    animation:blinkHurricaneForceWind 0.5s linear 16 forwards;
}

.locationCard--flood-warning-blink{
    animation:blinkFlood 0.5s linear 16 forwards;
}

.locationCard--red-flag-warning-blink{
    animation:blinkRedFlag 0.5s linear 16 forwards;
}

.locationCard--winter-storm-warning-blink{
    animation:blinkWinterStorm 0.5s linear 16 forwards;
}

.locationCard--extreme-cold-warning-blink{
    animation:blinkExtremeCold 0.5s linear 16 forwards;
}

.locationCard--extreme-heat-warning-blink{
    animation:blinkExtremeHeat 0.5s linear 16 forwards;
}

.locationCard--tornado-warning{
    background:#3a1414;
    border-color:#e74c3c;
}

/* PDS Tornado Warning and Tornado Emergency — the two tiers that get a
   CONSTANT flashing white border, per the user's request. No JS changes
   needed, renderLocationStatus() already lands on these classes
   generically once the blink-in finishes. Tornado Emergency also gets its
   own pink background (distinct from Hurricane Warning's pink); PDS
   keeps the exact same red as a plain Tornado Warning per the user's
   spec ("Same TOR Red Text"). 2026-09-14: this used to run alongside a
   second, ambient box-shadow glow-pulse animation (flashTornado/
   flashTornadoEmergency) that every steady-state warning card had — that
   glow was dropped app-wide as part of the Card Wall alignment pass (a
   dashboard full of simultaneously pulsing cards during a real outbreak
   was more distracting than informative); the border-flash here is
   unaffected and is now each tier's only ongoing animation. */
.locationCard--tornado-warning-pds{
    background:#3a1414;
    border-color:#e74c3c;
    animation:borderFlashPds 0.6s steps(1) infinite;
}

.locationCard--tornado-warning-emergency{
    background:#3a0f28;
    border-color:#ff1493;
    animation:borderFlashEmergency 0.6s steps(1) infinite;
}

@keyframes borderFlashPds{
    0%, 49%{ border-color:#ffffff; }
    50%, 100%{ border-color:#e74c3c; }
}

@keyframes borderFlashEmergency{
    0%, 49%{ border-color:#ffffff; }
    50%, 100%{ border-color:#ff1493; }
}

@keyframes flashTornadoEmergency{
    0%, 100%{ box-shadow:0 0 20px rgba(255,20,147,.4); }
    50%{ box-shadow:0 0 35px rgba(255,20,147,.9); }
}

.locationCard--severe-warning{
    background:#3a3214;
    border-color:#f1c40f;
}

.locationCard--tropical-storm-warning{
    background:#143830;
    border-color:#1abc9c;
}

/* Destructive/Considerable-tag Severe T-Storm Warning outrank plain Severe
   (and everything from Hurricane Warning down to Blizzard Warning) but
   aren't asked to look any different — same yellow as always, the
   rank/chime change is the whole point of splitting them into their own
   tiers. NOTE: severe-warning-considerable was added 2026-09-04 alongside
   the other three (ALERT_LEVEL_COLORS, ALERT_LEVEL_LABELS,
   ALERT_LEVEL_COCKPIT_GROUP, the .locationStatus-- text-color rule) but
   this card-background/blink pair was missed at the time — found
   2026-09-06 when a real Considerable-tagged card rendered with correct
   yellow status TEXT but no card background/border/flash at all, since
   renderLocationStatus() generically adds `locationCard--${status.level}`/
   `-blink` for whatever level string it's given, and no CSS rule existed
   for this one specific string. */
.locationCard--severe-warning-destructive, .locationCard--severe-warning-considerable{
    background:#3a3214;
    border-color:#f1c40f;
}

.locationCard--hurricane-warning{
    background:#3a1428;
    border-color:#ff4fa3;
}

.locationCard--flash-flood-warning{
    /* Blue-cyan, not green, as of 2026-09-05 — Flood/Flash-Flood used to
       share the exact "all clear" green, so a Flash Flood Emergency could
       read as identical to "nothing happening." See
       wxsentinel-color-scheme-proposal.md. */
    background:#0f333d;
    border-color:#29b6d6;
}

/* Considerable/Catastrophic-tag Flash Flood Warning — same green as
   plain, same reasoning as Severe Destructive above: the tag changes rank
   (and, for Catastrophic, the chime), not the base color. */
.locationCard--flash-flood-warning-considerable, .locationCard--flash-flood-warning-catastrophic{
    background:#0f333d;
    border-color:#29b6d6;
}

.locationCard--blizzard-warning{
    background:#142a3a;
    border-color:#3498db;
}

.locationCard--special-marine-warning{
    background:#241a35;
    border-color:#b19cd9;
}

.locationCard--extreme-wind-warning{
    background:#24272b;
    border-color:#e8ebee;
}

.locationCard--high-wind-warning{
    background:#24272b;
    border-color:#94a3b8;
}

.locationCard--storm-surge-warning{
    background:#14283a;
    border-color:#74b9ff;
}

.locationCard--hurricane-force-wind-warning{
    background:#26163a;
    border-color:#b16fce;
}

.locationCard--flood-warning{
    background:#0f333d;
    border-color:#29b6d6;
}

.locationCard--red-flag-warning{
    background:#3a1f10;
    border-color:#ff6b35;
}

.locationCard--winter-storm-warning{
    background:#251a3a;
    border-color:#9d76f8;
}

.locationCard--extreme-cold-warning{
    background:#0f2e33;
    border-color:#06b6d4;
}

.locationCard--extreme-heat-warning{
    background:#3a2814;
    border-color:#e67e22;
}

@keyframes flashTornado{
    0%, 100%{ box-shadow:0 0 20px rgba(231,76,60,.4); }
    50%{ box-shadow:0 0 35px rgba(231,76,60,.9); }
}

@keyframes flashSevere{
    0%, 100%{ box-shadow:0 0 20px rgba(241,196,15,.4); }
    50%{ box-shadow:0 0 35px rgba(241,196,15,.9); }
}

@keyframes flashTropicalStorm{
    0%, 100%{ box-shadow:0 0 20px rgba(26,188,156,.4); }
    50%{ box-shadow:0 0 35px rgba(26,188,156,.9); }
}

@keyframes flashHurricane{
    0%, 100%{ box-shadow:0 0 20px rgba(255,79,163,.4); }
    50%{ box-shadow:0 0 35px rgba(255,79,163,.9); }
}

@keyframes flashFlashFlood{
    0%, 100%{ box-shadow:0 0 20px rgba(41,182,214,.4); }
    50%{ box-shadow:0 0 35px rgba(41,182,214,.9); }
}

@keyframes flashBlizzard{
    0%, 100%{ box-shadow:0 0 20px rgba(52,152,219,.4); }
    50%{ box-shadow:0 0 35px rgba(52,152,219,.9); }
}

@keyframes flashFlood{
    0%, 100%{ box-shadow:0 0 20px rgba(41,182,214,.4); }
    50%{ box-shadow:0 0 35px rgba(41,182,214,.9); }
}

@keyframes flashWinterStorm{
    0%, 100%{ box-shadow:0 0 20px rgba(139,92,246,.4); }
    50%{ box-shadow:0 0 35px rgba(139,92,246,.9); }
}

@keyframes flashExtremeCold{
    0%, 100%{ box-shadow:0 0 20px rgba(6,182,212,.4); }
    50%{ box-shadow:0 0 35px rgba(6,182,212,.9); }
}

@keyframes flashExtremeHeat{
    0%, 100%{ box-shadow:0 0 20px rgba(230,126,34,.4); }
    50%{ box-shadow:0 0 35px rgba(230,126,34,.9); }
}

/* Added 2026-08-28 for the bulk alert-tier expansion. */

@keyframes flashSpecialMarine{
    0%, 100%{ box-shadow:0 0 20px rgba(177,156,217,.4); }
    50%{ box-shadow:0 0 35px rgba(177,156,217,.9); }
}

@keyframes flashExtremeWind{
    0%, 100%{ box-shadow:0 0 20px rgba(232,235,238,.4); }
    50%{ box-shadow:0 0 35px rgba(232,235,238,.9); }
}

@keyframes flashHighWind{
    0%, 100%{ box-shadow:0 0 20px rgba(148,163,184,.4); }
    50%{ box-shadow:0 0 35px rgba(148,163,184,.9); }
}

@keyframes flashStormSurge{
    0%, 100%{ box-shadow:0 0 20px rgba(116,185,255,.4); }
    50%{ box-shadow:0 0 35px rgba(116,185,255,.9); }
}

@keyframes flashHurricaneForceWind{
    0%, 100%{ box-shadow:0 0 20px rgba(142,68,173,.4); }
    50%{ box-shadow:0 0 35px rgba(142,68,173,.9); }
}

@keyframes flashRedFlag{
    0%, 100%{ box-shadow:0 0 20px rgba(255,107,53,.4); }
    50%{ box-shadow:0 0 35px rgba(255,107,53,.9); }
}

.locationName{
    font-size:calc(var(--fs-heading) * var(--card-scale, 1));
    font-weight:bold;
    margin-bottom:6px;
}

.locationCounty{
    font-size:calc(var(--fs-emphasis) * var(--card-scale, 1));
    color:#9fb3c8;
    margin-bottom:14px;
}

.locationStatus{
    font-size:calc(var(--fs-heading) * var(--card-scale, 1));
    font-weight:bold;
}

.locationExpires{
    font-size:calc(var(--fs-emphasis) * var(--card-scale, 1));
    color:#c5d2e0;
    margin-top:6px;
}


/* ==========================
   SECONDARY ALERTS (next 2 highest, shown inline under the main
   expiration on cards with enough room — see the @container rule
   further down and renderLocationStatus()'s otherAlerts handling in
   script.js). Hidden by default/on narrow cards, where the "+N more
   alerts" popover below falls back to listing every other alert instead.
========================== */

.locationSecondaryAlerts{
    display:none;
    flex-direction:column;
    gap:4px;
    margin-top:8px;
}

.locationSecondaryAlert{
    display:flex;
    flex-direction:column;
    gap:1px;
}

/* Only applied when this entry has a raw NWS feature behind it to open
   (see the `clickable` check in renderLocationStatus()) — a small
   rounded hit-area plus a hover/focus highlight so it visibly reads as
   its own clickable thing, distinct from the plain-text primary status
   above it and the rest of the (non-clickable) card body around it. */
.locationSecondaryAlert--clickable{
    cursor:pointer;
    border-radius:6px;
    padding:2px 6px;
    margin:0 -6px;
}

.locationSecondaryAlert--clickable:hover,
.locationSecondaryAlert--clickable:focus{
    background:rgba(255,255,255,.08);
}

.locationSecondaryAlert--clickable:focus:not(:focus-visible){
    outline:none;
}

.locationSecondaryAlertName{
    font-size:calc(var(--fs-body) * var(--card-scale, 1));
    font-weight:bold;
}

.locationSecondaryAlertExpires{
    font-size:calc(var(--fs-caption) * var(--card-scale, 1));
    color:#9fb3c8;
}


/* ==========================
   OTHER SIMULTANEOUS ALERTS ("+N more alerts")
========================== */

.locationOtherAlerts{
    position:relative;
    display:flex;
    align-items:center;
    justify-content:center;
    gap:6px;
    margin-top:8px;
}

.locationOtherAlerts--hidden{
    display:none;
}

.locationOtherAlertsCount{
    font-size:calc(var(--fs-body) * var(--card-scale, 1));
    color:#9fb3c8;
}

.locationOtherAlertsCount--expanded{
    display:none;
}

.locationOtherAlertsInfo{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    width:16px;
    height:16px;
    border-radius:50%;
    color:#8a9bb3;
    font-size:var(--fs-body);
    line-height:1;
    cursor:default;
}

.locationOtherAlertsInfo:hover,
.locationOtherAlertsInfo:focus{
    color:#c5d2e0;
}

.locationOtherAlertsInfo:focus:not(:focus-visible){
    outline:none;
}

.locationOtherAlertsPopover{
    display:none;
    position:absolute;
    bottom:calc(100% + 8px);
    left:50%;
    transform:translateX(-50%);
    z-index:20;
    width:220px;
    max-width:calc(100vw - 32px);
    background:#141b26;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:10px 12px;
    box-shadow:0 6px 18px rgba(0,0,0,.35);
    text-align:left;
    cursor:default;
}

.locationOtherAlertsInfo:hover ~ .locationOtherAlertsPopover,
.locationOtherAlertsInfo:focus ~ .locationOtherAlertsPopover{
    display:block;
}

.locationOtherAlertsRow{
    display:flex;
    flex-direction:column;
    gap:1px;
}

.locationOtherAlertsRow + .locationOtherAlertsRow{
    margin-top:8px;
    padding-top:8px;
    border-top:1px solid #2e3c4f;
}

.locationOtherAlertsRowName{
    font-size:var(--fs-caption);
    font-weight:bold;
    color:#f4f7fb;
}

.locationOtherAlertsRowExpires{
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

/* Expanded layout — only takes effect once THIS card is wide enough (see
   the container-type:inline-size rule on .locationCard above) to show the
   next two highest alerts inline without feeling cramped. Below this
   width, none of these rules apply and everything falls back to exactly
   the original single-alert-plus-popover layout.

   260px, not the 340px this originally shipped with — #locationGrid's own
   grid-template-columns (`repeat(auto-fit, minmax(calc(300px *
   --card-scale), 1fr))`) already guarantees every card is at least 300px
   wide whenever the grid has enough room to render more than one narrow
   column at all, which is the overwhelmingly common case (confirmed via a
   real screenshot: a 7-column dashboard sized its cards right around
   339px, just under the old 340px cutoff, so the "room for 2 more lines"
   layout was silently losing to the compact fallback on a card that
   plainly had the room). 260px sits safely under that 300px floor — a
   ~13px secondary-alert line has no trouble fitting a card that size —
   so the expanded layout now wins in effectively every normal desktop
   scenario, and the compact/popover fallback is reserved for what it was
   actually meant for: a viewport too narrow to give even a single grid
   column its full 300px (small phones). */
@container locationCard (min-width: 260px){

    .locationSecondaryAlerts{
        display:flex;
    }

    .locationOtherAlertsCount--compact{
        display:none;
    }

    .locationOtherAlertsCount--expanded{
        display:inline;
    }

    /* The first two rows are already shown inline above — the popover
       only needs to cover the overflow (a 4th+ simultaneous alert). */
    .locationOtherAlertsPopover .locationOtherAlertsRow:nth-child(-n+2){
        display:none;
    }

    /* Row 3 becomes the first VISIBLE row whenever there's any overflow
       to show, so it shouldn't inherit the "+ .locationOtherAlertsRow"
       divider meant to separate it from a now-hidden row 2. */
    .locationOtherAlertsPopover .locationOtherAlertsRow:nth-child(3){
        margin-top:0;
        padding-top:0;
        border-top:none;
    }

    /* Nothing left to overflow — both other alerts are already inline —
       so the "+N more" badge/icon has nothing to show and disappears
       entirely rather than sitting there as a dead 0-count control. */
    .locationOtherAlerts--noOverflow{
        display:none;
    }

}


/* ==========================
   LOCATION TAGS (Business/Enterprise only)
========================== */

.locationTags{
    display:flex;
    flex-wrap:wrap;
    gap:6px;
    justify-content:center;
    margin-top:10px;
}

.locationTagChip{
    display:inline-flex;
    align-items:center;
    gap:4px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:12px;
    padding:3px 6px 3px 10px;
    font-size:var(--fs-caption);
    color:#c5d2e0;
}

.locationTagChipRemove{
    background:none;
    border:none;
    color:#8a9bb3;
    font-size:var(--fs-body);
    line-height:1;
    cursor:pointer;
    padding:0;
    /* Anchors the 44x44 hit-area overlay in ACCESSIBILITY BASELINE. */
    position:relative;
}

.locationTagChipRemove:hover{
    color:#e74c3c;
}

/* Hidden by default — only shown while the card is in its rename/edit
   state (.locationCard--editing, toggled by the pencil icon in the
   top-right corner). Already-added tags (.locationTags, above) are NOT
   gated by this — those stay visible on the card at all times regardless
   of edit mode; only the "+ Tag" control itself is tucked away. */
.locationTagAdd{
    display:none;
    justify-content:center;
    align-items:center;
    gap:6px;
    flex-wrap:wrap;
    margin-top:8px;
}

.locationCard--editing .locationTagAdd{
    display:flex;
}

.locationTagAddBtn{
    background:none;
    border:1px dashed #2e3c4f;
    color:#9fb3c8;
    border-radius:12px;
    padding:3px 10px;
    font-size:var(--fs-caption);
    cursor:pointer;
}

.locationTagAddBtn:hover{
    border-color:#2ecc71;
    color:#2ecc71;
}

.locationTagInput{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:4px 8px;
    color:white;
    font-size:var(--fs-caption);
    width:120px;
}

.locationTagSaveBtn, .locationTagCancelBtn{
    background:#161b22;
    border:1px solid #2e3c4f;
    color:#c5d2e0;
    border-radius:6px;
    padding:4px 10px;
    font-size:var(--fs-caption);
    cursor:pointer;
}

.locationTagSaveBtn:hover{
    background:#2ecc71;
    color:#0d1117;
    border-color:#2ecc71;
}

.locationTagCancelBtn:hover{
    background:#2e3c4f;
}

.locationDetection{
    font-size:calc(var(--fs-caption) * var(--card-scale, 1));
    color:#c5d2e0;
    margin-top:2px;
}

.locationDamageThreat{
    font-size:calc(var(--fs-body) * var(--card-scale, 1));
    font-weight:bold;
    margin-top:4px;
    text-transform:uppercase;
    letter-spacing:1px;
}

/* NWS damage-threat tiers, rendered as filled badges rather than plain
   colored text (2026-09-14). These were previously the ONLY thing
   distinguishing a Destructive/Considerable/Catastrophic warning from its
   ordinary counterpart — same card color, same animation, same headline —
   so an escalation that NWS issues a handful of times a year read, at a
   glance, exactly like a routine warning. The headline now names the tier
   too (see getStatusForLocation() in alerts.js); this badge is the
   second, visual half of that fix.

   Deliberately not animated: the card's existing blink already carries
   "something just happened", and stacking a second motion cue on the most
   severe alerts is the wrong direction for a tool people watch for hours. */
.locationDamageThreat--considerable,
.locationDamageThreat--destructive,
.locationDamageThreat--catastrophic{
    display:inline-block;
    border-radius:4px;
    padding:2px 8px;
    margin-top:6px;
    font-size:calc(var(--fs-caption) * var(--card-scale, 1));
    letter-spacing:1.2px;
    color:#10161d;
}

.locationDamageThreat--considerable{ background:#ff8c42; }
.locationDamageThreat--destructive{ background:#ff1744; color:#fff; }
.locationDamageThreat--catastrophic{ background:#ff1744; color:#fff; font-weight:800; }

/* Detection basis (Observed/Radar Indicated) is NOT a severity tag — it's
   how the tornado was confirmed, not how bad it is — so as of 2026-09-05
   this no longer borrows the severity reds/oranges above (that was a real
   category-error collision, same one already fixed on the Alert History
   badges). Rendered as a small neutral pill instead. See
   wxsentinel-color-scheme-proposal.md. */
.locationDamageThreat--radar-indicated, .locationDamageThreat--observed{
    display:inline-block;
    color:#9db4c7;
    background:#1c2733;
    border:1px solid #33495e;
    border-radius:20px;
    padding:2px 10px;
    font-size:var(--fs-micro);
    font-weight:700;
    text-transform:uppercase;
    letter-spacing:.02em;
}

/* Hail size / wind gust — Tornado Warnings & Severe Thunderstorm Warnings
   only (see renderLocationStatus() in script.js). Sized between
   .locationDetection (12px) and .locationExpires (15px) — readable but
   clearly secondary to the warning name itself. */
.locationHazardInfo{
    font-size:calc(var(--fs-body) * var(--card-scale, 1));
    color:#c5d2e0;
    margin-top:4px;
}

/* "TOR Possible" — Severe Thunderstorm Warning only, shown below the hail/
   wind line when NWS has tagged the storm as showing tornado development
   potential (see isTornadoPossible() in alerts.js) — bold, uppercase orange,
   NWS's own special-case risk callout on a warning, not a measurement —
   deliberately not tornado-warning red, since this isn't an actual Tornado
   Warning. Empty text when not tagged, so it takes up no visible space. */
.locationTorPossible{
    font-size:calc(var(--fs-body) * var(--card-scale, 1));
    font-weight:bold;
    color:#ff6b35;
    margin-top:4px;
    text-transform:uppercase;
    letter-spacing:1px;
}

.locationStatus--clear{ color:#2ecc71; }
.locationStatus--tornado-warning{ color:#e74c3c; }
.locationStatus--tornado-warning-pds{ color:#e74c3c; }
.locationStatus--tornado-warning-emergency{ color:#ff1493; }
.locationStatus--hurricane-warning{ color:#ff4fa3; }
.locationStatus--severe-warning, .locationStatus--severe-warning-destructive, .locationStatus--severe-warning-considerable{ color:#f1c40f; }
/* Tropical Storm Warning, and every Watch below, recolored 2026-09-05 to
   resolve the old 8-way yellow collision — see
   wxsentinel-color-scheme-proposal.md. Each Watch is now a pale tint of
   its own hazard family's Warning color (text-only — no card background
   was added for Watches, by explicit request). */
.locationStatus--tropical-storm-warning{ color:#1abc9c; }
.locationStatus--flash-flood-warning, .locationStatus--flash-flood-warning-considerable, .locationStatus--flash-flood-warning-catastrophic{ color:#29b6d6; }
.locationStatus--tornado-watch{ color:#f4aea7; }
.locationStatus--hurricane-watch{ color:#ffb1d6; }
.locationStatus--tropical-storm-watch{ color:#98e1d2; }
.locationStatus--winter-storm-watch{ color:#eda3c8; }
.locationStatus--severe-watch{ color:#87ceeb; }
.locationStatus--winter-storm-warning{ color:#9d76f8; }
.locationStatus--blizzard-warning{ color:#3498db; }
.locationStatus--flood-warning{ color:#29b6d6; }
.locationStatus--flood-watch{ color:#9fdeeb; }
.locationStatus--extreme-cold-warning{ color:#06b6d4; }
.locationStatus--extreme-heat-warning{ color:#e67e22; }

/* Added 2026-08-28 for the bulk alert-tier expansion. */
.locationStatus--special-marine-warning{ color:#b19cd9; }
.locationStatus--extreme-wind-warning{ color:#e8ebee; }
.locationStatus--high-wind-warning{ color:#94a3b8; }
.locationStatus--storm-surge-warning{ color:#74b9ff; }
.locationStatus--hurricane-force-wind-warning{ color:#b16fce; }
.locationStatus--red-flag-warning{ color:#ff6b35; }


/* ==========================
   ALERT / ACCOUNT MODAL
========================== */

.alertModal{
    display:none;
    position:fixed;
    top:0;
    left:0;
    width:100vw;
    height:100vh;
    background:rgba(0,0,0,.75);
    z-index:1000;
    align-items:center;
    justify-content:center;
}

.alertModal--active{
    display:flex;
}

.alertModalContent{
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    width:min(900px, 90vw);
    max-height:85vh;
    overflow-y:auto;
    padding:24px;
    position:relative;
}

.alertModalContent--wide{
    width:min(1100px, 95vw);
}

/* Phase 3 (2026-09-14): every .alertModalContent is now a real focus
   target (tabindex="-1", see openModal() in script.js) so it can receive
   focus itself when a modal has no focusable child — give that an outline
   like any other focusable element gets, rather than the invisible-focus
   default browsers give elements with no border/background change. */
.alertModalContent:focus-visible{
    outline:2px solid var(--focus-ring);
    outline-offset:-2px;
}

/* 2026-10-13: was a flat 60vh, which left dead space under the 85vh modal
   cap — this modal's chrome is a title, two control rows and the legend,
   measured at ~205px, so 85vh-210px is the tallest the map can be while the
   modal still never scrolls. Smaller win than the alert modal's (about 70px
   on a 1080px window rather than 460px) but it is the same unused space.
   Floor kept well below the old value so a short window still works. */
#mapOverviewMap{
    width:100%;
    height:calc(85vh - 210px);
    min-height:320px;
    border-radius:8px;
    margin-bottom:8px;
}

.mapOverviewLegend{
    display:flex;
    gap:16px;
    margin-top:6px;
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

.mapOverviewLegendItem{
    display:flex;
    align-items:center;
    gap:6px;
}

.mapOverviewDot{
    width:10px;
    height:10px;
    border-radius:50%;
    display:inline-block;
}

.mapOverviewDot--clear{ background:#2ecc71; }
.mapOverviewDot--watch{ background:#f1c40f; }
.mapOverviewDot--warning{ background:#e74c3c; }

.reorderLocationsList{
    max-height:55vh;
    overflow-y:auto;
    margin-bottom:16px;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:6px;
}

.reorderLocationRow{
    display:flex;
    align-items:center;
    gap:12px;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    margin-bottom:6px;
}

.reorderLocationRow:last-child{
    margin-bottom:0;
}

/* SortableJS's own placeholder class for the row currently being dragged */
.reorderLocationRow.sortable-ghost{
    opacity:.4;
    border-color:#2ecc71;
}

.reorderDragHandle{
    cursor:grab;
    color:#8a9bb3;
    font-size:var(--fs-emphasis);
    line-height:1;
    padding:2px;
    touch-action:none;
    /* Anchors the 44x44 hit-area overlay in ACCESSIBILITY BASELINE. The
       overlay is a pseudo-element, so hit-testing still reports the handle
       itself as the event target and SortableJS's `handle` selector keeps
       matching — the grab area just got big enough to actually grab. */
    position:relative;
}

.reorderDragHandle:active{
    cursor:grabbing;
}

.reorderLocationInfo{
    display:flex;
    flex-direction:column;
    gap:2px;
    min-width:0;
}

.reorderLocationName{
    font-size:var(--fs-emphasis);
    font-weight:bold;
    color:#f4f7fb;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
}

.reorderLocationMeta{
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

.reorderLocationDeleteBtn{
    margin-left:auto;
    flex:0 0 auto;
    background:none;
    border:none;
    color:#8a9bb3;
    font-size:var(--fs-heading);
    line-height:1;
    cursor:pointer;
    padding:2px 6px;
    /* Anchors the 44x44 hit-area overlay in ACCESSIBILITY BASELINE. */
    position:relative;
}

.reorderLocationDeleteBtn:hover{
    color:#e74c3c;
}

.reorderPageDivider{
    display:flex;
    align-items:center;
    gap:8px;
    margin:10px 0;
    font-size:var(--fs-micro);
    text-transform:uppercase;
    letter-spacing:.5px;
    color:#8a9bb3;
}

.reorderPageDivider::before, .reorderPageDivider::after{
    content:"";
    flex:1;
    border-top:1px dashed #2e3c4f;
}

.reorderLocationsActions{
    display:flex;
    align-items:center;
    gap:12px;
    flex-wrap:wrap;
}

.reorderLocationsActions .manageBillingBtn{
    margin-top:0;
}

.reorderResetBtn{
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 16px;
    font-size:var(--fs-body);
    color:#9fb3c8;
    cursor:pointer;
}

.reorderResetBtn:hover{
    background:#1c2530;
    color:#c5d2e0;
}

/* A square 36px target. The glyph alone measured 21px wide, under the 24px
   floor, and this is the control someone reaches for one-handed to get out of
   a modal. The size change is in the hit area, not the glyph. */
.alertModalClose{
    position:absolute;
    top:12px;
    right:16px;
    display:flex;
    align-items:center;
    justify-content:center;
    width:36px;
    height:36px;
    background:none;
    border:none;
    color:#c5d2e0;
    font-size:var(--fs-hero);
    cursor:pointer;
    line-height:1;
}

/* ALERT DETAIL MODAL — severity header (Phase 3, 2026-09-14)
   ----------------------------------------------------------
   The alert-detail modal's own visual rebuild: a top accent border plus
   a small severity badge, both driven by --alertModalSeverityColor (set
   inline per-open in openAlertModalForAlert() from ALERT_LEVEL_COLORS —
   the same color language Priority Stack/Cockpit/Alert History already
   use, so a Tornado Emergency alert here reads as the same red the card
   and the history row used). Falls back to the original neutral
   #2e3c4f border color when the variable isn't set (defensive only —
   openAlertModalForAlert() always sets it before opening). */
.alertModalContent--alert{
    border-top:5px solid var(--alertModalSeverityColor, #2e3c4f);
}

.alertModalHeader{
    margin-bottom:16px;
    padding-right:32px;
}

.alertModalHeaderRow{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:8px 10px;
}

.alertModalSubtitle{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin-top:2px;
}

.alertModalCountdown{
    margin-left:auto;
    font-size:var(--fs-body);
    font-weight:700;
    letter-spacing:.02em;
    text-transform:uppercase;
    color:var(--alertModalSeverityColor, #9fb3c8);
    white-space:nowrap;
}

.alertModalSeverityBadge{
    display:inline-block;
    font-size:var(--fs-caption);
    font-weight:700;
    letter-spacing:.02em;
    text-transform:uppercase;
    padding:4px 12px;
    border-radius:999px;
    color:#0d1117;
    background:var(--alertModalSeverityColor, #2e3c4f);
    white-space:nowrap;
}

/* Same --hidden modifier-class convention used everywhere else in this
   file (.chatEmpty--hidden, .inviteMemberForm--hidden, etc.) rather than
   the native `hidden` attribute — a plain `hidden` would lose to this
   rule's own `display:inline-block` above at equal specificity since
   author CSS beats the UA stylesheet's [hidden] rule. */
.alertModalSeverityBadge--hidden{
    display:none;
}

.alertModalHeader #alertModalTitle{
    margin:0;
    padding-right:0;
}

#alertModalTitle{
    font-size:var(--fs-heading);
    margin-bottom:16px;
    padding-right:32px;
}

/* The map FILLS its column rather than sitting at a fixed height in it.
   2026-10-13: it was `height:220px`, and .alertModalMapCol is a grid item
   that stretches to the row height — which is set by the much taller left
   column (Take Action + stat tiles + the storm block + the full CAP
   description). Measured on a 1080px window with a hurricane warning open:
   the left column ran 712px and the right column used 252px of it, so 460px
   of the modal was empty. Josh noticed before any of this did.

   The height is now the row's, which means it is free: no breakpoint, no
   magic number, and it tracks whatever the left column happens to be.

   min-height is the old fixed height plus a little, as a FLOOR for a short
   alert (an advisory with no description and no stat tiles) where the row
   would otherwise collapse the map to nothing.

   max-height is the cap that keeps it inside the modal. .alertModalContent is
   `max-height:85vh; overflow-y:auto`, so when the left column is taller than
   the viewport the row — and with it a flex:1 map — would grow past the
   bottom of the screen and have to be scrolled to. 85vh minus the chrome
   above and below it (24+24 padding, ~60 header, 8 gap, ~34 controls) is
   85vh-196px — 24+24 padding, ~60 header, then below the map two control rows
   (the fit toggle and the radar row) at ~34 each with an 8px gap either side.
   Measured, not guessed: at 1000px tall the first cut used -150px, the map
   capped at 700px and the column measured 826px, leaving 42px of the dead
   space this was meant to remove. Note min-height WINS over max-height in CSS, which is the
   behaviour we want on a very short window: a usable map that scrolls beats a
   60px one that does not. */
#alertModalMap{
    width:100%;
    flex:1 1 auto;
    min-height:260px;
    max-height:calc(85vh - 196px);
    border-radius:8px;
}

.radarControlRow{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:10px;
    margin-bottom:10px;
}

.radarPlaybackRow{
    display:flex;
    align-items:center;
    gap:8px;
    margin-bottom:20px;
}

.radarPlaybackBtn{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:5px 10px;
    font-size:var(--fs-body);
    color:#9fb3c8;
    cursor:pointer;
    line-height:1;
}

/* Play/pause icon swap (added 2026-09-05, replacing the old
   playBtn.textContent = "▶"/"⏸" glyph swap in radarUpdateButtons()) — same
   pattern as #muteToggleBtn above: both icons always in the DOM, CSS picks
   one via the new .radarPlaybackBtn--playing class. Prev/Next buttons have
   only one (static) icon each, so they don't need this. */
.radarPlaybackBtn .iconPause{
    display:none;
}

.radarPlaybackBtn--playing .iconPlay{
    display:none;
}

.radarPlaybackBtn--playing .iconPause{
    display:inline;
}

.radarPlaybackBtn:hover:not(:disabled){
    background:#1c2530;
    color:#c5d2e0;
}

.radarPlaybackBtn:disabled{
    opacity:0.4;
    cursor:default;
}

.radarToggleLabel{
    display:flex;
    align-items:center;
    gap:8px;
    font-size:var(--fs-body);
    color:#c5d2e0;
    cursor:pointer;
}

.radarToggleLabel input[type="checkbox"]{
    width:16px;
    height:16px;
    cursor:pointer;
}

.radarAsOfLabel{
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

.mapStyleToggleBtn{
    display:flex;
    align-items:center;
    gap:6px;
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 12px;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    cursor:pointer;
    white-space:nowrap;
}

.mapStyleToggleBtn:hover{
    background:#1c2530;
    color:#c5d2e0;
}

.mapStyleToggleBtn svg{
    flex:0 0 auto;
}

/* Sun/moon icon swap (added 2026-09-05, replacing the old plain-text
   "☀️ Switch to Standard Map"/"🌓 Switch to Dark Map" glyph-in-label) —
   mapStyle defaults to "dark" (see script.js), so the sun icon (offering
   to switch to the standard/light map) is the un-modified default; the
   .mapStyleToggleBtn--standard modifier (added by updateMapStyleButtons()
   whenever mapStyle is "standard") swaps to the moon icon instead. */
.mapStyleToggleBtn .iconMoon{
    display:none;
}

.mapStyleToggleBtn--standard .iconSun{
    display:none;
}

.mapStyleToggleBtn--standard .iconMoon{
    display:inline;
}

/* Cockpit map "Labels on/off" toggle (2026-09-17 redesign) — same visual
   family as .mapStyleToggleBtn right above (border/padding/hover), just a
   plain text toggle rather than an icon swap since there's no light/dark
   pairing to represent, only on/off. --active marks the current on state
   (defaulted on — see cockpitLabelsEnabled in script.js) so it reads as a
   pressed segmented-control button rather than a plain outline button. */
.mapLabelsToggleBtn{
    display:flex;
    align-items:center;
    gap:6px;
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 12px;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    cursor:pointer;
    white-space:nowrap;
}

.mapLabelsToggleBtn:hover{
    background:#1c2530;
    color:#c5d2e0;
}

.mapLabelsToggleBtn--active{
    background:#1c2530;
    color:#e8ebee;
    border-color:#3a4a5e;
}

/* Named marker labels on the Cockpit map itself — a permanent Leaflet
   tooltip per warning-level marker (see renderCockpitMap() in script.js),
   not the hover-only default. Leaflet's own tooltip chrome (white
   background, border, arrow) is overridden to match the app's dark theme;
   background color is set inline per marker via the tooltipopen listener
   (ALERT_LEVEL_COLORS[level]), same reasoning ALERT_LEVEL_COLORS already
   exists for elsewhere — reading the color at render time instead of
   needing a matching per-level CSS class that could silently fall out of
   sync. */
/* Label sitting on an alert polygon (Map Overview + Cockpit), added
   2026-09-28. Names the hazard so the polygon isn't relying on its colour
   alone — see the comment in renderAlertOverlays() for why colour can't
   carry it (28 levels, 22 hexes, and a hard ceiling of 14 mutually
   distinguishable colours at this app's contrast requirement).

   Ink is #0d1117 on a background painted from ALERT_LEVEL_COLORS at runtime.
   Every one of those 22 hexes was verified to clear 4.5:1 against that ink
   during the 2026-09-17 contrast pass, which is what makes a single fixed
   text colour safe here rather than needing a per-level rule.

   pointer-events:none so the label never eats the click that opens the alert
   detail modal — the polygon underneath owns it. */
.alertOverlayLabel.leaflet-tooltip{
    padding:3px 8px;
    border:none;
    border-radius:4px;
    color:#0d1117;
    font-size:var(--fs-micro);
    font-weight:700;
    letter-spacing:.2px;
    white-space:nowrap;
    box-shadow:0 2px 6px rgba(0,0,0,.45);
    pointer-events:none;
}

/* Leaflet draws a little arrow on directional tooltips; a centred one has
   nothing to point at. */
.alertOverlayLabel.leaflet-tooltip::before{
    display:none;
}

.cockpitMapLabel.leaflet-tooltip{
    padding:3px 7px;
    border:none;
    border-radius:4px;
    color:#0d1117;
    font-size:var(--fs-micro);
    font-weight:700;
    box-shadow:0 2px 6px rgba(0,0,0,.4);
}

.cockpitMapLabel.leaflet-tooltip::before{
    display:none;
}

/* Set by updateMapLabelCrowding() on any permanent map label — a hazard
   polygon's or a Cockpit facility name's — that would land on top of a label
   already kept, or on top of a facility marker that isn't its own.

   Without this, adjacent warning polygons during an outbreak stack their
   labels into unreadability, and a single wide label covers the very dots the
   map exists to show. The polygon keeps its colour, weight and dash; the
   marker keeps its colour and popup; zooming in brings the label back.

   Both label kinds carry it, so this is deliberately NOT scoped to either
   class. It is listed after both of their rule blocks: at equal specificity
   (0,2,0) source order is what decides, and neither of those blocks sets
   `display` anyway. */
.leaflet-tooltip.mapLabelCrowdedOut{
    display:none;
}

.alertModalTextBlock{
    margin-bottom:16px;
}

/* :not([class]) — "the generic section heading of this block", which is what
   this rule has always meant. Same treatment, same reason, as
   `.alertModalTextBlock p:not([class])` further down; read that comment for
   the full argument.

   Written bare it was (0,1,1) and beat every (0,1,0) class on an h3 inside
   the block. Measured 2026-10-13 by tools/check-cascade.mjs, one element
   affected: .dangerZoneTitle asked for #e74c3c and rendered #9fb3c8 — the
   Delete This Account heading was slate grey, not red, from the moment the
   danger zone moved inside an .alertModalTextBlock earlier the same day.
   Nothing about .dangerZoneTitle was wrong; it just never applied.

   Of the 22 h3 elements in these blocks, 21 are unclassed and want exactly
   this; the one that carries a class styles itself in full below. */
.alertModalTextBlock h3:not([class]){
    font-size:var(--fs-emphasis);
    color:#9fb3c8;
    text-transform:uppercase;
    letter-spacing:1px;
    margin-bottom:6px;
}

/* SETTINGS TABS (Phase 4, 2026-09-15) — same look/pattern as
   .forecastDayTabs/.forecastDayTab (same blue accent, same underline
   style) for one consistent tabs convention across the app, just with
   4 items instead of 3 and no horizontal-scroll fallback needed (labels
   here are short enough to never wrap even at the modal's narrowest
   width, unlike Forecast's day tabs at 390px). .settingsTabPanel
   deliberately has no `display` rule of its own — its visibility is
   entirely the native `hidden` attribute (toggled in switchSettingsTab(),
   script.js), same as the Forecast detail panels already do it. */
.settingsTabs{
    display:flex;
    flex-wrap:wrap;
    gap:4px;
    margin:0 0 16px;
    border-bottom:1px solid #2e3c4f;
}

.settingsTab{
    background:none;
    border:none;
    border-bottom:2px solid transparent;
    color:#9fb3c8;
    font-size:var(--fs-body);
    font-weight:bold;
    padding:8px 14px;
    cursor:pointer;
}

.settingsTab:hover{
    color:#c5d2e0;
}

.settingsTab--active{
    color:#4fa3ff;
    border-bottom-color:#4fa3ff;
}

/* Section heading + a short one-line description on the same row, instead
   of a full paragraph underneath — used where the fuller explanation
   (Alert Priority) isn't needed every time the modal opens. */
.settingsHeaderRow{
    display:flex;
    align-items:baseline;
    flex-wrap:wrap;
    gap:10px;
    margin-bottom:10px;
}

/* ==========================
   DELETING AN ACCOUNT (2026-10-07)

   The only red in Settings, and the only red in the admin panel that is not
   "remove one member". Red is reserved here for exactly this reason — see
   the note on .menuPanelItem--danger: diluting the colour that means "think
   before you click" costs you the one moment it exists for.
========================== */

.deleteAccountBlock--hidden,
.deleteAccountOffer--hidden,
.deletionPendingNotice--hidden{
    display:none;
}

/* A left border rather than a filled panel. A red box around the last block
   of Settings reads as an error the moment the tab opens, which it is not —
   nothing is wrong, there is simply something dangerous available. */
.dangerZone{
    border-left:3px solid #5a2a2a;
    padding-left:14px;
}

/* Two elements wear this class and they are different sizes of thing: the
   settings block heading (an h3 inside .alertModalTextBlock) and the delete
   confirmation modal's own title (an h2). The bare rule carries the one thing
   both want — the red — and the h3 rule below restates the block-heading
   look, which it used to inherit from `.alertModalTextBlock h3` back when
   that selector reached classed headings too. Spelled out here rather than
   won back with a more specific ancestor selector: that is the trick that
   produced this bug and the sign-out one. */
.dangerZoneTitle{
    color:#e74c3c;
}

h3.dangerZoneTitle{
    font-size:var(--fs-emphasis);
    text-transform:uppercase;
    letter-spacing:1px;
    margin-bottom:6px;
}

.dangerZoneList{
    margin:0 0 12px 18px;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    line-height:1.6;
}

/* .settingsActionBtn--danger lives down beside .settingsActionBtn itself,
   NOT here. Same specificity, and the base rule is declared ~2000 lines
   below this point: defined here the modifier lost the cascade silently and
   the Delete button rendered as an ordinary white one. Measured, after
   shipping exactly that bug on .adminBtn--caution earlier the same day. */

.deletionPendingNotice{
    background:#2a1f0a;
    border:1px solid #6b5416;
    border-radius:8px;
    padding:12px 14px;
    margin-bottom:12px;
}

.deletionPendingNotice p{
    color:#f1c40f;
    font-size:var(--fs-caption);
    line-height:1.55;
    margin-bottom:10px;
}

.deleteConfirmLabel{
    display:block;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin:14px 0 6px;
}

.deleteConfirmInput{
    width:100%;
    background:#0d1117;
    border:1px solid #5a2a2a;
    border-radius:6px;
    padding:10px 12px;
    color:white;
    font-size:var(--fs-body);
    font-family:inherit;
    margin-bottom:14px;
}

.deleteConfirmInput:focus{
    outline:2px solid #e74c3c;
    outline-offset:1px;
}

.alertModalContent--narrow{
    width:min(520px, 92vw);
}

/* ---- the admin card's two deletion states ---- */

.accountDeletedNotice,
.accountDeletionRequestedNotice{
    border-radius:8px;
    padding:10px 12px;
    margin:12px 0;
    font-size:var(--fs-caption);
    line-height:1.55;
}

/* Red for "this is deleted", amber for "they asked". Two different facts,
   and an admin scanning the list has to be able to tell them apart without
   reading — one is reversible by a button on the card, the other has not
   happened yet. */
.accountDeletedNotice{
    background:#2e0f0f;
    border:1px solid #5a2a2a;
    color:#f5b5ae;
}

.accountDeletionRequestedNotice{
    background:#2a1f0a;
    border:1px solid #6b5416;
    color:#f3d98b;
}

.accountDeletionReason{
    display:block;
    margin-top:6px;
    color:#c5d2e0;
    font-style:italic;
}

/* Footer and header navigation links sit on their own rather than inside a
   sentence, so the WCAG inline exception does not cover them and a 16px line
   box is not a thumb target. padding-block takes them to 26px without moving
   anything else, because a footer row has the vertical room to spare.
   
   The links INSIDE paragraphs are deliberately left alone: they are inline
   targets within a line of text, which the exception does cover, and padding
   them would open gaps in the prose.
   
   The dashboard's own footer (.footerLegalLinks, .footerLinkBtn) is the same
   shape as the marketing one and gets the same treatment, which is why both
   are in this one rule rather than two that could drift. */
/* .marketingSignInLink was listed here too and did nothing: its own rule
   further down already sets padding:10px, which is both later and larger, so
   this line was dead weight that read as a fix. Removed once the shadow scan
   pointed at it. */
.marketingFooter a,
.marketingFooter button,
.footerLegalLinks a{
    display:inline-block;
    padding-block:5px;
}

/* ==========================
   BUYING FOR AN ORGANIZATION (about page, 2026-10-08)

   Left-aligned inside a centred marketing section, deliberately. Everything
   else on that page is a pitch and reads fine centred; this is a list
   somebody works down while answering their purchasing office, and centred
   list text is measurably harder to scan because every line starts in a
   different place.
========================== */

.procBlock{
    background:#111823;
    border:1px solid #2e3c4f;
    border-radius:12px;
    padding:26px 28px;
    margin-top:20px;
    text-align:left;
}

.procBlock h4{
    font-size:var(--fs-heading);
    margin-bottom:8px;
    text-align:left;
}

.procEyebrow{
    display:block;
    font-size:var(--fs-caption);
    letter-spacing:1.4px;
    text-transform:uppercase;
    color:#5fb3f0;
    margin-bottom:10px;
}

.procLead{
    color:#c5d2e0;
    line-height:1.65;
    margin-bottom:18px;
    max-width:74ch;
}

.procChecklist{
    list-style:none;
    margin:0 0 18px;
    padding:0;
    max-width:78ch;
}

/* A rule between rows rather than bullets: these are six separate answers,
   not six parts of one thought, and the separator is what lets someone stop
   reading after the one they came for. */
.procChecklist li{
    position:relative;
    padding:9px 0 9px 30px;
    color:#c5d2e0;
    line-height:1.6;
    font-size:var(--fs-body);
    border-bottom:1px solid #1d2733;
}

.procChecklist li:last-child{
    border-bottom:0;
}

.procChecklist li::before{
    content:"\2713";
    position:absolute;
    left:4px;
    top:9px;
    color:#2ecc71;
    font-weight:bold;
}

.procChecklist strong{
    color:#ffffff;
}

.procRow{
    display:flex;
    gap:10px;
    flex-wrap:wrap;
    align-items:center;
    margin-top:4px;
}

/* The Free tier's one hard limitation, on the pricing card itself. Amber
   rather than red: it is a boundary of the free plan, not an error, and the
   page should not look like something is wrong with it. */
.marketingPricingCaveat{
    display:inline-block;
    margin-top:6px;
    color:#f1c40f;
    font-size:var(--fs-caption);
    line-height:1.45;
}

/* One save for a whole settings tab, pinned to the bottom of the modal's
   scroll area (2026-10-07; the Display tab had six buttons, one per block).

   Sticky and not just placed last: the Display tab runs well past a screen,
   and a save you have to scroll five blocks to reach is a save people press
   on the wrong block. The sticky container is .alertModalContent, which is
   the overflow-y:auto element — nothing between here and it may clip.

   The negative side margins and matching padding let the bar's background
   run the full width of the modal's padding box, so content scrolling under
   it disappears at the modal edge rather than at the text column. */
.settingsSaveBar{
    position:sticky;
    bottom:-24px;              /* cancels .alertModalContent's 24px padding */
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    /* Button at the right-hand end, status line filling the space to its
       left. Both save bars do this as of 2026-10-08; it started as an
       Alerts-only modifier and became the rule when the Display tab was
       matched to it, so the modifier is gone rather than carried by every
       instance — a modifier nothing varies is a modifier nobody can read.

       justify-content rather than DOM order plus the status line's
       `flex:1 1 180px`. That ordering trick right-aligns the button only
       while both fit on one line, and this bar wraps: at 390px the content
       box is 299px against 180 + 12 + 124 = 316, so the status took a line
       of its own and the button dropped to the LEFT of the next one, which
       reads as a mistake. This handles both cases — they share a line and
       the status fills it, or they wrap and each line is still end-aligned.

       Both bars therefore put the status line FIRST in the DOM. */
    justify-content:flex-end;
    gap:12px;
    margin:20px -24px -24px;
    padding:14px 24px;
    background:#161b22;
    border-top:1px solid #2e3c4f;
    /* Above the blocks it covers, below the modal's own close button. */
    z-index:2;
}

/* The status line sits beside the button, not under it, and must not shove
   the button off the row when it is long. */
.settingsSaveBar .addLocationError{
    flex:1 1 180px;
    min-width:0;
    margin:0;
}


.settingsHeaderRow h3{
    margin-bottom:0;
    white-space:nowrap;
}

.settingsHintInline{
    font-size:var(--fs-caption);
    color:#8a9bb3;
}

/* :not([class]) — "an unclassed body paragraph in this block", which is what
   this rule has always meant.

   Written as a bare `.alertModalTextBlock p` it was specificity (0,1,1) and
   beat every (0,1,0) class on a paragraph inside the block. Measured
   2026-10-01 by tools/check-cascade.mjs: .settingsHint (×3), .recipientUsage,
   .notificationContactsLoading and .quietHoursBreakthroughNote were ALL
   rendering at 16px instead of the 12–14px they ask for — four separate
   caption/hint classes silently promoted to --fs-emphasis across the settings
   and notification modals, which is most of the small print in this product.
   Nothing about those rules was wrong; they just never applied.

   pre-wrap is for NWS alert text, whose newlines are real content. It was
   reaching hand-written copy too, so each hint rendered index.html's own
   source indentation as leading spaces. That was patched narrowly for
   .settingsHint/.settingsHintInline in 2026-09-29 — the other four classed
   paragraphs were never covered, and this subsumes that patch rather than
   adding a third special case: of the eleven <p> elements in these blocks,
   seven carry a class and style themselves, and four are the alert text this
   rule is actually for. */
.alertModalTextBlock p:not([class]){
    font-size:var(--fs-emphasis);
    line-height:1.5;
    white-space:pre-wrap;
}

/* ALERT DETAIL MODAL — Phase 5 redesign (2026-09-15): two-column layout
   (text content left, map demoted to a smaller right column), a
   prominent severity-bordered "Take Action" instruction block, a stat-
   tile row for Damage Threat/Detection/Max Hail/Max Gust (only ever
   showing whichever of those the alert actually carries — same
   "don't show what isn't there" convention as the card's own
   .locationHazardInfo line, see renderLocationStatus() in script.js),
   and an "Other Active Here" chip row for simultaneous secondary alerts
   at the same location. The map's old checkbox+dark/light-toggle+
   Prev/Next row is replaced by simplified Radar/Standard pills + a
   single Play button — map style (dark/light) still toggles fine from
   Map Overview/Cockpit, updateMapStyleButtons() already no-ops on a
   missing button, so dropping it here only removes it from this one
   surface. */
.alertModalBody{
    display:grid;
    grid-template-columns:1.4fr 1fr;
    gap:20px;
}

.alertModalTakeAction{
    border:1px solid var(--alertModalSeverityColor, #2e3c4f);
    border-left-width:4px;
    border-radius:8px;
    padding:12px 14px;
    margin-bottom:16px;
    background:#1a2029;
}

.alertModalTakeAction h3{
    font-size:var(--fs-body);
    color:var(--alertModalSeverityColor, #9fb3c8);
    text-transform:uppercase;
    letter-spacing:1px;
    margin-bottom:6px;
}

.alertModalTakeAction p{
    font-size:var(--fs-emphasis);
    line-height:1.5;
    white-space:pre-wrap;
    margin:0;
}

.alertModalStatTiles{
    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(128px, 1fr));
    gap:8px;
    margin-bottom:16px;
}

.alertModalStatTiles--hidden{
    display:none;
}

.alertModalStatTile{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:8px 10px;
    min-width:0; /* lets the value below shrink/wrap instead of forcing the grid track wider than minmax()'s 128px */
}

.alertModalStatTileLabel{
    font-size:var(--fs-micro);
    color:#8a9bb3;
    text-transform:uppercase;
    letter-spacing:.05em;
    margin-bottom:3px;
    white-space:nowrap;
}

.alertModalStatTileValue{
    font-size:var(--fs-body);
    font-weight:700;
    color:#e7edf3;
    overflow-wrap:break-word;
}

.alertModalStatTileValue--catastrophic, .alertModalStatTileValue--destructive{ color:#ff1744; }
.alertModalStatTileValue--considerable{ color:#ff8c42; }

.alertModalOtherAlerts--hidden{
    display:none;
}

.alertModalOtherAlertsChips{
    display:flex;
    flex-wrap:wrap;
    gap:6px;
}

.alertModalOtherAlertChip{
    display:inline-flex;
    align-items:center;
    gap:6px;
    background:#1c2733;
    border:1px solid #33495e;
    border-radius:999px;
    padding:4px 10px;
    font-size:var(--fs-caption);
    color:#c5d2e0;
}

.alertModalOtherAlertChip--clickable{
    cursor:pointer;
}

.alertModalOtherAlertChip--clickable:hover{
    background:#26323f;
}

.alertModalOtherAlertChipDot{
    width:8px;
    height:8px;
    border-radius:50%;
    flex:0 0 auto;
    background:var(--chipColor, #9fb3c8);
}

/* A flex column so #alertModalMap can take `flex:1` and absorb the row
   height. `min-height:0` because a flex item's default `min-height:auto`
   refuses to shrink below its content, which would push the column past the
   row and reintroduce the overflow the map's max-height exists to avoid.
   position:relative stays — .radarAsOfLabel--floating is positioned to it. */
.alertModalMapCol{
    position:relative;
    display:flex;
    flex-direction:column;
    gap:8px;
    min-height:0;
}

.alertModalMapControls{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:8px;
    flex:0 0 auto;
}

/* The fit toggle (2026-10-13, phase B). Its own row above the radar
   controls rather than a third group inside them: that row is already
   `space-between` with pills at one end and the play button at the other,
   and a narrow right column cannot take a third item without wrapping.

   Hidden — not disabled, not empty — whenever there is no attributed storm,
   which is most alerts. A toggle whose second state does nothing is worse
   than no toggle. */
.alertMapFitRow{
    display:flex;
    align-items:center;
    gap:8px;
    flex:0 0 auto;
}

.alertMapFitRow--hidden{
    display:none;
}

.alertMapFitLabel{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    text-transform:uppercase;
    letter-spacing:1px;
}

.radarStylePills{
    display:inline-flex;
    border:1px solid #2e3c4f;
    border-radius:999px;
    overflow:hidden;
}

.radarStylePill{
    background:transparent;
    border:none;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    font-weight:600;
    padding:6px 14px;
    cursor:pointer;
}

.radarStylePill:hover{
    color:#c5d2e0;
}

.radarStylePill--active{
    background:var(--alertModalSeverityColor, #2e3c4f);
    color:#0d1117;
}

.radarAsOfLabel--floating{
    position:absolute;
    top:8px;
    left:8px;
    background:rgba(13,17,23,.75);
    border-radius:6px;
    padding:3px 8px;
    z-index:5;
    pointer-events:none;
}

@media (max-width:700px){
    .alertModalBody{
        grid-template-columns:1fr;
    }
}

.dndTimeRow{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:8px;
    margin:10px 0;
}

.dndTimeRow label{
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.dndTimeRow input[type="time"]{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    color:white;
    font-size:var(--fs-body);
}

.inviteMemberForm{
    margin-bottom:0;
}


/* ==========================
   ALERT HISTORY
========================== */

.historyModalContent{
    width:min(760px, 92vw);
}

.historyModalIntro{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin-bottom:16px;
}

.historyLoading,
.historyEmpty{
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.historyLoading--hidden,
.historyEmpty--hidden,
.historyError--hidden,
.historyTable--hidden{
    display:none;
}

.historyTableWrap{
    overflow-x:auto;
}

.historyTable{
    width:100%;
    border-collapse:collapse;
    font-size:var(--fs-body);
}

.historyTable th,
.historyTable td{
    padding:10px 12px;
    text-align:left;
    border-bottom:1px solid #2e3c4f;
    white-space:nowrap;
}

.historyTable th{
    color:#9fb3c8;
    font-size:var(--fs-caption);
    text-transform:uppercase;
    letter-spacing:.5px;
    font-weight:normal;
}

.historyTable tbody tr:hover{
    background:#161b22;
}

.historyTableSub{
    color:#9fb3c8;
    font-size:var(--fs-caption);
}

/* Severity + detection badges for the Alert column — added 2026-09-05,
   replacing the old plain bold-colored-text .historyAlertLabel span. Not
   to be confused with the .historyBadge Warning/Watch tag that was
   deliberately removed 2026-09-01 (redundant with the label text itself,
   e.g. "Severe Thunderstorm Warning" already says "Warning"): this badge
   carries color, not text, and states a genuinely separate fact —
   severity tier via the alert's own ALERT_LEVEL_COLORS background (same
   color language as the dashboard cards and Cockpit's status boxes, via
   renderHistoryTable() in script.js), with detection basis (Observed /
   Radar Indicated / Possible, Tornado Warnings only) as a second, smaller
   neutral chip beside it rather than folded into one run-on string. */
.historyAlertBadge{
    display:inline-block;
    font-size:var(--fs-caption);
    font-weight:700;
    padding:3px 10px;
    border-radius:6px;
    color:#0d1117;
    white-space:nowrap;
}

.historyDetectionBadge{
    display:inline-block;
    font-size:var(--fs-micro);
    font-weight:600;
    padding:2px 8px;
    border-radius:999px;
    border:1px solid #2e3c4f;
    color:#9fb3c8;
    margin-left:6px;
    white-space:nowrap;
}

.historyFilters{
    margin-bottom:14px;
    padding-bottom:14px;
    border-bottom:1px solid #2e3c4f;
}

.historyFilters input[type="date"]{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 10px;
    color:white;
    font-size:var(--fs-body);
    font-family:inherit;
    color-scheme:dark;
}

.historyFilterClearBtn{
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 12px;
    font-size:var(--fs-body);
    color:#9fb3c8;
    cursor:pointer;
}

.historyFilterClearBtn:hover{
    background:#1c2530;
    color:#c5d2e0;
}

.historyActionsRow{
    display:flex;
    align-items:center;
    gap:12px;
    margin-bottom:14px;
}

.historyActionsRow .manageBillingBtn{
    margin-top:0;
}

.historyTruncatedNote{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin-bottom:10px;
}

.historyTruncatedNote--hidden{
    display:none;
}


/* ==========================
   FORECAST VIEW (SPC convective outlook, grouped by forecast office)
========================== */

.forecastModalContent{
    max-height:85vh;
    overflow-y:auto;
}

.forecastSections{
    display:flex;
    flex-direction:column;
    gap:16px;
}

.forecastSection{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:14px 16px;
}

.forecastSectionTitle{
    font-size:var(--fs-emphasis);
    font-weight:bold;
    margin-bottom:8px;
}

.forecastSectionLocations{
    display:flex;
    flex-direction:column;
    gap:4px;
    margin-bottom:10px;
}

/* Phase 2 — day tabs, risk summary strip, per-location badges + detail
   panel. Deliberately a different visual language from the dashboard's
   live-alert indicators (colored text/dot on a dark card, blinking for
   warnings): these are calm, filled pills in SPC's own official outlook
   colors, so an outlook never reads as urgent the way an active alert
   does. */

.forecastDayTabs{
    display:flex;
    gap:4px;
    margin:14px 0 12px;
    border-bottom:1px solid #2e3c4f;
}

.forecastDayTab{
    background:none;
    border:none;
    border-bottom:2px solid transparent;
    color:#9fb3c8;
    font-size:var(--fs-body);
    font-weight:bold;
    padding:8px 14px;
    cursor:pointer;
}

.forecastDayTab:hover{
    color:#c5d2e0;
}

.forecastDayTab--active{
    color:#4fa3ff;
    border-bottom-color:#4fa3ff;
}

.forecastRiskStrip{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    margin-bottom:14px;
    min-height:1px;
}

.forecastRiskStripItem{
    font-size:var(--fs-caption);
    font-weight:bold;
    color:#1a1a1a;
    padding:4px 10px;
    border-radius:999px;
    border:1px solid;
    white-space:nowrap;
}

.forecastRiskStripItem--none{
    background:#2e3c4f;
    border-color:#3d4f66;
    color:#c5d2e0;
}

/* "We couldn't reach SPC" — visually distinct from the neutral grey of
   "No Outlook" so a data-availability problem can't be mistaken for a quiet
   day. Amber, matching .connectionStatus--degraded: same meaning, same
   colour language, across two different parts of the app (2026-09-14). */
.forecastRiskStripItem--unavailable{
    background:#3a2f1c;
    border-color:#7a5a1e;
    color:#f0b954;
    cursor:help;
}

.forecastLocationRow{
    display:flex;
}

.forecastLocationBadgeBtn{
    display:flex;
    align-items:center;
    gap:8px;
    background:none;
    border:none;
    padding:2px 0;
    cursor:pointer;
    text-align:left;
    width:100%;
}

.forecastLocationName{
    font-size:var(--fs-body);
    color:#c5d2e0;
}

.forecastRiskBadge{
    display:inline-block;
    flex-shrink:0;
    font-size:var(--fs-micro);
    font-weight:bold;
    color:#1a1a1a;
    padding:3px 9px;
    border-radius:999px;
    border:1px solid;
    min-width:34px;
    text-align:center;
}

.forecastRiskBadge--none{
    background:#2e3c4f;
    border-color:#3d4f66;
    color:#9fb3c8;
}

.forecastRiskBadge--unavailable{
    background:#3a2f1c;
    border-color:#7a5a1e;
    color:#f0b954;
    cursor:help;
}

.forecastRiskBadge--detail{
    font-size:var(--fs-body);
    padding:4px 12px;
}

.forecastDetailPanel{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:12px 14px;
    margin:2px 0 8px;
}

.forecastDetailHeader{
    display:flex;
    align-items:center;
    gap:10px;
    margin-bottom:10px;
}

.forecastDetailLocationName{
    font-size:var(--fs-body);
    font-weight:bold;
}

/* Which NWS office(s) forecast this location. Two of them when the coverage
   radius crosses a CWA boundary — see resolveNeighbourOffices() in script.js.
   Quiet by design: it answers "whose outlook am I looking at", which is a
   question you only have once, not a number you watch. */
.forecastDetailOffices{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin:2px 0 8px;
    overflow-wrap:anywhere;
}

.forecastHazardChips{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    margin-bottom:10px;
}

.forecastHazardChip{
    display:flex;
    align-items:center;
    gap:6px;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    font-size:var(--fs-caption);
}

.forecastHazardChip--none{
    opacity:0.6;
}

/* Deliberately NOT dimmed like --none above: an unavailable hazard is a
   thing the user should notice, not a thing to fade into the background. */
.forecastHazardChip--unavailable{
    border-color:#7a5a1e;
    cursor:help;
}

.forecastHazardChip--unavailable .forecastHazardChipValue{
    color:#f0b954;
}

.forecastHazardChipName{
    color:#9fb3c8;
}

.forecastHazardChipValue{
    font-weight:bold;
    color:#c5d2e0;
}

.forecastHazardChipSig{
    background:#e74c3c;
    color:white;
    font-size:var(--fs-micro);
    font-weight:bold;
    padding:1px 5px;
    border-radius:4px;
}

.forecastDiscussionText{
    font-size:var(--fs-body);
    line-height:1.5;
    color:#c5d2e0;
    background:#161b22;
    border-radius:6px;
    padding:10px 12px;
    margin-bottom:10px;
}

.forecastDiscussionText--unavailable{
    color:#9fb3c8;
    font-style:italic;
}

.forecastOutlookLink{
    display:inline-block;
    font-size:var(--fs-body);
    font-weight:bold;
    color:#4fa3ff;
    text-decoration:none;
}

.forecastOutlookLink:hover{
    text-decoration:underline;
}

.forecastUnresolvedNote{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin-top:16px;
}

.forecastUnresolvedNote--hidden{
    display:none;
}

/* Mobile — every .forecast* rule above sits outside all of the file's
   other @media blocks, so nothing here had a narrow-viewport pass yet.
   Two real overflow risks: the location-name rows and the detail header
   are flex rows with no min-width:0 (same class of bug as the footer
   fix above — a long location name won't shrink and can drag the modal,
   and the page under it, wider than the viewport), and the day-tab strip
   doesn't wrap. overflow-x:hidden on the modal content is a backstop,
   same fix already used on .cockpitModalContent for the same reason. */
@media (max-width:640px){
    .forecastModalContent{
        overflow-x:hidden;
    }

    .forecastLocationRow{
        min-width:0;
    }

    .forecastLocationBadgeBtn{
        min-width:0;
    }

    .forecastLocationName{
        min-width:0;
        overflow:hidden;
        text-overflow:ellipsis;
        white-space:nowrap;
    }

    .forecastDetailHeader{
        min-width:0;
    }

    .forecastDetailLocationName{
        min-width:0;
        overflow:hidden;
        text-overflow:ellipsis;
        white-space:nowrap;
    }

    .forecastDayTabs{
        flex-wrap:nowrap;
        overflow-x:auto;
        -webkit-overflow-scrolling:touch;
    }

    .forecastDayTab{
        flex-shrink:0;
        padding:8px 11px;
        font-size:var(--fs-body);
    }

    .forecastSection{
        padding:12px 14px;
    }
}


/* ==========================
   TEAM CHAT
========================== */

/* Icon/badge flex layout only — this class deliberately carries no colour,
   border or padding of its own.

   The button lives in the nav rail as of the 2026-09-15 restyle (#chatBtn
   carries .menuPanelItem .navRailItem .chatToggleBtn), so .navRailItem is
   what dresses it. An earlier comment here claimed it inherited its base
   look from ".statusBarActions button"; that rule was deleted 2026-10-08
   for reaching into the ☰ dropdown and cancelling Sign Out's red, and this
   button had already stopped depending on it two restyles earlier. */
.chatToggleBtn{
    display:flex;
    align-items:center;
    gap:6px;
}

/* Small unread indicator dot, left of the icon/text (DOM order — the
   button is already a flex row via .chatToggleBtn above) — no count on
   the button itself, just "there's something new"; the exact count is
   still available via the button's title/tooltip (updateChatUnreadBadge()
   in script.js). The soft outer ring makes it read as a live status pip
   rather than a stray dot. */
.chatUnreadDot{
    width:9px;
    height:9px;
    border-radius:50%;
    background:#f1c40f;
    box-shadow:0 0 0 3px rgba(241,196,15,.22);
    flex-shrink:0;
}

.chatUnreadDot--hidden{
    display:none;
}

.chatModalContent{
    width:min(600px, 92vw);
    max-height:80vh;
    display:flex;
    flex-direction:column;
}

/* Title sits tight above the intro line now, and the intro line grows a
   bottom rule — together they read as one header block, cleanly
   separated from the scrolling message list below instead of the
   title/intro/messages all bleeding into each other with equal spacing. */
#chatModalTitle{
    margin-bottom:4px;
}

#chatModal .historyModalIntro{
    margin-bottom:14px;
    padding-bottom:14px;
    border-bottom:1px solid #232d3a;
}

.chatMessages{
    display:flex;
    flex-direction:column;
    gap:14px;
    flex:1;
    min-height:120px;
    max-height:50vh;
    overflow-y:auto;
    margin:0 0 12px;
    padding-right:4px;
    scrollbar-width:thin;
    scrollbar-color:#2e3c4f transparent;
}

.chatMessages::-webkit-scrollbar{
    width:6px;
}

.chatMessages::-webkit-scrollbar-track{
    background:transparent;
}

.chatMessages::-webkit-scrollbar-thumb{
    background:#2e3c4f;
    border-radius:6px;
}

.chatMessages::-webkit-scrollbar-thumb:hover{
    background:#3d4f66;
}

.chatEmpty{
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:6px;
    margin:auto;
    padding:24px 0;
    font-size:var(--fs-body);
    color:#8a9bb3;
    text-align:center;
}

.chatEmpty::before{
    content:"💬";
    font-size:var(--fs-heading);
    opacity:.45;
}

.chatEmpty--hidden{
    display:none;
}

.chatError--hidden{
    display:none;
}

/* One row = an identity avatar beside a column of meta/bubble/image,
   instead of the sender/time text stacked directly above the bubble.
   Reads the way Slack/iMessage/Discord lay out a message: an identity
   anchor beside the content, not repeated text sitting above it. */
.chatMessageRow{
    display:flex;
    align-items:flex-end;
    gap:8px;
    max-width:82%;
}

.chatMessageRow--own{
    align-self:flex-end;
    flex-direction:row-reverse;
}

.chatAvatar{
    flex:0 0 auto;
    width:28px;
    height:28px;
    border-radius:50%;
    background:#26313f;
    border:1px solid #2e3c4f;
    color:#c5d2e0;
    font-size:var(--fs-micro);
    font-weight:700;
    display:flex;
    align-items:center;
    justify-content:center;
}

.chatAvatar--own{
    background:#1d3a2e;
    border-color:#2ecc71;
    color:#2ecc71;
}

.chatMessageCol{
    display:flex;
    flex-direction:column;
    gap:3px;
    min-width:0;
}

.chatMessageRow--own .chatMessageCol{
    align-items:flex-end;
}

.chatMessageMeta{
    display:flex;
    align-items:baseline;
    gap:6px;
    padding:0 2px;
    font-size:var(--fs-micro);
}

.chatMessageSender{
    font-weight:600;
    color:#c5d2e0;
}

.chatMessageTime{
    color:#5f7086;
}

.chatMessageBody{
    background:#1c2530;
    border:1px solid #262f3d;
    border-radius:14px;
    border-bottom-left-radius:4px;
    padding:9px 13px;
    font-size:var(--fs-body);
    line-height:1.42;
    color:#eef2f7;
    text-align:left;
    white-space:pre-wrap;
    word-break:break-word;
    box-shadow:0 1px 2px rgba(0,0,0,.2);
}

.chatMessageRow--own .chatMessageBody{
    background:#1d3a2e;
    border-color:#28543f;
    border-bottom-left-radius:14px;
    border-bottom-right-radius:4px;
}

.chatMessageImageWrap{
    line-height:0;
}

.chatMessageImageWrap--error{
    padding:8px 0;
    font-size:var(--fs-caption);
    line-height:1.4;
    color:#8a9bb3;
    font-style:italic;
}

.chatMessageImage{
    max-width:220px;
    max-height:220px;
    border-radius:12px;
    border:1px solid #262f3d;
    display:block;
    cursor:zoom-in;
    transition:filter .12s ease;
}

.chatMessageImage:hover{
    filter:brightness(1.06);
}

.chatImagePreview{
    display:flex;
    align-items:center;
    gap:10px;
    margin-bottom:10px;
    padding:6px 8px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:10px;
}

.chatImagePreview--hidden{
    display:none;
}

.chatImagePreviewImg{
    width:44px;
    height:44px;
    object-fit:cover;
    border-radius:8px;
}

.chatImagePreviewRemove{
    background:none;
    border:none;
    border-radius:6px;
    color:#8a9bb3;
    font-size:var(--fs-subhead);
    cursor:pointer;
    line-height:1;
    margin-left:auto;
    padding:4px;
    /* Anchors the 44x44 hit-area overlay in ACCESSIBILITY BASELINE. */
    position:relative;
}

.chatImagePreviewRemove:hover{
    background:#232d3a;
    color:#f4f7fb;
}

.chatForm{
    display:flex;
    gap:8px;
    align-items:flex-end;
    padding-top:12px;
    border-top:1px solid #232d3a;
}

.chatImageBtn{
    flex:0 0 auto;
    width:38px;
    height:38px;
    display:flex;
    align-items:center;
    justify-content:center;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:50%;
    font-size:var(--fs-emphasis);
    line-height:1;
    cursor:pointer;
    transition:background .12s ease, border-color .12s ease;
}

.chatImageBtn:hover{
    background:#1c2530;
    border-color:#3d4f66;
}

.chatInput{
    flex:1;
    /* A <textarea> with no cols= attribute has an intrinsic ~20ch
       preferred width, which without this can stop flex:1 from actually
       shrinking the box below that on a narrow surface (the Cockpit
       panel especially) — wrapping its own placeholder/text mid-word and
       pushing the row wider than its container instead of just wrapping
       cleanly within it. */
    min-width:0;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:18px;
    padding:9px 14px;
    color:white;
    font-size:var(--fs-body);
    font-family:inherit;
    line-height:1.4;
    resize:none;
    transition:border-color .12s ease;
}

.chatInput:focus{
    border-color:#2ecc71;
}

.chatInput:focus:not(:focus-visible){
    outline:none;
}

.chatSendBtn{
    flex:0 0 auto;
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:18px;
    padding:9px 20px;
    font-size:var(--fs-body);
    font-weight:700;
    cursor:pointer;
    transition:filter .12s ease, transform .05s ease;
}

.chatSendBtn:hover{
    filter:brightness(1.08);
}

.chatSendBtn:active{
    transform:scale(.97);
}

.chatSendBtn:disabled{
    opacity:.55;
    cursor:default;
    filter:none;
    transform:none;
}

/* ==========================
   DOCKED VIEW MODALS (2026-09-28)
   ==========================
   .alertModal--view is the same modal machinery as every other
   .alertModal — same markup, same openModal()/closeModal() helpers, same
   close button — but pinned to the right of the nav rail instead of
   covering the whole viewport. The rail stays visible AND clickable
   while it's up, so getting from Cockpit to Forecast is one click on the
   rail rather than close-then-cross-the-screen-then-open.

   Cockpit is the only one wearing it today. Map Overview and Forecast
   can adopt it by adding the class and passing { view: true } to
   openModal() — that's deliberately all it takes.

   ONLY above 900px. Below that .navRail is display:none, so there is
   nothing to dock beside and the standard centered overlay is still the
   right answer; this block leaves the <900px layout completely alone.

   left:76px is the rail's own flex-basis. Its 3px right border is
   already inside that number thanks to the global box-sizing:border-box
   at the top of this file, so this is ONE value to keep in sync with
   .navRail, not two. If the rail is ever resized, this moves with it.
========================== */

/* THE rule that makes a view a view rather than a popup (2026-09-28), and
   the one thing here that is NOT scoped to the breakpoint.

   Every .alertModal shares z-index:1000, so they stack in DOM order — and
   #cockpitModal is the LAST one in index.html. That put it on top of
   Settings, Map, Forecast and everything else declared earlier in the
   file, so opening Forecast while Cockpit was up opened it UNDERNEATH
   Cockpit. Docking the geometry beside the rail didn't touch this: it was
   still sitting in the modal layer.

   999 takes it out of that layer. A view belongs BETWEEN the dashboard and
   the modals: above every layer the board itself uses (its highest is 20 —
   the sticky header and the filter/menu panels), so it covers the
   dashboard completely; and one below the modal layer, so every real modal
   still opens over it with its scrim across the whole viewport. That's
   what a modal is for, and it's the behavior Cockpit shouldn't have had.

   Unconditional on purpose. The docking below only applies at >=900px, but
   the DOM-order problem exists at every width — on a phone Cockpit would
   still swallow any modal opened over it. Same number works for both: it's
   above the board and below the modals either way. */
/* Back control on a modal opened on top of another (2026-10-01).

   Settings, Account, Manage Tags and Notification Settings used to close each
   other rather than stack, because every .alertModal shares z-index:1000 and
   they paint in DOM order — so "on top" was decided by position in
   index.html, not by which was opened last. openModal() now bumps a nested
   modal's z-index, which lets them stack, and this is the way back down.

   Not in any modal's markup: whether a given modal HAS a parent is a runtime
   fact. Notification Settings reached from inside Settings needs this;
   reached directly it must not have it. renderModalBackControl() decides. */
.alertModalBack{
    display:inline-flex;
    align-items:center;
    gap:6px;
    align-self:flex-start;
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 12px;
    margin-bottom:12px;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
    min-height:36px;
}

.alertModalBack:hover{
    background:#18212c;
    color:white;
    border-color:#2ecc71;
}

.alertModalBack--hidden{
    display:none;
}

.alertModal--view{
    z-index:999;
}

@media (min-width:900px){

    .alertModal--view{
        left:76px;
        width:calc(100vw - 76px);

        /* Opaque, not the usual rgba(0,0,0,.75) scrim. A scrim means
           "what's behind this is inert and you're blocked from it",
           which is now exactly the wrong message — the rail beside it is
           live. A view paints its own ground instead of dimming the
           page. Same value as body's background so the seam at the
           rail's border reads as one surface, not a floating panel. */
        background:#0d1117;

        /* .alertModal centers its card; a docked view has no card to
           center — the content stretches to fill the docked area. */
        align-items:stretch;
        justify-content:stretch;
    }

    /* Beats .cockpitModalContent's own 96vw/92vh on specificity (0,2,0
       vs 0,1,0), which is the point: those numbers exist to make a
       centered card big, and a docked view isn't a card. Dropping the
       border and radius matters as much as the sizing — a rounded,
       outlined rectangle is most of what makes something read as a
       popup sitting on top of the page. */
    .alertModal--view .alertModalContent{
        width:100%;
        max-width:none;
        height:100%;
        max-height:none;
        border:none;
        border-radius:0;
    }

}

/* ==========================
   COCKPIT VIEW (1/4 chat, 1/2 map, 1/4 status boxes — a quick-look
   screen for every location at once; see openCockpitModal() in script.js)
========================== */

.cockpitToggleBtn{
    display:flex;
    align-items:center;
    gap:6px;
}

/* Overrides the standard .alertModalContent's fixed max-width — the
   Cockpit is meant to fill most of the screen, not sit in a centered
   card like every other modal. overflow-x:hidden is the actual fix for
   the bottom scrollbar/cut-off-chat-panel bug: .alertModalContent only
   ever sets overflow-y, so any sub-pixel/rounding overflow from the
   3-column layout below used to bleed out sideways instead of being
   contained, dragging the whole page's scrollbar out with it. */
.cockpitModalContent{
    width:96vw;
    max-width:96vw;
    height:92vh;
    max-height:92vh;
    display:flex;
    flex-direction:column;
    overflow-x:hidden;
}

/* CSS Grid instead of flexbox here on purpose — three fr-unit columns are
   guaranteed by spec to divide up (container width - gaps) exactly, with
   no leftover to overflow. The flexbox version this replaced used
   percentage flex-basis (25%/50%/25%) alongside a real gap, which sized
   itself against the container's full width, gaps and all, so the three
   panels together came out wider than the space actually available —
   this is what was pushing the whole layout past the right edge and
   clipping the chat panel. */
.cockpitLayout{
    display:grid;
    grid-template-columns: 1fr 2fr 1fr;
    gap:16px;
    flex:1;
    min-height:0;
    min-width:0;
    margin-top:8px;
    overflow:hidden;
}

.cockpitChatPanel{
    display:flex;
    flex-direction:column;
    min-width:0;
    min-height:0;
    border-left:1px solid #2e3c4f;
    padding-left:16px;
}

/* The only one of the three Cockpit panels whose content doesn't
   self-explain at a glance (the map is obviously a map, the status boxes
   carry their own labels) — a small eyebrow label orients the panel
   without needing a full header row like the popup modal gets. */
.cockpitChatPanelLabel{
    margin-bottom:8px;
    font-size:var(--fs-micro);
    font-weight:700;
    letter-spacing:.6px;
    text-transform:uppercase;
    color:#8a9bb3;
}

/* Fills the whole chat panel's height instead of the popup's own
   .chatMessages fixed 50vh cap — the panel itself is what's bounded here. */
.cockpitChatMessages{
    flex:1;
    max-height:none;
    min-height:0;
    margin-bottom:10px;
}

.cockpitMapPanel{
    display:flex;
    flex-direction:column;
    min-width:0;
    min-height:0;
}

.cockpitMap{
    flex:1;
    min-height:0;
    min-width:0;
    border-radius:8px;
    margin-bottom:8px;
}

.cockpitStatusPanel{
    min-width:0;
    min-height:0;
    overflow-y:auto;
    overflow-x:hidden;
    padding-right:16px;
    border-right:1px solid #2e3c4f;
}

.cockpitStatusBoxes{
    display:flex;
    flex-direction:column;
    gap:10px;
}

/* Quiet and below the status boxes, not a primary action. Cockpit's job is
   watching; adding a site is something you do once and then go back to
   watching, so it should be findable without competing with the thing on
   screen that might be a tornado warning. */
.cockpitAddLocationBtn{
    display:flex;
    align-items:center;
    justify-content:center;
    gap:7px;
    width:100%;
    margin-top:12px;
    background:transparent;
    color:#9fb3c8;
    border:1px dashed #2e3c4f;
    border-radius:6px;
    padding:9px 12px;
    font-size:var(--fs-caption);
    cursor:pointer;
    min-height:var(--tap-min);
}

.cockpitAddLocationBtn:hover{
    background:#161b22;
    color:#c5d2e0;
    border-color:#4a5c72;
}

.cockpitStatusBox{
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    border-radius:10px;
    padding:16px 10px;
    color:#0d1117;
    text-align:center;
}

.cockpitStatusBoxCount{
    font-size:var(--fs-heading);
    font-weight:bold;
    line-height:1.2;
}

.cockpitStatusBoxLabel{
    font-size:var(--fs-caption);
    font-weight:600;
}

/* Every box is clickable — opens the "locations affected" overlay (see
   #cockpitLocationsOverlay below). The "locations clear" footer pill
   (.cockpitClearFooter, further down) deliberately isn't — nothing to
   drill into there. */
.cockpitStatusBox--clickable{
    cursor:pointer;
    transition:transform .1s ease, box-shadow .1s ease;
}

.cockpitStatusBox--clickable:hover{
    transform:translateY(-2px);
    box-shadow:0 4px 10px rgba(0,0,0,.35);
}

/* Severity-weighted status boxes (2026-09-17 redesign) — size/typography
   now scales down through 4 tiers by severity RANK among what's currently
   active (rank 0 = tier-primary is always the single most severe thing
   happening, however many locations it counts), not by raw count. See the
   big comment above renderCockpitStatusBoxes() in script.js for the full
   reasoning; only padding/font-size differ here; .cockpitStatusBox above
   still supplies the shared flex/center/border-radius/color base. */
.cockpitStatusBox--tier-primary{
    padding:22px 16px;
}

.cockpitStatusBox--tier-primary .cockpitStatusBoxCount{
    font-size:var(--fs-hero);
}

.cockpitStatusBox--tier-primary .cockpitStatusBoxLabel{
    font-size:var(--fs-body);
}

/* Up to 2 affected location names, rank-0 box only — see boxHtml() in
   script.js. Single line with an ellipsis rather than wrapping, so one
   long name can't blow out the whole status column's width. */
.cockpitStatusBoxLocations{
    margin-top:6px;
    max-width:100%;
    font-size:var(--fs-micro);
    font-weight:600;
    opacity:.85;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
}

.cockpitStatusBox--tier-secondary{
    padding:16px 14px;
}

.cockpitStatusBox--tier-secondary .cockpitStatusBoxCount{
    font-size:var(--fs-display);
}

/* Damage-tag annotation, rank-1 box only, only when that family actually
   carries one right now (cockpitTagAnnotation() in script.js returns null
   otherwise, and boxHtml() just skips this line entirely). */
.cockpitStatusBoxTag{
    margin-top:4px;
    font-size:var(--fs-micro);
    font-weight:700;
    text-transform:uppercase;
    letter-spacing:.4px;
    opacity:.85;
}

.cockpitStatusBox--tier-compact{
    padding:10px 12px;
}

.cockpitStatusBox--tier-compact .cockpitStatusBoxCount{
    font-size:var(--fs-subhead);
}

.cockpitStatusBox--tier-compact .cockpitStatusBoxLabel{
    font-size:var(--fs-micro);
}

/* Rank 3+ — two smallest boxes per row rather than each getting its own
   full-width row, so a quiet 4th/5th active level doesn't cost as much
   vertical space as the tiers that actually need it. */
.cockpitStatusBoxPairedRow{
    display:flex;
    gap:8px;
}

.cockpitStatusBox--tier-paired{
    flex:1 1 0;
    padding:8px;
}

.cockpitStatusBox--tier-paired .cockpitStatusBoxCount{
    font-size:var(--fs-emphasis);
}

.cockpitStatusBox--tier-paired .cockpitStatusBoxLabel{
    font-size:var(--fs-micro);
}

/* Watches — a compact text legend instead of full-size boxes (they're
   already the "quieter" tier everywhere else in the app: Priority Stack's
   own watch rows, the dashboard cards' compact-row treatment). Rows are
   still clickable through to the same overlay every box opens. */
.cockpitWatchLegend{
    display:flex;
    flex-direction:column;
    gap:6px;
    margin-top:2px;
    padding-top:10px;
    border-top:1px solid #2e3c4f;
}

.cockpitWatchLegendHeading{
    margin-bottom:2px;
    font-size:var(--fs-micro);
    font-weight:700;
    letter-spacing:.6px;
    text-transform:uppercase;
    color:#8a9bb3;
}

.cockpitWatchLegendRow{
    display:flex;
    align-items:center;
    gap:8px;
    padding:2px 0;
    font-size:var(--fs-caption);
    color:#c5d2e0;
    cursor:pointer;
}

.cockpitWatchLegendRow:hover{
    color:#e8ebee;
}

.cockpitWatchLegendSwatch{
    flex:0 0 auto;
    width:10px;
    height:10px;
    border-radius:2px;
}

.cockpitWatchLegendCount{
    font-weight:700;
}

.cockpitWatchLegendLabel{
    color:#9fb3c8;
}

/* "Locations clear" — demoted from a large leading green box to a small
   footer pill, last thing in the column instead of first. */
.cockpitClearFooter{
    display:flex;
    align-items:center;
    gap:6px;
    margin-top:4px;
    padding:8px 10px;
    border-radius:8px;
    background:rgba(46,204,113,.12);
    color:#2ecc71;
    font-size:var(--fs-caption);
    font-weight:600;
}

.cockpitClearFooter svg{
    flex:0 0 auto;
}

/* Responsive fallback (per the original ask: "if individual screen space
   does not permit that to happen cleanly, automatically default to the
   current setup") — below this width the 3-column layout doesn't have
   room to stay legible, so it stacks vertically in DOM order: status
   boxes (which flow into a wrapping row rather than a column once
   stacked, so they don't turn into one long scroll on a narrow screen),
   then map, then chat. */
@media (max-width: 900px){

    .cockpitModalContent{
        width:98vw;
        max-width:98vw;
        height:96vh;
        max-height:96vh;
    }

    .cockpitLayout{
        grid-template-columns: 1fr;
        overflow-y:auto;
        overflow-x:hidden;
    }

    .cockpitChatPanel, .cockpitMapPanel, .cockpitStatusPanel{
        border:none;
        padding:0;
    }

    .cockpitChatMessages{
        max-height:200px;
    }

    .cockpitMap{
        height:300px;
        flex:none;
    }

    .cockpitStatusBoxes{
        flex-direction:row;
        flex-wrap:wrap;
    }

    .cockpitStatusBox{
        flex:1 1 100px;
    }

    /* The paired-row/watch-legend/clear-footer wrappers are siblings of
       the tiered .cockpitStatusBox elements inside the now flex-wrap:wrap
       .cockpitStatusBoxes row above — without their own flex-basis they'd
       just shrink to content width like any other wrapped item and end up
       squeezed between boxes instead of forming their own row. .cockpitStatusBox
       itself (rule right above) still gets its 100px basis regardless of
       nesting depth, class selectors don't care where in the DOM they are. */
    .cockpitStatusBoxPairedRow,
    .cockpitWatchLegend,
    .cockpitClearFooter{
        flex:1 1 100%;
    }

    .cockpitStatusBox--tier-primary .cockpitStatusBoxCount{
        font-size:var(--fs-display);
    }

    .cockpitStatusBox--tier-secondary .cockpitStatusBoxCount{
        font-size:var(--fs-heading);
    }

    /* The actual "clipping together" bug (2026-09-17): min-height:0 on
       these three panels (base rule, above) is needed on desktop, where
       all three sit in one shared grid ROW and are meant to be squeezed
       to that row's height and scroll internally rather than grow it.
       Stacked to one column here, each panel gets its OWN row instead —
       but min-height:0 still applied, so an auto-sized row was free to
       compress BELOW its content's natural height (confirmed by
       measuring it directly: the map panel's row resolved to ~251px
       while its fixed-height 300px map didn't shrink to fit, so the map
       — and the radar controls below it — bled down over the chat
       panel's messages). Resetting to the default (auto) here lets each
       stacked row size to its own content instead, which is what
       .cockpitLayout's own overflow-y:auto (set just above) is already
       set up to scroll past once the stack is taller than the modal. */
    .cockpitStatusPanel,
    .cockpitMapPanel,
    .cockpitChatPanel{
        min-height:auto;
    }

    /* .cockpitStatusPanel's own overflow-y:auto (base rule — desktop's
       narrow left column can need its own internal scroll when there are
       many status boxes) turned out to matter here too, and not just as
       an unrelated leftover: per the grid sizing spec, a grid item's
       automatic minimum size is 0 whenever its overflow isn't visible,
       regardless of min-height:auto above — so without this, the status
       panel's row still collapsed to 0 height and its boxes rendered
       floating outside their own (invisible) row, overlapping the map
       panel below. Not needed on mobile anyway: the status boxes switch
       to a wrapping row layout here (see .cockpitStatusBoxes below) and
       the whole stack already scrolls via .cockpitLayout itself. */
    .cockpitStatusPanel{
        overflow:visible;
    }

}

/* Cockpit "locations affected" overlay (see #cockpitLocationsOverlay in
   index.html, openCockpitLocationsOverlay() in script.js). Deliberately
   its own fixed layer rather than another .alertModal instance — every
   .alertModal shares z-index:1000 and stacks in DOM order rather than
   open/close recency, so opening one "on top of" the already-open Cockpit
   modal can silently render behind it. 1100 here guarantees this always
   sits above Cockpit (and above any other modal) regardless of markup
   order. Same show/hide pattern as .alertModal--active otherwise. */
.cockpitLocationsOverlay{
    display:none;
    position:fixed;
    top:0;
    left:0;
    width:100vw;
    height:100vh;
    background:rgba(0,0,0,.75);
    z-index:1100;
    align-items:center;
    justify-content:center;
}

.cockpitLocationsOverlay--active{
    display:flex;
}

.cockpitLocationsOverlayPanel{
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    width:min(420px, 90vw);
    max-height:80vh;
    overflow-y:auto;
    padding:24px;
    position:relative;
}

.cockpitLocationsOverlayTitle{
    font-size:var(--fs-subhead);
    margin-bottom:14px;
    padding-right:32px;
}

.cockpitLocationsOverlayList{
    display:flex;
    flex-direction:column;
    gap:8px;
}

.cockpitLocationsOverlayRow{
    display:flex;
    flex-direction:column;
    align-items:flex-start;
    gap:2px;
    width:100%;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:10px 12px;
    color:inherit;
    font:inherit;
    text-align:left;
    cursor:pointer;
}

.cockpitLocationsOverlayRow:hover{
    border-color:#5b7290;
}

.cockpitLocationsOverlayRowName{
    font-weight:600;
}

.cockpitLocationsOverlayRowMeta{
    font-size:var(--fs-caption);
    color:#9fb0c3;
}

.inviteMemberForm--hidden{
    display:none;
}

.accountOwnerOnly--hidden{
    display:none;
}

.inviteMemberForm select{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    color:white;
    font-size:var(--fs-emphasis);
}


/* ==========================
   NOTIFICATION CONTACTS
========================== */

.notificationContactsList{
    display:flex;
    flex-direction:column;
    gap:8px;
    margin-bottom:14px;
}

/* Plan allowance line above the contacts roster. Reads higher than the number
   of visible rows on a multi-member account, by design: a teammate's own alert
   settings occupy a slot and are not shown to anyone else. */
.recipientUsage{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin:0 0 10px;
}

.recipientUsage strong{
    color:#e6edf3;
    font-variant-numeric:tabular-nums;
}

.recipientUsage--warn strong{
    color:#f1c40f;
}

/* #f1c40f on #0d1117 is 11.4:1 — measured, not guessed. Amber rather than red:
   being at a plan limit is a boundary, not a fault. */
.recipientUsage--full{
    display:inline-block;
    margin-left:8px;
    padding:1px 7px;
    color:#f1c40f;
    background:rgba(241, 196, 15, .12);
    border:1px solid rgba(241, 196, 15, .45);
    border-radius:4px;
}

/* Above the CONTRACTED amount, but nothing is blocking — an Enterprise
   account past its quoted recipients can still add one. Deliberately cooler
   than .recipientUsage--full: this is a prompt to re-quote, not a fault, and
   it must not read as "you are blocked". #9fb3c8 on #0d1117 is 8.79:1. */
.recipientUsage--over{
    display:inline-block;
    margin-left:8px;
    padding:1px 7px;
    color:#9fb3c8;
    background:rgba(46, 60, 79, .4);
    border:1px solid #2e3c4f;
    border-radius:4px;
}

.notificationContactsLoading,
.notificationContactsEmpty{
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.notificationContactRow{
    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:12px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:10px 14px;
}

.notificationContactName{
    font-size:var(--fs-emphasis);
    font-weight:bold;
}

.notificationContactMeta{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin-top:2px;
}

.notificationContactActions{
    display:flex;
    gap:8px;
    flex-shrink:0;
}

.notificationContactEditBtn,
.notificationContactDeleteBtn{
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 12px;
    font-size:var(--fs-body);
    color:#c5d2e0;
    cursor:pointer;
    white-space:nowrap;
}

.notificationContactEditBtn:hover{
    background:#1c2530;
}

.notificationContactDeleteBtn:hover{
    background:rgba(231, 76, 60, .15);
    border-color:#e74c3c;
    color:#e74c3c;
}

.notificationContactForm{
    padding-top:4px;
    border-top:1px solid #2e3c4f;
}

/* IDs used (not just the class) so this beats .authForm button's
   higher-specificity background/color/border declarations. Manage Tags'
   Cancel button reuses this same rule (and the --hidden class below) rather
   than duplicating it, since it needs the identical muted/outline look. */
#notificationContactCancelBtn,
#manageTagCancelBtn{
    background:transparent;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
}

#notificationContactCancelBtn:hover,
#manageTagCancelBtn:hover{
    background:#1c2530;
    color:#c5d2e0;
}

.notificationContactCancelBtn--hidden{
    display:none;
}

/* ==========================
   MANAGE TAGS
========================== */

.manageTagsSettingsBlock--hidden{
    display:none;
}

.manageTagsList{
    display:flex;
    flex-direction:column;
    gap:8px;
    margin-bottom:14px;
}

.manageTagsEmpty{
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.manageTagsRow{
    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:12px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:10px 14px;
}

.manageTagsName{
    font-size:var(--fs-emphasis);
    font-weight:bold;
}

.manageTagsActions{
    display:flex;
    gap:8px;
    flex-shrink:0;
}

.manageTagsEditBtn,
.manageTagsDeleteBtn{
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 12px;
    font-size:var(--fs-body);
    color:#c5d2e0;
    cursor:pointer;
    white-space:nowrap;
}

.manageTagsEditBtn:hover{
    background:#1c2530;
}

.manageTagsDeleteBtn:hover{
    background:rgba(231, 76, 60, .15);
    border-color:#e74c3c;
    color:#e74c3c;
}

.manageTagForm{
    padding-top:4px;
    border-top:1px solid #2e3c4f;
}


/* ==========================
   PLAN / LOCATION USAGE
========================== */

/* Scoped to the Account modal's #memberUsageLabel only, as of 2026-09-06 —
   the dashboard toolbar's usage label was renamed to .dashboardUsageLabel
   (see the "Moved 2026-09-05" comment above) so the two no longer collide. */
.locationUsageLabel{
    font-size:var(--fs-body);
    color:#9fb3c8;
    font-weight:bold;
    display:flex;
    align-items:center;
}

.locationUsageLabel--atLimit{
    color:#f1c40f;
}

#memberUsageLabel{
    margin-bottom:10px;
}

.plansTable{
    display:flex;
    flex-direction:column;
    gap:10px;
    margin-top:10px;
}

.plansTableLoading{
    color:#9fb3c8;
    font-size:var(--fs-body);
}

.planRow{
    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:12px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:12px 14px;
}

.planRow--current{
    border-color:#2ecc71;
}

.planRowAction{
    display:flex;
    gap:8px;
    flex-wrap:wrap;
    justify-content:flex-end;
}

.planRowName{
    font-size:var(--fs-emphasis);
    font-weight:bold;
}

.planRowDetail{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin-top:2px;
}

.planRowCurrent{
    font-size:var(--fs-body);
    font-weight:bold;
    color:#2ecc71;
}

.planRowActionBtn{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:8px 16px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
    white-space:nowrap;
}

.planRowActionBtn:hover{
    background:#27ae60;
}

.planRowActionBtn--monthly{
    background:transparent;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    font-weight:normal;
}

.planRowActionBtn--monthly:hover{
    background:#1c2530;
    color:#c5d2e0;
}

/* Added 2026-09-06 — the annual button (script.js's renderPlansTable())
   has always applied planRowActionBtn--annual alongside the base class,
   but no rule for that modifier ever existed here, so it was silently
   riding on .planRowActionBtn's own bare styling with nothing of its own.
   That happened to look right today only because the base class's solid
   green look IS the intended "primary/best value" treatment — but that
   was implicit and fragile: any future change to the shared base style
   (e.g. neutralizing it so both --monthly/--annual apply their own look)
   would have silently broken annual's styling with no rule here to fall
   back on. Pinned explicitly, matching --monthly's own explicit pair. */
.planRowActionBtn--annual{
    background:#2ecc71;
    color:#0d1117;
    border:none;
}

.planRowActionBtn--annual:hover{
    background:#27ae60;
}

.planRowActionBadge{
    display:block;
    font-size:var(--fs-micro);
    font-weight:normal;
    margin-top:2px;
    opacity:.85;
}

/* Primary/commit-action button (Save X, Export CSV, Generate PDF, Manage
   Billing, Accept invite, Switch account...) — added 2026-09-05 as part of
   the UI sharpen-up pass. Previously this was styled identically to
   secondary/utility buttons like .historyFilterClearBtn/.reorderResetBtn
   (same dark fill, same gray border), so there was no visual weight telling
   you which button actually commits a change. Now reuses the same accent
   green (#2ecc71) the app already uses elsewhere for a primary/positive
   action (chatSendBtn, locationNameOkBtn, addLocationForm/authForm submit
   buttons) instead of introducing a new color. */
.manageBillingBtn{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:8px 16px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
    margin-top:12px;
}

.manageBillingBtn:hover{
    filter:brightness(1.08);
}

.manageBillingBtn:disabled{
    opacity:.6;
    cursor:default;
    filter:none;
}

/* Used for the one non-primary action that shares this button family today
   — the "Decline" button next to "Accept" in the Pending Invites list (see
   renderPendingInvitesList() in script.js). Full override (not just
   background, as before) so it actually reads as neutral/secondary instead
   of a transparent-background version of the primary green. */
.manageBillingBtn--secondary{
    background:transparent;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
}

.manageBillingBtn--secondary:hover{
    background:#1c2530;
    color:#c5d2e0;
    filter:none;
}

/* Account switcher (added 2026-08-27) + Pending Invites modal — same card
   look as .notificationContactRow, reused rather than reinvented since
   both are "one row per item, name + actions on the right" lists. */
.accountSwitcherBlock--hidden{
    display:none;
}

.accountSwitcherList,
#pendingInvitesList{
    display:flex;
    flex-direction:column;
    gap:8px;
    margin-top:8px;
}

.accountSwitcherRow,
.pendingInviteRow{
    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:12px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:10px 14px;
}

.accountSwitcherRowLabel,
.pendingInviteRowLabel{
    font-size:var(--fs-body);
    color:#c5d2e0;
}

.accountSwitcherActiveBadge{
    font-size:var(--fs-caption);
    font-weight:bold;
    color:#3fb950;
}

.pendingInviteRowActions{
    display:flex;
    gap:8px;
    flex-shrink:0;
}

.pendingInviteRowActions .manageBillingBtn{
    margin-top:0;
}


/* ==========================
   FOOTER
========================== */

/* Three-column grid rather than flex + space-between — with a plain flex
   row, adding a 3rd/4th item makes "space-between" divide leftover space
   between all of them, so the pagination controls only land near the
   middle by coincidence, drifting whenever the left or right group's
   width changes (e.g. once "Contact Us" got added to the legal links).
   The center column here is always exactly centered on the footer,
   independent of how wide the left/right content is. */
#footer{
    display:grid;
    grid-template-columns:1fr auto 1fr;
    align-items:center;
    gap:8px 16px;
    font-size:var(--fs-body);
    color:#8a9bb3;
    padding:8px 24px;
    border-top:1px solid #2e3c4f;
}

.footerLeftGroup{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:16px;
    justify-self:start;
    min-width:0;
}

#footer #lastUpdated{
    justify-self:end;
    text-align:right;
    /* Grid items default to min-width:auto, so without this the text's
       natural width forces the 1fr column (and the whole grid) wider than
       the viewport on a phone instead of shrinking. Truncate rather than
       wrap so the footer stays one line at any width. */
    min-width:0;
    overflow:hidden;
    text-overflow:ellipsis;
    white-space:nowrap;
}

/* Overrides for #paginationControls now that it lives in the footer
   instead of stacked under #locationGrid — no longer needs the standalone
   block's centering/top-margin, just sits centered as the grid's middle
   column. min-width:0 for the same reason as #lastUpdated above: it's a
   grid item and needs permission to shrink below its content's width. */
#footer .paginationControls{
    margin-top:0;
    justify-self:center;
    min-width:0;
}

/* Mobile fix (2026-09-15) — the 3-column 1fr/auto/1fr layout above assumes
   the two side columns only ever need a little room, but Card Wall's Prev/
   Page N of N/Next controls are real buttons that don't shrink, and on a
   phone they were claiming most of the ~340px available and forcing
   .footerLeftGroup + #lastUpdated into slivers too narrow for their own
   content — visually overlapping the pagination controls instead of
   actually fitting the "shrink to fit" the base rules' min-width:0/
   ellipsis were relying on (reported as the copyright/legal links and
   "Last updated" text rendering on top of Prev/Next). Below this width the
   three pieces stack as full-width rows instead of fighting over one: no
   more competing for the same horizontal space, so nothing needs to
   overlap or clip. Priority Stack never shows pagination at all (see
   computePages()'s priority-stack branch in script.js) so it only ever
   hits the plain 2-row case (links, then "Last updated") even here. */
@media (max-width:640px){
    #footer{
        grid-template-columns:1fr;
        justify-items:start;
        gap:8px;
    }

    .footerLeftGroup{
        justify-self:start;
    }

    #footer .paginationControls{
        justify-self:start;
    }

    #footer #lastUpdated{
        justify-self:start;
        text-align:left;
    }
}


/* ==========================
   AUTH SCREEN
========================== */

.appRoot{
    display:flex;
    flex-direction:column;
    min-height:100vh;
    flex:1;
}

/* Nav rail row-layout override (Phase 4, 2026-09-15) — deliberately placed
   here, immediately after .appRoot's own base rule, rather than up in the
   nav rail section near the top of this file. Both rules target the same
   .appRoot selector at equal specificity, so with two same-specificity
   rules the LAST one in the file wins regardless of which is wrapped in a
   media query — this base rule used to sit after the nav rail's row
   override in source order, which meant this unconditional
   flex-direction:column silently beat the media query's flex-direction:row
   at every width ≥900px, collapsing the rail+content row back into a
   single stacked column (rail rendering as a short block up top instead
   of a full-height sidebar, and header/main/footer all going full-width
   underneath it — reported 2026-09-15 as "everything moved around, top
   banner is now in the center of the screen" right after this phase's
   first push). Fixed by moving the override to fire after this rule
   instead of before it.

   Scoped to :has(> .navRail) on 2026-09-18. This override exists purely to
   put the rail beside the content, but `.appRoot` is a shared class and
   admin.html's #adminRoot uses it too — with no rail anywhere in that page.
   So admin.html was getting flex-direction:row for a sidebar it doesn't
   have, which laid its <header> and <main> out side by side: the header
   squeezed into a narrow, vertically-centred left column with the content
   shoved to the right of it, and a dead band of empty background where the
   rail would have been. Reported 2026-09-18 while signing in to the admin
   panel; it had been broken since Phase 4 shipped, just unnoticed because
   admin.html is URL-only and rarely opened. The :has() makes the override
   say what it actually means — go row ONLY when there is a rail to sit
   beside — so any future page that reuses .appRoot without a rail stacks
   correctly by default instead of inheriting this bug. */
@media (min-width:900px){
    .appRoot:has(> .navRail){
        flex-direction:row;
    }

    /* Header slim-down (2026-09-15): by this width #statusBar's only real
       content was the logo (the rail has its own brand mark) and the Live
       status + date/clock — everything else it used to hold (Add Location,
       Map, History, Settings, Team Chat, Cockpit...) had already moved into
       the rail during the Phase 4 restyle. Live + date/clock now live in
       .navRailStatus, near the top of the rail just under the brand mark
       (same #element ids reused there under new rail-scoped ids — see the
       comment on .navRailStatus). Below 900px there's no rail, so #statusBar (with the
       original #connectionStatus/#date/#clock, untouched) is still what
       renders — this rule doesn't touch that. */
    #statusBar{
        display:none;
    }
}

.appRoot--hidden{
    display:none;
}

.authScreen{
    display:flex;
    align-items:center;
    justify-content:center;
    min-height:100vh;
    padding:24px;
}

.authScreen--hidden{
    display:none;
}

.authCard{
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    padding:32px;
    width:min(380px, 100%);
    text-align:center;
}

.authLogo{
    /* Sized a bit larger than the shared --brand-logo-height on purpose
       (2026-08-26, at the user's request) — still derived from the shared
       variable so a future global resize scales this proportionally too,
       just with its own +20px on top. max-width keeps it from overflowing
       the 380px-wide auth card on very narrow screens. */
    height:calc(var(--brand-logo-height) + 20px);
    width:auto;
    max-width:100%;
    margin-bottom:8px;
}

.authSubtitle{
    color:#9fb3c8;
    font-size:var(--fs-body);
    margin-bottom:24px;
}

.authForm{
    display:flex;
    flex-direction:column;
    gap:12px;
    text-align:left;
}

/* Turnstile / contact-form CAPTCHA widgets (added 2026-08-27) — the widget
   itself has a fixed ~300px width regardless of its container, so center
   it within the otherwise-stretched .authForm column. */
.authTurnstile,
.contactTurnstile{
    align-self:center;
}

/* :not([class]) for the same reason as .alertModalTextBlock h3/p: this is the
   base look for the UNCLASSED controls in these forms, and written bare it
   was (0,1,1) and beat every control that styles itself.
   tools/check-cascade.mjs, 2026-10-13: #reportIssueCategory asked for 10px
   12px padding, #c5d2e0 and --fs-body and rendered 12px 14px, white and
   --fs-emphasis. .reportIssueTextarea and .quietHoursTimezoneSelect sit in
   the same forms and were losing the same declarations.
   Of the 16 controls inside a .authForm, 13 are unclassed and want this; the
   three that carry a class each set background, border, radius, padding,
   colour, font and width themselves, so nothing is left bare. */
.authForm input:not([class]),
.authForm textarea:not([class]),
.authForm select:not([class]){
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:12px 14px;
    color:white;
    font-size:var(--fs-emphasis);
    font-family:inherit;
    resize:vertical;
    width:100%;
}

.demoForm{
    max-width:520px;
}

.demoFormRow{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:12px;
}

@media (max-width:480px){
    .demoFormRow{
        grid-template-columns:1fr;
    }
}

.authForm button{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:12px 20px;
    font-size:var(--fs-emphasis);
    font-weight:bold;
    cursor:pointer;
    margin-top:4px;
}

.authForm button:disabled{
    opacity:.6;
    cursor:default;
}

.authError{
    color:#e74c3c;
    font-size:var(--fs-body);
}

.authError--info{
    color:#87ceeb;
}

.authError--hidden{
    display:none;
}

.authToggleModeBtn{
    background:none;
    border:none;
    color:#9fb3c8;
    font-size:var(--fs-body);
    cursor:pointer;
    margin-top:20px;
    text-decoration:underline;
}

.authHidden{
    display:none !important;
}

/* INVITE SIGNUP (2026-10-13) — the banner, the locked email, and the confirm
   field. All three are absent from an ordinary visit: the sign-in screen is
   unchanged, which matters because it is the only screen every single user
   sees. */

.authInviteBanner{
    display:flex;
    flex-direction:column;
    gap:4px;
    text-align:left;
    margin-bottom:14px;
    padding:12px 14px;
    background:#10233a;
    border:1px solid #1d4b78;
    border-left:3px solid #0f75bc;
    border-radius:8px;
}

.authInviteBanner--hidden{
    display:none;
}

.authInviteBannerTitle{
    font-size:var(--fs-caption);
    font-weight:700;
    letter-spacing:1px;
    text-transform:uppercase;
    color:#74b9ff;
}

.authInviteBannerBody{
    font-size:var(--fs-body);
    line-height:1.5;
    color:#c5d2e0;
}

/* readOnly rather than disabled, so the value still submits and a screen
   reader still announces it — a disabled input is skipped entirely, which
   would leave the invitee with no way to hear which address they are joining
   as.

   THE WHOLE INPUT, not just the two overrides. `.authForm input` was narrowed
   to `:not([class])` on 2026-10-13, so the moment this class lands on
   #authEmail the base rule stops matching it and the field loses its
   background, border, radius, padding, font and width. The first draft of
   this set only `background` and `color` (with !important, which would not
   have helped) and produced an unstyled browser-default box.

   That is the documented cost of `:not([class])`: a classed control styles
   itself in full. Spelled out rather than won back with !important — see the
   sign-out bug. */
.authEmailLocked{
    background:#0a0e14;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:12px 14px;
    color:#9fb3c8;
    font-size:var(--fs-emphasis);
    font-family:inherit;
    width:100%;
    cursor:not-allowed;
}

/* Full styling, for the same reason as .authEmailLocked above — and this one
   is the proof that the trap is easy to fall into: it was caught in a
   SCREENSHOT, as a white browser-default box sitting under a correctly styled
   password field, one element after fixing the identical problem.
   `.authForm input:not([class])` does not match a classed input. Either give
   the control no class, or style it completely. */
.authPasswordConfirm{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:12px 14px;
    color:white;
    font-size:var(--fs-emphasis);
    font-family:inherit;
    width:100%;
}

.authPasswordConfirm--hidden{
    display:none;
}

.authPasswordMatch{
    display:block;
    font-size:var(--fs-caption);
    color:#e8b4b0;
    margin-top:-6px;
}

/* Green only once they actually match. Red-by-default while someone is still
   typing their second password is noise, not feedback — see
   updatePasswordMatchNote(), which shows nothing at all until the field has
   something in it. */
.authPasswordMatch--ok{
    color:#49ba6f;
}

.authPasswordMatch--hidden{
    display:none;
}

.authPasswordHint{
    display:block;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin-top:-6px;
}

.authPasswordHint--hidden{
    display:none;
}

.authTosRow{
    display:flex;
    align-items:flex-start;
    gap:8px;
    font-size:var(--fs-caption);
    line-height:1.4;
    color:#9fb3c8;
    cursor:pointer;
}

.authTosRow--hidden{
    display:none;
}

.authTosRow input[type="checkbox"]{
    flex:0 0 auto;
    width:14px;
    height:14px;
    margin-top:2px;
    cursor:pointer;
}

.authTosRow a{
    color:#87ceeb;
    text-decoration:underline;
}

.authLegalLinks{
    display:flex;
    align-items:center;
    justify-content:center;
    gap:6px;
    margin-top:10px;
    font-size:var(--fs-caption);
}

.authLegalLinks a{
    color:#8a9bb3;
    text-decoration:underline;
}

.authLegalLinks span{
    color:#8a9bb3;
}

.footerLegalLinks{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:6px;
}

/* nowrap so a narrow footer wraps this list BETWEEN links (Terms of
   Service / Privacy Policy / Contact Us each staying on one line, per the
   flex-wrap above) rather than breaking a single link's own words across
   lines — the awkward "Terms" / "of" / "Service" stack this used to
   produce once .footerLeftGroup got squeezed by the paginated-footer
   overlap bug (see the mobile #footer rules below) or just a small phone
   screen. */
.footerLegalLinks a{
    color:inherit;
    text-decoration:underline;
    white-space:nowrap;
}

/* Report an Issue is a button, not a link — it opens a modal rather than
   navigating — but it sits in a row of links and must read as one of them.
   Everything here is undoing the browser's button defaults; the font-size
   inherit matters most, since a footer-sized row with one 13px system-font
   button in it looks like a mistake. */
.footerLinkBtn{
    background:none;
    border:none;
    /* padding-block, not padding:0. This button was listed in the grouped
       touch-target rule above and stayed 16px tall anyway: this rule is
       declared later at equal specificity, so its padding:0 won and the fix
       did nothing. Measured, which is the only reason it was noticed. */
    padding:5px 0;
    display:inline-block;
    margin:0;
    font:inherit;
    color:inherit;
    text-decoration:underline;
    white-space:nowrap;
    cursor:pointer;
}

.footerLinkBtn:hover{
    color:#e8ebee;
}

/* ---- Report an Issue modal ---- */

.reportIssueLabel{
    display:block;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin:14px 0 6px;
}

.reportIssueSelect,
.reportIssueTextarea{
    width:100%;
    background:#0d1117;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    font-family:inherit;
    font-size:var(--fs-body);
}

.reportIssueSelect{
    min-height:var(--tap-min);
}

.reportIssueTextarea{
    resize:vertical;
    min-height:120px;
    line-height:1.5;
}

.reportIssueSelect:focus,
.reportIssueTextarea:focus{
    outline:2px solid #0f75bc;
    outline-offset:1px;
}

/* Amber, not red: this is a redirection, not a failure. #f1c40f on #161b22
   measures 11.4:1 — the same pairing the recipient-allowance line uses. */
.reportIssueUrgent{
    font-size:var(--fs-caption);
    color:#f1c40f;
    line-height:1.6;
    margin:14px 0 0;
    padding:10px 12px;
    background:rgba(241, 196, 15, .08);
    border-radius:6px;
}

.reportIssueActions{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
    margin-top:16px;
}


/* ==========================
   ADMIN DASHBOARD (admin.html)
========================== */

.adminHeader{
    display:flex;
    justify-content:space-between;
    align-items:center;
    padding:16px 24px;
    background:#1b2430;
    border-bottom:3px solid #2e3c4f;
    flex-wrap:wrap;
    gap:12px;
}

/* Logo lockup in place of the old plain "WxSentinel Admin" h1 text
   (2026-09-18) — same move already made for about/terms/privacy via
   .marketingHeaderLogo, and for the same reason: the wordmark lives in the
   logo, so a heading repeating it said the name twice. The old
   ".adminHeader h1" rule is gone with it — the only h1 in this header is
   now .adminHeaderBadge, which carries its own type. Height comes from the
   shared --brand-logo-height, so this tracks the dashboard header and the
   sign-in card, including their automatic drop to 40px below 640px. */
.adminHeaderBrand{
    display:flex;
    align-items:center;
    gap:14px;
    min-width:0;
}

.adminHeaderLogo{
    height:var(--brand-logo-height);
    width:auto;
    display:block;
}

/* "Admin" is a qualifier on the wordmark beside it, not a competing
   title, so it's set quiet and small and separated by a rule rather than
   sized up to heading weight. */
.adminHeaderBadge{
    font-size:var(--fs-caption);
    font-weight:700;
    letter-spacing:1.4px;
    text-transform:uppercase;
    color:#9fb3c8;
    padding-left:14px;
    border-left:2px solid #2e3c4f;
    white-space:nowrap;
}

/* Sign-in card twin of .adminHeaderBadge — sits under the logo inside
   .authCard, which is centred, so it needs no rule/divider to separate it. */
.adminAuthBadge{
    font-size:var(--fs-caption);
    font-weight:700;
    letter-spacing:1.4px;
    text-transform:uppercase;
    color:#9fb3c8;
    margin-bottom:6px;
}

.adminMain{
    flex:1;
    padding:24px;
    display:flex;
    flex-direction:column;
    gap:24px;
    max-width:1000px;
    width:100%;
    margin:0 auto;
}

.adminSection{
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    padding:20px;
}

.adminSectionTitle{
    font-size:var(--fs-emphasis);
    color:#9fb3c8;
    text-transform:uppercase;
    letter-spacing:1px;
    margin-bottom:16px;
}

.adminUnauthorized{
    text-align:center;
    color:#e74c3c;
    font-size:var(--fs-emphasis);
    padding:60px 20px;
}

.adminEmptyState, .adminLoadingState{
    color:#9fb3c8;
    font-size:var(--fs-body);
    padding:12px 0;
}

.createAccountForm{
    display:flex;
    flex-wrap:wrap;
    gap:12px;
    align-items:center;
}

/* DIRECT CHILDREN ONLY.
   
   These three rules style the form's own top row — company name, owner's
   email, plan select. Written as descendant selectors (`.createAccountForm
   input`), they also reached the six Contract Terms boxes nested inside
   .contractGrid, with two visible consequences:
   
     * `min-width:180px` forced each contract input wider than its grid track
       (measured: 180px boxes in a 146px track at 1024px wide), so the six
       boxes spilled out of their cells and OVERLAPPED each other. Five
       overlapping pairs at 1024px and 900px; only clean at ~1280px and above,
       which is why it looked fine on a maximised window.
     * `padding:10px 12px` beat .contractInput's own `7px 9px`, because
       `.createAccountForm input` (0,1,1) outranks `.contractInput` (0,1,0).
       The contract boxes were never getting the compact padding they define.
   
   The child combinator fixes both at the source rather than adding overrides
   to .contractInput, which would have left the same trap set for the next
   thing nested in this form. `flex` is also meaningless on a grid item, so
   scoping it here removes a declaration that only ever looked like it worked. */
.createAccountForm > input, .createAccountForm > select{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    color:white;
    font-size:var(--fs-body);
}

.createAccountForm > input[type="text"]{
    flex:2;
    min-width:180px;
}

.createAccountForm > input[type="email"]{
    flex:2;
    min-width:200px;
}

.createAccountForm button{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:10px 20px;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
}

.createAccountForm button:disabled{
    opacity:.6;
    cursor:default;
}

.adminStatusMsg{
    width:100%;
    font-size:var(--fs-body);
    color:#9fb3c8;
}

.adminStatusMsg--error{
    color:#e74c3c;
}

.adminStatusMsg--info{
    color:#87ceeb;
}

.accountList{
    display:flex;
    flex-direction:column;
    gap:16px;
}

.accountCard{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:16px;
}

.accountCardTop{
    display:flex;
    justify-content:space-between;
    align-items:flex-start;
    flex-wrap:wrap;
    gap:10px;
    margin-bottom:10px;
}

.accountCardName{
    font-size:var(--fs-subhead);
    font-weight:bold;
}

.accountCardMeta{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin-top:4px;
}

.accountBadge{
    display:inline-block;
    font-size:var(--fs-micro);
    font-weight:bold;
    text-transform:uppercase;
    letter-spacing:.5px;
    padding:3px 8px;
    border-radius:4px;
    background:#2e3c4f;
    color:#c5d2e0;
    margin-left:6px;
}

.accountBadge--active{
    background:#163a26;
    color:#2ecc71;
}

.accountBadge--past_due{
    background:#3a3214;
    color:#f1c40f;
}

.accountBadge--canceled{
    background:#3a1414;
    color:#e74c3c;
}

.accountStripeLink{
    font-size:var(--fs-caption);
    color:#87ceeb;
    text-decoration:underline;
}

.planChangeRow{
    display:flex;
    gap:8px;
    align-items:center;
    flex-wrap:wrap;
    margin-bottom:12px;
}

.planChangeRow select{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 10px;
    color:white;
    font-size:var(--fs-body);
}

.adminBtn{
    background:#161b22;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 14px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
    white-space:nowrap;
}

.adminBtn:hover{
    background:#2e3c4f;
}

.adminBtn:disabled{
    opacity:.5;
    cursor:default;
}

.adminBtn--primary{
    background:#2ecc71;
    color:#0d1117;
    border:none;
}

.adminBtn--primary:hover{
    background:#27ae60;
}

.adminBtn--danger{
    color:#e74c3c;
    border-color:#5a2a2a;
}

/* #2e0f0f, not the #3a1414 it was. Found by the contrast sweep added
   2026-10-07: #e74c3c on #3a1414 measures 4.26:1, just under the 4.5:1
   floor, so the Remove button was hardest to read at the moment the pointer
   was on it. Pre-existing and unrelated to the --caution duplicate; it only
   surfaced because that check measures the composited result instead of
   reading the declared colour. */
.adminBtn--danger:hover{
    background:#2e0f0f;
}

/* An action that reaches into a customer's data, or promotes something into
   the tier people scan first during an outbreak. Not destructive, so not
   red — but it must not sit in a row of ordinary buttons looking like one.
   
   DEFINED ONCE, HERE, AND THAT IS THE FIX. There were two .adminBtn--caution
   blocks, 800 lines apart: this shape, and an older solid-yellow fill with
   dark text from the Alert Priority work. Source order gave the later block's
   `background:#f1c40f` the win while this block's `!important` kept its
   `color:#f1c40f` — so "Open Their Dashboard" rendered yellow text on a
   yellow background, measured at 1.00:1. The label was never missing; it was
   painted in the background colour, and only appeared on hover because the
   hover rule here carried its own !important.
   
   Shaped like --danger on purpose. They are the two halves of one ternary in
   the tier confirm dialog (admin-tiers.js), and before this the same button
   was outlined-red going one way and solid-yellow going the other.
   
   No !important anywhere: those existed only to fight the duplicate.
   #f1c40f on #161b22 measures 10.41:1, and 7.53:1 on the hover fill. */
.adminBtn--caution{
    color:#f1c40f;
    border-color:rgba(241, 196, 15, .45);
}

.adminBtn--caution:hover{
    background:#3a3410;
}

.memberList, .inviteList{
    display:flex;
    flex-direction:column;
    gap:6px;
    margin-top:6px;
}

.memberRow, .inviteRow{
    display:flex;
    justify-content:space-between;
    align-items:center;
    flex-wrap:wrap;
    gap:8px;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 12px;
}

.memberRowInfo, .inviteRowInfo{
    font-size:var(--fs-body);
}

.memberRowEmail, .inviteRowEmail{
    font-weight:bold;
}

.memberRowRole, .inviteRowRole{
    color:#9fb3c8;
    text-transform:capitalize;
    margin-left:6px;
}

.inviteRowPending{
    color:#f1c40f;
    font-size:var(--fs-micro);
    text-transform:uppercase;
    letter-spacing:.5px;
    margin-left:6px;
}

.memberRowActions, .inviteRowActions{
    display:flex;
    gap:6px;
    flex-wrap:wrap;
}

.accountSubSectionLabel{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    text-transform:uppercase;
    letter-spacing:.5px;
    margin-top:12px;
    margin-bottom:4px;
}



/* ==========================
   ENTERPRISE QUOTE MODEL (admin.html)
   Added 2026-09-27. Ported from the internal quoting artifact, restyled onto
   this page's existing palette rather than carrying its own: #161b22 section,
   #0d1117 inset, #2e3c4f borders, #9fb3c8 meta, #87ceeb for the interactive
   blue, and the same #2ecc71 / #e74c3c that already mean good and bad on the
   account badges above.

   Numbers use Consolas rather than a web font on purpose — the CSP sets
   font-src 'self', so nothing can be pulled from Google Fonts, and the
   global *{font-family:Segoe UI...} rule means every numeric class here has
   to name its own family to win (a class selector beats the universal one).
========================== */

.quoteToggle{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:12px;
    width:100%;
    background:none;
    border:none;
    padding:0;
    color:#9fb3c8;
    cursor:pointer;
    text-align:left;
    min-height:var(--tap-min);
}

.quoteToggle:hover{
    color:#c5d2e0;
}

/* .adminSectionTitle carries a 16px bottom margin that makes sense when the
   title sits above content; inside the toggle row it just pushes the chevron
   off-centre. */
.quoteToggleTitle{
    margin-bottom:0;
}

.quoteIntro{
    font-size:var(--fs-body);
    color:#9fb3c8;
    margin:16px 0 0;
    max-width:70ch;
    line-height:1.5;
}

.quoteIntro strong{
    color:white;
    font-weight:600;
}

.quoteAccountName{
    font-size:var(--fs-body);
    color:#87ceeb;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    margin-top:12px;
    line-height:1.5;
}

/* minmax(0,1fr), not 1fr: a bare 1fr is minmax(auto,1fr), so the track floors
   at the content's min-width and the wide stress-test table pushes the whole
   column past the viewport on a narrow screen. */
.quoteGrid{
    display:grid;
    grid-template-columns:minmax(0,320px) minmax(0,1fr);
    gap:16px;
    align-items:start;
    margin-top:16px;
}

.quoteGrid > div{
    min-width:0;
}

@media (max-width:860px){
    .quoteGrid{
        grid-template-columns:minmax(0,1fr);
    }
}

.quotePanel{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:16px;
    min-width:0;
}

.quotePanel + .quotePanel{
    margin-top:14px;
}

.quotePanelTitle{
    font-size:var(--fs-micro);
    color:#8a9bb3;
    text-transform:uppercase;
    letter-spacing:1px;
    font-weight:bold;
    margin-bottom:14px;
}

/* ---- sliders + click-to-type readouts ---- */

.quoteCtl{
    margin-bottom:18px;
}

.quoteCtl:last-child{
    margin-bottom:0;
}

.quoteCtlHead{
    display:flex;
    justify-content:space-between;
    align-items:baseline;
    gap:10px;
}

.quoteCtlLabel{
    font-size:var(--fs-body);
    font-weight:bold;
}

.quoteValWrap{
    display:inline-flex;
    align-items:baseline;
}

/* A real <button>, so it is focusable and clickable without any role or
   tabindex patching, and it swaps in place for a text input. */
.quoteValBtn{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-emphasis);
    font-weight:bold;
    color:#87ceeb;
    background:transparent;
    border:0;
    margin:0 -6px;
    padding:2px 6px;
    border-radius:5px;
    cursor:pointer;
    line-height:1.35;
    text-decoration:underline dotted;
    text-underline-offset:4px;
    text-decoration-color:#2e3c4f;
}

.quoteValBtn:hover{
    background:#161b22;
    text-decoration-color:#87ceeb;
}

.quoteValInput{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-emphasis);
    font-weight:bold;
    color:#87ceeb;
    text-align:right;
    background:#161b22;
    border:1px solid #87ceeb;
    border-radius:5px;
    padding:2px 6px;
    width:7.5ch;
    max-width:120px;
    line-height:1.35;
    appearance:none;
}

.quoteNote{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    margin:3px 0 9px;
    line-height:1.45;
}

.quoteRange{
    width:100%;
    margin:0;
    height:22px;
    background:transparent;
    -webkit-appearance:none;
    appearance:none;
    cursor:pointer;
}

.quoteRange::-webkit-slider-runnable-track{
    height:5px;
    border-radius:3px;
    background:#161b22;
    border:1px solid #2e3c4f;
}

.quoteRange::-moz-range-track{
    height:5px;
    border-radius:3px;
    background:#161b22;
    border:1px solid #2e3c4f;
}

.quoteRange::-webkit-slider-thumb{
    -webkit-appearance:none;
    appearance:none;
    width:19px;
    height:19px;
    margin-top:-8px;
    border-radius:50%;
    background:#87ceeb;
    border:2px solid #0d1117;
    box-shadow:0 1px 4px rgba(0,0,0,.45);
}

.quoteRange::-moz-range-thumb{
    width:19px;
    height:19px;
    border-radius:50%;
    background:#87ceeb;
    border:2px solid #0d1117;
    box-shadow:0 1px 4px rgba(0,0,0,.45);
}

.quoteSwitchRow{
    display:flex;
    align-items:flex-start;
    gap:11px;
}

.quoteSwitchRow input{
    margin:3px 0 0;
    width:17px;
    height:17px;
    accent-color:#87ceeb;
    flex:0 0 auto;
    cursor:pointer;
}

.quoteSwitchRow label{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    cursor:pointer;
    line-height:1.5;
}

.quoteSwitchRow label b{
    color:white;
    display:block;
}

/* ---- current-contract inputs ----
   Typed fields, not sliders, on purpose: these get read off an invoice, and
   the price in particular is a negotiated number the formula cannot
   reproduce. */

.quoteMiniGrid,
.contractGrid{
    display:grid;
    grid-template-columns:repeat(auto-fit,minmax(128px,1fr));
    gap:11px;
    margin-top:14px;
}

.quoteMiniField label,
.contractField label{
    display:block;
    font-size:var(--fs-micro);
    text-transform:uppercase;
    letter-spacing:.6px;
    color:#8a9bb3;
    font-weight:bold;
    margin-bottom:4px;
}

.quoteMiniInput{
    width:100%;
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-body);
    font-weight:bold;
    color:white;
    text-align:right;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 9px;
}

.quoteMiniInput--price{
    color:#87ceeb;
}

/* ---- hero ---- */

.quoteHero{
    background:linear-gradient(180deg,rgba(135,206,235,.10),transparent 70%);
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:18px;
    margin-bottom:14px;
}

.quoteHeroLabel{
    font-size:var(--fs-micro);
    letter-spacing:1px;
    text-transform:uppercase;
    color:#8a9bb3;
    font-weight:bold;
    margin-bottom:6px;
}

.quoteHeroRow{
    display:flex;
    align-items:baseline;
    gap:12px;
    flex-wrap:wrap;
}

.quoteHeroNum{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-hero);
    font-weight:bold;
    line-height:1;
}

.quoteHeroUnit{
    color:#8a9bb3;
    font-size:var(--fs-body);
}

.quoteHeroAnnual{
    color:#9fb3c8;
    font-size:var(--fs-caption);
    margin-top:8px;
}

.quoteHeroAnnual b{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    color:white;
}

.quotePill{
    display:inline-flex;
    align-items:center;
    font-size:var(--fs-micro);
    font-weight:bold;
    padding:2px 9px;
    border-radius:999px;
    border:1px solid currentColor;
    white-space:nowrap;
}

.quotePill--good{
    color:#2ecc71;
    background:#163a26;
}

.quotePill--warn{
    color:#f1c40f;
    background:#3a3214;
}

.quotePill--bad{
    color:#e74c3c;
    background:#3a1414;
}

/* ---- tiles ---- */

.quoteTiles{
    display:grid;
    grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
    gap:10px;
}

.quoteTiles--wide{
    grid-template-columns:repeat(auto-fit,minmax(185px,1fr));
}

.quoteTile{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:12px 13px;
}

.quoteTile--hero{
    background:rgba(135,206,235,.08);
    border-color:#87ceeb;
}

.quoteTileLabel{
    font-size:var(--fs-micro);
    color:#8a9bb3;
    text-transform:uppercase;
    letter-spacing:.7px;
    font-weight:bold;
    margin-bottom:5px;
}

.quoteTileVal{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-heading);
    font-weight:bold;
    line-height:1.15;
}

.quoteTile--hero .quoteTileVal{
    font-size:var(--fs-display);
}

.quoteTileSub{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    margin-top:3px;
}

/* ---- tables ---- */

.quoteTableWrap{
    overflow-x:auto;
    min-width:0;
    margin-top:12px;
}

.quoteTable{
    width:100%;
    border-collapse:collapse;
    font-size:var(--fs-caption);
}

.quoteTable th,
.quoteTable td{
    text-align:left;
    padding:8px 9px;
    border-bottom:1px solid #1e2733;
    white-space:nowrap;
}

.quoteTable th{
    font-size:var(--fs-micro);
    text-transform:uppercase;
    letter-spacing:.7px;
    color:#8a9bb3;
    font-weight:bold;
}

.quoteTable tr:last-child td{
    border-bottom:none;
}

.quoteTable td.quoteNum,
.quoteTable th.quoteNum{
    text-align:right;
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
}

.quoteTable td.quoteRowLabel{
    font-weight:bold;
}

.quoteTable td.quoteNeg{
    color:#8a9bb3;
}

.quoteTable td.quoteStrong{
    font-weight:bold;
}

.quoteTable td.quoteChg{
    font-weight:bold;
}

.quoteTable td.quoteUp{
    color:#2ecc71;
}

.quoteTable td.quoteDown{
    color:#e74c3c;
}

.quoteTable td.quoteFlat{
    color:#8a9bb3;
    font-weight:normal;
}

/* Shared state colours for the tile values and the loss case. */
.quoteLoss{
    color:#e74c3c;
}

.quoteWarnText{
    color:#f1c40f;
}

.quoteGoodText{
    color:#2ecc71;
}

/* ---- callouts ---- */

.quoteCallout{
    border-radius:8px;
    padding:12px 14px;
    font-size:var(--fs-caption);
    line-height:1.55;
    border:1px solid;
    margin-top:12px;
    color:#c5d2e0;
}

.quoteCallout b{
    display:block;
    margin-bottom:3px;
}

/* The inline "Note:" inside a callout must not become a block like the
   heading above — it sits mid-sentence. */
.quoteCallout b.quoteInlineB{
    display:inline;
    margin-bottom:0;
}

.quoteCallout--good{
    background:rgba(46,204,113,.10);
    border-color:#2ecc71;
}

.quoteCallout--warn{
    background:rgba(241,196,15,.10);
    border-color:#f1c40f;
}

.quoteCallout--bad{
    background:rgba(231,76,60,.10);
    border-color:#e74c3c;
}

.quoteCallout--good b{
    color:#2ecc71;
}

.quoteCallout--warn b{
    color:#f1c40f;
}

.quoteCallout--bad b{
    color:#e74c3c;
}

/* ---- price build-up ---- */

.quoteBuild{
    font-size:var(--fs-caption);
}

.quoteBuild div{
    display:flex;
    justify-content:space-between;
    gap:12px;
    padding:7px 0;
    border-bottom:1px dashed #1e2733;
    color:#c5d2e0;
}

.quoteBuild div:last-child{
    border-bottom:none;
    border-top:1px solid #2e3c4f;
    font-weight:bold;
    color:white;
    padding-top:10px;
}

.quoteBuild span:last-child{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
}

/* ---- export ---- */

.quoteExportRow{
    display:flex;
    gap:8px;
    flex-wrap:wrap;
    margin-bottom:10px;
}

.quoteSummaryOut{
    width:100%;
    margin-top:10px;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:12px;
    color:#c5d2e0;
    font-family:Consolas, "Courier New", monospace;
    font-size:var(--fs-caption);
    line-height:1.5;
    resize:vertical;
}

/* ---- assumptions ---- */

.quoteAssump{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    line-height:1.6;
}

.quoteAssump p{
    margin:0 0 9px;
}

.quoteAssump p:last-child{
    margin-bottom:0;
}

.quoteAssump b{
    color:#c5d2e0;
}

.quoteAssump code{
    font-family:Consolas, "Courier New", monospace;
    background:#161b22;
    padding:1px 5px;
    border-radius:4px;
    color:#c5d2e0;
}



/* ==========================
   FORECAST PHASE 3 — multi-hazard outlooks (2026-09-28)
   Family filter chips, per-row family tags, and the family label on each
   risk-strip item. The label is never omitted in favour of colour alone:
   the four NOAA palettes overlap, so a red pill is Moderate on the
   excessive-rainfall scale and Major on the winter one.
========================== */

.forecastFamilyChips{
    display:flex;
    flex-wrap:wrap;
    gap:6px;
    margin-bottom:10px;
}

.forecastFamilyChips:empty{
    display:none;
}

.forecastFamilyChip{
    background:#161b22;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    border-radius:999px;
    padding:5px 13px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
    min-height:32px;
}

.forecastFamilyChip:hover{
    background:#2e3c4f;
    color:white;
}

.forecastFamilyChip--active{
    background:#0f75bc;
    border-color:#0f75bc;
    color:white;
}

/* One row per severity tier. Colour is confined to the 5px bar and the tag
   outline — the flat per-location list this replaced leaned on filled colour
   chips, which read as metadata tags rather than as danger, and gave a
   Moderate-risk site the same visual weight as a Slight one. */
.forecastTierRows{
    display:flex;
    flex-direction:column;
}

.forecastTierRow{
    border-bottom:1px solid #1e2733;
}

.forecastTierRow:last-child{
    border-bottom:none;
}

.forecastTierBtn{
    display:flex;
    align-items:flex-start;
    gap:14px;
    width:100%;
    text-align:left;
    background:none;
    border:none;
    padding:13px 4px;
    cursor:pointer;
    min-height:var(--tap-min);
    /* A <button> does NOT inherit color — it falls back to the UA's
       `buttontext`, which is near-black. Clearing background and border
       without also setting color left the tier name as black text on the
       #161b22 panel. Set here rather than only on the child so nothing
       added inside this button later inherits the wrong colour. */
    color:white;
}

.forecastTierBtn:hover{
    background:#0d1117;
}

.forecastTierBar{
    width:5px;
    flex:0 0 5px;
    align-self:stretch;
    min-height:38px;
    border-radius:3px;
}

/* Tabular so 1, 3 and 12 stack in a column the eye can run down — this is
   the number people are scanning for. */
.forecastTierCount{
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-heading);
    font-weight:bold;
    line-height:1.1;
    flex:0 0 32px;
    text-align:right;
    color:white;
}

.forecastTierBody{
    flex:1;
    min-width:0;
}

.forecastTierName{
    display:block;
    font-size:var(--fs-emphasis);
    font-weight:bold;
    line-height:1.3;
    color:white;
}

.forecastTierSummary{
    display:block;
    font-size:var(--fs-caption);
    color:#8a9bb3;
    margin-top:3px;
    line-height:1.45;
}

.forecastTierTag{
    margin-left:auto;
    flex:0 0 auto;
    font-size:var(--fs-micro);
    font-weight:bold;
    text-transform:uppercase;
    letter-spacing:.6px;
    padding:3px 9px;
    border-radius:4px;
    border:1px solid currentColor;
    background:transparent;
    white-space:nowrap;
}

.forecastTierLocations{
    display:flex;
    flex-wrap:wrap;
    gap:6px;
    padding:0 4px 13px 65px;
}

/* The [hidden] attribute only sets display:none at user-agent specificity,
   so the class rule above beat it and every tier rendered expanded. Any
   element toggled by `hidden` that also sets `display` in a class needs this
   companion rule — same trap as .historyEmpty--hidden elsewhere in this file,
   which is why those use a class instead. */
.forecastTierLocations[hidden]{
    display:none;
}

.forecastTierLocation{
    font-size:var(--fs-caption);
    color:#c5d2e0;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:4px;
    padding:3px 9px;
}

/* ==========================
   TIER ROW DETAIL (2026-09-28)
   ==========================
   Styles forecastTierDetailHtml()'s per-location rows — the SPC tornado/
   wind/hail probabilities and SIG flag that the Phase 3 rework left
   unreachable. A convective tier expands into one of these per site; the
   other families still expand into the plain .forecastTierLocation chips
   above, since they publish no probability layers.

   .forecastTierLocations is a flex-wrap row, so each detail row takes the
   full width to stack under its own heading rather than tiling beside the
   next site's chips. */
.forecastDetailRow{
    flex:0 0 100%;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
}

.forecastDetailRowHead{
    display:flex;
    align-items:baseline;
    justify-content:space-between;
    gap:12px;
    flex-wrap:wrap;
    margin-bottom:9px;
}

/* Explicit, not inherited. The tier row above is a <button>, which does not
   inherit color — that produced near-black headings on this same panel once
   already (see .forecastTierBtn's note). These rows sit in a <div> so they
   would inherit correctly today, but stating it costs nothing and makes the
   panel immune to the same mistake if it ever moves. */
.forecastDetailRowHead .forecastDetailLocationName{
    color:#fff;
}

/* Overrides .forecastOutlookLink's --fs-body/bold: here it's a secondary
   action beside a site name, not the panel's main call to action. */
.forecastDetailRow .forecastOutlookLink{
    font-size:var(--fs-caption);
    font-weight:600;
}

/* Last child in the row — no trailing gap under the chips. */
.forecastDetailRow .forecastHazardChips{
    margin-bottom:0;
}

.forecastDetailNote{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    font-style:italic;
}

/* The hazard family inside a risk-strip chip. Four families' official NOAA
   palettes overlap — red is Moderate on one scale and Major on another — so
   the colour alone can't say which hazard a chip is about. Sits on the
   chip's own coloured fill, so it takes the same dark ink as its parent
   rather than a muted grey, which would not survive on a yellow fill. */
.forecastRiskStripFamily{
    font-size:var(--fs-micro);
    font-weight:700;
    text-transform:uppercase;
    letter-spacing:.6px;
    opacity:.75;
    margin-right:5px;
}

.forecastFoot{
    display:flex;
    flex-wrap:wrap;
    gap:16px;
    align-items:center;
    margin-top:13px;
    padding-top:12px;
    border-top:1px solid #1e2733;
    font-size:var(--fs-caption);
}

.forecastFootOk{
    color:#3ecf8e;
    font-weight:600;
}

.forecastFootWarn{
    color:#f1c40f;
    font-weight:600;
}

.forecastAllClear{
    color:#3ecf8e;
    font-size:var(--fs-body);
    font-weight:600;
    padding:20px 2px;
}

.forecastRefreshRow{
    display:flex;
    align-items:center;
    gap:12px;
    flex-wrap:wrap;
    margin-top:16px;
    padding-top:14px;
    border-top:1px solid #2e3c4f;
}

.forecastRefreshBtn{
    background:#161b22;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 16px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
    min-height:36px;
}

.forecastRefreshBtn:hover{
    background:#2e3c4f;
    color:white;
}

.forecastRefreshBtn:disabled{
    opacity:.6;
    cursor:default;
}

.forecastRefreshNote{
    font-size:var(--fs-caption);
    color:#8a9bb3;
}

.forecastEmptyFiltered{
    color:#8a9bb3;
    font-size:var(--fs-body);
    padding:18px 2px;
}

/* ==========================
   CONTRACT TERMS (admin.html)
   Added 2026-09-28. The negotiated per-account limits an Enterprise account
   is hand-built with, from the figures the quote model produced.

   .contractGrid and .contractField label share their rules with the quote
   model's mini inputs above — same control, same job, same panel. The INPUT
   is defined separately rather than aliased because it appears on two
   different surfaces: the create form sits on an .adminSection (#161b22) and
   the per-account editor sits inside an .accountCard (#0d1117), so the fill
   has to invert between them or one of the two looks wrong.
========================== */

.contractInput{
    width:100%;
    font-family:Consolas, "Courier New", monospace;
    font-variant-numeric:tabular-nums;
    font-size:var(--fs-body);
    font-weight:bold;
    color:white;
    text-align:right;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 9px;
}

.accountCard .contractInput{
    background:#161b22;
}

.contractInput--price{
    color:#87ceeb;
}

.contractTermsBlock{
    margin-top:16px;
    padding-top:16px;
    border-top:1px solid #2e3c4f;
    width:100%;
}

.contractTermsTitle{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    text-transform:uppercase;
    letter-spacing:.7px;
    font-weight:bold;
}

.contractTermsTitle span{
    text-transform:none;
    letter-spacing:0;
    font-weight:normal;
    color:#8a9bb3;
}

.contractNotes{
    width:100%;
    margin-top:11px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:9px 11px;
    color:white;
    font-size:var(--fs-body);
    resize:vertical;
}

.accountCard .contractNotes{
    background:#161b22;
}

.contractActions{
    display:flex;
    gap:8px;
    flex-wrap:wrap;
    margin-top:11px;
    align-items:center;
}

/* Summary chips on an account card. Deliberately "used / limit" rather than
   just the limit — how close they are to it is the number that matters when
   you open this page. */
.contractSummary{
    display:flex;
    flex-wrap:wrap;
    gap:6px;
    margin-top:10px;
}

.contractChip{
    display:inline-flex;
    align-items:center;
    gap:5px;
    font-size:var(--fs-micro);
    font-weight:bold;
    padding:3px 9px;
    border-radius:4px;
    background:#161b22;
    border:1px solid #2e3c4f;
    color:#c5d2e0;
    white-space:nowrap;
}

.contractChipLabel{
    color:#8a9bb3;
    font-weight:600;
    text-transform:uppercase;
    letter-spacing:.4px;
}

/* Over an advisory recipient limit. Amber, not red: nothing is broken and
   nobody was blocked — this says "re-quote them", not "fix it now". */
.contractChip--over{
    background:#3a3214;
    border-color:#f1c40f;
    color:#f1c40f;
}

.contractChip--over .contractChipLabel{
    color:#f1c40f;
}

.contractChip--price{
    background:#163a26;
    border-color:#2ecc71;
    color:#2ecc71;
}

.contractChip--price .contractChipLabel{
    color:#2ecc71;
}

.contractChip--unset{
    color:#8a9bb3;
    font-weight:normal;
}

.contractEditForm{
    margin-top:12px;
    padding-top:12px;
    border-top:1px dashed #2e3c4f;
}

.contractWarn{
    font-size:var(--fs-caption);
    color:#f1c40f;
    margin-top:8px;
    line-height:1.5;
}

/* ==========================
   MARKETING PAGE (about.html)
========================== */

.marketingPage{
    max-width:1100px;
    margin:0 auto;
    padding:0 24px 60px;
    width:100%;
}

.marketingHeader{
    display:flex;
    justify-content:space-between;
    align-items:center;
    padding:24px 0;
    flex-wrap:wrap;
    gap:12px;
}

/* about.html/terms.html/privacy.html all use this in place of the plain
   "WxSentinel" h1 text (2026-08-26) — shares --brand-logo-height with the
   dashboard/sign-in logos so all four stay in sync. The old
   ".marketingHeader h1" rule is gone since no page puts an h1 inside
   .marketingHeader anymore (reset-password.html's own h1 lives outside
   this class, untouched). */
.marketingHeaderLogo{
    height:var(--brand-logo-height);
    width:auto;
    display:block;
}

.marketingSignInLink{
    background:#2ecc71;
    color:#0d1117;
    padding:10px 20px;
    border-radius:6px;
    font-weight:bold;
    font-size:var(--fs-body);
    text-decoration:none;
}

.marketingSignInLink:hover{
    background:#27ae60;
}

.marketingHero{
    text-align:center;
    padding:60px 20px;
}

.marketingHero h2{
    font-size:var(--fs-hero);
    line-height:1.3;
    margin-bottom:16px;
}

.marketingHero p{
    font-size:var(--fs-subhead);
    color:#9fb3c8;
    max-width:640px;
    margin:0 auto 32px;
    line-height:1.6;
}

.marketingHeroActions{
    display:flex;
    gap:16px;
    justify-content:center;
    flex-wrap:wrap;
}

.marketingCta{
    background:#2ecc71;
    color:#0d1117;
    padding:14px 28px;
    border-radius:8px;
    font-weight:bold;
    font-size:var(--fs-emphasis);
    text-decoration:none;
}

.marketingCta:hover{
    background:#27ae60;
}

.marketingCtaSecondary{
    background:transparent;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    padding:14px 28px;
    border-radius:8px;
    font-weight:bold;
    font-size:var(--fs-emphasis);
    text-decoration:none;
}

.marketingCtaSecondary:hover{
    background:#161b22;
}

.marketingSection{
    padding:48px 0;
    border-top:1px solid #2e3c4f;
    text-align:center;
}

.marketingSection h3{
    font-size:var(--fs-display);
    margin-bottom:20px;
}

.marketingSection > p{
    font-size:var(--fs-emphasis);
    color:#c5d2e0;
    line-height:1.7;
    max-width:760px;
    margin-left:auto;
    margin-right:auto;
}

/* ==========================
   LEGAL PAGES (Terms of Service / Privacy Policy)
   .legalSection sits on the same <section> as .marketingSection (see
   terms.html/privacy.html) and only overrides text-align — everything
   else about .marketingSection's spacing/border still applies. Equal
   specificity to .marketingSection (both single classes), so this wins
   purely by coming later in the file; keep it below .marketingSection if
   this block ever moves. Long-form legal text reads far worse centered
   than any of the marketing copy .marketingSection was designed for, so
   this un-centers it — the block itself still sits centered on the page
   via .legalContent's margin:0 auto, only the text inside is left-aligned.
   The rest of this block fills in heading/paragraph/list spacing that
   .marketingSection never needed (it only styles a bare h3 + p), since the
   page-wide *{margin:0} reset otherwise leaves every legal heading,
   paragraph, and list packed together with zero space between them.
========================== */

.legalSection{
    text-align:left;
}

.legalContent{
    max-width:760px;
    margin:0 auto;
}

.legalContent h2{
    font-size:var(--fs-hero);
    margin-bottom:8px;
}

.legalContent .legalUpdated{
    color:#9fb3c8;
    font-size:var(--fs-body);
    margin-bottom:32px;
}

.legalContent h3{
    font-size:var(--fs-heading);
    margin-top:32px;
    margin-bottom:12px;
}

.legalContent h3:first-of-type{
    margin-top:0;
}

.legalContent p{
    font-size:var(--fs-emphasis);
    color:#c5d2e0;
    line-height:1.7;
    margin-bottom:16px;
}

.legalContent ul{
    margin:0 0 16px;
    padding-left:24px;
}

.legalContent li{
    font-size:var(--fs-emphasis);
    color:#c5d2e0;
    line-height:1.7;
    margin-bottom:8px;
}

.legalContent a{
    color:#4fa3ff;
}

.legalContent a:hover{
    text-decoration:underline;
}

.marketingFeatureGrid{
    display:grid;
    grid-template-columns:repeat(auto-fill, minmax(260px, 1fr));
    gap:20px;
}

.marketingFeatureCard{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:10px;
    padding:20px;
}

.marketingFeatureCard h4{
    font-size:var(--fs-subhead);
    margin-bottom:8px;
    color:#2ecc71;
}

.marketingFeatureCard p{
    font-size:var(--fs-body);
    color:#9fb3c8;
    line-height:1.6;
}

.marketingPricingGrid{
    display:grid;
    grid-template-columns:repeat(5, 1fr);
    gap:14px;
    align-items:stretch;
}

@media (max-width:860px){
    .marketingPricingGrid{
        grid-template-columns:repeat(3, 1fr);
    }
}

@media (max-width:600px){
    .marketingPricingGrid{
        grid-template-columns:repeat(2, 1fr);
    }
}

@media (max-width:420px){
    .marketingPricingGrid{
        grid-template-columns:1fr;
    }
}

.marketingPricingCard{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:10px;
    padding:16px 12px;
    text-align:center;
    display:flex;
    flex-direction:column;
}

.marketingPricingCard--highlight{
    border-color:#2ecc71;
}

.marketingPricingCard h4{
    font-size:var(--fs-emphasis);
    margin-bottom:6px;
}

.marketingPricingPrice{
    font-size:var(--fs-subhead);
    font-weight:bold;
    margin-bottom:4px;
}

.marketingPricingPrice span{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    font-weight:normal;
}

.marketingPricingSavings{
    font-size:var(--fs-micro);
    font-weight:bold;
    color:#2ecc71;
    margin-bottom:2px;
}

.marketingPricingDetail{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin-top:10px;
    line-height:1.6;
}

/* These sit inside sentences, so they keep the look and the line flow of a
   text link, and the WCAG inline exception applies to them. But "Contact us"
   is the only route to Enterprise from its pricing card, and at 14px tall it
   is a poor thumb target on a phone.
   
   The ::after extends the clickable area 8px above and below WITHOUT touching
   layout, because it is absolutely positioned. Padding would have pushed the
   surrounding sentence apart; this does not move a single pixel of text. */
.marketingLinkBtn{
    position:relative;
    background:none;
    border:none;
    padding:0;
    color:#2ecc71;
    font-size:inherit;
    font-family:inherit;
    text-decoration:underline;
    cursor:pointer;
}

.marketingLinkBtn::after{
    content:"";
    position:absolute;
    inset:-8px -4px;
}

.marketingLinkBtn:hover{
    color:#27ae60;
}

.marketingDemoLink{
    font-size:var(--fs-heading);
    font-weight:bold;
    text-decoration:none;
    text-align:center;
    display:block;
    width:100%;
    margin-top:28px;
}

.marketingDemoLink:hover{
    text-decoration:underline;
}

.contactModalContent{
    width:min(480px, 90vw);
    text-align:left;
}

.contactModalIntro{
    font-size:var(--fs-body);
    color:#9fb3c8;
    line-height:1.5;
    margin-bottom:16px;
}

.contactPreferenceRow{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:16px;
    font-size:var(--fs-body);
    color:#c5d2e0;
    margin-top:-2px;
}

.contactPreferenceLabel{
    color:#9fb3c8;
}

.contactPreferenceOption{
    display:flex;
    align-items:center;
    gap:6px;
    cursor:pointer;
}

.contactPreferenceOption input[type="radio"]{
    width:16px;
    height:16px;
    cursor:pointer;
}

.contactHoneypot{
    position:absolute;
    left:-9999px;
    width:1px;
    height:1px;
    overflow:hidden;
}

.tierCompareWrap{
    margin-top:10px;
    overflow-x:auto;
    border:1px solid #2e3c4f;
    border-radius:10px;
}

.tierCompareTable{
    width:100%;
    border-collapse:collapse;
    background:#161b22;
    font-size:var(--fs-body);
}

.tierCompareTable th,
.tierCompareTable td{
    padding:12px 16px;
    text-align:center;
    border-bottom:1px solid #2e3c4f;
    white-space:nowrap;
}

.tierCompareTable td:first-child,
.tierCompareTable th:first-child{
    text-align:left;
    color:#9fb3c8;
    font-weight:normal;
}

.tierCompareTable thead th{
    color:#e6edf3;
    font-size:var(--fs-emphasis);
    padding-top:16px;
    padding-bottom:16px;
}

.tierCompareTable tbody tr:last-child td{
    border-bottom:none;
}

.tierCompareTable--highlight{
    background:rgba(46, 204, 113, .08);
    color:#2ecc71;
}

.tierCompareTable--yes{
    color:#2ecc71;
    font-weight:bold;
}

/* #7a8b9c, was #5c6b7a. The cross is the ONLY thing telling a buyer a
   feature is absent, and at the old grey it measured 3.16:1 on the page
   ground (2.76:1 in the highlighted ProPlus column, which is tinted), under
   the 4.5:1 floor for text this size. Quiet is right for "not included";
   unreadable is not.
   
   Landed on #8596a7 rather than the first try at #7a8b9c: the highlighted
   ProPlus column paints an 8% green tint behind the cell, which lifts the
   background and cost about 15% of the ratio, leaving that one column at
   4.32:1 while the rest passed. Measured per column, not once. */
.tierCompareTable--no{
    color:#8596a7;
}

.tierCompareTable--sub{
    font-size:var(--fs-micro);
    color:#9fb3c8;
    font-weight:normal;
}

/* Sits under the comparison table and defines "recipient", because that is
   the question a number in the recipient rows immediately raises — and
   getting it wrong is how someone buys Business believing its 15 dashboard
   users are 15 people who get texted. */
.tierCompareNote{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    line-height:1.7;
    margin:14px 0 0;
    max-width:80ch;
    /* The section centres its text; a three-line paragraph centred ragged on
       both sides reads as a pull quote rather than a footnote. */
    text-align:left;
    padding:0 2px;
}

.tierCompareNote strong{
    color:#e8ebee;
}

/* An inline trigger in a sentence, not a button sat on its own line. */
.tierCompareNote .marketingLinkBtn{
    font-size:inherit;
}

/* 24x24, was 16x16. A standalone control, not a link inside a sentence, and
   16px is below the 24x24 floor for a touch target. The circle still LOOKS
   16px because the extra space is padding around it rather than a bigger
   disc, so nothing on the table moved. */
.tierTooltipTrigger{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    width:24px;
    height:24px;
    margin-left:4px;
    background:#2e3c4f;
    color:#c5d2e0;
    border:none;
    border-radius:50%;
    font-size:var(--fs-micro);
    font-weight:bold;
    line-height:1;
    padding:0;
    cursor:pointer;
    vertical-align:middle;
}

.tierTooltipTrigger:hover,
.tierTooltipTrigger:focus{
    background:#2ecc71;
    color:#0d1117;
}

.tierTooltipTrigger:focus:not(:focus-visible){
    outline:none;
}

.tierTooltipBox{
    display:none;
    position:fixed;
    max-width:240px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:10px 12px;
    font-size:var(--fs-caption);
    font-weight:normal;
    text-transform:none;
    letter-spacing:normal;
    color:#c5d2e0;
    line-height:1.5;
    text-align:left;
    z-index:2000;
    box-shadow:0 4px 12px rgba(0,0,0,.4);
    white-space:normal;
}

.tierTooltipBox--visible{
    display:block;
}

.marketingFooter{
    display:flex;
    justify-content:space-between;
    align-items:center;
    padding:32px 0;
    border-top:1px solid #2e3c4f;
    font-size:var(--fs-body);
    color:#8a9bb3;
    flex-wrap:wrap;
    gap:12px;
}

.marketingFooter a{
    color:#87ceeb;
    text-decoration:underline;
}

.marketingFooterContactBtn{
    background:none;
    border:none;
    color:#87ceeb;
    font-size:var(--fs-body);
    font-family:inherit;
    text-decoration:underline;
    cursor:pointer;
    padding:0;
}

.marketingLink{
    display:block;
    text-align:center;
    color:#9fb3c8;
    font-size:var(--fs-body);
    margin-top:16px;
    text-decoration:underline;
}


/* ==========================
   ABOUT-PAGE PRODUCT DEMO (about.html #wxDemo, driven by js/about-demo.js)
   Added 2026-09-26. A "video-style" walkthrough that sits under the
   marketing hero: a scripted replica of the dashboard that plays like a
   video (autoplays when scrolled into view, play/pause, scrubbable
   progress bar, chapters, captions, optional chime).

   Built as HTML/CSS/JS rather than a real video file on purpose: it stays
   pixel-sharp at any size, costs nothing in Netlify bandwidth, and its
   colours are the dashboard's own.

   Everything here is namespaced .wxDemo* / .wxCard* and deliberately does
   NOT reuse the dashboard's own component classes (.locationCard,
   .navRail, .alertModal, ...). Those carry viewport media queries — the
   nav rail, for one, is display:none below 900px — and the replica lives
   on a fixed-size stage that is scaled with a transform, so viewport
   breakpoints would reshape it in ways that make no sense inside the
   frame. What IS shared: the colour values (copied from the matching
   dashboard rules — keep them in sync if an alert colour ever changes) and
   the blinkTornado/blinkSevere keyframes, neither of which is
   media-dependent.

   The .warningItem--* ticker colours this used to name were deleted on
   2026-10-06; the live ticker reads ALERT_LEVEL_COLORS directly now. The
   replica still carries its own copy, which is correct here — it is a
   marketing screenshot of one fixed scenario, not a live view, and it
   should not change when the model does.

   Two stage sizes: 1280x800 (16:10, desktop layout with the nav rail) and,
   when the section is narrower than 640px, 480x640 (3:4, the phone
   layout with the top bar). js/about-demo.js picks one via the
   .wxDemo--compact class and scales the stage to the frame's width.
========================== */

.wxDemo{
    padding:0 0 56px;
    text-align:center;
}

.wxDemoEyebrow{
    font-size:var(--fs-caption);
    font-weight:700;
    letter-spacing:1.4px;
    text-transform:uppercase;
    color:#9fb3c8;
    margin-bottom:14px;
}

/* No JS → no player (it would just be a dead frame). The text version
   below it still renders. */
.wxDemo:not(.wxDemo--ready) .wxDemoPlayer{
    display:none;
}

.wxDemoPlayer{
    margin:0 auto;
    width:100%;
}

.wxDemo--compact .wxDemoPlayer{
    max-width:480px;
}

.wxDemoFrame{
    position:relative;
    width:100%;
    aspect-ratio:16 / 10;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:12px;
    overflow:hidden;
    box-shadow:0 18px 50px rgba(0,0,0,.45);
    text-align:left;
    cursor:pointer;
    user-select:none;
    -webkit-user-select:none;
}

.wxDemo--compact .wxDemoFrame{
    aspect-ratio:3 / 4;
}

/* The replica itself. Fixed logical size; js/about-demo.js sets
   transform:scale() so it fills the frame's width exactly. */
.wxDemoStage{
    position:absolute;
    top:0;
    left:0;
    width:1280px;
    height:800px;
    transform-origin:0 0;
    overflow:hidden;
    background:#0d1117;
    color:white;
    font-size:var(--fs-body);
    pointer-events:none;
}

.wxDemo--compact .wxDemoStage{
    width:480px;
    height:640px;
}

/* Seeking/resizing snaps everything to its new state rather than
   animating from wherever it was. */
.wxDemo--instant .wxDemoStage *,
.wxDemo--instant .wxDemoStage *::before,
.wxDemo--instant .wxDemoStage *::after,
.wxDemo--instant .wxDemoOverlay,
.wxDemo--instant .wxDemoSampleTag{
    transition:none !important;
}

.wxDemoApp{
    position:absolute;
    inset:0;
    display:flex;
}

/* ---- Nav rail (desktop stage only) ---- */

.wxDemoRail{
    flex:0 0 80px;
    width:80px;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:3px;
    padding:14px 6px 12px;
    background:#1b2430;
    border-right:3px solid #2e3c4f;
}

.wxDemo--compact .wxDemoRail{
    display:none;
}

.wxDemoRailLogo{
    display:block;
    width:66px;
    height:auto;
    margin-bottom:6px;
}

.wxDemoLive{
    display:flex;
    align-items:center;
    gap:5px;
    font-size:9px;
    font-weight:bold;
    letter-spacing:.3px;
    text-transform:uppercase;
    color:#2ecc71;
}

.wxDemoLive::before{
    content:"";
    width:7px;
    height:7px;
    border-radius:50%;
    background:#2ecc71;
}

.wxDemoRailClock{
    font-size:var(--fs-micro);
    font-weight:bold;
    white-space:nowrap;
    margin-top:2px;
}

.wxDemoRailDivider{
    width:100%;
    height:1px;
    background:#2e3c4f;
    margin:7px 0;
    flex:0 0 auto;
}

.wxDemoRailItem{
    width:100%;
    display:flex;
    flex-direction:column;
    align-items:center;
    gap:4px;
    padding:7px 2px;
    border-radius:6px;
    color:#c5d2e0;
    font-size:var(--fs-micro);
    font-weight:600;
    line-height:1.15;
    text-align:center;
    transition:background .15s ease;
}

.wxDemoRailItem svg{
    width:16px;
    height:16px;
    flex:0 0 auto;
}

.wxDemoRailItem--active{
    background:#0f75bc;
    color:white;
}

.wxDemoRailItem--hover{
    background:#2e3c4f;
}

.wxDemoRailSpacer{
    flex:1;
}

.wxDemoRailItem--danger{
    color:#e74c3c;
}

.wxDemoAvatar{
    display:flex;
    align-items:center;
    justify-content:center;
    width:30px;
    height:30px;
    margin-top:6px;
    border-radius:50%;
    background:#2e3c4f;
    border:1px solid #4a5b6f;
    color:#c5d2e0;
    font-size:var(--fs-body);
    font-weight:bold;
}

/* ---- Main column ---- */

.wxDemoMain{
    flex:1;
    min-width:0;
    display:flex;
    flex-direction:column;
}

/* Top bar — phone stage only (mirrors the dashboard's <900px header). */
.wxDemoTopbar{
    display:none;
}

.wxDemo--compact .wxDemoTopbar{
    display:flex;
    align-items:center;
    gap:10px;
    padding:10px 14px;
    background:#1b2430;
    border-bottom:3px solid #2e3c4f;
}

.wxDemoTopbarLogo{
    display:block;
    height:34px;
    width:auto;
}

.wxDemoMenuBtn{
    display:flex;
    align-items:center;
    gap:4px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    font-size:var(--fs-caption);
    font-weight:bold;
    transition:background .15s ease;
}

.wxDemoMenuBtn--hover{
    background:#2e3c4f;
}

.wxDemoTopbarStatus{
    margin-left:auto;
    display:flex;
    flex-direction:column;
    align-items:flex-end;
    gap:2px;
}

.wxDemoTopbarClock{
    font-size:var(--fs-emphasis);
    font-weight:bold;
    white-space:nowrap;
}

/* ---- Scrolling warning ticker ---- */

.wxDemoBanner{
    position:relative;
    flex:0 0 auto;
    height:0;
    overflow:hidden;
    background:#1b2430;
    border-bottom:0 solid #2e3c4f;
    transition:height .3s ease, border-bottom-width .3s ease;
}

.wxDemoBanner--active{
    height:46px;
    border-bottom-width:3px;
}

.wxDemo--compact .wxDemoBanner--active{
    height:34px;
}

.wxDemoBannerText{
    position:absolute;
    top:50%;
    left:0;
    white-space:nowrap;
    font-size:var(--fs-heading);
    font-weight:bold;
    will-change:transform;
}

.wxDemo--compact .wxDemoBannerText{
    font-size:var(--fs-emphasis);
}

/* ---- Board ---- */

.wxDemoWorkspace{
    flex:1;
    min-height:0;
    display:flex;
    flex-direction:column;
    padding:18px 20px 20px;
}

.wxDemo--compact .wxDemoWorkspace{
    padding:12px 14px 14px;
}

.wxDemoToolbar{
    flex:0 0 auto;
    display:flex;
    align-items:center;
    gap:8px;
    margin-bottom:14px;
}

.wxDemo--compact .wxDemoToolbar{
    margin-bottom:10px;
}

.wxDemoViewToggle{
    display:flex;
    border:1px solid #2e3c4f;
    border-radius:8px;
    overflow:hidden;
}

.wxDemoViewBtn{
    background:#0d1117;
    color:#9fb3c8;
    padding:8px 14px;
    font-size:var(--fs-body);
    transition:background .2s ease, color .2s ease, filter .15s ease;
}

.wxDemoViewBtn + .wxDemoViewBtn{
    border-left:1px solid #2e3c4f;
}

.wxDemoViewBtn--active{
    background:#0f75bc;
    color:#ffffff;
    font-weight:bold;
}

.wxDemoViewBtn--pressed{
    filter:brightness(1.35);
}

.wxDemo--compact .wxDemoViewBtn{
    padding:6px 10px;
    font-size:var(--fs-caption);
}

.wxDemoSearch{
    min-width:220px;
    margin-right:auto;
    padding:8px 12px 8px 34px;
    background-color:#161b22;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%239fb3c8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M21 21l-4.3-4.3'/%3E%3C/svg%3E");
    background-repeat:no-repeat;
    background-position:12px center;
    border:1px solid #2e3c4f;
    border-radius:6px;
    color:#9fb3c8;
    font-size:var(--fs-body);
}

.wxDemo--compact .wxDemoSearch{
    display:none;
}

.wxDemoFilterBtn{
    display:flex;
    align-items:center;
    gap:6px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 14px;
    font-size:var(--fs-body);
    font-weight:bold;
}

.wxDemoFilterBtn svg{
    width:16px;
    height:16px;
}

.wxDemo--compact .wxDemoFilterBtn{
    margin-left:auto;
    padding:6px 10px;
    font-size:var(--fs-caption);
}

.wxDemoBoard{
    position:relative;
    flex:1;
    min-height:0;
}

/* ---- Location cards ----
   Absolutely positioned by js/about-demo.js so a switch between Card Wall
   and Priority Stack can animate every card from its old spot to its new
   one. Colours mirror .locationCard--<level> / .locationStatus--<level>. */

.wxCard{
    position:absolute;
    top:0;
    left:0;
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:flex-start;
    padding:16px;
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    text-align:center;
    line-height:1.2;
    overflow:hidden;
    transition:transform .75s cubic-bezier(.2,.8,.2,1), width .75s cubic-bezier(.2,.8,.2,1), height .75s cubic-bezier(.2,.8,.2,1), opacity .45s ease, background .3s ease, border-color .3s ease;
    will-change:transform;
}

/* A flex column shrinks overflow:hidden children to nothing before it
   lets content spill — keep every line at its natural height instead. */
.wxCard > *{
    flex-shrink:0;
}

.wxCardName{
    max-width:100%;
    font-size:var(--fs-heading);
    font-weight:bold;
    margin-bottom:6px;
    white-space:nowrap;
    overflow:hidden;
    text-overflow:ellipsis;
}

.wxCardCounty{
    font-size:var(--fs-emphasis);
    color:#9fb3c8;
    margin-bottom:14px;
    white-space:nowrap;
}

.wxCardStatus{
    font-size:var(--fs-heading);
    font-weight:bold;
    line-height:1.2;
    color:#2ecc71;
}

.wxCardPill{
    display:none;
    margin-top:6px;
    padding:2px 10px;
    color:#9db4c7;
    background:#1c2733;
    border:1px solid #33495e;
    border-radius:20px;
    font-size:var(--fs-micro);
    font-weight:700;
    text-transform:uppercase;
    letter-spacing:.02em;
    white-space:nowrap;
}

.wxCardHazard{
    margin-top:4px;
    font-size:var(--fs-body);
    color:#c5d2e0;
    white-space:pre;
}

.wxCardExpires{
    margin-top:6px;
    font-size:var(--fs-emphasis);
    color:#c5d2e0;
    white-space:nowrap;
}

.wxCardHazard:empty,
.wxCardExpires:empty{
    display:none;
}

.wxCard--watch .wxCardStatus{
    color:#f4aea7;
}

.wxCard--severe{
    background:#3a3214;
    border-color:#f1c40f;
}

.wxCard--severe .wxCardStatus{
    color:#f1c40f;
}

.wxCard--tornado{
    background:#3a1414;
    border-color:#e74c3c;
}

.wxCard--tornado .wxCardStatus{
    color:#e74c3c;
}

.wxCard--tornado .wxCardPill{
    display:inline-block;
}

/* Same blink-in the dashboard uses (its keyframes, shorter run). */
.wxCard--blink.wxCard--tornado{
    animation:blinkTornado .5s linear 6;
}

.wxCard--blink.wxCard--severe{
    animation:blinkSevere .5s linear 6;
}

/* Priority Stack: Life-Threatening and Warnings tiers (desktop) —
   tighter vertical rhythm so a full warning fits the tier's row height. */
.wxCard--tight{
    padding:14px 16px;
}

.wxCard--tight .wxCardName{
    margin-bottom:4px;
}

.wxCard--tight .wxCardCounty{
    margin-bottom:8px;
}

.wxCard--tight .wxCardPill,
.wxCard--tight .wxCardExpires{
    margin-top:4px;
}

/* Priority Stack: Watches tier — one compact row per site. */
.wxCard--row{
    flex-direction:row;
    align-items:center;
    justify-content:flex-start;
    gap:12px;
    padding:0 16px;
    text-align:left;
}

.wxCard--row .wxCardName{
    font-size:var(--fs-subhead);
    margin:0;
}

.wxCard--row .wxCardCounty{
    font-size:var(--fs-caption);
    color:#8a9bb3;
    margin:0;
}

.wxCard--row .wxCardStatus{
    font-size:var(--fs-body);
    margin-left:auto;
    white-space:nowrap;
}

.wxCard--row .wxCardExpires{
    font-size:var(--fs-caption);
    margin:0;
}

.wxCard--row .wxCardPill,
.wxCard--row .wxCardHazard{
    display:none;
}

/* Priority Stack: Clear tier — folded into the one-line summary. */
.wxCard--collapsed{
    opacity:0;
}

/* Phone stage: smaller card type throughout. */
.wxCard--mini{
    padding:10px 8px;
}

.wxCard--mini .wxCardName{
    font-size:var(--fs-body);
    margin-bottom:2px;
}

.wxCard--mini .wxCardCounty{
    font-size:var(--fs-micro);
    margin-bottom:8px;
}

.wxCard--mini .wxCardStatus{
    font-size:var(--fs-body);
}

.wxCard--mini .wxCardPill{
    margin-top:4px;
    padding:1px 8px;
}

.wxCard--mini .wxCardHazard{
    display:none;
}

.wxCard--mini .wxCardExpires{
    margin-top:4px;
    font-size:var(--fs-micro);
}

.wxCard--med{
    padding:12px 10px;
}

.wxCard--med .wxCardName{
    font-size:var(--fs-emphasis);
    margin-bottom:2px;
}

.wxCard--med .wxCardCounty{
    font-size:var(--fs-caption);
    margin-bottom:8px;
}

.wxCard--med .wxCardStatus{
    font-size:var(--fs-body);
}

.wxCard--med .wxCardHazard{
    display:none;
}

.wxCard--med .wxCardExpires{
    margin-top:4px;
    font-size:var(--fs-caption);
}

.wxDemo--compact .wxCard--row{
    gap:8px;
    padding:0 12px;
}

.wxDemo--compact .wxCard--row .wxCardName{
    font-size:var(--fs-body);
}

.wxDemo--compact .wxCard--row .wxCardExpires{
    display:none;
}

/* Priority Stack tier headings + the clear summary line. */
.wxDemoTierLabel{
    position:absolute;
    top:0;
    left:0;
    padding-bottom:6px;
    border-bottom:1px solid #2e3c4f;
    color:#9fb3c8;
    font-size:var(--fs-body);
    font-weight:bold;
    letter-spacing:.04em;
    text-transform:uppercase;
    opacity:0;
    transition:opacity .45s ease .2s, transform .75s cubic-bezier(.2,.8,.2,1);
}

.wxDemoTierLabel--critical{
    color:#ff8fa3;
    border-bottom-color:#5c2233;
}

.wxDemoTierLabel--visible{
    opacity:1;
}

.wxDemo--compact .wxDemoTierLabel{
    font-size:var(--fs-caption);
    padding-bottom:4px;
}

.wxDemoClearSummary{
    position:absolute;
    top:0;
    left:0;
    display:flex;
    align-items:center;
    padding:0 16px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    color:#9fb3c8;
    font-size:var(--fs-body);
    opacity:0;
    transition:opacity .45s ease .25s, transform .75s cubic-bezier(.2,.8,.2,1);
}

.wxDemoClearSummary--visible{
    opacity:1;
}

.wxDemo--compact .wxDemoClearSummary{
    font-size:var(--fs-caption);
    padding:0 12px;
}

/* "Chime plays" callout — an annotation on the video, not dashboard UI,
   so it's styled as a light speech bubble rather than an app element. */
.wxDemoChimeNote{
    position:absolute;
    top:0;
    left:0;
    z-index:12;
    display:flex;
    align-items:center;
    gap:6px;
    padding:6px 12px;
    background:#f4f7fb;
    color:#0d1117;
    border-radius:999px;
    font-size:var(--fs-body);
    font-weight:700;
    box-shadow:0 6px 18px rgba(0,0,0,.45);
    opacity:0;
    transform:translateY(6px);
    transition:opacity .25s ease, transform .25s ease;
}

.wxDemoChimeNote svg{
    width:16px;
    height:16px;
}

.wxDemoChimeNote--visible{
    opacity:1;
    transform:none;
}

.wxDemo--compact .wxDemoChimeNote{
    font-size:var(--fs-caption);
    padding:4px 10px;
}

/* ---- Alert detail modal ---- */

.wxDemoModalLayer{
    position:absolute;
    inset:0;
    z-index:20;
    display:flex;
    align-items:center;
    justify-content:center;
    background:rgba(0,0,0,.72);
    opacity:0;
    visibility:hidden;
    transition:opacity .3s ease, visibility 0s linear .3s;
}

.wxDemoModalLayer--open{
    opacity:1;
    visibility:visible;
    transition:opacity .3s ease;
}

.wxDemoModal{
    position:relative;
    width:900px;
    padding:22px 24px;
    background:#161b22;
    border:2px solid #2e3c4f;
    border-top:5px solid #e74c3c;
    border-radius:10px;
    transform:translateY(14px) scale(.98);
    transition:transform .35s cubic-bezier(.2,.8,.2,1);
}

.wxDemoModalLayer--open .wxDemoModal{
    transform:none;
}

.wxDemo--compact .wxDemoModal{
    width:456px;
    padding:14px 14px 16px;
}

.wxDemoModalClose{
    position:absolute;
    top:10px;
    right:14px;
    width:30px;
    height:30px;
    display:flex;
    align-items:center;
    justify-content:center;
    border-radius:6px;
    color:#9fb3c8;
    font-size:var(--fs-display);
    line-height:1;
    transition:background .15s ease;
}

.wxDemoModalClose--hover{
    background:#2e3c4f;
    color:white;
}

.wxDemoModalHeader{
    padding-right:36px;
}

.wxDemoModalHeaderRow{
    display:flex;
    align-items:center;
    gap:8px 10px;
}

.wxDemoBadge{
    display:inline-block;
    padding:4px 12px;
    border-radius:999px;
    background:#e74c3c;
    color:#0d1117;
    font-size:var(--fs-caption);
    font-weight:700;
    letter-spacing:.02em;
    text-transform:uppercase;
    white-space:nowrap;
}

.wxDemoModalTitle{
    font-size:var(--fs-heading);
    font-weight:bold;
    white-space:nowrap;
}

.wxDemoModalCountdown{
    margin-left:auto;
    color:#e74c3c;
    font-size:var(--fs-body);
    font-weight:700;
    letter-spacing:.02em;
    text-transform:uppercase;
    white-space:nowrap;
}

.wxDemoModalSub{
    margin-top:2px;
    color:#9fb3c8;
    font-size:var(--fs-body);
}

.wxDemoModalBody{
    display:grid;
    grid-template-columns:1.4fr 1fr;
    gap:20px;
    margin-top:16px;
}

.wxDemo--compact .wxDemoModalHeaderRow{
    flex-wrap:wrap;
}

.wxDemo--compact .wxDemoModalTitle{
    font-size:var(--fs-subhead);
}

.wxDemo--compact .wxDemoModalCountdown{
    margin-left:0;
    font-size:var(--fs-caption);
}

.wxDemo--compact .wxDemoModalBody{
    grid-template-columns:1fr;
    gap:12px;
    margin-top:12px;
}

.wxDemo--compact .wxDemoModalMapCol{
    order:-1;
}

.wxDemoTakeAction{
    margin-bottom:14px;
    padding:12px 14px;
    background:#1a2029;
    border:1px solid #e74c3c;
    border-left-width:4px;
    border-radius:8px;
}

.wxDemoTakeAction h4,
.wxDemoModalSection h4{
    margin-bottom:6px;
    font-size:var(--fs-caption);
    letter-spacing:1px;
    text-transform:uppercase;
}

.wxDemoTakeAction h4{
    color:#e74c3c;
    font-size:var(--fs-body);
}

.wxDemoTakeAction p{
    font-size:var(--fs-emphasis);
    line-height:1.5;
}

.wxDemoTiles{
    display:grid;
    grid-template-columns:repeat(3, 1fr);
    gap:8px;
    margin-bottom:14px;
}

.wxDemoTile{
    padding:8px 10px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
}

.wxDemoTileLabel{
    margin-bottom:3px;
    color:#8a9bb3;
    font-size:var(--fs-micro);
    letter-spacing:.05em;
    text-transform:uppercase;
    white-space:nowrap;
}

.wxDemoTileValue{
    color:#e7edf3;
    font-size:var(--fs-body);
    font-weight:700;
    white-space:nowrap;
}

.wxDemoModalSection{
    margin-bottom:12px;
}

.wxDemoModalSection h4{
    color:#8a9bb3;
}

.wxDemoModalSection p{
    color:#c5d2e0;
    font-size:var(--fs-body);
    line-height:1.55;
}

.wxDemoChip{
    display:inline-flex;
    align-items:center;
    gap:6px;
    padding:4px 10px;
    background:#1c2733;
    border:1px solid #33495e;
    border-radius:999px;
    color:#c5d2e0;
    font-size:var(--fs-caption);
}

.wxDemoChip i{
    width:8px;
    height:8px;
    border-radius:50%;
    background:#f4aea7;
}

.wxDemo--compact .wxDemoTakeAction{
    margin-bottom:10px;
    padding:10px 12px;
}

.wxDemo--compact .wxDemoTakeAction p{
    font-size:var(--fs-body);
}

.wxDemo--compact .wxDemoTiles{
    margin-bottom:10px;
}

.wxDemo--compact .wxDemoTileValue{
    font-size:var(--fs-caption);
}

.wxDemo--compact .wxDemoModalDesc{
    display:none;
}

.wxDemo--compact .wxDemoModalSection{
    margin-bottom:0;
}

.wxDemoModalMapCol{
    display:flex;
    flex-direction:column;
    gap:8px;
    min-width:0;
}

.wxDemoMap{
    position:relative;
    overflow:hidden;
    background:#121a24;
    border:1px solid #2e3c4f;
    border-radius:8px;
}

.wxDemoMap svg{
    display:block;
}

.wxDemoMap--modal{
    height:318px;
}

.wxDemo--compact .wxDemoMap--modal{
    height:176px;
}

.wxDemoMapStamp{
    position:absolute;
    top:8px;
    left:8px;
    padding:3px 8px;
    background:rgba(13,17,23,.82);
    border:1px solid #2e3c4f;
    border-radius:6px;
    color:#c5d2e0;
    font-size:var(--fs-micro);
    font-weight:600;
}

.wxDemoMapControls{
    display:flex;
    align-items:center;
    gap:6px;
}

.wxDemoPill{
    padding:4px 12px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:999px;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    font-weight:600;
}

.wxDemoPill--on{
    background:#0f75bc;
    border-color:#0f75bc;
    color:white;
}

.wxDemoMapPlay{
    margin-left:auto;
    display:flex;
    align-items:center;
    justify-content:center;
    width:28px;
    height:28px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    color:#c5d2e0;
}

.wxDemoMapPlay svg{
    width:12px;
    height:12px;
}

/* ---- Cockpit view ---- */

.wxDemoCockpitLayer{
    position:absolute;
    inset:0;
    z-index:30;
    background:rgba(0,0,0,.72);
    opacity:0;
    visibility:hidden;
    transition:opacity .35s ease, visibility 0s linear .35s;
}

.wxDemoCockpitLayer--open{
    opacity:1;
    visibility:visible;
    transition:opacity .35s ease;
}

.wxDemoCockpit{
    position:absolute;
    inset:18px 22px;
    display:flex;
    flex-direction:column;
    padding:18px 20px;
    background:#161b22;
    border:2px solid #2e3c4f;
    border-radius:10px;
    transform:scale(.98);
    transition:transform .4s cubic-bezier(.2,.8,.2,1);
}

.wxDemoCockpitLayer--open .wxDemoCockpit{
    transform:none;
}

.wxDemo--compact .wxDemoCockpit{
    inset:10px;
    padding:12px;
}

.wxDemoCockpitTitle{
    font-size:var(--fs-heading);
    font-weight:bold;
}

.wxDemo--compact .wxDemoCockpitTitle{
    font-size:var(--fs-subhead);
}

.wxDemoCockpitGrid{
    flex:1;
    min-height:0;
    display:grid;
    grid-template-columns:1fr 2fr 1fr;
    gap:16px;
    margin-top:10px;
}

.wxDemo--compact .wxDemoCockpitGrid{
    grid-template-columns:1fr;
    grid-template-rows:auto minmax(0, 1fr) auto;
    gap:10px;
    margin-top:8px;
}

.wxDemoCockpitStatus{
    display:flex;
    flex-direction:column;
    gap:10px;
    min-width:0;
    padding-right:16px;
    border-right:1px solid #2e3c4f;
}

.wxDemo--compact .wxDemoCockpitStatus{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:8px;
    padding:0;
    border:none;
}

.wxDemoBox{
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    padding:16px 10px;
    border-radius:10px;
    color:#0d1117;
    text-align:center;
}

.wxDemoBox--tornado{
    background:#e74c3c;
}

.wxDemoBox--severe{
    background:#f1c40f;
}

.wxDemoBox--primary{
    padding:22px 16px;
}

.wxDemoBoxCount{
    font-size:var(--fs-display);
    font-weight:bold;
    line-height:1.2;
}

.wxDemoBox--primary .wxDemoBoxCount{
    font-size:var(--fs-hero);
}

.wxDemoBoxLabel{
    font-size:var(--fs-caption);
    font-weight:600;
}

.wxDemoBox--primary .wxDemoBoxLabel{
    font-size:var(--fs-body);
}

.wxDemoBoxLocs{
    margin-top:6px;
    font-size:var(--fs-micro);
    font-weight:600;
    opacity:.85;
    white-space:nowrap;
}

.wxDemo--compact .wxDemoBox,
.wxDemo--compact .wxDemoBox--primary{
    padding:10px 8px;
}

.wxDemo--compact .wxDemoBox--primary .wxDemoBoxCount,
.wxDemo--compact .wxDemoBoxCount{
    font-size:var(--fs-heading);
}

.wxDemo--compact .wxDemoBox--primary .wxDemoBoxLabel,
.wxDemo--compact .wxDemoBoxLabel{
    font-size:var(--fs-micro);
}

.wxDemo--compact .wxDemoBoxLocs{
    display:none;
}

.wxDemoWatchLegend{
    display:flex;
    flex-direction:column;
    gap:6px;
    padding-top:10px;
    border-top:1px solid #2e3c4f;
}

.wxDemoWatchLegendHeading{
    color:#8a9bb3;
    font-size:var(--fs-micro);
    font-weight:700;
    letter-spacing:.6px;
    text-transform:uppercase;
}

.wxDemoWatchRow{
    display:flex;
    align-items:center;
    gap:8px;
    color:#c5d2e0;
    font-size:var(--fs-caption);
}

.wxDemoWatchRow i{
    width:10px;
    height:10px;
    border-radius:3px;
    background:#f4aea7;
}

.wxDemoClearPill{
    align-self:flex-start;
    margin-top:auto;
    padding:4px 12px;
    background:#163a26;
    border:1px solid #28543f;
    border-radius:999px;
    color:#2ecc71;
    font-size:var(--fs-caption);
    font-weight:700;
    white-space:nowrap;
}

.wxDemo--compact .wxDemoWatchLegend{
    flex-direction:row;
    align-items:center;
    padding-top:0;
    border-top:none;
}

.wxDemo--compact .wxDemoWatchLegendHeading{
    display:none;
}

.wxDemo--compact .wxDemoClearPill{
    align-self:center;
    justify-self:end;
    margin-top:0;
}

.wxDemoCockpitMapPanel{
    display:flex;
    flex-direction:column;
    gap:8px;
    min-width:0;
    min-height:0;
}

.wxDemoMap--cockpit{
    flex:1;
    min-height:0;
}

.wxDemoRadarRow{
    display:flex;
    align-items:center;
    gap:8px;
    color:#c5d2e0;
    font-size:var(--fs-caption);
    white-space:nowrap;
}

.wxDemoCheck{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    width:14px;
    height:14px;
    border-radius:3px;
    background:#0f75bc;
    color:white;
    font-size:var(--fs-micro);
    font-weight:bold;
}

.wxDemoRadarAsOf{
    margin-left:auto;
    color:#8a9bb3;
}

.wxDemo--compact .wxDemoRadarRow .wxDemoPill{
    display:none;
}

.wxDemoCockpitChat{
    display:flex;
    flex-direction:column;
    min-width:0;
    min-height:0;
    padding-left:16px;
    border-left:1px solid #2e3c4f;
}

.wxDemo--compact .wxDemoCockpitChat{
    height:132px;
    padding:8px 0 0;
    border-left:none;
    border-top:1px solid #2e3c4f;
}

.wxDemoChatLabel{
    margin-bottom:8px;
    color:#8a9bb3;
    font-size:var(--fs-micro);
    font-weight:700;
    letter-spacing:.6px;
    text-transform:uppercase;
}

.wxDemo--compact .wxDemoChatLabel{
    margin-bottom:4px;
}

.wxDemoChatMsgs{
    flex:1;
    min-height:0;
    display:flex;
    flex-direction:column;
    justify-content:flex-end;
    gap:12px;
    overflow:hidden;
    margin-bottom:10px;
}

.wxDemo--compact .wxDemoChatMsgs{
    gap:6px;
    margin-bottom:0;
}

.wxDemoMsg{
    display:none;
    align-items:flex-end;
    gap:8px;
    max-width:92%;
    flex:0 0 auto;
}

.wxDemoMsg--in{
    display:flex;
    animation:wxDemoMsgIn .35s ease-out;
}

@keyframes wxDemoMsgIn{
    from{ opacity:0; transform:translateY(10px); }
    to{ opacity:1; transform:none; }
}

.wxDemoMsg--own{
    align-self:flex-end;
    flex-direction:row-reverse;
}

.wxDemoMsgAvatar{
    flex:0 0 auto;
    display:flex;
    align-items:center;
    justify-content:center;
    width:28px;
    height:28px;
    border-radius:50%;
    background:#26313f;
    border:1px solid #2e3c4f;
    color:#c5d2e0;
    font-size:var(--fs-micro);
    font-weight:700;
}

.wxDemoMsg--own .wxDemoMsgAvatar{
    background:#1d3a2e;
    border-color:#2ecc71;
    color:#2ecc71;
}

.wxDemoMsgCol{
    display:flex;
    flex-direction:column;
    gap:3px;
    min-width:0;
}

.wxDemoMsg--own .wxDemoMsgCol{
    align-items:flex-end;
}

.wxDemoMsgMeta{
    display:flex;
    gap:6px;
    padding:0 2px;
    font-size:var(--fs-micro);
}

.wxDemoMsgMeta b{
    color:#c5d2e0;
    font-weight:600;
}

.wxDemoMsgMeta span{
    color:#5f7086;
}

.wxDemoMsgBody{
    padding:9px 13px;
    background:#1c2530;
    border:1px solid #262f3d;
    border-radius:14px;
    border-bottom-left-radius:4px;
    color:#eef2f7;
    font-size:var(--fs-body);
    line-height:1.42;
}

.wxDemoMsg--own .wxDemoMsgBody{
    background:#1d3a2e;
    border-color:#28543f;
    border-bottom-left-radius:14px;
    border-bottom-right-radius:4px;
}

.wxDemo--compact .wxDemoMsgAvatar{
    width:22px;
    height:22px;
}

.wxDemo--compact .wxDemoMsgMeta{
    display:none;
}

.wxDemo--compact .wxDemoMsgBody{
    padding:5px 10px;
    font-size:var(--fs-caption);
    line-height:1.35;
}

.wxDemoChatForm{
    display:flex;
    gap:8px;
}

.wxDemo--compact .wxDemoChatForm{
    display:none;
}

.wxDemoChatInput{
    flex:1;
    padding:10px 12px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    color:#5f7086;
    font-size:var(--fs-body);
}

.wxDemoChatSend{
    display:flex;
    align-items:center;
    padding:0 14px;
    background:#2ecc71;
    border-radius:8px;
    color:#0d1117;
    font-size:var(--fs-body);
    font-weight:bold;
}

/* ---- Pointer (desktop) / tap ripple (phone) ---- */

.wxDemoCursor{
    position:absolute;
    top:0;
    left:0;
    z-index:50;
    width:0;
    height:0;
    opacity:0;
    transition:opacity .25s ease;
}

.wxDemoCursor--visible{
    opacity:1;
}

.wxDemoCursorArrow{
    position:absolute;
    top:-2px;
    left:-3px;
    width:26px;
    height:26px;
    filter:drop-shadow(0 2px 3px rgba(0,0,0,.6));
}

.wxDemo--compact .wxDemoCursorArrow{
    display:none;
}

.wxDemoCursorRing{
    position:absolute;
    top:-18px;
    left:-18px;
    width:36px;
    height:36px;
    border:3px solid rgba(255,255,255,.95);
    border-radius:50%;
    opacity:0;
}

.wxDemoCursor--click .wxDemoCursorRing{
    animation:wxDemoClickRing .5s ease-out;
}

@keyframes wxDemoClickRing{
    from{ opacity:1; transform:scale(.35); }
    to{ opacity:0; transform:scale(1.5); }
}

/* ---- Real-size layers over the stage (not scaled) ---- */

.wxDemoSampleTag{
    position:absolute;
    bottom:8px;
    left:50%;
    transform:translateX(-50%);
    z-index:4;
    padding:3px 8px;
    background:rgba(13,17,23,.85);
    border:1px solid #2e3c4f;
    border-radius:6px;
    color:#9fb3c8;
    font-size:var(--fs-micro);
    font-weight:700;
    letter-spacing:.8px;
    text-transform:uppercase;
    pointer-events:none;
    opacity:0;
    transition:opacity .3s ease;
}

.wxDemoSampleTag--visible{
    opacity:1;
}

/* Bottom-centre on the desktop stage; on the phone one, the gap in the
   top bar between the Menu button and the clock — the spots that stay
   clear of content in every scene. */
.wxDemo--compact .wxDemoSampleTag{
    top:8px;
    bottom:auto;
    left:59%;
    padding:2px 6px;
    letter-spacing:.5px;
}

.wxDemoOverlay{
    position:absolute;
    inset:0;
    z-index:5;
    display:flex;
    flex-direction:column;
    align-items:center;
    justify-content:center;
    gap:12px;
    padding:24px;
    text-align:center;
    background:radial-gradient(ellipse at 50% 38%, rgba(15,117,188,.25), transparent 62%), rgba(13,17,23,.95);
    opacity:0;
    visibility:hidden;
    transition:opacity .6s ease, visibility 0s linear .6s;
    cursor:default;
}

.wxDemoOverlay--visible{
    opacity:1;
    visibility:visible;
    transition:opacity .6s ease;
}

.wxDemoOverlay--intro{
    cursor:pointer;
}

.wxDemoOverlayLogo{
    display:block;
    width:min(340px, 72%);
    height:auto;
}

.wxDemoOverlayTitle{
    max-width:640px;
    font-size:var(--fs-heading);
    font-weight:bold;
    line-height:1.3;
}

.wxDemoOverlaySub{
    max-width:560px;
    color:#c5d2e0;
    font-size:var(--fs-body);
    line-height:1.5;
}

.wxDemoOverlayMeta{
    color:#8a9bb3;
    font-size:var(--fs-caption);
    font-weight:600;
    letter-spacing:.6px;
    text-transform:uppercase;
}

.wxDemoOverlayActions{
    display:flex;
    flex-wrap:wrap;
    justify-content:center;
    gap:12px;
    margin-top:6px;
}

.wxDemoOverlayActions .marketingCtaSecondary{
    font-family:inherit;
    cursor:pointer;
}

@media (min-width:900px){
    .wxDemoOverlayTitle{
        font-size:var(--fs-display);
    }
}

.wxDemoBigPlay,
.wxDemoResume{
    display:flex;
    align-items:center;
    justify-content:center;
    width:76px;
    height:76px;
    border:none;
    border-radius:50%;
    background:#2ecc71;
    color:#0d1117;
    cursor:pointer;
    box-shadow:0 8px 24px rgba(0,0,0,.45);
    transition:transform .15s ease, background .15s ease, opacity .3s ease;
}

.wxDemoBigPlay:hover,
.wxDemoResume:hover{
    background:#27ae60;
    transform:scale(1.06);
}

.wxDemoBigPlay svg,
.wxDemoResume svg{
    width:30px;
    height:30px;
    margin-left:4px;
}

.wxDemoBigPlay--hidden{
    visibility:hidden;
    opacity:0;
}

.wxDemoResume{
    position:absolute;
    left:50%;
    top:50%;
    z-index:6;
    margin:-38px 0 0 -38px;
    opacity:0;
    visibility:hidden;
    transition:opacity .2s ease, visibility 0s linear .2s, transform .15s ease, background .15s ease;
}

.wxDemoResume--visible{
    opacity:.92;
    visibility:visible;
    transition:opacity .2s ease, transform .15s ease, background .15s ease;
}

.wxDemoReplay{
    margin-top:4px;
    padding:8px 12px;
    background:none;
    border:none;
    color:#9fb3c8;
    font-family:inherit;
    font-size:var(--fs-body);
    text-decoration:underline;
    cursor:pointer;
}

.wxDemoReplay:hover{
    color:#c5d2e0;
}

/* ---- Caption + controls under the frame ---- */

.wxDemoCaption{
    min-height:3em;
    max-width:780px;
    margin:14px auto 0;
    color:#e6edf3;
    font-size:var(--fs-subhead);
    line-height:1.45;
    transition:opacity .2s ease;
}

.wxDemoCaption--fading{
    opacity:0;
}

.wxDemo--compact .wxDemoCaption{
    min-height:4.4em;
    font-size:var(--fs-emphasis);
}

.wxDemoBar{
    display:flex;
    align-items:center;
    gap:10px;
    margin-top:6px;
}

.wxDemoBtn{
    display:inline-flex;
    align-items:center;
    justify-content:center;
    gap:6px;
    min-width:var(--tap-min);
    min-height:var(--tap-min);
    padding:0 12px;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:8px;
    color:#e6edf3;
    font-family:inherit;
    font-size:var(--fs-body);
    font-weight:bold;
    cursor:pointer;
}

.wxDemoBtn:hover{
    background:#2e3c4f;
}

.wxDemoBtn svg{
    width:18px;
    height:18px;
    flex:0 0 auto;
}

.wxDemoSoundBtn[aria-pressed="true"]{
    border-color:#2ecc71;
    color:#2ecc71;
}

/* Muted speaker while off, sound waves while on — the icon states the
   current state, the same way the label does. */
.wxDemoSoundBtn .wxDemoSoundIconOn,
.wxDemoSoundBtn[aria-pressed="true"] .wxDemoSoundIconOff{
    display:none;
}

.wxDemoSoundBtn[aria-pressed="true"] .wxDemoSoundIconOn{
    display:block;
}

@media (max-width:520px){
    .wxDemoSoundLabel{
        display:none;
    }
}

.wxDemoTrack{
    position:relative;
    flex:1;
    min-width:60px;
    height:var(--tap-min);
    cursor:pointer;
    touch-action:none;
}

.wxDemoTrack::before{
    content:"";
    position:absolute;
    left:0;
    right:0;
    top:50%;
    height:6px;
    margin-top:-3px;
    background:#2e3c4f;
    border-radius:3px;
}

.wxDemoTrackFill{
    position:absolute;
    left:0;
    top:50%;
    width:0;
    height:6px;
    margin-top:-3px;
    background:#2ecc71;
    border-radius:3px;
}

.wxDemoTrackTick{
    position:absolute;
    top:50%;
    width:2px;
    height:6px;
    margin:-3px 0 0 -1px;
    background:#0d1117;
}

.wxDemoTrackThumb{
    position:absolute;
    top:50%;
    left:0;
    width:14px;
    height:14px;
    margin:-7px 0 0 -7px;
    background:#ffffff;
    border-radius:50%;
    box-shadow:0 1px 4px rgba(0,0,0,.5);
}

.wxDemoTime{
    color:#9fb3c8;
    font-size:var(--fs-caption);
    font-variant-numeric:tabular-nums;
    white-space:nowrap;
}

@media (max-width:380px){
    .wxDemoTime{
        display:none;
    }
}

.wxDemoChapters{
    display:flex;
    flex-wrap:wrap;
    justify-content:center;
    gap:8px;
    margin-top:10px;
}

/* One swipeable row on phones instead of three rows of pills. */
@media (max-width:640px){
    .wxDemoChapters{
        flex-wrap:nowrap;
        justify-content:flex-start;
        overflow-x:auto;
        scrollbar-width:none;
        padding-bottom:2px;
    }
    .wxDemoChapters::-webkit-scrollbar{
        display:none;
    }
    .wxDemoChapter{
        flex:0 0 auto;
    }
}

.wxDemoChapter{
    min-height:32px;
    padding:6px 12px;
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:999px;
    color:#9fb3c8;
    font-family:inherit;
    font-size:var(--fs-caption);
    font-weight:600;
    cursor:pointer;
}

.wxDemoChapter:hover{
    border-color:#4a5b6f;
    color:#c5d2e0;
}

.wxDemoChapter--active{
    background:#0f75bc;
    border-color:#0f75bc;
    color:#ffffff;
}

.wxDemoChapter--active:hover{
    color:#ffffff;
}

/* Text version — the accessible alternative to the visual demo, and what
   shows on its own if JS doesn't run. */
.wxDemoTranscript{
    max-width:780px;
    margin:18px auto 0;
    text-align:center;
    color:#9fb3c8;
    font-size:var(--fs-body);
}

.wxDemoTranscript summary{
    display:inline-block;
    color:#87ceeb;
    text-decoration:underline;
    cursor:pointer;
}

.wxDemoTranscript[open]{
    text-align:left;
}

.wxDemoTranscript[open] summary{
    margin-bottom:10px;
}

.wxDemoTranscript ol{
    padding-left:22px;
    line-height:1.6;
}

.wxDemoTranscript li + li{
    margin-top:6px;
}

.wxDemoTranscript b{
    color:#c5d2e0;
}

/* Reduced motion: the demo never autoplays (see about-demo.js), and when
   played by hand it swaps motion for plain state changes. */
@media (prefers-reduced-motion:reduce){

    .wxCard,
    .wxDemoTierLabel,
    .wxDemoClearSummary,
    .wxDemoModal,
    .wxDemoCockpit,
    .wxDemoBanner,
    .wxDemoChimeNote{
        transition-duration:.01s !important;
        transition-delay:0s !important;
    }

    .wxCard--blink.wxCard--tornado,
    .wxCard--blink.wxCard--severe,
    .wxDemoMsg--in,
    .wxDemoCursor--click .wxDemoCursorRing{
        animation:none;
    }

}


/* ==========================
   SETTINGS MODAL
========================== */

.settingsRow{
    display:flex;
    gap:8px;
    flex-wrap:wrap;
}

/* The settings action buttons.

   This was a hand-maintained list of ids — #exportLocationsBtn,
   #importLocationsBtn, #accountBtn, #notificationSettingsBtn, #manageTagsBtn,
   #muteToggleBtn — repeated across three rules. #replaySetupTourBtn was added
   to the Account tab and never added to the list, so the Setup Tour button
   rendered as a bare browser button next to five styled ones. Nothing was
   wrong with its markup; it simply was not on a list it had no way of knowing
   about.

   That is the third time this shape has bitten: the alert-level checkbox grid
   had its layout copied per id until a third caller rendered as one long
   column, and the ticker kept a private colour table that drifted from the
   model (deleted 2026-10-06). A class means the eighth button cannot be born
   unstyled. */
.settingsActionBtn{
    background:#0d1117;
    color:white;
    border:1px solid #2e3c4f;
    border-radius:6px;
    font-weight:bold;
    cursor:pointer;
}

/* display:flex added 2026-09-05 (icon-consistency pass, part 2) so the
   inline svg icons line up cleanly with their text. The icon-only mute
   toggle is excluded — it sets its own centred inline-flex below. */
.settingsActionBtn:not(.settingsActionBtn--icon){
    display:flex;
    align-items:center;
    gap:8px;
    padding:8px 14px;
    font-size:var(--fs-body);
}

.settingsActionBtn:hover{
    background:#2e3c4f;
}

.settingsActionBtn svg{
    flex:0 0 auto;
}

/* AFTER .settingsActionBtn, deliberately. A modifier declared before the
   class it modifies, at equal specificity, does nothing at all — and does it
   silently, which is how a Delete button ends up looking like Save.
   
   Text and border, no fill: the filled treatment stays reserved for the
   confirmation dialog's final button, the one that actually acts. */
.settingsActionBtn--danger{
    color:#e74c3c;
    border-color:#5a2a2a;
}

.settingsActionBtn--danger:hover{
    background:#2e0f0f;
}

/* There was no disabled treatment at all, so the confirm button on the
   delete dialog — which starts disabled until the account name is typed —
   looked exactly as pressable as an enabled one. A guard nobody can see is
   a guard that reads as a broken button. */
.settingsActionBtn:disabled{
    opacity:.45;
    cursor:not-allowed;
}

.settingsActionBtn:disabled:hover{
    background:#0d1117;
}

/* Mute/unmute icon swap (added 2026-09-05, replacing the old
   btn.textContent = "🔇"/"🔊" glyph swap in updateMuteButton()) — both
   icons sit in the DOM always; CSS shows exactly one based on the existing
   .muteToggleBtn--muted class updateMuteButton() already toggled before
   this change, so the JS fix here is a one-line deletion, not a rewrite. */
#muteToggleBtn{
    display:inline-flex;
    align-items:center;
    justify-content:center;
}

#muteToggleBtn .muteIconOff{
    display:none;
}

#muteToggleBtn.muteToggleBtn--muted .muteIconOn{
    display:none;
}

#muteToggleBtn.muteToggleBtn--muted .muteIconOff{
    display:inline;
}

.settingsHint{
    font-size:var(--fs-body);
    color:#9fb3c8;
    line-height:1.6;
    margin-bottom:12px;
}

.alertLevelSelectAllRow{
    display:flex;
    gap:8px;
    margin-bottom:8px;
}

/* Doubled class — (0,2,0) — because these buttons also appear inside
   .authForm (the notification-contact form), where `.authForm button` (0,1,1)
   is the big green primary style for Add contact. Select All / Select None are
   not primary actions; measured 2026-10-01 they were taking that rule's
   padding and hover colour, so two quiet secondary controls were rendering
   with a submit button's weight.

   Doubling rather than scoping under .authForm: the same class is used in
   Alert Settings, which is not inside a form, and one rule that works in both
   places cannot fall out of step with itself. Same device already used by
   .locationCard.locationCard--pstier-critical for the same reason. */
.alertLevelSelectAllBtn.alertLevelSelectAllBtn{
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:5px 10px;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    cursor:pointer;
}

.alertLevelSelectAllBtn.alertLevelSelectAllBtn:hover{
    background:#0d1117;
    color:#c5d2e0;
}

.alertLevelCheckboxes{
    display:flex;
    flex-direction:column;
    gap:10px;
    margin-bottom:14px;
}

/* The Warnings/Watches two-column split.
   
   #alertLevelCheckboxes is Alert Settings, which has its own renderer (it
   re-parents the Alert Priority block into the Watches column, so it can't
   share one). Everything else gets .alertLevelCheckboxes--split applied by
   renderAlertLevelCheckboxGrid() in script.js.
   
   Both selectors are on ONE rule deliberately. This declaration used to be
   copied per-id, once for Alert Settings and once for the contact form, and
   the copies were identical. Adding a third list ("My alerts") produced
   correct markup that stacked into a single 20-row column, because there was
   no reason to suspect the layout lived somewhere that had to be told about
   each new caller by hand. */
#alertLevelCheckboxes,
.alertLevelCheckboxes--split{
    display:grid;
    grid-template-columns:1fr 1fr;
    gap:0 24px;
}

.alertLevelColumn{
    display:flex;
    flex-direction:column;
    gap:10px;
}

.alertLevelColumnTitle{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    text-transform:uppercase;
    letter-spacing:.5px;
    margin-bottom:2px;
}

/* Alert Priority gets moved here (into the Watches column, under its
   checkboxes) by renderAlertLevelCheckboxes() in script.js, to use the
   empty space left by Watches having fewer entries than Warnings. Needs
   its own top spacing/divider since it's no longer a standalone
   .alertModalTextBlock sitting between two others — .alertLevelColumn's
   flex gap alone reads as "part of the checkbox list" without it. */
.alertPriorityBlock{
    margin-top:8px;
    margin-bottom:0;
    padding-top:14px;
    border-top:1px solid #2e3c4f;
}

/* Audio Alerts is re-parented into the Watches column too, under Alert
   Priority (2026-10-08). Same divider treatment for the same reason: inside
   .alertLevelColumn's flex gap, a block with no rule of its own reads as one
   more checkbox in the list above it.

   The column is roughly half the modal, so everything in here has to survive
   at ~400px. .muteRow already wraps; the override checkbox grid is given its
   single column below rather than inheriting the two-column split, which at
   this width produced labels broken across three lines. */
.audioAlertsBlock{
    margin-top:8px;
    margin-bottom:0;
    padding-top:14px;
    border-top:1px solid #2e3c4f;
}

.audioAlertsBlock #muteOverrideCheckboxes{
    grid-template-columns:1fr;
    row-gap:10px;
}

/* Two settings buttons on one line where there is room, stacked where there
   is not. Not a bare flex row: these are .settingsActionBtn, which is
   inline-flex and would otherwise sit shoulder to shoulder with no gap. */
.settingsActionRow{
    display:flex;
    flex-wrap:wrap;
    gap:10px;
    align-items:center;
}

@media (max-width:480px){
    #alertLevelCheckboxes,
    .alertLevelCheckboxes--split{
        grid-template-columns:1fr;
        /* Once the columns stack, a zero row-gap runs the WATCHES heading
           straight into the last Warnings checkbox and the two groups read as
           one undifferentiated list. */
        row-gap:18px;
    }
}

/* min-height so a single-line row is still a real touch target. A row whose
   label wraps was already 32px; one that fits on a line measured 16px, which
   is under the 24px floor, and these are the controls that decide which
   alerts reach you. Rows that are already taller are unaffected. */
.alertLevelCheckboxRow{
    display:flex;
    align-items:center;
    min-height:32px;
    gap:10px;
    font-size:var(--fs-body);
    cursor:pointer;
}

.alertLevelCheckboxRow input[type="checkbox"]{
    width:16px;
    height:16px;
    cursor:pointer;
}

/* ==========================
   MY ALERTS + QUIET HOURS
   (Notification Settings modal. "My alerts" is this user's own recipient
   row; the quiet-hours block below is shared by it and each notification
   contact — see renderSelfContactSection() and the contact form in
   js/script.js.)
========================== */

/* Each channel is one row: the on/off control, then what it would actually
   reach. Keeping the address on the same line as its switch is the whole
   point — "Text me" checked with an empty number was the exact shape of the
   old silent failure this section exists to end. */
.selfContactChannels{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:10px;
    margin:8px 0;
}

.selfContactAddress{
    font-size:var(--fs-body);
    color:#9fb3c8;
    word-break:break-all;
}

.selfContactPhoneInput{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    color:white;
    font-size:var(--fs-body);
    min-width:0;
    /* Width, not flex-basis: the parent switches to a COLUMN at phone width,
       where flex-basis governs height instead. `flex:1 1 180px` here rendered
       a 180px-tall text box on a phone — a defect no assertion caught and
       exactly one screenshot did. */
    width:100%;
    max-width:260px;
}

.selfContactSubhead{
    font-size:var(--fs-body);
    color:#c5d2e0;
    font-weight:bold;
    margin-right:auto;
}

/* The two channel toggles on the contact form, matching the "My alerts"
   rows above so the same decision reads the same way on both screens. */
.contactChannelRow{
    display:flex;
    flex-wrap:wrap;
    gap:8px 20px;
    margin:10px 0;
}

.quietHoursBlock{
    margin:14px 0 4px;
    padding-top:12px;
    border-top:1px solid #222c38;
}

.quietHoursTimezoneSelect{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    color:white;
    font-size:var(--fs-body);
}

/* The quiet-hours row appears in two places, and one of them sits inside
   .authForm, whose inputs are `width:100%` block elements by design (every
   other field in that form is a full-width stack). Without this, "From
   [22:00] To [07:00] [Central]" rendered as five full-width rows in the
   contact form while reading as one tidy line under My alerts — same markup,
   same class, two completely different layouts.
   
   Scoped to .quietHoursBlock so it overrides .authForm's width only for the
   controls that belong to this row, leaving every other field in that form
   exactly as it was. */
.quietHoursBlock .dndTimeRow input[type="time"],
.quietHoursBlock .dndTimeRow select{
    width:auto;
    flex:0 0 auto;
    padding:6px 10px;
    font-size:var(--fs-body);
}

.quietHoursBlock .dndTimeRow label{
    flex:0 0 auto;
    margin:0;
}

/* The Select All / Select None pair is a secondary control in both places.
   Inside .authForm it was picking up that form's primary-button styling and
   rendering as two large green buttons — louder than the Add contact button
   they sit above, and visibly different from the identical pair under My
   alerts. IDs, because .authForm button is what it has to outrank. */
#notificationContactSelectAllBtn,
#notificationContactSelectNoneBtn{
    width:auto;
    background:transparent;
    border:1px solid #2e3c4f;
    color:#c5d2e0;
    font-size:var(--fs-caption);
    padding:4px 10px;
    margin:0;
}

#notificationContactSelectAllBtn:hover,
#notificationContactSelectNoneBtn:hover{
    background:#1c2530;
    color:white;
}

/* The promise the quiet-hours setting makes, stated in full. Deliberately
   not styled as a warning: it is reassurance — the reason it is safe to
   silence the overnight window at all — and an alarm-coloured box here
   would read as "something is wrong". */
.quietHoursBreakthroughNote{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    line-height:1.5;
    margin:8px 0 0;
    padding:8px 10px;
    background:rgba(46, 60, 79, .25);
    border-left:2px solid #4a5d75;
    border-radius:0 4px 4px 0;
}

.quietHoursBreakthroughNote strong{
    color:#c5d2e0;
}

/* Three stacked lines, not a wrapped paragraph — the list in the middle is
   the part a reader is actually scanning when deciding whether it is safe to
   silence their phone overnight, and inlining it with the sentences either
   side buried it. */
.quietHoursNoteHead,
.quietHoursNoteList,
.quietHoursNoteTail{
    display:block;
}

.quietHoursNoteHead{
    color:#c5d2e0;
    font-weight:bold;
}

.quietHoursNoteList{
    color:#e6edf3;
    margin:3px 0 5px;
    line-height:1.6;
}

/* ==========================
   CONTACT ROW STATE
   (Which channels a contact is actually reachable on — and when they
   aren't. Before this, a contact with a phone number that wasn't being
   texted rendered identically to one with no number at all.)
========================== */

.contactChannelOn{
    color:#9fb3c8;
}

/* On file but switched off. Struck through rather than hidden, so the
   difference between "not configured" and "configured and paused" is
   visible at a glance instead of requiring the edit form to be opened.

   #7c8da3 is 5.58:1 on #0d1117 — dimmer than the "on" state (#9fb3c8, 8.79:1)
   while still clearing 4.5:1 for body text. The obvious choice here was a
   step darker again, which measured 4.38:1 and would have failed; the strike
   itself is carrying the "off" meaning, so the colour doesn't need to. */
.contactChannelOff{
    color:#7c8da3;
    text-decoration:line-through;
    text-decoration-thickness:1px;
}

/* A number the carrier is refusing. #f1c40f on this badge's own tinted
   background is 9.2:1 (measured, not estimated) — this is
   the single most likely explanation for "the front desk never got it", so
   it is a badge on the row rather than something the owner has to go
   looking for. */
.contactOptOutBadge{
    display:inline-block;
    margin-left:8px;
    padding:2px 7px;
    font-size:var(--fs-caption);
    font-weight:normal;
    color:#f1c40f;
    background:rgba(241, 196, 15, .12);
    border:1px solid rgba(241, 196, 15, .45);
    border-radius:4px;
    vertical-align:middle;
}

/* Neither channel on: quieter than the STOP badge because it is a choice
   someone made rather than a failure, but still stated, because a contact
   that receives nothing while sitting in a list titled "Notification
   Contacts" is worth one line of honesty. */
.contactMutedBadge{
    display:inline-block;
    margin-left:8px;
    padding:2px 7px;
    font-size:var(--fs-caption);
    font-weight:normal;
    color:#9fb3c8;
    background:rgba(46, 60, 79, .4);
    border:1px solid #2e3c4f;
    border-radius:4px;
    vertical-align:middle;
}

.notificationContactClearStopBtn{
    background:transparent;
    border:1px solid rgba(241, 196, 15, .45);
    border-radius:6px;
    padding:6px 12px;
    font-size:var(--fs-body);
    color:#f1c40f;
    cursor:pointer;
    white-space:nowrap;
}

.notificationContactClearStopBtn:hover{
    background:rgba(241, 196, 15, .15);
    border-color:#f1c40f;
}

/* The full explanation, shown in "My alerts" where there is room for it —
   the roster badge above is the same fact compressed to fit a row. */
.contactOptOutNotice{
    font-size:var(--fs-caption);
    color:#c5d2e0;
    line-height:1.5;
    margin:10px 0;
    padding:10px 12px;
    background:rgba(241, 196, 15, .1);
    border-left:3px solid #f1c40f;
    border-radius:0 4px 4px 0;
}

.contactOptOutNotice strong{
    color:#f1c40f;
}

@media (max-width:700px){

    /* At phone width the address under its own switch reads better than
       beside it — a long email address otherwise forces the row to wrap
       mid-address and the switch loses its label. */
    .selfContactChannels{
        align-items:flex-start;
        flex-direction:column;
        gap:6px;
    }

    .selfContactPhoneInput{
        max-width:100%;
        width:100%;
        flex:0 0 auto;
    }

    .notificationContactRow{
        align-items:flex-start;
        flex-direction:column;
    }

    .notificationContactActions{
        width:100%;
    }

}


/* ==========================================================================
   ADMIN — PANELS, COLLAPSED ACCOUNTS, ALERT PRIORITY        (2026-10-01)

   The admin page was one long scroll: a creation form, then the accounts
   list, then a collapsed quote model. Four tools stacked, so the one you
   came for started below the fold. These styles turn it into a nav bar plus
   one visible panel, collapse each account down to its name, and add the
   Alert Priority window.

   Palette is the page's existing one, unchanged: #0d1117 ground, #161b22
   raised, #2e3c4f lines, #c5d2e0 ink, #9fb3c8 muted.
   ========================================================================== */

/* ---- navigation ---- */

.adminNav{
    display:flex;
    flex-wrap:wrap;
    gap:8px;
    border-bottom:1px solid #2e3c4f;
    padding-bottom:12px;
}

.adminNavBtn{
    background:transparent;
    color:#9fb3c8;
    border:1px solid transparent;
    border-radius:6px;
    padding:9px 16px;
    font-size:var(--fs-caption);
    font-weight:bold;
    letter-spacing:.04em;
    text-transform:uppercase;
    cursor:pointer;
    min-height:var(--tap-min);
}

.adminNavBtn:hover{
    color:#c5d2e0;
    background:#161b22;
}

/* aria-current is the state, so it is also the selector — there is no
   separate class to fall out of step with what a screen reader is told. */
.adminNavBtn[aria-current="page"]{
    background:#161b22;
    border-color:#2e3c4f;
    color:#e8ebee;
}

.adminPanel{
    display:flex;
    flex-direction:column;
    gap:24px;
}

/* An author `display` beats the UA stylesheet's `[hidden]{display:none}` no
   matter how low its specificity, so the rule above showed every panel at
   once with the hidden attribute still sitting on three of them. Invisible
   to anything reading the markup, because the attribute IS there and the
   JS that set it worked perfectly. Any `display` on an element toggled with
   .hidden needs this beside it. Caught by verify-admin-panels. */
.adminPanel[hidden]{
    display:none;
}

/* ---- accounts, collapsed to a row ---- */

.accountList{
    gap:8px;
}

/* Overrides the padding on .accountCard above: a <details> pads its summary
   and its body separately, so a padded wrapper would double the inset on
   every open card. Later in the file at equal specificity, so it wins. */
.accountCard{
    padding:0;
    overflow:hidden;
}

.accountSummary{
    display:flex;
    align-items:center;
    gap:10px;
    padding:12px 16px;
    cursor:pointer;
    min-height:var(--tap-min);
    list-style:none;           /* Firefox draws its own marker otherwise */
    user-select:none;
}

.accountSummary::-webkit-details-marker{ display:none; }

.accountSummary:hover{
    background:#161b22;
}

.accountSummaryName{
    font-size:var(--fs-emphasis);
    font-weight:bold;
    color:#e8ebee;
    flex:1;
    min-width:0;
    overflow-wrap:anywhere;
}

.accountSummaryChevron{
    color:#9fb3c8;
    flex:0 0 auto;
    transition:transform .15s ease;
}

.accountCard[open] > .accountSummary > .accountSummaryChevron{
    transform:rotate(90deg);
}

.accountCardBody{
    padding:0 16px 16px;
    border-top:1px solid #2e3c4f;
    padding-top:14px;
}

/* A WxSentinel admin currently has support access to this account.
   
   Deliberately not dismissible and deliberately not subtle. This is the one
   banner on the dashboard that exists for the CUSTOMER's benefit rather than
   the product's, and a support-access notice somebody can close is a notice
   that gets closed.
   
   Amber rather than red: nothing is wrong, somebody is helping. #f1c40f on
   #0d1117 measures 12.6:1. */
.supportAccessBanner{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
    font-size:var(--fs-caption);
    color:#f1c40f;
    line-height:1.6;
    margin:0 0 10px;
    padding:10px 14px;
    background:rgba(241, 196, 15, .08);
    border:1px solid rgba(241, 196, 15, .35);
    border-radius:6px;
}

.supportAccessBanner[hidden]{
    display:none;
}

.supportAccessBanner strong{
    color:#ffe79a;
}

/* ==========================
   ADMIN SHELL — refresh markers, search, create-account entry
========================== */

/* A background refresh says so without taking the data away. Quiet by
   design: it appears every few minutes and must never compete with an
   account's details for attention. */
.adminRefreshing{
    font-size:var(--fs-micro);
    color:#6b7f95;
    letter-spacing:.04em;
    text-transform:uppercase;
    margin-bottom:10px;
}

/* A refresh that FAILED, over data that is still correct as of the last
   successful load. Amber, not red — nothing is broken on screen, it is just
   older than it looks.
   #f1c40f on #0d1117 measures 12.6:1. */
.adminRefreshError{
    font-size:var(--fs-caption);
    color:#f1c40f;
    line-height:1.6;
    margin-bottom:12px;
    padding:9px 12px;
    background:rgba(241, 196, 15, .08);
    border:1px solid rgba(241, 196, 15, .3);
    border-radius:6px;
}

.accountsHeaderActions{
    display:flex;
    align-items:center;
    gap:8px;
    flex-wrap:wrap;
}

.accountSearchLabel{
    display:flex;
    align-items:center;
    color:#6b7f95;
    margin-right:-28px;      /* tucks the icon inside the field */
    padding-left:10px;
    position:relative;
    z-index:1;
    pointer-events:none;
}

.accountSearch{
    background:#0d1117;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 12px 8px 32px;
    font-size:var(--fs-caption);
    font-family:inherit;
    min-height:var(--tap-min);
    /* WAS min-width:min(260px, 100%), and that is what pushed "+ New Account"
       out through the right edge of the card — by exactly 59px, at every
       width from 640px up.
       
       A PERCENTAGE in a min-width is invisible while the browser works out
       how wide the parent wants to be: against an indefinite container a
       percentage resolves to auto, so .accountsHeaderActions was measured
       with this input contributing its bare default width (201px) and came
       out 327px. Then layout ran, 100% finally had a definite 327px to
       resolve against, min() picked 260px, and the input took the 59px it
       had never been counted for. The parent was already the wrong size by
       then, so the button went outside it. The overflow is constant because
       it is the gap between those two numbers, not a lack of room — there
       were 400px spare in that header.
       
       A definite width is counted in both passes. max-width:100% still keeps
       it inside a narrow card, and min-width:0 lets it shrink before the row
       wraps instead of shoving a sibling out. */
    width:260px;
    max-width:100%;
    min-width:0;
}

.accountSearch:focus{
    outline:2px solid #0f75bc;
    outline-offset:1px;
}

.accountSearch::placeholder{
    color:#5c6b7a;
}

/* Says how many rows the filter is hiding. Without it, a search that matches
   nothing looks identical to an account list that is empty — which is the one
   way a filter like this causes a real scare. */
.accountSearchSummary{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin-bottom:10px;
}

/* The admin sign-out reuses .menuPanelItem for its colour and icon spacing,
   but that class is built for a dropdown row — width:100% and a left text
   align. In a header it has to be a button again. */
#adminSignOutBtn.menuPanelItem{
    width:auto;
    flex:0 0 auto;
    text-align:center;
}

/* On a wide screen the search and the New Account button belong on one line
   with the heading. They may wrap below 700px, where the header stacks
   anyway. */
@media (min-width:700px){
    .accountsHeaderActions{
        flex-wrap:nowrap;
    }
}

.createAccountTaxToggle{
    margin:4px 0 12px;
}

.adminPrimaryBtn{
    background:#2ecc71;
    color:#0d1117;
    border:none;
    border-radius:6px;
    padding:8px 14px;
    font-size:var(--fs-caption);
    font-weight:bold;
    cursor:pointer;
    min-height:var(--tap-min);
    white-space:nowrap;
}

.adminPrimaryBtn:hover{
    filter:brightness(1.08);
}

.adminBackBtn{
    display:inline-flex;
    align-items:center;
    gap:7px;
    background:transparent;
    color:#9fb3c8;
    border:none;
    padding:0;
    margin-bottom:12px;
    font-size:var(--fs-caption);
    font-family:inherit;
    cursor:pointer;
}

.adminBackBtn:hover{
    color:#c5d2e0;
    text-decoration:underline;
}

/* Visually hidden, still read aloud. The search field's icon is decorative;
   the label is what a screen reader announces. */
/* .srOnly was declared a second time here, ~11,000 lines after the first and
   one property short of it (no clip-path). Identical intent, so the second
   copy did nothing except hide the fact that there were two. Removed
   2026-10-08; the definition near the top of this file is the one. */

/* ---- system health ---- */

.healthClock{
    display:flex;
    align-items:baseline;
    gap:10px;
}

.healthClockTime{
    font-size:var(--fs-subhead);
    font-weight:bold;
    color:#e8ebee;
    font-variant-numeric:tabular-nums;
}

.healthCheckedAt{
    font-size:var(--fs-micro);
    color:#6b7f95;
    font-variant-numeric:tabular-nums;
}

/* A dot on the nav item, so a problem is visible from any other panel. */
.healthNavDot{
    display:inline-block;
    width:8px;
    height:8px;
    border-radius:50%;
    margin-left:7px;
    vertical-align:middle;
}

.healthNavDot--stale{ background:#f1c40f; }
.healthNavDot--dead{  background:#e74c3c; }

.healthBanner{
    font-size:var(--fs-caption);
    line-height:1.7;
    margin-bottom:14px;
    padding:11px 14px;
    border-radius:6px;
}

.healthBanner--stale{
    color:#f1c40f;
    background:rgba(241, 196, 15, .08);
    border:1px solid rgba(241, 196, 15, .3);
}

/* Red here and nowhere else on this page. A pipeline that has not succeeded
   in an hour is the one state on this screen that means customers are not
   being alerted.
   #ff6b5e on #0d1117 measures 6.8:1 — the plain #e74c3c is 4.95:1 and this
   is a block of running text, not a label. */
.healthBanner--dead{
    color:#ff6b5e;
    background:rgba(231, 76, 60, .1);
    border:1px solid rgba(231, 76, 60, .4);
}

.healthCards{
    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));
    gap:12px;
}

.healthCard{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-left-width:3px;
    border-radius:8px;
    padding:14px 16px;
}

.healthCard--ok{    border-left-color:#2ecc71; }
.healthCard--stale{ border-left-color:#f1c40f; }
.healthCard--dead{  border-left-color:#e74c3c; }

.healthCardHead{
    display:flex;
    align-items:center;
    gap:8px;
    flex-wrap:wrap;
    margin-bottom:8px;
}

.healthDot{
    width:9px;
    height:9px;
    border-radius:50%;
    flex:0 0 auto;
}

.healthDot--ok{    background:#2ecc71; }
.healthDot--stale{ background:#f1c40f; }
.healthDot--dead{  background:#e74c3c; }

.healthCardName{
    font-weight:bold;
    color:#e8ebee;
    font-size:var(--fs-caption);
    flex:1;
    min-width:0;
}

.healthCardStatus{
    font-size:var(--fs-micro);
    color:#9fb3c8;
    text-transform:uppercase;
    letter-spacing:.5px;
}

.healthCardAge{
    font-size:var(--fs-body);
    color:#c5d2e0;
    margin-bottom:8px;
}

.healthCardAge strong{
    color:#e8ebee;
}

.healthCardBlurb{
    font-size:var(--fs-micro);
    color:#6b7f95;
    line-height:1.6;
    margin:0 0 10px;
}

.healthCardStats{
    display:flex;
    gap:12px;
    flex-wrap:wrap;
    font-size:var(--fs-micro);
    color:#9fb3c8;
    font-variant-numeric:tabular-nums;
}

.healthCardError{
    font-size:var(--fs-micro);
    color:#f1c40f;
    line-height:1.6;
    margin:10px 0 0;
    overflow-wrap:anywhere;
}

/* The honest limits of a self-hosted monitor, on the page rather than only in
   a runbook nobody opens. */
.healthFooterNote{
    margin-top:16px;
    padding-top:14px;
    border-top:1px solid #2e3c4f;
}

.healthFooterNote p{
    font-size:var(--fs-micro);
    color:#6b7f95;
    line-height:1.7;
    max-width:80ch;
}

.healthFooterNote strong{
    color:#9fb3c8;
}

/* ---- audit log ---- */

.auditList{
    display:flex;
    flex-direction:column;
    gap:6px;
}

.auditRow{
    display:flex;
    align-items:baseline;
    gap:12px;
    flex-wrap:wrap;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 14px;
}

.auditAction{
    font-weight:bold;
    color:#e8ebee;
    font-size:var(--fs-caption);
}

.auditTarget{
    color:#9fb3c8;
    font-size:var(--fs-caption);
    flex:1;
    min-width:0;
    overflow-wrap:anywhere;
}

.auditWho{
    color:#6b7f95;
    font-size:var(--fs-micro);
}

.auditWhen{
    color:#6b7f95;
    font-size:var(--fs-micro);
    white-space:nowrap;
    font-variant-numeric:tabular-nums;
}

.auditDetail{
    flex-basis:100%;
    color:#6b7f95;
    font-size:var(--fs-micro);
    font-family:ui-monospace, Menlo, Consolas, monospace;
    overflow-wrap:anywhere;
}

/* An action that granted someone access to a customer's data is not the same
   as a plan change, and should not look like one. */
.auditRow--access{
    border-color:rgba(241, 196, 15, .35);
}

.auditRow--access .auditAction{
    color:#f1c40f;
}

/* ==========================
   ADMIN INBOX
   Contact requests, demo requests and in-app issue reports.
========================== */

.adminSectionHeader{
    display:flex;
    justify-content:space-between;
    align-items:center;
    flex-wrap:wrap;
    gap:12px;
    margin-bottom:16px;
}

.adminSectionHeader .adminSectionTitle{
    margin-bottom:0;
}

/* The open count beside the nav item. Neutral, not red: a queue with work in
   it is the normal state of a queue, and a permanent red badge stops meaning
   anything within a week. */
.adminNavCount{
    display:inline-block;
    margin-left:7px;
    font-size:var(--fs-micro);
    font-weight:bold;
    color:#0d1117;
    background:#9fb3c8;
    border-radius:9px;
    padding:1px 6px;
    font-variant-numeric:tabular-nums;
}

.inboxFilters{
    display:flex;
    gap:6px;
    flex-wrap:wrap;
}

.inboxFilterBtn{
    background:#0d1117;
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 12px;
    font-size:var(--fs-caption);
    cursor:pointer;
    min-height:var(--tap-min);
}

.inboxFilterBtn:hover{
    background:#1b2430;
    color:#c5d2e0;
}

.inboxFilterBtn--active{
    background:#1b2430;
    color:#e8ebee;
    border-color:#4a5c72;
    font-weight:bold;
}

/* The one state nothing else in the product would reveal: stored, but the
   notification never went out. Amber rather than red — it is work to do, not
   a fault, and red is reserved here for destructive actions.
   #f1c40f on #0d1117 measures 12.6:1. */
.inboxUnsentWarning{
    font-size:var(--fs-caption);
    color:#f1c40f;
    line-height:1.6;
    margin-bottom:14px;
    padding:11px 14px;
    background:rgba(241, 196, 15, .08);
    border:1px solid rgba(241, 196, 15, .3);
    border-radius:6px;
}

.inboxList{
    display:flex;
    flex-direction:column;
    gap:8px;
}

.inboxCard{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:8px;
    overflow:hidden;
}

.inboxSummary{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
    padding:12px 16px;
    cursor:pointer;
    min-height:var(--tap-min);
    list-style:none;           /* Firefox draws its own marker otherwise */
    user-select:none;
}

.inboxSummary::-webkit-details-marker{ display:none; }

.inboxSummary:hover{
    background:#1b2430;
}

.inboxKind{
    font-size:var(--fs-micro);
    font-weight:bold;
    text-transform:uppercase;
    letter-spacing:.5px;
    border-radius:4px;
    padding:3px 7px;
    flex:0 0 auto;
}

.inboxKind--contact{ background:#1b2430; color:#9fb3c8; }
.inboxKind--demo{    background:rgba(46, 204, 113, .15); color:#2ecc71; }
.inboxKind--issue{   background:rgba(15, 117, 188, .18); color:#5fb3f0; }
/* The only chip in this list that is a request to end something. Red, and
   the one place red appears in the Inbox. */
.inboxKind--deletion{ background:rgba(231, 76, 60, .16); color:#e8897f; }

.inboxWho{
    font-weight:bold;
    color:#e8ebee;
    flex:1;
    min-width:0;
    overflow-wrap:anywhere;
}

.inboxWhen{
    font-size:var(--fs-caption);
    color:#6b7f95;
    white-space:nowrap;
    flex:0 0 auto;
}

.inboxChip{
    font-size:var(--fs-micro);
    color:#9fb3c8;
    border:1px solid #2e3c4f;
    border-radius:9px;
    padding:2px 8px;
    flex:0 0 auto;
}

.inboxChip--unsent{
    color:#f1c40f;
    border-color:rgba(241, 196, 15, .45);
    background:rgba(241, 196, 15, .08);
    font-weight:bold;
}

.inboxChip--open{ color:#5fb3f0; border-color:rgba(15, 117, 188, .45); }
.inboxChip--closed{ color:#6b7f95; }

.inboxBody{
    padding:14px 16px 16px;
    border-top:1px solid #2e3c4f;
}

.inboxContact{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    margin:0 0 10px;
    overflow-wrap:anywhere;
}

.inboxContact a{ color:#5fb3f0; }

.inboxMessage{
    color:#c5d2e0;
    line-height:1.6;
    margin:0 0 12px;
    white-space:pre-wrap;
    overflow-wrap:anywhere;
}

.inboxContext{
    font-size:var(--fs-micro);
    color:#6b7f95;
    line-height:1.6;
    margin:0 0 10px;
    overflow-wrap:anywhere;
}

.inboxContext--warn{ color:#f1c40f; }

.inboxNotes{
    font-size:var(--fs-caption);
    color:#9fb3c8;
    border-left:2px solid #2e3c4f;
    padding-left:10px;
    margin:0 0 12px;
}

.inboxActions{
    display:flex;
    align-items:center;
    gap:8px;
    flex-wrap:wrap;
}

.inboxActionBtn{
    background:#0d1117;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 12px;
    font-size:var(--fs-caption);
    cursor:pointer;
    min-height:var(--tap-min);
}

.inboxActionBtn:hover{
    background:#1b2430;
}

.inboxActionStatus{
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

/* ---- alert priority: frame ---- */

.tiersHeader{
    display:flex;
    justify-content:space-between;
    align-items:flex-start;
    flex-wrap:wrap;
    gap:12px;
    margin-bottom:16px;
}

.tiersHeader .adminSectionTitle{
    margin-bottom:6px;
}

.tiersLede{
    color:#9fb3c8;
    font-size:var(--fs-caption);
    line-height:1.55;
    max-width:70ch;
}

.tiersLede strong{ color:#c5d2e0; }

.tiersDirty{
    font-size:var(--fs-micro);
    font-weight:bold;
    color:#f1c40f;
    white-space:nowrap;
    padding-top:4px;
}

.tiersDirty--clean{ color:#9fb3c8; font-weight:normal; }

.tiersError{
    color:#e74c3c;
    font-size:var(--fs-body);
    padding:12px 0;
}

.tiersSplit{
    display:flex;
    gap:18px;
    align-items:flex-start;
}

.tiersSplit > *{ min-width:0; }

.tiersCol{ flex:0 0 430px; }

/* The list is 28 rows tall, so the detail pane sticks: the selected tier's
   chime and the global sound controls stay in view while you scroll down to
   the tier you want to drag. */
.tiersDetailCol{
    flex:1;
    display:flex;
    flex-direction:column;
    gap:12px;
    position:sticky;
    top:16px;
}

.tiersList{
    list-style:none;
    margin:0;
    padding:0;
    display:flex;
    flex-direction:column;
    gap:4px;
}

/* ---- alert priority: a row ---- */

.tierRow{
    display:flex;
    align-items:center;
    gap:9px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:8px 11px;
    cursor:grab;
    min-height:38px;
}

.tierRow:hover{ border-color:#3d4f66; }

.tierRow--selected{
    border-color:#2ecc71;
    background:#121a16;
}

/* The group reads as one block at a glance — a left edge rather than a fill,
   so it does not fight the per-tier colour swatch. */
.tierRow--critical{
    border-left:3px solid #e74c3c;
}

.tierRow--dragging{ opacity:.45; cursor:grabbing; }
.tierRow--over{ border-color:#2ecc71; }

.tierGrip{ color:#4a5a6e; display:flex; flex:0 0 auto; }

.tierRank{
    font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size:var(--fs-micro);
    color:#6b7f95;
    width:20px;
    text-align:right;
    flex:0 0 auto;
    font-variant-numeric:tabular-nums;
}

.tierSwatch{
    width:10px;
    height:10px;
    border-radius:2px;
    flex:0 0 auto;
}

.tierName{
    flex:1;
    min-width:0;
    font-size:var(--fs-caption);
    color:#c5d2e0;
    overflow-wrap:anywhere;
}

.tierTagFlag,
.tierChimeBadge{
    font-size:9px;
    font-weight:bold;
    letter-spacing:.06em;
    padding:2px 6px;
    border-radius:3px;
    flex:0 0 auto;
    white-space:nowrap;
}

.tierTagFlag{
    color:#9fb3c8;
    border:1px solid #2e3c4f;
}

.tierChimeBadge{
    color:#0d1117;
    background:#9fb3c8;
}

/* ---- alert priority: the static cutoff line ---- */

.tierCutoff{
    display:flex;
    align-items:center;
    gap:8px;
    padding:6px 11px;
    margin:3px 0;
    border-top:2px solid #e74c3c;
    border-bottom:1px solid #3a1414;
    background:#1a0f0f;
    /* No grab cursor and no grip: the line does not move. Tiers are dragged
       across it, which is the whole interaction model. */
    cursor:default;
}

.tierCutoffLock{ color:#e74c3c; flex:0 0 auto; }

.tierCutoffLabel{
    flex:1;
    font-size:var(--fs-micro);
    font-weight:bold;
    letter-spacing:.08em;
    text-transform:uppercase;
    color:#e74c3c;
}

.tierCutoffCount{
    font-size:var(--fs-micro);
    color:#9fb3c8;
    font-variant-numeric:tabular-nums;
}

/* ---- alert priority: detail pane ---- */

.adminCard{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:15px 17px;
}

.adminCardKicker{
    display:block;
    font-size:var(--fs-micro);
    color:#6b7f95;
    letter-spacing:.08em;
    text-transform:uppercase;
}

.adminCardKicker em{ font-style:normal; color:#4a5a6e; }

.adminCardTitle{
    font-size:var(--fs-subhead);
    font-weight:bold;
    color:#e8ebee;
    margin:5px 0 13px;
}

.tierDetailRow{
    display:flex;
    align-items:center;
    justify-content:space-between;
    gap:12px;
    margin-bottom:12px;
}

.tierDetailRow label{
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

.tierDetailRow select{
    background:#161b22;
    color:#c5d2e0;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:7px 10px;
    font-size:var(--fs-caption);
    min-height:var(--tap-min);
}

.tierNote{
    font-size:var(--fs-micro);
    line-height:1.6;
    color:#9fb3c8;
    border-left:2px solid #2e3c4f;
    padding-left:10px;
    margin-top:12px;
}

.tierNote strong{ color:#c5d2e0; }
.tierNote--muted{ color:#6b7f95; }
.tierNote--muted strong{ color:#9fb3c8; }


.tierSlider{
    display:grid;
    grid-template-columns:1fr auto;
    gap:4px 10px;
    margin-bottom:12px;
}

.tierSlider label{
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

.tierSlider output{
    font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size:var(--fs-micro);
    color:#c5d2e0;
    font-variant-numeric:tabular-nums;
}

.tierSlider input[type="range"]{
    grid-column:1 / -1;
    width:100%;
    accent-color:#2ecc71;
}

/* ---- alert priority: actions ---- */

.tiersActions{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
    justify-content:flex-end;
    margin-top:18px;
    padding-top:16px;
    border-top:1px solid #2e3c4f;
}

.tiersActions .adminStatusMsg{
    margin-right:auto;
}

.adminStatusMsg--ok{ color:#2ecc71; }


/* ---- alert priority: the confirm dialogs ---- */

.tierScrim{
    position:fixed;
    inset:0;
    background:rgba(5, 8, 12, .72);
    display:flex;
    align-items:center;
    justify-content:center;
    padding:20px;
    z-index:1200;
}

.tierDialog{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-top:3px solid #2e3c4f;
    border-radius:10px;
    padding:20px 22px;
    max-width:460px;
    width:100%;
    max-height:85vh;
    overflow-y:auto;
}

.tierDialog--warn{ border-top-color:#e74c3c; }
.tierDialog--caution{ border-top-color:#f1c40f; }

.tierDialogKicker{
    display:block;
    font-size:var(--fs-micro);
    font-weight:bold;
    letter-spacing:.1em;
    text-transform:uppercase;
    margin-bottom:6px;
}

.tierDialog--warn .tierDialogKicker{ color:#e74c3c; }
.tierDialog--caution .tierDialogKicker{ color:#f1c40f; }

.tierDialog h2{
    font-size:var(--fs-subhead);
    font-weight:bold;
    color:#e8ebee;
    margin:0 0 10px;
    text-wrap:balance;
}

.tierDialogBody{
    font-size:var(--fs-caption);
    line-height:1.6;
    color:#9fb3c8;
    margin:0 0 14px;
}

.tierDialogBody--muted{ color:#6b7f95; }

.tierDialogDelta{
    display:flex;
    align-items:center;
    gap:8px;
    flex-wrap:wrap;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    font-size:var(--fs-caption);
    color:#c5d2e0;
    margin-bottom:14px;
}

.tierDialogArrow{ color:#6b7f95; }

.tierDialogCount{
    margin-left:auto;
    font-size:var(--fs-micro);
    color:#9fb3c8;
    font-variant-numeric:tabular-nums;
}

.tierDialogTypeLabel{
    display:block;
    font-size:var(--fs-micro);
    color:#9fb3c8;
    margin-bottom:6px;
}

.tierDialogTypeLabel strong{
    font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
    color:#e8ebee;
}

.tierDialog input[type="text"]{
    width:100%;
    background:#0d1117;
    color:#e8ebee;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:10px 12px;
    font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size:var(--fs-body);
    letter-spacing:.08em;
    min-height:var(--tap-min);
}

.tierDialogActions{
    display:flex;
    justify-content:flex-end;
    gap:8px;
    margin-top:16px;
    flex-wrap:wrap;
}

.tierDiffList{
    list-style:none;
    margin:0 0 14px;
    padding:0;
    display:flex;
    flex-direction:column;
    gap:6px;
    max-height:320px;
    overflow-y:auto;
}

.tierDiffList li{
    font-size:var(--fs-caption);
    color:#c5d2e0;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:5px;
    padding:7px 10px;
}

/* ---- narrow screens ---- */

@media (max-width: 900px){

    .tiersSplit{ flex-direction:column; }
    .tiersCol{ flex:1 1 auto; width:100%; }

    .tiersDetailCol{
        width:100%;
        position:static;   /* nothing to stick to in a single column */
    }

    .adminNav{ gap:4px; }
    .adminNavBtn{ padding:9px 11px; }

}


/* ==========================================================================
   ADMIN — SALES TAX EXEMPTION                               (2026-10-01)

   Most WxSentinel customers are exempt entities, so this is a common path
   rather than an edge case. The design priority is the expiry state: a
   certificate that lapsed is the thing that costs money in an audit, and it
   has to be legible while scanning the collapsed account list, not two
   clicks deep.

   Palette is the page's existing one: #0d1117 ground, #161b22 raised,
   #2e3c4f lines, #c5d2e0 ink, #9fb3c8 muted.
   ========================================================================== */

/* ---- the chip on the collapsed account row ---- */

.taxChip{
    font-size:9px;
    font-weight:bold;
    letter-spacing:.06em;
    padding:3px 7px;
    border-radius:3px;
    white-space:nowrap;
    flex:0 0 auto;
}

.taxChip--ok{
    color:#2ecc71;
    border:1px solid #1e5134;
}

/* Amber and red are the two that matter. They are deliberately louder than
   the plain EXEMPT chip, because a lapsed certificate is the only state in
   this feature that needs someone to do something. */
.taxChip--expiring{
    color:#0d1117;
    background:#f1c40f;
}

.taxChip--expired,
.taxChip--missing{
    color:#0d1117;
    background:#e74c3c;
}

/* ---- the section inside the expanded card ---- */

.taxSection{
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:14px 16px;
    margin-bottom:14px;
}

.taxToggle{
    display:flex;
    align-items:center;
    gap:9px;
    flex-wrap:wrap;
    cursor:pointer;
    font-size:var(--fs-body);
    color:#c5d2e0;
    min-height:var(--tap-min);
}

.taxToggle input[type="checkbox"]{
    width:17px;
    height:17px;
    accent-color:#2ecc71;
    cursor:pointer;
    flex:0 0 auto;
}

.taxToggleHint{
    font-size:var(--fs-micro);
    color:#6b7f95;
}

.taxBanner{
    font-size:var(--fs-caption);
    line-height:1.6;
    border-left:3px solid #2e3c4f;
    padding:8px 0 8px 11px;
    margin:12px 0 0;
}

.taxBanner--expiring{
    border-left-color:#f1c40f;
    color:#c9b458;
}

.taxBanner--expired,
.taxBanner--missing{
    border-left-color:#e74c3c;
    color:#e08b84;
}

/* ---- a certificate row ---- */

.taxCurrent{ margin-top:12px; }

.taxCertRow{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:9px 11px;
    margin-bottom:6px;
}

.taxCertRow--current{
    border-color:#3d5a45;
}

.taxCertJurisdiction{
    font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size:var(--fs-micro);
    font-weight:bold;
    letter-spacing:.06em;
    color:#e8ebee;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:4px;
    padding:3px 7px;
    flex:0 0 auto;
}

.taxCertMeta{
    flex:1;
    min-width:0;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    overflow-wrap:anywhere;
}

.taxCertNoDoc,
.taxCertSuperseded{
    font-size:var(--fs-micro);
    color:#6b7f95;
    flex:0 0 auto;
}

.adminBtn--small{
    padding:5px 10px;
    font-size:var(--fs-micro);
}

/* ---- the form ---- */

.taxForm{
    margin-top:14px;
    padding-top:14px;
    border-top:1px solid #2e3c4f;
}

.taxFormTitle{
    font-size:var(--fs-caption);
    font-weight:bold;
    color:#c5d2e0;
    margin:0 0 12px;
}

.taxFormTitle span{
    font-weight:normal;
    color:#6b7f95;
}

.taxGrid{
    display:grid;
    grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));
    gap:12px;
    margin-bottom:12px;
}

.taxField{
    display:flex;
    flex-direction:column;
    gap:4px;
    min-width:0;
}

.taxField--wide{ grid-column:span 2; }

.taxField label{
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

.taxFieldHint{
    font-size:var(--fs-micro);
    color:#6b7f95;
}

/* The date inputs inherit .contractInput, whose fill was chosen against the
   old always-expanded card. Inside .taxSection the ground is #0d1117 and the
   native picker icon needs the dark scheme or it renders as a black glyph on
   a dark field. */
.taxSection .contractInput{
    background:#161b22;
    color-scheme:dark;
    /* .contractInput is right-aligned because every field it was built for
       is a number in a column. A certificate number and a state code are
       neither, and right-aligned text reads as a mistake. */
    text-align:left;
}

.taxUpload{
    display:flex;
    align-items:center;
    gap:10px;
    flex-wrap:wrap;
    margin-bottom:12px;
}

.taxUploadLabel{
    display:flex;
    align-items:center;
    gap:9px;
    flex-wrap:wrap;
    font-size:var(--fs-caption);
    color:#9fb3c8;
    cursor:pointer;
}

.taxUploadLabel input[type="file"]{
    font-size:var(--fs-micro);
    color:#9fb3c8;
    max-width:100%;
}

.taxUploadStatus{
    font-size:var(--fs-micro);
    color:#2ecc71;
}

.taxSection .contractNotes{
    width:100%;
    margin-bottom:12px;
}

.taxHistory{
    margin-top:14px;
    padding-top:12px;
    border-top:1px solid #2e3c4f;
}

/* Stripe reachable is the happy path; "saved here but not in Stripe" is
   neither success nor failure and needs its own colour, or it reads as a
   clean save. */
.adminStatusMsg--warn{ color:#f1c40f; }

@media (max-width: 600px){
    .taxField--wide{ grid-column:span 1; }
}

/* ==========================
   TROPICAL CYCLONE POSITIONS (NHC)
   ==========================

   The marker, the age chip under it, and the popup. See the block comment on
   the matching section in js/script.js for why the age is on the MAP rather
   than only in the popup.
========================== */

/* Leaflet gives a divIcon a white background and a border by default, which
   on a dark basemap is a white square behind the symbol. */
.nhcStormIcon,
.nhcStormAge{
    background:none;
    border:none;
}

.nhcStormIcon svg{
    display:block;
    filter:drop-shadow(0 0 6px rgba(0,0,0,.65));
}

/* The chip rides below the symbol (negative iconAnchor Y in the divIcon) and
   must not take pointer events — the symbol under it is the click target, and
   a 120px-wide invisible box would otherwise eat clicks on anything nearby. */
.nhcStormAge{
    pointer-events:none;
    text-align:center;
}

.nhcStormAge span{
    display:inline-block;
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:5px;
    padding:3px 8px;
    font-size:var(--fs-micro);
    color:#9fb3c8;
    white-space:nowrap;
}

/* Past two hours the position is old enough that a glance should register it.
   The threshold lives in NHC_AGE_AMBER_MS in script.js; this rule only paints
   what that decided. */
.nhcStormAge--amber span{
    color:#f1c40f;
    border-color:#5a4a16;
}

.nhcPop{
    font-size:var(--fs-caption);
    color:#c5d2e0;
}

.nhcPopTitle{
    font-size:var(--fs-body);
    font-weight:bold;
    margin:0 0 2px;
}

.nhcPopSub{
    margin:0 0 10px;
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

.nhcPopRow{
    display:flex;
    justify-content:space-between;
    gap:14px;
    padding:3px 0;
}

.nhcPopRow span:first-child{ color:#7c8da3; }
.nhcPopRow span:last-child{ font-variant-numeric:tabular-nums; }

.nhcPopHead{
    margin:11px 0 4px;
    padding-top:9px;
    border-top:1px solid #2e3c4f;
    font-size:var(--fs-micro);
    letter-spacing:.5px;
    text-transform:uppercase;
    color:#7c8da3;
}

.nhcPopList{
    margin:0;
    padding:0;
    list-style:none;
}

.nhcPopList li{
    display:flex;
    align-items:center;
    gap:8px;
    padding:4px 0;
    border-top:1px solid #222c38;
    font-size:var(--fs-micro);
}

.nhcPopList li:first-child{ border-top:none; }

.nhcPopList li i{
    width:9px;
    height:9px;
    border-radius:2px;
    flex:0 0 auto;
}

/* The level name sits right, away from the site name, and must not push the
   row wider than the popup. */
.nhcPopList li span{
    margin-left:auto;
    color:#7c8da3;
    text-align:right;
}

.nhcPopAge{
    margin:11px 0 0;
    padding-top:9px;
    border-top:1px solid #2e3c4f;
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

.nhcPopAge--amber{ color:#f1c40f; }

/* Deliberately the only red thing in this popup. It is the sentence that
   stops the line being read as a cone. */
.nhcPopWarn{
    margin:9px 0 0;
    padding:8px 10px;
    border-radius:6px;
    background:#1a1414;
    font-size:var(--fs-micro);
    color:#e8b4b0;
}

.nhcPopLink{
    display:inline-block;
    margin-top:9px;
    color:#74b9ff;
    font-size:var(--fs-micro);
}

/* ==========================
   STORM CONTEXT IN THE ALERT MODAL (2026-10-13)
   ==========================

   One block between the stat tiles and the NWS description, shown only for a
   tropical alert whose storm could be attributed. Bordered like
   .alertModalTakeAction so it reads as a sibling panel rather than more body
   text — but in the STORM'S family colour, not --alertModalSeverityColor: it
   is about the system, not about this warning's rank.
========================== */

.alertStorm{
    border:1px solid #33495e;
    border-left:4px solid var(--stormColor, #ff4fa3);
    border-radius:8px;
    padding:12px 14px;
    margin-bottom:16px;
    background:#1a2029;
}

/* Same --hidden class convention as .alertModalStatTiles--hidden next to it,
   not the native [hidden] attribute: author CSS would beat the UA stylesheet's
   [hidden] rule and the block would stay visible. That exact mistake showed
   every Admin panel at once on 2026-10-01. */
.alertStorm--hidden{
    display:none;
}

.alertStormHead{
    display:flex;
    align-items:baseline;
    flex-wrap:wrap;
    gap:6px 10px;
    margin-bottom:8px;
}

.alertStormHead h3{
    font-size:var(--fs-body);
    margin:0;
    letter-spacing:1px;
    text-transform:uppercase;
    color:var(--stormColor, #ff4fa3);
}

.alertStormName{
    font-size:var(--fs-emphasis);
    font-weight:bold;
    color:#e7edf3;
}

.alertStormMeta{
    font-size:var(--fs-caption);
    color:#9fb3c8;
}

/* The one line set at emphasis size, above the storm's own statistics. The
   modal is about this site; the storm's vitals are already a click away on
   the map. */
.alertStormRelation{
    font-size:var(--fs-emphasis);
    line-height:1.45;
    margin:0 0 10px;
    color:#e7edf3;
}

.alertStormRelation b{
    color:#fff;
}

/* The ambiguous case is a question, not a headline — it should not shout in
   the same voice as a resolved attribution. */
.alertStormRelation--ask{
    font-size:var(--fs-body);
    color:#c5d2e0;
}

.alertStormForecast{
    display:flex;
    flex-wrap:wrap;
    gap:6px;
    margin-bottom:10px;
}

.alertStormChip{
    display:inline-flex;
    align-items:center;
    gap:6px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:999px;
    padding:4px 10px;
    font-size:var(--fs-caption);
    color:#c5d2e0;
    font-variant-numeric:tabular-nums;
}

.alertStormChip span{
    color:#7c8da3;
}

.alertStormAge{
    display:flex;
    align-items:center;
    gap:7px;
    margin:0;
    font-size:var(--fs-micro);
    color:#9fb3c8;
}

/* Past NHC_AGE_AMBER_MS in script.js. This rule only paints what that
   decided; the threshold is not repeated here. */
.alertStormAge--amber{
    color:#f1c40f;
}

.alertStormAge i{
    width:7px;
    height:7px;
    border-radius:50%;
    background:currentColor;
    flex:0 0 auto;
}

/* The only red thing in this block, and the sentence that stops the forecast
   line being read as a cone. Word-for-word the one the map popup carries. */
.alertStormCaveat{
    margin:9px -14px -12px;
    padding:9px 14px 11px;
    border-top:1px solid #2e3c4f;
    background:#1a1414;
    font-size:var(--fs-micro);
    color:#e8b4b0;
}

.alertStormLink{
    display:inline-block;
    margin-top:9px;
    color:#74b9ff;
    font-size:var(--fs-micro);
}

/* "Show the centre on the map" (phase B). The draft of this block carried a
   distance chip that looked clickable and was not — Josh asked, reasonably,
   where the map showing the storm actually was. This is that chip made real:
   it switches the map's fit to include the centre.

   A <button>, not an <a>: it moves the view on this page. Styled as a quiet
   inline control rather than a primary action, because the warned area is
   still the thing the alert is about and this is an aside.
   --tap-min on the block axis keeps it a legal touch target (WCAG 2.5.8)
   without making it look like a button bar. */
.alertStormShowOnMap{
    display:inline-flex;
    align-items:center;
    gap:6px;
    min-height:var(--tap-min);
    margin-top:2px;
    padding:0;
    background:none;
    border:none;
    border-bottom:1px dashed #44566e;
    color:#74b9ff;
    font-size:var(--fs-micro);
    font-family:inherit;
    cursor:pointer;
}

.alertStormShowOnMap:hover{
    color:#9ecbff;
    border-bottom-color:#74b9ff;
}

/* The arrow is decoration, so it is drawn here rather than put in the markup
   where a screen reader would read it as part of the label. */
.alertStormShowOnMap::after{
    content:"\2197";
    font-size:1.1em;
    line-height:1;
}

.alertStormPick{
    display:flex;
    flex-direction:column;
    gap:6px;
    margin-top:8px;
}

.alertStormPickBtn{
    display:flex;
    align-items:center;
    gap:9px;
    width:100%;
    text-align:left;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
    padding:8px 10px;
    color:#c5d2e0;
    font-size:var(--fs-caption);
    font-family:inherit;
    cursor:pointer;
}

.alertStormPickBtn:hover{
    background:#1c2733;
}

.alertStormPickBtn b{
    color:#e7edf3;
}

.alertStormPickBtn .dist{
    margin-left:auto;
    color:#7c8da3;
    font-variant-numeric:tabular-nums;
}

.alertStormPickDot{
    width:9px;
    height:9px;
    border-radius:50%;
    flex:0 0 auto;
}

/* ============================================================
   ROLE VISIBILITY (2026-10-13)
   ============================================================
   One class on <body>, set by applyRoleVisibility() at sign-in, and every
   control a given role must not see is listed here rather than gated at a
   dozen render sites. The per-card Edit button is the reason: cards re-render
   every ten seconds, so a JS gate there has to be remembered on every redraw.

   SPECIFICITY: `body.roleMember #someId` is (1,1,1) and `body.roleMember .cls`
   is (0,2,1), both of which beat the component's own rule, so none of this
   needs !important — which is the flag that hid the mobile Sign Out button's
   red for two days.

   THESE RULES ARE NOT THE PERMISSION. Each one has a matching RLS policy
   behind it (locations_insert_admins, account_notification_contacts_shared,
   and so on). Hiding a button stops a mistake; RLS stops an attempt. */

/* --- member: reads the board, edits nothing but their own alerts --- */

body.roleMember #addLocationToggleBtn,
body.roleMember #addLocationForm,
body.roleMember #importLocationsBtn,
body.roleMember #downloadTemplateBtn,
body.roleMember #importFileInput,
body.roleMember #manageTagsBtn,
body.roleMember #alertRecipientsBtn,
body.roleMember .locationEditBtn{
    display:none;
}

/* Export CSV stays. Reading out the locations they can already see on screen
   takes nothing away from anyone, and a monitoring user who cannot get a list
   of what they are monitoring is obstructed rather than restricted.

   Reorder Locations stays too: it writes user_preferences.location_order,
   which is that person's own view, not the account's data. "Cannot edit
   locations" is about the locations, not about the order they appear in on
   one screen. */

/* --- not the owner: no billing, no deleting the account --- */

body.roleNotOwner #manageBillingBtn,
body.roleNotOwner #deleteAccountBlock,
body.roleNotOwner #plansTable{
    display:none;
}

/* ---- people on this account (2026-10-13) ---- */

.accountMembersBlock--hidden{
    display:none;
}

.accountMembersList{
    display:flex;
    flex-direction:column;
    gap:6px;
    margin-top:8px;
}

.accountMemberRow{
    display:flex;
    align-items:center;
    flex-wrap:wrap;
    gap:8px;
    padding:9px 11px;
    background:#0d1117;
    border:1px solid #2e3c4f;
    border-radius:8px;
}

.accountMemberWho{
    flex:1 1 180px;
    min-width:0;
    overflow-wrap:anywhere;
    font-size:var(--fs-body);
    color:#c5d2e0;
}

.accountMemberYou{
    font-size:var(--fs-caption);
    color:#7c8da3;
}

.accountMemberControls{
    display:flex;
    align-items:center;
    gap:6px;
    flex:0 0 auto;
}

/* Classed, so it styles itself in full — `.authForm select:not([class])` does
   not match it, and this control does not live in an .authForm anyway. */
.accountMemberRole{
    background:#161b22;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    color:#c5d2e0;
    font-size:var(--fs-caption);
    font-family:inherit;
    min-height:var(--tap-min);
}

.accountMemberRoleFixed{
    padding:6px 10px;
    font-size:var(--fs-caption);
    font-weight:600;
    color:#9fb3c8;
}

.accountMemberTransferBtn,
.accountMemberRemoveBtn{
    background:transparent;
    border:1px solid #2e3c4f;
    border-radius:6px;
    padding:6px 10px;
    color:#9fb3c8;
    font-size:var(--fs-caption);
    font-family:inherit;
    cursor:pointer;
    min-height:var(--tap-min);
}

.accountMemberTransferBtn:hover{
    border-color:#0f75bc;
    color:#74b9ff;
}

/* Red on hover only. A destructive control that is red at rest reads as an
   error state on a row where nothing is wrong. */
.accountMemberRemoveBtn:hover{
    border-color:#8c3b32;
    color:#e8b4b0;
}

.accountMembersLoading{
    margin:0;
    font-size:var(--fs-caption);
    color:#7c8da3;
}
