@charset "utf-8";
/* ============================================================================
   COMPONENTS

   Hand-written CSS for components added from here on. Everything in this file
   styles a *named thing with states* — an alert band with severity levels, an
   air-quality chip with index bands, a metric chip that is either promoted or
   demoted — which is what CSS classes are good at and what utility classes are
   not.

   WHY NOT TAILWIND
   The Tailwind surface is frozen at the ~117 rules already generated into
   tailwind-utils.css. Existing utilities stay available and can be used freely
   in markup; what stops is *adding new ones*. Regenerating tailwind-utils.css
   is a manual step with no CI gate, and its failure mode is silent — a class
   that was never generated simply does not style, with no error anywhere. That
   is a bad property to multiply across a large feature push.

   Two further reasons specific to this codebase:
     - Many classes only ever appear inside innerHTML template strings, so the
       content globs have to cover the .js files. If any script ever moves into
       a subdirectory, './*.js' silently stops matching it (see input.css).
     - Theming here is custom-property based. Static Tailwind v3 output cannot
       participate without arbitrary-value syntax like bg-[var(--panel)], which
       is strictly worse than writing background: var(--panel).

   HOW TO WRITE A COMPONENT HERE
   Reference the tokens defined at the top of styles.css — --panel, --text,
   --text-muted, --panel-border, --focus-ring and friends. Never hardcode a
   colour that should change with the weather or the scheme; if a component
   needs a colour the tokens do not cover, add the token rather than the literal.
   ============================================================================ */

/* Semantic status colours. Separate from the weather palette on purpose: these
   must mean the same thing regardless of what the sky is doing. */
:root {
    --status-good: #15803d;
    --status-good-bg: #dcfce7;
    --status-moderate: #a16207;
    --status-moderate-bg: #fef9c3;
    --status-poor: #b91c1c;
    --status-poor-bg: #fee2e2;
    --status-severe: #7f1d1d;
    --status-severe-bg: #fecaca;
}

:root[data-scheme="dark"] {
    --status-good: #86efac;
    --status-good-bg: rgba(21, 128, 61, 0.22);
    --status-moderate: #fde047;
    --status-moderate-bg: rgba(161, 98, 7, 0.25);
    --status-poor: #fca5a5;
    --status-poor-bg: rgba(185, 28, 28, 0.25);
    --status-severe: #fecaca;
    --status-severe-bg: rgba(127, 29, 29, 0.35);
}

/* ---- Inline error and status messages ----
   These were Tailwind's text-red-500 / text-red-600 in innerHTML strings, which is
   3.68:1 on a light panel — below AA, and on the one kind of text a user most needs
   to be able to read. A semantic class also means the colour can invert for the dark
   palettes, which a fixed utility class cannot. */
.msg-error {
    color: var(--status-poor);
}

.msg-notice {
    color: var(--text-muted);
}

/* Precipitation percentage on the hourly cards. Was Tailwind's text-blue-500 at
   3.52:1 on the light card and worse on the dark one. */
.precip-label {
    color: var(--precip-text);
}

:root {
    --precip-text: #1d4ed8;
}

:root[data-scheme="dark"] {
    --precip-text: #93c5fd;
}

/* ---- The "now" panel ----
   Hero on the left, promoted metric chips below it, and the full grid behind a
   disclosure. Nothing is hidden — the drawer is always there and remembers whether
   it was open — but prominence is earned. See metrics.js for how that is decided. */

/* A centred vertical stack. Each row gets the full width — the metric lists used to
   be flex ITEMS competing with the hero, which is what collapsed them — but the
   content within each row is centred, which is what made the original panel feel
   calm and balanced rather than crammed against the left edge. */
#current {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
    text-align: center;
}

.now-hero {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 1.1rem;
    flex-wrap: wrap;
}

.now-icon {
    font-size: 4rem;
    line-height: 1;
}

.now-main {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    text-align: left;
}

.now-temp {
    font-size: clamp(2.5rem, 8vw, 3.25rem);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -0.02em;
}

.now-condition {
    font-size: 1.05rem;
    font-weight: 500;
}

.now-sub {
    font-size: 0.9rem;
}

/* The generated sentence. Given real weight — it is the most useful line on the
   page, and burying it in the same size as the rest wastes it. */
.now-headline {
    margin: 1.1rem 0 0;
    font-size: 1.05rem;
    font-weight: 500;
    line-height: 1.45;
    text-wrap: balance;
    max-width: 46ch;
}

/* ---- Nowcast ----
   Only on screen when there is precipitation now or within two hours, so it is
   allowed real weight — when it appears it is the most useful line on the page. */
.nowcast {
    width: 100%;
    max-width: 30rem;
    margin: 1.1rem auto 0;
    padding: 0.75rem 0.9rem 0.6rem;
    border-radius: 0.7rem;
    background: color-mix(in srgb, var(--tone) calc(var(--tone-alpha) * 100%), transparent);
    border: 1px solid color-mix(in srgb, var(--tone) 38%, transparent);
}

.nowcast-summary {
    margin: 0 0 0.6rem;
    font-size: 1rem;
    font-weight: 600;
    text-align: center;
    color: var(--tone, var(--text));
    text-wrap: balance;
}

.nowcast-chart {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 2.6rem;
}

.nowcast-bar {
    flex: 1;
    min-height: 2px;
    border-radius: 2px 2px 0 0;
    background: var(--tone, var(--text-muted));
    /* A dry quarter-hour still draws a hairline, so the gap between two showers
       reads as part of the same chart rather than as missing data. */
    opacity: 0.85;
}

.nowcast-bar[style*="height:0%"] {
    opacity: 0.25;
}

.nowcast-axis {
    display: flex;
    justify-content: space-between;
    margin-top: 0.25rem;
    font-size: 0.68rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* ---- Advisories ---- */
.advisory-list {
    list-style: none;
    margin: 0.9rem 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

.advisory {
    font-size: 0.875rem;
    font-weight: 500;
    padding: 0.5rem 0.75rem;
    border-radius: 0.5rem;
    border-left: 3px solid currentColor;
}

.advisory[data-tone="warn"] {
    color: var(--status-moderate);
    background: var(--status-moderate-bg);
}

.advisory[data-tone="cold"] {
    color: var(--advisory-cold);
    background: var(--advisory-cold-bg);
}

:root {
    --advisory-cold: #0369a1;
    --advisory-cold-bg: rgba(3, 105, 161, 0.1);
}

:root[data-scheme="dark"] {
    --advisory-cold: #bae6fd;
    --advisory-cold-bg: rgba(125, 211, 252, 0.14);
}

/* ---- Metric chips ---- */
/* auto-fit with a real minimum, NOT a fixed column count.
   A fixed `repeat(6, 1fr)` divides whatever width it is given, so in a cramped
   container it produced 40px columns and wrapped labels one character per line
   ("29. 83 inH g"). With a minimum track the grid reflows to fewer columns instead
   of ever making a chip too narrow to read. */
.metric-strip,
.metric-grid {
    list-style: none;
    margin: 1.1rem 0 0;
    padding: 0;
    display: grid;
    /* auto-fit with a real minimum, NOT a fixed column count. A fixed
       `repeat(6, 1fr)` divides whatever width it is given, so in a cramped container
       it produced 40px columns and wrapped labels one character per line. */
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: 0.7rem;
    width: 100%;
}

/* The chips previously used --card on a --panel background, which on the light
   palettes are within a few percent of each other — so twelve of them read as one
   undifferentiated block. These tints are derived from the text colour rather than
   the palette, so they hold their separation on every weather background. */
.metric-chip {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    padding: 0.65rem 0.7rem;
    border-radius: 0.7rem;
    background: var(--chip-bg);
    border: 1px solid var(--chip-border);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    min-width: 0;
    text-align: center;
}

:root {
    --chip-bg: rgba(15, 23, 42, 0.045);
    --chip-border: rgba(15, 23, 42, 0.12);

    /* Tone colours. Every one clears AA against its own tint on the DARKEST light
       palette (fog/cloudy day), which is the worst case — checked, not guessed.
       UV follows the published WMO scale, so the colours mean what people expect. */
    --tone-good: #166534;
    --tone-mild: #854d0e;
    --tone-warn: #9a3412;
    --tone-severe: #b91c1c;
    --tone-extreme: #7e22ce;
    --tone-cold: #075985;
    --tone-wet: #155e75;
    --tone-alpha: 0.12;
}

:root[data-scheme="dark"] {
    --chip-bg: rgba(255, 255, 255, 0.07);
    --chip-border: rgba(255, 255, 255, 0.14);

    --tone-good: #86efac;
    --tone-mild: #fcd34d;
    --tone-warn: #fdba74;
    --tone-severe: #fca5a5;
    --tone-extreme: #d8b4fe;
    --tone-cold: #7dd3fc;
    --tone-wet: #67e8f9;
    --tone-alpha: 0.16;
}

/* A toned chip tints its background and colours its LABEL. The value stays
   --text so the number itself is always maximum contrast — the colour is the
   interpretation, not the data. */
.metric-chip[data-tone="good"] { --tone: var(--tone-good); }
.metric-chip[data-tone="mild"] { --tone: var(--tone-mild); }
.metric-chip[data-tone="warn"] { --tone: var(--tone-warn); }
.metric-chip[data-tone="severe"] { --tone: var(--tone-severe); }
.metric-chip[data-tone="extreme"] { --tone: var(--tone-extreme); }
.metric-chip[data-tone="cold"] { --tone: var(--tone-cold); }
.metric-chip[data-tone="wet"] { --tone: var(--tone-wet); }

.metric-chip[data-tone]:not([data-tone="calm"]) {
    background: color-mix(in srgb, var(--tone) calc(var(--tone-alpha) * 100%), transparent);
    border-color: color-mix(in srgb, var(--tone) 38%, transparent);
}

.metric-chip[data-tone]:not([data-tone="calm"]) .metric-label,
.metric-chip[data-tone]:not([data-tone="calm"]) .metric-detail {
    color: var(--tone);
}

.metric-label {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--text-muted);
    /* Long labels stay on one line and ellipsis rather than clipping out of the
       chip, which is what "PRECIPITATION" was doing. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.metric-value {
    font-size: 1rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.25;
    /* Wrap at spaces only. `break-word` is what let "29.83 inHg" split mid-token
       into "29. 83 inH g" when the chip was narrow. */
    overflow-wrap: normal;
    word-break: keep-all;
}

.metric-detail {
    font-size: 0.72rem;
    color: var(--text-muted);
    line-height: 1.3;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* A metric with no reading still holds its place, so the grid does not reflow
   between refreshes. */
.metric-chip[data-empty="1"] {
    opacity: 0.55;
}

/* ---- The disclosure ---- */
/* This read as plain text before — nothing said it could be opened. It now looks
   like the control it is: a pill with a chevron that rotates when expanded. */
/* Deliberately NOT a centred column flex. `align-items: center` shrink-wraps a flex
   item on the cross axis, which collapsed the grid inside to a single 136px column
   however wide the panel was. Block layout keeps the grid full width; the summary is
   centred on its own. */
.metric-more {
    margin-top: 1rem;
    width: 100%;
    display: block;
    text-align: center;
}

/* Suppress the native triangle in both engines so the chevron below is the only marker. */
.metric-more>summary::-webkit-details-marker {
    display: none;
}

.metric-more>summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-muted);
    padding: 0.4rem 0.9rem;
    border: 1px solid var(--chip-border);
    border-radius: 999px;
    background: var(--chip-bg);
    transition: color 0.2s ease, border-color 0.2s ease;
}

.metric-more>summary::after {
    content: '';
    width: 0.45rem;
    height: 0.45rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translate(-1px, -1px);
    transition: transform 0.2s ease;
}

.metric-more[open]>summary::after {
    transform: rotate(-135deg) translate(-1px, -1px);
}

.metric-more>summary:hover {
    color: var(--text);
    border-color: var(--text-muted);
}

.metric-more>summary:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
}

/* ---- Severe weather alerts ----
   Sits above everything, and only exists when something is active. Full-bleed within
   the content column so it cannot be mistaken for one of the metric chips. */
#alert-band {
    /* LOAD-BEARING, not decoration — the same rule styles.css:217 documents for
       .content-section, and for the same reason.

       #weather-effects-container is position:fixed with z-index:0 and an OPAQUE sky
       gradient. A positioned element with z-index:0 paints above non-positioned block
       boxes, so without a stacking context of its own this band is painted over by the
       weather overlay: it holds its space and stays clickable (the overlay is
       pointer-events:none, so clicks fall through) but renders completely invisible.

       Any new direct child of .container needs this. Computed styles all read
       "visible" in that state, so it cannot be caught by inspecting them — only by
       looking at the page. */
    position: relative;
    z-index: 1;

    margin-bottom: 1.25rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

#alert-band[hidden] {
    display: none;
}

.alert-band {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    text-align: left;
    padding: 0.85rem 1rem;
    border-radius: 0.75rem;
    border: 1px solid var(--tone);
    /* Stronger than a metric chip on purpose — this one is meant to interrupt. */
    background: color-mix(in srgb, var(--tone) 16%, transparent);
    color: var(--text);
    cursor: pointer;
    font: inherit;
    transition: background 0.2s ease;
}

.alert-band:hover {
    background: color-mix(in srgb, var(--tone) 24%, transparent);
}

.alert-band[data-tone="extreme"] { --tone: var(--tone-extreme); }
.alert-band[data-tone="severe"] { --tone: var(--tone-severe); }
.alert-band[data-tone="warn"] { --tone: var(--tone-warn); }
.alert-band[data-tone="mild"] { --tone: var(--tone-mild); }

.alert-band-icon {
    font-size: 1.35rem;
    line-height: 1;
    color: var(--tone);
    flex-shrink: 0;
}

.alert-band-text {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
    flex: 1;
}

.alert-band-event {
    font-weight: 700;
    color: var(--tone);
    line-height: 1.25;
}

.alert-band-meta {
    font-size: 0.8rem;
    color: var(--text-muted);
}

.alert-band-more {
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--tone);
    border: 1px solid var(--tone);
    border-radius: 999px;
    padding: 0.1rem 0.5rem;
    flex-shrink: 0;
}

.alert-band-cue {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--tone);
    flex-shrink: 0;
}

.alert-band-rest {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}

.alert-band-small {
    font: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    cursor: pointer;
    padding: 0.3rem 0.65rem;
    border-radius: 999px;
    border: 1px solid var(--tone);
    background: color-mix(in srgb, var(--tone) 10%, transparent);
    color: var(--tone);
}

.alert-band-small[data-tone="extreme"] { --tone: var(--tone-extreme); }
.alert-band-small[data-tone="severe"] { --tone: var(--tone-severe); }
.alert-band-small[data-tone="warn"] { --tone: var(--tone-warn); }
.alert-band-small[data-tone="mild"] { --tone: var(--tone-mild); }

.alert-band:focus-visible,
.alert-band-small:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
}

/* ---- Alert detail sheet ---- */
#alert-modal {
    width: 560px;
    max-height: 80vh;
    overflow-y: auto;
    text-align: left;
}

.alert-modal-meta {
    font-size: 0.82rem;
    color: var(--text-muted);
    margin: 0 0 0.35rem;
}

.alert-modal-area {
    font-size: 0.85rem;
    font-weight: 600;
    margin: 0 0 0.9rem;
}

/* NWS bodies are pre-wrapped plain text with * WHAT / * WHERE bullets — preserving the
   newlines keeps that structure instead of running it into one paragraph. */
.alert-modal-body,
.alert-modal-instruction {
    font-size: 0.875rem;
    line-height: 1.55;
    white-space: pre-wrap;
}

.alert-modal-instruction {
    margin-top: 1rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--panel-border);
}

.alert-modal-instruction strong {
    display: block;
    margin-bottom: 0.35rem;
    font-size: 0.7rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ---- View switcher ----
   The segmented control for the top-level routes. Its children are anchors, not
   buttons (see index.html), so the link defaults have to be undone here. */

:root {
    /* Measured against their own foreground, not inherited from the graph tabs:
       white on #0369a1 is 5.93:1, and #0f172a on #38bdf8 is 8.33:1. Reusing
       --graph-tab-active for both schemes would have put white text on a pale
       blue in dark mode, which is around 2:1. */
    --nav-active-bg: #0369a1;
    --nav-active-text: #ffffff;
}

:root[data-scheme="dark"] {
    --nav-active-bg: #38bdf8;
    --nav-active-text: #0f172a;
}

#view-nav {
    /* LOAD-BEARING, not decoration — the same rule styles.css:217 documents for
       .content-section and components.css does for #alert-band.
       #weather-effects-container is position:fixed z-index:0 with an OPAQUE sky
       gradient, and a positioned z-index:0 element paints above non-positioned
       block boxes. Without this the switcher holds its space and stays clickable
       (the overlay is pointer-events:none) but renders completely invisible.
       Any new direct child of .container needs this too. */
    position: relative;
    z-index: 1;

    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 0.25rem;
    width: fit-content;
    max-width: 100%;
    margin: 0 auto 1.5rem;
    padding: 0.25rem;
    border-radius: 999px;
    background: var(--panel);
    border: 1px solid var(--panel-border);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.view-tab {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    /* 2.75rem is 44px — the touch-target guideline the header buttons already
       follow. The vertical padding alone would leave these at about 36px. */
    min-height: 2.75rem;
    padding: 0 1.1rem;
    border-radius: 999px;
    font-size: 0.875rem;
    font-weight: 600;
    white-space: nowrap;
    text-decoration: none;
    color: var(--text-muted);
    transition: background-color 0.2s ease, color 0.2s ease;
}

.view-tab svg {
    width: 1.05rem;
    height: 1.05rem;
    flex-shrink: 0;
}

.view-tab:hover {
    color: var(--text);
    background: var(--card);
}

.view-tab[aria-current="page"] {
    color: var(--nav-active-text);
    background: var(--nav-active-bg);
}

/* The UA default focus ring is invisible against several of the sky gradients,
   which is why every interactive element here declares its own. */
.view-tab:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
}

/* Route switching. [hidden] is only `display: none` at UA-stylesheet priority, so
   any class-based display rule on these panels would beat it. Making it explicit
   here means a panel from the other route cannot leak into the layout. */
[data-view][hidden] {
    display: none !important;
}

/* ---- Sky & Sun ----
   Two cards side by side on anything wider than a phone, stacked below it. The
   score chips reuse .metric-chip verbatim so the two panels share one visual
   language rather than inventing a second one. */

.sky-grid {
    display: grid;
    /* auto-fit with a real minimum, not two fixed columns — the same reason
       .metric-grid uses it. A fixed 1fr 1fr squeezes the sun arc to illegibility
       in a narrow container instead of dropping to one column. */
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1rem;
    align-items: stretch;
}

.sky-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    padding: 0.9rem;
    border-radius: 0.7rem;
    background: var(--chip-bg);
    border: 1px solid var(--chip-border);
}

.sky-card-moon {
    flex-direction: row;
    gap: 1rem;
}

.sun-arc {
    width: 100%;
    max-width: 17rem;
    height: auto;
}

.sun-arc-horizon {
    stroke: var(--chip-border);
    stroke-width: 1;
}

.sun-arc-track {
    fill: none;
    stroke: var(--sun-track);
    stroke-width: 2;
    stroke-dasharray: 3 5;
    stroke-linecap: round;
}

/* The amber fill is only about 1.9:1 on a light card — fine as a shape, too weak as
   the one thing you are meant to find. The rim is the track colour, which measures
   4.55:1, so the marker keeps a readable edge in both schemes. */
.sun-arc-marker {
    fill: var(--sun-marker);
    stroke: var(--sun-track);
    stroke-width: 1.5;
}

/* Below the horizon the sun is drawn as an outline: still placed, plainly not up. */
.sun-arc-marker[data-up="0"] {
    fill: none;
    stroke: var(--sun-track);
    stroke-width: 2;
}

.moon-disc {
    width: 3.6rem;
    height: 3.6rem;
    flex-shrink: 0;
}

.moon-shadow {
    fill: var(--moon-shadow);
}

.moon-lit {
    fill: var(--moon-lit);
}

.sky-moon-text {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.sky-endpoints {
    display: flex;
    justify-content: center;
    gap: 1.75rem;
}

.sky-endpoints > span {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.1rem;
}

.sky-label {
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.sky-value {
    font-size: 1rem;
    font-weight: 600;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}

.sky-note {
    margin: 0;
    font-size: 0.85rem;
    text-align: center;
    color: var(--text-muted);
    text-wrap: balance;
}

.sky-windows {
    list-style: none;
    margin: 1rem 0 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
    gap: 0.7rem;
}

.sky-windows li {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    padding: 0.55rem 0.6rem;
    border-radius: 0.6rem;
    background: var(--chip-bg);
    border: 1px solid var(--chip-border);
    text-align: center;
}

/* The en-dash range is the widest thing in these cells; let it shrink rather than
   push the grid sideways. */
.sky-windows .sky-value {
    font-size: 0.9rem;
    white-space: nowrap;
}

.sky-scores {
    margin-top: 0.7rem;
}

/* The score chips are buttons, so the UA button defaults have to be undone. They
   inherit the .metric-chip look exactly — an explanation you can open should not
   look like a different kind of thing from the reading it explains. */
.score-chip {
    font: inherit;
    cursor: pointer;
    text-align: center;
    width: 100%;
    transition: border-color 0.15s ease, transform 0.15s ease;
}

.score-chip:hover {
    border-color: var(--tone, var(--text-muted));
}

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

.score-chip[aria-expanded="true"] {
    border-color: var(--tone, var(--text-muted));
}

/* The "why?" affordance. Kept inside .metric-detail so it shares the chip's muted
   colour and cannot fight the value above it for attention. */
.score-chip .metric-detail::after {
    content: ' \25BE';
    font-size: 0.7em;
}

.score-chip[aria-expanded="true"] .metric-detail::after {
    content: ' \25B4';
}

.score-why {
    margin-top: 0.7rem;
    padding: 0.85rem 1rem;
    border-radius: 0.7rem;
    background: var(--chip-bg);
    border: 1px solid var(--chip-border);
}

.score-why-title {
    margin: 0 0 0.55rem;
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.score-why-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.score-why-list li {
    font-size: 0.9rem;
    line-height: 1.45;
    color: var(--text);
    padding-left: 0.75rem;
    border-left: 3px solid var(--why-effect);
}

.score-why-list li[data-effect="good"] { --why-effect: var(--tone-good); }
.score-why-list li[data-effect="mixed"] { --why-effect: var(--tone-mild); }
.score-why-list li[data-effect="poor"] { --why-effect: var(--tone-warn); }

/* Midnight sun and polar night. Given real weight — inside the polar circles it is
   the single most important thing the panel has to say. */
.sky-polar {
    margin: 0;
    padding: 1.4rem 0;
    font-size: 1.05rem;
    font-weight: 600;
    text-align: center;
    color: var(--text);
    text-wrap: balance;
}

.sky-footnote {
    margin: 0.9rem 0 0;
    font-size: 0.8rem;
    text-align: center;
    color: var(--text-muted);
}

:root {
    --sun-track: #b45309;
    --sun-marker: #f59e0b;
    --moon-shadow: rgba(15, 23, 42, 0.16);
    --moon-lit: #e2e8f0;
}

:root[data-scheme="dark"] {
    --sun-track: #fbbf24;
    --sun-marker: #fcd34d;
    --moon-shadow: rgba(226, 232, 240, 0.14);
    --moon-lit: #f1f5f9;
}

/* The lit disc is near-white in both schemes, so on a light panel it needs an
   edge or it disappears into the card. */
:root:not([data-scheme="dark"]) .moon-lit {
    stroke: #94a3b8;
    stroke-width: 0.75;
}

/* ---- Air quality ----
   The panel needs BOTH a matching route and a reading. The router owns the first
   through [data-view][hidden]; this owns the second. Without it the router would
   reveal an empty box on every clean-air load and every failed request. */

/* On a clean day the panel is just this drawer — the headline number is a chip in
   the hero strip, so repeating it here would be duplication. Reuses .metric-more
   wholesale so the two drawers on the page look and behave identically. */

/* Same tone vocabulary as the metric chips, so a colour means the same thing in
   both places. Declared on the elements that carry data-tone. */
[data-tone="good"] { --tone: var(--tone-good); }
[data-tone="mild"] { --tone: var(--tone-mild); }
[data-tone="warn"] { --tone: var(--tone-warn); }
[data-tone="severe"] { --tone: var(--tone-severe); }
[data-tone="extreme"] { --tone: var(--tone-extreme); }
[data-tone="cold"] { --tone: var(--tone-cold); }
[data-tone="wet"] { --tone: var(--tone-wet); }

/* Pollutant breakdown and pollen both reuse the chip grid. */
.air-grid {
    margin-top: 1rem;
}

.air-subhead {
    margin: 1.2rem 0 0.55rem;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    text-align: center;
    color: var(--text-muted);
}

/* 24-hour curve */
.air-curve {
    width: 100%;
    max-width: 40rem;
    height: auto;
    margin: 1rem auto 0;
    display: block;
}

.air-curve-line {
    fill: none;
    stroke: var(--tone, var(--text-muted));
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.air-curve-fill {
    fill: var(--tone, var(--text-muted));
    opacity: 0.14;
}

.air-curve-now {
    stroke: var(--text-muted);
    stroke-width: 1;
    stroke-dasharray: 2 3;
}

.air-curve-label {
    font-size: 10px;
    fill: var(--text-muted);
}

/* ---- Clickable chips and their explainers ----
   A number like "UV 7" means nothing without the scale it sits on, so any chip with
   more to say becomes a button and opens one. */

/* The <li> becomes a plain grid slot; the button inside it is what looks like a chip. */
.metric-slot {
    display: flex;
    min-width: 0;
}

.metric-slot > .metric-chip {
    width: 100%;
    font: inherit;
    cursor: pointer;
    /* The dots are positioned against this. */
    position: relative;
    padding-top: 0.8rem;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

/* Three dots, drawn with box-shadow rather than a glyph: U+22EF and "•••" both
   render at wildly different sizes across platforms, and this is a 3px circle
   everywhere. Same reason the emoji set is being replaced elsewhere. */
.metric-chip[data-explain]::after {
    content: '';
    position: absolute;
    top: 0.45rem;
    right: 0.75rem;
    width: 3px;
    height: 3px;
    border-radius: 50%;
    background: currentColor;
    box-shadow: 5px 0 0 currentColor, -5px 0 0 currentColor;
    color: var(--text-muted);
    opacity: 0.55;
    transition: opacity 0.15s ease;
}

.metric-chip[data-explain]:hover::after,
.metric-chip[data-explain][aria-expanded="true"]::after {
    opacity: 1;
}

.metric-chip[data-explain]:hover {
    border-color: var(--tone, var(--text-muted));
}

.metric-chip[data-explain][aria-expanded="true"] {
    border-color: var(--tone, var(--text-muted));
}

.metric-chip[data-explain]:focus-visible {
    outline: 3px solid var(--focus-ring);
    outline-offset: 2px;
}

.metric-explain {
    margin-top: 0.7rem;
    padding: 0.9rem 1rem 1rem;
    border-radius: 0.7rem;
    background: var(--chip-bg);
    border: 1px solid var(--chip-border);
    text-align: left;
}

.explain-note {
    margin: 0.85rem 0 0;
    font-size: 0.9rem;
    line-height: 1.5;
    color: var(--text);
    text-wrap: pretty;
}

.explain-note:first-child {
    margin-top: 0;
}

/* ---- The band scale ----
   Segments are proportional to their real widths, so the bar doubles as a picture
   of how the bands are spaced. */
.scale {
    position: relative;
    /* Room above for the marker label and below for the boundary ticks. */
    padding: 1.75rem 0 1.25rem;
}

.scale-track {
    position: relative;
    height: 0.6rem;
    border-radius: 999px;
    overflow: hidden;
}

.scale-seg {
    position: absolute;
    top: 0;
    height: 100%;
    background: var(--tone, var(--text-muted));
    /* Flat blocks would read as one bar on the palettes where two tones are close. */
    box-shadow: inset -1px 0 0 var(--panel);
}

.scale-marker {
    position: absolute;
    top: 0;
    /* The marker sits ON the value, so the pointer is centred and the label is
       nudged back inside the bar at the extremes by the clamps below. */
    transform: translateX(-50%);
    display: flex;
    flex-direction: column;
    align-items: center;
    white-space: nowrap;
}

/* Near the ends the label anchors to its own edge rather than its centre. The
   pointer follows the alignment, so it still sits exactly on the value. */
.scale-marker[data-edge="start"] {
    transform: translateX(0);
    align-items: flex-start;
}

.scale-marker[data-edge="end"] {
    transform: translateX(-100%);
    align-items: flex-end;
}

.scale-marker::after {
    content: '';
    width: 0;
    height: 0;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-top: 6px solid var(--text);
    margin-top: 0.15rem;
}

.scale-marker-text {
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--tone, var(--text));
    background: var(--panel);
    padding: 0.1rem 0.4rem;
    border-radius: 999px;
    border: 1px solid var(--chip-border);
}

.scale-ticks {
    position: relative;
    height: 1rem;
}

.scale-tick {
    position: absolute;
    top: 0.25rem;
    transform: translateX(-50%);
    font-size: 0.68rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* ---- Place search ---- */
.search-combo {
    position: relative;
    /* Only ranks against its siblings inside the header, which is its own stacking
       context — see the note on #header-bg in styles.css. Escaping the header at all
       is that rule's job, not this one's. */
    z-index: 1;
}

#city-suggestions {
    position: absolute;
    top: calc(100% + 0.35rem);
    left: 0;
    right: 0;
    margin: 0;
    padding: 0.25rem;
    list-style: none;
    border-radius: 0.6rem;
    background: var(--modal);
    border: 1px solid var(--panel-border);
    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.22);
    max-height: 17rem;
    overflow-y: auto;
    text-align: left;
}

#city-suggestions [role="option"] {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.5rem 0.6rem;
    border-radius: 0.4rem;
    cursor: pointer;
}

/* Driven by aria-selected's own class rather than :hover, so the pointer and the
   arrow keys highlight the same row the same way. */
#city-suggestions [role="option"].is-active {
    background: var(--nav-active-bg);
}

#city-suggestions [role="option"].is-active .suggestion-name,
#city-suggestions [role="option"].is-active .suggestion-detail {
    color: var(--nav-active-text);
}

.suggestion-name {
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--text);
}

.suggestion-detail {
    font-size: 0.78rem;
    color: var(--text-muted);
}

/* Visually hidden, still announced. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* ---- High contrast ----
   Riding on the token layer, so this is a handful of overrides rather than a
   parallel stylesheet. Only tokens are redefined; no component rule repeats. */
@media (prefers-contrast: more) {
    :root {
        --text: #000000;
        --text-heading: #10203a;
        --text-muted: #303a46;
        --panel: #ffffff;
        --panel-border: #303a46;
        --card: #ffffff;
        --modal: #ffffff;
        --graph-ground: #ffffff;
        --graph-marker: #1b2430;
        --graph-temp-label: #000000;
        --graph-celestial: #10203a;
        --tooltip-bg: #ffffff;
        --tooltip-text: #000000;
        --tooltip-muted: #303a46;
        --chart-text: #000000;
        --chart-grid: rgba(0, 0, 0, 0.4);
        --focus-ring: #000000;
    }

    :root[data-scheme="dark"] {
        --text: #ffffff;
        --text-heading: #eaf2ff;
        --text-muted: #d4dde8;
        --panel: #0a0f16;
        --panel-border: #d4dde8;
        --card: #0a0f16;
        --modal: #0a0f16;
        --graph-ground: #0a0f16;
        --graph-marker: #eaf2ff;
        --graph-temp-label: #ffffff;
        --graph-celestial: #ffffff;
        --tooltip-bg: #0a0f16;
        --tooltip-text: #ffffff;
        --tooltip-muted: #d4dde8;
        --chart-text: #ffffff;
        --chart-grid: rgba(255, 255, 255, 0.45);
        --focus-ring: #ffffff;
    }

    /* Panels are translucent by default and sit over an animated overlay, so at
       high contrast the backdrop has to stop showing through the text. */
    .content-section,
    .daily-grid-card,
    #view-nav {
        border-width: 2px;
        border-style: solid;
    }
}

/* ---- Loading placeholders -------------------------------------------------

   Every panel used to show the same thing while it waited: a 100px box with a
   spinner in it. That is wrong twice over. It says "wait" without saying what for,
   and 100px is a lie about the space the content needs — #current lands at 340px
   and the sky panel at 380px, so the page jumped by hundreds of pixels the moment
   data arrived, and because #current is first every panel below it moved too.

   These shapes match the real geometry, so the swap costs no layout at all. They
   are sized from the rules above rather than guessed: .sk-hero from .now-icon's
   4rem line plus the two-line .now-main beside it, .sk-chip from
   .metric-strip's minmax(8.5rem, 1fr) track, .sk-day from the min-h-[160px] the
   daily cards carry.
   --------------------------------------------------------------------------- */

.sk {
    background: var(--chip-bg);
    border-radius: 0.5rem;
    position: relative;
    overflow: hidden;
}

/* The sweep. A moving highlight rather than a pulsing opacity, because a panel of
   blocks all breathing in unison reads as a broken page. */
.sk::after {
    content: '';
    position: absolute;
    inset: 0;
    transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, var(--sk-sheen), transparent);
    animation: skSweep 1.4s ease-in-out infinite;
}

:root {
    --sk-sheen: rgba(255, 255, 255, 0.55);
}

:root[data-scheme="dark"] {
    --sk-sheen: rgba(255, 255, 255, 0.07);
}

@keyframes skSweep {
    100% { transform: translateX(100%); }
}

@media (prefers-reduced-motion: reduce) {
    .sk::after { animation: none; }
}

/* At high contrast a grey block on a grey ground is nothing at all, so the
   placeholder becomes an outline and the sweep goes entirely. */
@media (prefers-contrast: more) {
    .sk {
        background: transparent;
        border: 1px dashed var(--panel-border);
    }

    .sk::after { animation: none; background: none; }
}

/* #current — hero, then the chip strip, then the "All conditions" pill. */
.sk-current {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
}

.sk-hero {
    height: 5.9rem;
    width: min(18rem, 80%);
}

.sk-strip {
    /* Copies .metric-strip exactly, including the 1.1rem top margin that is the
       entire gap between hero and strip — #current sets gap: 0. */
    margin: 1.1rem 0 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
    gap: 0.7rem;
    width: 100%;
}

/* 81px is the measured chip height, not a guess. Six on a wide screen; the strip
   promotes only four below 640px, so the last two are hidden rather than reserving a
   third row that never appears. The breakpoint must be the exact complement of
   promotionSlots()'s `(min-width: 640px)` — a max-width of 639px leaves widths between
   639 and 640 matching neither, where the placeholder and the render disagree. */
.sk-chip { height: 81px; }

@media (max-width: 639.98px) {
    .sk-strip .sk-chip:nth-child(n+5) { display: none; }
}

.sk-headline {
    height: 49px;
    width: min(22rem, 92%);
    margin-top: 0.6rem;
}

/* The "All conditions" drawer is 367px open and 0 closed, and whether it is open is a
   preference restored from localStorage — so it is knowable before any data arrives.
   The inline script in <head> stamps data-details-open, and only then is the space
   reserved. Reserving it unconditionally would strand 367px of blank panel for
   everyone who keeps the drawer shut. */
.sk-drawer { display: none; }

:root[data-details-open="1"] .sk-drawer {
    display: block;
    height: 367px;
    width: 100%;
    margin-top: 1rem;
}

/* The open drawer's 367px already includes its own summary row, so reserving the pill
   as well double-counts it. Measured: 40px over before this. */
:root[data-details-open="1"] .sk-summary { display: none; }

/* All the heights here are measured, and several differ by breakpoint because the
   content reflows: the drawer's grid is one tall column on a phone and a short wide
   one on a desktop, 367px against 204px. */
@media (min-width: 640px) {
    :root[data-details-open="1"] .sk-drawer { height: 204px; }
}

.sk-summary {
    height: 1.9rem;
    width: 9.5rem;
    margin-top: 1rem;
    border-radius: 999px;
}

/* The 7-day row. Same responsive column counts the real grid uses, so the cards
   land where the placeholders were. */
.sk-days {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem;
    width: 100%;
}

.sk-day { height: 172px; }

@media (min-width: 640px) {
    /* Four across: wider cards, and the summary line stops wrapping. */
    .sk-day { height: 192px; }
}

@media (min-width: 1024px) {
    /* Seven across, so each card is narrow again. */
    .sk-day { height: 192px; }
}

@media (min-width: 640px) {
    .sk-days { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
    .sk-days { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}

/* Sky. Two cards on a wide screen, stacked below ~34rem — which is why the height
   goes on the card and not the grid: a fixed grid height would over-reserve by a
   whole card once .sky-grid drops to one column. */
.sk-sky {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 1rem;
    width: 100%;
}

/* Side by side the grid stretches them equal, so one height is right. Stacked, it is
   not: the sun card is 237px and the moon card 88px, and giving both 212px
   over-reserves by roughly 120px on a phone. .sky-grid drops to one column at its own
   minmax(15rem) threshold, so the media query matches that rather than a device guess. */
.sk-sky-card { height: 217px; }

@media (max-width: 33.99rem) {
    .sk-sky > .sk-sky-card:first-child { height: 237px; }
    .sk-sky > .sk-sky-card:last-child { height: 88px; }
}

/* The panel is not just the two cards. Below them sit the golden-hour/blue-hour
   windows, the sunset and stargazing score chips and a footnote — 298px of the
   panel's 681px at 375px wide, all unconditional, all previously unreserved. */
.sk-windows {
    height: 201px;
    margin-top: 1rem;
}

.sk-scores {
    height: 78px;
    margin-top: 1rem;
}

.sk-footnote {
    height: 19px;
    width: 60%;
    margin-top: 0.75rem;
}

@media (min-width: 34rem) {
    .sk-windows { height: 60px; }
}

/* The chart's own box is already a hard 340px, so this only fills it. */
.sk-graph {
    height: 100%;
    width: 100%;
    border-radius: 0.5rem;
}

/* The six overlay buttons are a fixed count, so the row height is knowable. Without
   this the control is 0px until the module evaluates and the whole 340px chart
   below it jumps down. */
.graph-series-control {
    min-height: 2.1rem;
}

@media (max-width: 640px) {
    .graph-series-control { min-height: 4.9rem; }
}

/* The chart's placeholder has to fill its container, which is the fixed-height one. */
#hourly-graph-loading {
    height: 100%;
    width: 100%;
}
