/*
 * The Graphs pages -- Day, Week, Month, Year -- at every width, and the
 * charts on them.
 * Copyright (C) John Kline 2019-2026
 * See LICENSE for your rights.
 *
 * The pages in the panel grammar the celestial pages, the forecast tabs
 * and the Highs/Lows page settled on: white sections with a hairline
 * border on the light page, accent eyebrows, muted captions, everything
 * sized in em so the phone layout scales with one .gr font-size rule under
 * the phone query -- the way css/tides.css scales .xg-area and
 * css/almanac.css scales .al.
 *
 * THEMING.  The text and surface colors are the shared --ls-* tokens
 * promoted into css/liveseasons.css, not a fifth private copy of the same
 * measured ladder.  What is added here is a SERIES palette -- one token
 * per measured quantity -- because a chart needs colors the page ladder
 * does not have, and every one of them has to be picked twice.
 * .theme-dark redefines all of them.  There is deliberately NO
 * prefers-color-scheme rule: until the site offers the switch, a reader
 * whose OS prefers dark must keep seeing the pages as designed.
 *
 * NO MARK IN THE CHARTS CARRIES A COLOR.  bin/user/liveseasons.py emits
 * classes only.  That is what makes the dark pass a stylesheet change
 * rather than a regeneration, and it is the whole reason these pages are
 * drawn instead of plotted: WeeWX's ImageGenerator, which drew them until
 * 11.0, bakes its colors into pixels at report time.
 *
 * THE SERIES PALETTE IS NOT DECORATION.  Each pair was chosen for its
 * ground: the light values against #ffffff, the dark ones against the
 * dark surface.  Temperature stays the site's --ls-hi red and dew point
 * the forecast page's teal, so a reader carries one vocabulary from tab
 * to tab.
 */

:root {
    /* series */
    --g-temp:#aa4444;
    --g-dew:#2f7d70;
    --g-wind:#3378a0;
    --g-gust:#7a5aa0;
    --g-rain:#2f6ea8;
    --g-press:#5a6480;
    --g-hum:#4a7fa8;
    --g-sun:#a8761f;
    --g-uv:#8a4fa0;
    --g-co2:#5f7a45;
    --g-rx:#547089;
    --g-volt:#8a6a3f;
    /* furniture */
    --g-night:#e9eef4;
    --g-grid:#e2e6eb;
    --g-grid-2:#ccd3db;
    --g-ribbon:#f1f4f7;
    /* The EPA air-quality categories, desaturated from AirNow's own
       swatches so a full-height band sits BEHIND a line instead of
       shouting over it, but kept in the order and hue every US reader
       already knows. */
    --g-aqi-good:#d9ecd2;
    --g-aqi-mod:#f6efc9;
    --g-aqi-usg:#f6dcc4;
    --g-aqi-unh:#f2cdcd;
    --g-aqi-vunh:#e6d6ea;
    --g-aqi-haz:#ecccd5;
    --g-aqi-good-ink:#3d6b34;
    --g-aqi-mod-ink:#7a6a1c;
    --g-aqi-usg-ink:#8a5320;
    --g-aqi-unh-ink:#8f3a3a;
    --g-aqi-vunh-ink:#6b3a72;
    --g-aqi-haz-ink:#7c2338;
}

.theme-dark {
    --g-temp:#e0705a;
    --g-dew:#71caba;
    --g-wind:#6aa9d6;
    --g-gust:#a98cd8;
    --g-rain:#5b9dd9;
    --g-press:#9aa3c4;
    --g-hum:#77b5da;
    --g-sun:#e0a94a;
    --g-uv:#dda5f8;
    --g-co2:#96bf74;
    --g-rx:#8fa6bd;
    --g-volt:#e1b273;
    --g-night:#060b1e;
    --g-grid:#232b4c;
    --g-grid-2:#33406e;
    --g-ribbon:#151d3c;
    --g-aqi-good:#13281a;
    --g-aqi-mod:#2b2711;
    --g-aqi-usg:#2d1f11;
    --g-aqi-unh:#2d1717;
    --g-aqi-vunh:#241528;
    --g-aqi-haz:#2a1119;
    --g-aqi-good-ink:#8dc48a;
    --g-aqi-mod-ink:#ccbb6a;
    --g-aqi-usg-ink:#edad76;
    --g-aqi-unh-ink:#ffa09f;
    --g-aqi-vunh-ink:#d9aae4;
    --g-aqi-haz-ink:#d97a8e;
}

/* ---- shell ---------------------------------------------------------- */
.gr { color: var(--ls-ink); text-align: left; }
.gr .grwrap {
    max-width: 77.5rem;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: 1.125rem;
}
.gr section {
    background: var(--ls-surface);
    border: .0625rem solid var(--ls-line);
    padding: .8125rem 1rem 1rem;
}
/* The eyebrow IS the section's heading -- every <section> carries an
   h2-h6, which the Nu checker enforces and the site's other panel pages
   already satisfy.  Everything a heading brings by default is reset. */
.gr .eyebrow {
    color: var(--ls-accent);
    font-weight: 600;
    font-size: 0.8em;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    margin: 0 0 .625rem;
    border: 0;
    padding: 0;
}
.gr .eyebrow .sub {
    color: var(--ls-faint);
    font-weight: 400;
    letter-spacing: 0.02em;
    text-transform: none;
    margin-left: .625rem;
}
.gr .grhead {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 1.5rem;
    border-bottom: .125rem solid var(--ls-head-rule);
    padding-bottom: .5rem;
}
.gr h2.gtitle {
    margin: 0;
    font-size: 2.1em;
    font-weight: 600;
    color: var(--ls-accent);
    letter-spacing: 0.02em;
}
.gr h2.gtitle .over { color: var(--ls-muted); font-weight: 400; }
.gr .grhead .meta {
    color: var(--ls-muted);
    font-size: 0.92em;
    text-align: right;
    line-height: 1.6;
    white-space: nowrap;
}
.gr .caption {
    margin: .5rem 0 0;
    color: var(--ls-muted);
    font-size: 0.92em;
    line-height: 1.5;
}
.gr .gotop { text-align: center; clear: both; margin: 1.375rem 0 0; }

/* THE PILL IS THE LINE, not a <br> before the words.
   These two heads stack the countdown above their span line, and a
   LEADING <br> reserves a line box whether or not the pill beside it is
   showing -- so with the pill hidden the head stood a line taller than
   it needed, blank.  Making the pill a block gives it its own line
   exactly when it has one to give.
   THE [hidden] COMPANION IS NOT OPTIONAL.  `.pill[hidden]{display:none}`
   in css/liveseasons.css is (0,2,0); the rule above it here is (0,4,0)
   and would win, leaving a pill that can never be hidden -- which is the
   state countdownIdle uses to say nothing is pending.
   AND margin-left:auto, WITHOUT WHICH THIS IS A REGRESSION.  These two
   heads are right-aligned, and text-align moves INLINE content only -- a
   block box ignores it and sits at the content edge.  Measured before
   the margin went in: the Seismograph pill landed 373px left of where it
   had been and the Graphs pill 33px.  The pill carries an explicit width
   (.pill--count is 2.8em), so an auto start margin is what puts a block
   of known width back against the far edge. */
.gr .grhead .meta > .pill--count { display: block; margin-left: auto; }
.gr .grhead .meta > .pill--count[hidden] { display: none; }

/* ---- card grid ------------------------------------------------------ */
.gr .grid2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.125rem;
    /* stretch, the default, said out loud: two cards side by side are
       one row of the page and a row whose boxes are different heights
       reads as a mistake.  bin/user/liveseasons.py draws every detail
       chart at one height so the boxes start close; this closes the rest,
       which is the caption and statline differing in length. */
    align-items: stretch;
}
/* A column, so the chart sits under the head and the stretch has
   somewhere to put its slack. */
.gr .grid2 > section { display: flex; flex-direction: column; }
/* And the caption sits on the FLOOR of the card, so the slack lands
   between the statline and the caption -- where it reads as paragraph
   spacing -- instead of as a hole below the last line of text.  Every
   caption in a row then shares a baseline.
   Structural on purpose: the alternative is padding each caption with
   prose until the line counts happen to match, which holds only at one
   width, in one language, with one unit label -- 'mbar' and 'inHg' wrap
   differently, and the phone layout is set at a different size
   entirely. */
.gr .grid2 > section .caption { margin-top: auto; padding-top: .5rem; }
.gr .cardhead {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: .75rem;
    margin-bottom: .375rem;
}
.gr .cardhead .now {
    font-size: 1.25em;
    font-weight: 600;
    color: var(--ls-ink);
    white-space: nowrap;
}
.gr .cardhead .now .u { font-size: 0.68em; font-weight: 400; color: var(--ls-muted); }
.gr .statline {
    display: flex;
    gap: 1.25rem;
    flex-wrap: wrap;
    margin: .375rem 0 0;
    color: var(--ls-muted);
    font-size: 0.86em;
}
.gr .statline b { color: var(--ls-ink-2); font-weight: 600; }
.gr .statline b.hi { color: var(--ls-hi); }
.gr .statline b.lo { color: var(--ls-lo); }

/* ---- charts --------------------------------------------------------- */
.gr .chartwrap { position: relative; }
.gr svg.chart {
    width: 100%;
    height: auto;
    display: block;
    touch-action: pan-y;
    cursor: crosshair;
}
.gr svg.chart .night { fill: var(--g-night); }
.gr svg.chart .hgrid { stroke: var(--g-grid); stroke-width: 1; fill: none; }
.gr svg.chart .vgrid { stroke: var(--g-grid-2); stroke-width: 1; stroke-dasharray: 2 3;
    fill: none; }
.gr svg.chart .ylab {
    fill: var(--ls-muted); font-size: min(calc(10px + var(--ls-svg-step)), max(10px, var(--ls-svg-cap))); text-anchor: end;
    font-family: 'Open Sans', arial, sans-serif;
}
.gr svg.chart .ylab.uv { fill: var(--g-uv); text-anchor: start; }
.gr svg.chart .ylab.dew { fill: var(--g-dew); text-anchor: start; }
.gr svg.chart .ylab.volt { fill: var(--g-volt); text-anchor: start; }
.gr svg.chart .xlab {
    fill: var(--ls-muted); font-size: min(calc(10.5px + var(--ls-svg-step)), max(10.5px, var(--ls-svg-cap))); text-anchor: middle;
    font-family: 'Open Sans', arial, sans-serif;
}
.gr svg.chart .striplab {
    fill: var(--ls-faint); font-size: min(calc(9.5px + var(--ls-svg-step)), max(9.5px, var(--ls-svg-cap))); letter-spacing: 0.08em;
    text-transform: uppercase; font-family: 'Open Sans', arial, sans-serif;
}
.gr svg.chart .striplab.uv { fill: var(--g-uv); text-anchor: end; }
.gr svg.chart .striplab.dew { fill: var(--g-dew); text-anchor: end; }
.gr svg.chart .plab {
    fill: var(--ls-faint); font-size: min(calc(9.5px + var(--ls-svg-step)), max(9.5px, var(--ls-svg-cap))); letter-spacing: 0.09em;
    font-weight: 600; font-family: 'Open Sans', arial, sans-serif;
}
/* A panel label that is also an anchor down to that measure's detail
   card.  Signaled as a link -- accent color, dotted underline, pointer --
   and NOT with a caret glyph, which beside BAROMETER would read as a
   falling reading rather than as "jump down". */
.gr svg.chart a.plink { cursor: pointer; }
.gr svg.chart a.plink .plab {
    fill: var(--ls-accent);
    text-decoration: underline dotted;
    text-underline-offset: 2px;
}
.gr svg.chart a.plink:hover .plab { text-decoration: underline solid; }
.gr section[id] { scroll-margin-top: 1rem; }
.gr svg.chart .emptynote {
    fill: var(--ls-faint); font-size: min(calc(12px + var(--ls-svg-step)), max(12px, var(--ls-svg-cap))); text-anchor: middle; font-style: italic;
    font-family: 'Open Sans', arial, sans-serif;
}
.gr svg.chart .emptynote.sm { font-size: min(calc(10px + var(--ls-svg-step)), max(10px, var(--ls-svg-cap))); }
.gr svg.chart .refline { stroke: var(--g-co2); stroke-width: 1; stroke-dasharray: 4 3;
    opacity: 0.7; fill: none; }

/* series marks.  Every line class sets fill:none so a path is a stroke;
   the paired area/band fills are separate classes over the top of it. */
.gr svg.chart .tline { fill: none; stroke: var(--g-temp); stroke-width: 2;
    stroke-linejoin: round; stroke-linecap: round; }
.gr svg.chart .dline { fill: none; stroke: var(--g-dew); stroke-width: 1.6;
    stroke-dasharray: 5 3; stroke-linejoin: round; }
.gr svg.chart .wline { fill: none; stroke: var(--g-wind); stroke-width: 1.7; }
.gr svg.chart .gline { fill: none; stroke: var(--g-gust); stroke-width: 1.4; }
.gr svg.chart .pline { fill: none; stroke: var(--g-press); stroke-width: 1.8; }
.gr svg.chart .hline { fill: none; stroke: var(--g-hum); stroke-width: 1.8; }
.gr svg.chart .cline { fill: none; stroke: var(--g-co2); stroke-width: 1.8; }
.gr svg.chart .aline { fill: none; stroke: var(--ls-ink-3); stroke-width: 1.8; }
.gr svg.chart .uline { fill: none; stroke: var(--g-uv); stroke-width: 1.2; opacity: 0.85; }
.gr svg.chart .rxline { fill: none; stroke: var(--g-rx); stroke-width: 1.6; }
.gr svg.chart .voltline { fill: none; stroke: var(--g-volt); stroke-width: 1.6; }

/* the fill under a line, and the min-to-max band that replaces it on an
   aggregated period.  path.area / path.env win over the line rule above
   on specificity, which is what turns fill:none back on. */
.gr svg.chart path.area { stroke: none; }
.gr svg.chart path.area.wline { fill: var(--g-wind); opacity: 0.22; }
.gr svg.chart path.area.gline { fill: var(--g-gust); opacity: 0.16; }
.gr svg.chart path.area.hline { fill: var(--g-hum); opacity: 0.14; }
.gr svg.chart path.area.pline { fill: var(--g-press); opacity: 0.08; }
.gr svg.chart path.area.cline { fill: var(--g-co2); opacity: 0.1; }
.gr svg.chart path.area.aline { fill: var(--ls-ink-3); opacity: 0.06; }
.gr svg.chart path.env { stroke: none; opacity: 0.2; }
.gr svg.chart path.env.tline { fill: var(--g-temp); }
.gr svg.chart path.env.hline { fill: var(--g-hum); }
.gr svg.chart path.env.pline { fill: var(--g-press); }
.gr svg.chart path.env.cline { fill: var(--g-co2); }
.gr svg.chart path.env.aline { fill: var(--ls-ink-3); opacity: 0.14; }
.gr svg.chart .spread { fill: var(--g-dew); opacity: 0.09; stroke: none; }

.gr svg.chart .ribbon { fill: var(--g-ribbon); }
.gr svg.chart .barb path { fill: none; stroke-width: 1.4; stroke-linecap: round;
    stroke-linejoin: round; }
/* Calm in the caption ink, NOT the gridline color: at 9 px a gridline-
   gray arrow on a white card is invisible, and this town is calm most
   of the day, so the whole ribbon vanished on most days.  Direction is
   information at 2 mph too. */
.gr svg.chart .barb.w0 path { stroke: var(--ls-muted); }
.gr svg.chart .barb.w1 path { stroke: var(--g-wind); }
.gr svg.chart .barb.w2 path { stroke: var(--g-gust); stroke-width: 1.8; }
/* Direction recorded, speed not: the arrow still flies the right way,
   drawn dashed in the calm ink so it claims no strength. */
.gr svg.chart .barb.wx path { stroke: var(--ls-muted); stroke-dasharray: 2 2; }
.gr svg.chart .aqib.good { fill: var(--g-aqi-good); }
.gr svg.chart .aqib.mod { fill: var(--g-aqi-mod); }
.gr svg.chart .aqib.usg { fill: var(--g-aqi-usg); }
.gr svg.chart .aqib.unh { fill: var(--g-aqi-unh); }
.gr svg.chart .aqib.vunh { fill: var(--g-aqi-vunh); }
.gr svg.chart .aqib.haz { fill: var(--g-aqi-haz); }
.gr svg.chart .aqilab { font-size: min(calc(9.5px + var(--ls-svg-step)), max(9.5px, var(--ls-svg-cap))); font-family: 'Open Sans', arial, sans-serif; }
.gr svg.chart .aqilab.good { fill: var(--g-aqi-good-ink); }
.gr svg.chart .aqilab.mod { fill: var(--g-aqi-mod-ink); }
.gr svg.chart .aqilab.usg { fill: var(--g-aqi-usg-ink); }
.gr svg.chart .aqilab.unh { fill: var(--g-aqi-unh-ink); }
.gr svg.chart .aqilab.vunh { fill: var(--g-aqi-vunh-ink); }
.gr svg.chart .aqilab.haz { fill: var(--g-aqi-haz-ink); }
.gr svg.chart .clearsky { fill: var(--g-sun); opacity: 0.16; stroke: none; }
.gr svg.chart .rarea { fill: var(--g-sun); opacity: 0.5; stroke: none; }
.theme-dark .gr svg.chart .rarea { opacity: 0.42; }
.theme-dark .gr svg.chart .clearsky { opacity: 0.2; }
.gr svg.chart .rainbar { fill: var(--g-rain); opacity: 0.85; }

/* markers */
.gr svg.chart .hiM.dot { fill: var(--ls-hi); }
.gr svg.chart .loM.dot { fill: var(--ls-lo); }
/* The halo: a stroke in the card's own color, painted under the glyphs by
   the paint-order="stroke" attribute GraphCharts._marker writes, so a note
   stays legible where it crosses a line -- the temperature line beside a
   moved high, the dew point under every low.  Sized in em so the phone's
   19-px note gets a halo in proportion. */
.gr svg.chart .hiM.note { fill: var(--ls-hi); font-size: min(calc(10.5px + var(--ls-svg-step)), max(10.5px, var(--ls-svg-cap))); font-weight: 600;
    font-family: 'Open Sans', arial, sans-serif;
    stroke: var(--ls-surface); stroke-width: .3em; stroke-linejoin: round; }
.gr svg.chart .loM.note { fill: var(--ls-lo); font-size: min(calc(10.5px + var(--ls-svg-step)), max(10.5px, var(--ls-svg-cap))); font-weight: 600;
    font-family: 'Open Sans', arial, sans-serif;
    stroke: var(--ls-surface); stroke-width: .3em; stroke-linejoin: round; }

/* The crosshair is painted LAST, so it sits above every mark in the frame
   -- including the panel labels that link down to the detail cards.
   Without pointer-events:none it silently eats their clicks the moment the
   pointer enters the chart, which is always: the pointer has to cross the
   chart to reach a label inside it.  The crosshair is decoration; the
   handlers that drive it live on the <svg>, so it never needs an event of
   its own.  `off`, not the html hidden attribute: hidden is an HTML thing
   and is not valid on an SVG <g>. */
.gr svg.chart .cross { pointer-events: none; }
.gr svg.chart .cross.off { display: none; }
.gr svg.chart .cx { stroke: var(--ls-ink-3); stroke-width: 1; stroke-dasharray: 3 2;
    opacity: 0.75; fill: none; }
.gr svg.chart .cdots circle { stroke: var(--ls-surface); stroke-width: 1.4; }
/* The dot on each curve under the crosshair.  It carries its SERIES class,
   and every line series sets `fill: none` -- which on a <circle> means an
   invisible dot, not a colored one.  So each series names its fill again
   here, at a higher specificity than the line rule above. */
.gr svg.chart .cdots .tline { fill: var(--g-temp); }
.gr svg.chart .cdots .dline { fill: var(--g-dew); }
.gr svg.chart .cdots .wline { fill: var(--g-wind); }
.gr svg.chart .cdots .gline { fill: var(--g-gust); }
.gr svg.chart .cdots .aline { fill: var(--ls-ink-3); }
.gr svg.chart .cdots .pline { fill: var(--g-press); }
.gr svg.chart .cdots .hline { fill: var(--g-hum); }
.gr svg.chart .cdots .cline { fill: var(--g-co2); }
.gr svg.chart .cdots .uline { fill: var(--g-uv); opacity: 1; }
.gr svg.chart .cdots .rxline { fill: var(--g-rx); }
.gr svg.chart .cdots .voltline { fill: var(--g-volt); }
.gr svg.chart .cdots .rainbar { fill: var(--g-rain); }
.gr svg.chart .cdots .rarea { fill: var(--g-sun); opacity: 1; }
.gr svg.chart .cdots .clearsky { fill: var(--g-sun); opacity: 0.45; }

.gr .readout {
    position: absolute;
    top: 0;
    background: var(--ls-surface);
    border: .0625rem solid var(--ls-rule);
    padding: .375rem .625rem;
    font-size: 0.82em;
    color: var(--ls-ink-2);
    pointer-events: none;
    white-space: nowrap;
    line-height: 1.55;
    z-index: 3;
}
.gr .readout b { display: block; color: var(--ls-ink); font-size: 1.02em; }
.gr .readout .ro { display: flex; justify-content: space-between; gap: .875rem; }
.gr .readout .ro i { font-style: normal; color: var(--ls-muted); }
.gr .readout .ro .val { font-weight: 600; }
.gr .readout .sw {
    display: inline-block; width: .625rem; height: .1875rem; border-radius: .125rem;
    margin-right: .375rem; vertical-align: middle;
}
.gr .readout .sw.tline { background: var(--g-temp); }
.gr .readout .sw.dline { background: var(--g-dew); }
.gr .readout .sw.wline { background: var(--g-wind); }
.gr .readout .sw.gline { background: var(--g-gust); }
.gr .readout .sw.aline { background: var(--ls-ink-3); }
.gr .readout .sw.pline { background: var(--g-press); }
.gr .readout .sw.hline { background: var(--g-hum); }
.gr .readout .sw.cline { background: var(--g-co2); }
.gr .readout .sw.rarea { background: var(--g-sun); }
.gr .readout .sw.clearsky { background: var(--g-sun); opacity: 0.35; }
.gr .readout .sw.uline { background: var(--g-uv); }
.gr .readout .sw.rxline { background: var(--g-rx); }
.gr .readout .sw.voltline { background: var(--g-volt); }
.gr .readout .sw.rainbar { background: var(--g-rain); }
.gr .readout .sw.none { background: none; }

/* legend */
.gr .legend {
    display: flex; flex-wrap: wrap; gap: .375rem 1.375rem;
    color: var(--ls-muted); font-size: 0.84em; margin: .5rem 0 0;
}
.gr .legend span { display: flex; align-items: center; gap: .4375rem; }
/* Reserved, so a card with nothing to name is exactly as tall as one
   with a key.  Without this, whether two cards in a row match depends on
   which sensors a station has -- and leveling a row by giving both
   cards something to say is how prose ends up on a page to fill space. */
.gr .grid2 > section .legend { min-height: 1.25em; margin-top: .5rem; }
.gr .legend i { width: 1rem; height: .1875rem; display: inline-block; border-radius: .125rem; }
.gr .legend .sw-t { background: var(--g-temp); }
.gr .legend .sw-d { background: none; border-top: .1875rem dashed var(--g-dew); height: 0; }
.gr .legend .sw-n { background: var(--g-night); height: .75rem;
    border: .0625rem solid var(--ls-hair); }
/* the card keys: one swatch per mark a card actually draws */
.gr .legend .sw-hum { background: var(--g-hum); }
.gr .legend .sw-dew { background: none; border-top: .1875rem dashed var(--g-dew); height: 0; }
.gr .legend .sw-wind { background: var(--g-wind); height: .5625rem; }
.gr .legend .sw-gust { background: var(--g-gust); height: .5625rem; opacity: 0.5; }
.gr .legend .sw-barb { background: none; border-top: .125rem solid var(--g-wind);
    border-right: .125rem solid var(--g-wind); width: .5625rem; height: .5625rem;
    transform: rotate(45deg); }
.gr .legend .sw-sun { background: var(--g-sun); height: .5625rem; }
.gr .legend .sw-clear { background: var(--g-sun); height: .5625rem; opacity: 0.3; }
.gr .legend .sw-uv { background: var(--g-uv); }
.gr .legend .sw-aqi { background: var(--ls-ink-3); }
.gr .legend .sw-band { background: var(--g-aqi-good); height: .625rem;
    border: .0625rem solid var(--g-aqi-mod); }
.gr .legend .sw-co2 { background: var(--g-co2); }
.gr .legend .sw-ref { background: none; border-top: .1875rem dashed var(--g-co2); height: 0; }
.gr .legend .sw-rx { background: var(--g-rx); }
.gr .legend .sw-volt { background: var(--g-volt); }

/* ---- the contact sheet (the phone layout) -------------------------- */
.gr .sheet {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: .0625rem;
    background: var(--ls-hair);
    border: .0625rem solid var(--ls-hair);
}
.gr .chip {
    background: var(--ls-surface);
    padding: .5625rem .6875rem .4375rem;
    border: 0;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
    display: block;
    width: 100%;
}
.gr .chip:hover { background: var(--ls-tint); }
.gr .chip.on { background: var(--ls-tint); box-shadow: inset .1875rem 0 0 var(--ls-accent); }
/* --ls-muted, NOT --ls-faint.  In dark the faint tier on the chip's own
   tinted ground measures 4.38, under the 4.5 AA bar -- the only text on
   these pages that fails it.  The chip label is the word that says WHICH
   measure the number below it belongs to, so it is not decoration that
   can afford to sit under the bar.  Muted takes the worst of the 36
   labels to 5.55 in dark; in light it moves 5.45 to 6.15, which is a
   visible darkening and is the price of fixing this with the token
   rather than a dark-only override.  The worst case is the SELECTED
   chip, whose --ls-tint ground is lighter than its neighbors' in light
   and lighter in dark too -- measured on all four period pages, not
   predicted from the token pair.
   Chips are phone-only (9 per page, none on the desktop twins), so this
   rule reaches nothing else. */
.gr .chip .ck {
    display: block;
    color: var(--ls-muted); font-size: 0.72em; letter-spacing: 0.07em;
    text-transform: uppercase;
}
.gr .chip .cv { display: block; font-size: 1.3em; font-weight: 600;
    color: var(--ls-ink); line-height: 1.2; }
.gr .chip .cv .u { font-size: 0.55em; font-weight: 400; color: var(--ls-muted); }
.gr .chip svg.spark { width: 100%; display: block; margin-top: .125rem; }
/* One height for every sparkline, set here rather than left to the
   viewBox: a chip that spans the row is twice as wide, and with the
   aspect preserved its curve came out twice as tall as its neighbors'. */
@media (max-width: 600px){.gr .chip svg.spark{ height: 3.7em; }}
.gr svg.spark path.sparkarea { opacity: 0.13; stroke: none; }
.gr svg.spark path.sparkarea.tline { fill: var(--g-temp); }
.gr svg.spark path.sparkarea.wline { fill: var(--g-wind); }
.gr svg.spark path.sparkarea.hline { fill: var(--g-hum); }
.gr svg.spark path.sparkarea.pline { fill: var(--g-press); }
.gr svg.spark path.sparkarea.aline { fill: var(--ls-ink-3); }
.gr svg.spark path.sparkarea.cline { fill: var(--g-co2); }
.gr svg.spark path.sparkarea.rarea { fill: var(--g-sun); }
.gr svg.spark path.sparkarea.rainbar { fill: var(--g-rain); }
.gr svg.spark path.sparkarea.rxline { fill: var(--g-rx); }
.gr svg.spark path.sparkarea.voltline { fill: var(--g-volt); }
.gr svg.spark path.tline { fill: none; stroke: var(--g-temp); stroke-width: 1.6; }
.gr svg.spark path.wline { fill: none; stroke: var(--g-wind); stroke-width: 1.6; }
.gr svg.spark path.hline { fill: none; stroke: var(--g-hum); stroke-width: 1.6; }
.gr svg.spark path.pline { fill: none; stroke: var(--g-press); stroke-width: 1.6; }
.gr svg.spark path.aline { fill: none; stroke: var(--ls-ink-3); stroke-width: 1.6; }
.gr svg.spark path.cline { fill: none; stroke: var(--g-co2); stroke-width: 1.6; }
.gr svg.spark path.rarea { fill: none; stroke: var(--g-sun); stroke-width: 1.6; }
.gr svg.spark path.rainbar { fill: none; stroke: var(--g-rain); stroke-width: 1.8; }
.gr svg.spark path.rxline { fill: none; stroke: var(--g-rx); stroke-width: 1.6; }
.gr svg.spark path.voltline { fill: none; stroke: var(--g-volt); stroke-width: 1.6; }
.gr svg.spark .sparkdot.tline { fill: var(--g-temp); }
.gr svg.spark .sparkdot.wline { fill: var(--g-wind); }
.gr svg.spark .sparkdot.hline { fill: var(--g-hum); }
.gr svg.spark .sparkdot.pline { fill: var(--g-press); }
.gr svg.spark .sparkdot.aline { fill: var(--ls-ink-3); }
.gr svg.spark .sparkdot.cline { fill: var(--g-co2); }
.gr svg.spark .sparkdot.rarea { fill: var(--g-sun); }
.gr svg.spark .sparkdot.rainbar { fill: var(--g-rain); }
.gr svg.spark .sparkdot.rxline { fill: var(--g-rx); }
.gr svg.spark .sparkdot.voltline { fill: var(--g-volt); }
.gr .detail[hidden] { display: none; }

/* ---- the period nav ------------------------------------------------- */
/* Day / Week / Month / Year is a page-level tab strip and is styled with
   the other five, in css/liveseasons.css.  What stood here was the 2019
   segmented control -- centered, bordered, the selection a solid fill --
   which made this the one family whose switcher did not look like the
   site's.  Nothing replaces it here; the shared rules do the work. */

/* ---- the phone layout (under the width query) ----------------------
   The phone is NOT a narrow layout.  Like every other page on this site it
   is laid out on the same 1124-px canvas at every width -- the root rule
   in css/liveseasons.css scales the canvas down to the screen.  So the entire job here is SIZE: the type goes up, the grids
   lose columns, and the chart labels are scaled back up by hand because
   the SVG viewBox does not grow with them.

   The numbers are the shipped ones, measured off the live phone pages
   rather than picked: content root 30px (css/forecast.css's phone .fc),
   menubar 36, submenu 32; in-chart ylab 17 and xlab 19, likewise the
   phone .fc's.  Matching
   them is the point -- two chart pages on one phone must not be set at two
   different scales. */
@media (max-width: 600px){.gr{ font-size: min(calc(1.875rem + var(--ls-step)), max(1.875rem, var(--ls-cap))); }}
@media (max-width: 600px){.gr .grwrap{ gap: 1.625rem; }}
@media (max-width: 600px){.gr section{ padding: 1.125rem 1.25rem 1.375rem; }}
@media (max-width: 600px){.gr .grhead{ padding-bottom: .75rem; }}
@media (max-width: 600px){.gr h2.gtitle{ font-size: 1.5em; }}
/* THE SPAN LINE, at the size every other phone panel sets its subtitle.
   0.62em put it at 18.6 layout px, which the phone scales by 390/1124 --
   about 6.5 real pixels, the same smudge the eyebrow note below this
   describes and fixes.  This line was missed by that pass because the
   countdown pill shared the block with it and gave the corner something
   to read; the pill rides the strip above now and these three words are
   the whole of it.
   0.95em is not a new number: it is what .sq .sub (Seismograph) and
   .rs .sub (Radar, Satellite, Sat. Bands) have always set the same line
   to, off the same 30px phone root -- 28.5 layout px, about 10 real,
   which the eyebrow note names as the site's readable size.  The desktop
   is untouched at 0.92em, where 14.72px against a 33.6px title is
   already right. */
@media (max-width: 600px){.gr .grhead .meta{ font-size: 0.95em; }}
/* THE NAMES.  Every one of these is what a measure is CALLED, and at
   0.6em they were 18 layout px -- which the phone then scales by
   390/1124, so about 6 real pixels.  The site's own readable size is the
   30 px content root, which lands at about 10.  These are set to reach
   that: it is the difference between a label and a smudge, and on the
   contact sheet the name is the thing being chosen from.
   Only the NAMES are changed; the figures beside them were already
   large. */
@media (max-width: 600px){.gr .eyebrow{ font-size: 0.85em; margin-bottom: .875rem; }}
@media (max-width: 600px){.gr .eyebrow .sub{ margin-left: .75rem; }}
@media (max-width: 600px){.gr .caption{ font-size: 0.8em; margin-top: .75rem; }}
@media (max-width: 600px){.gr .statline{ font-size: 0.72em; gap: 1.625rem; margin-top: .75rem; }}
@media (max-width: 600px){.gr .cardhead{ margin-bottom: .625rem; }}
@media (max-width: 600px){.gr .cardhead .now{ font-size: 1.2em; }}
/* Two chips across, not four: at this scale a chip has to be a comfortable
   thumb target, and 550 layout px is 191 real px on a 390-px screen. */
@media (max-width: 600px){.gr .sheet{ grid-template-columns: 1fr 1fr; }}
/* An odd number of chips leaves the last one beside an empty cell, and
   the sheet's grid background shows through it as a gray box.  A station
   drops a chip whenever it lacks a sensor, so the count is not fixed and
   cannot be designed around -- the last chip takes the whole row when
   there is nothing to pair it with. */
@media (max-width: 600px){.gr .sheet .chip:last-child:nth-child(odd){ grid-column: 1 / -1; }}
@media (max-width: 600px){.gr .chip{ padding: 1rem 1.125rem .75rem; }}
@media (max-width: 600px){.gr .chip .ck{ font-size: 0.95em; letter-spacing: 0.04em; }}
@media (max-width: 600px){.gr .chip .cv{ font-size: 1.25em; }}
@media (max-width: 600px){.gr .chip.on{ box-shadow: inset .375rem 0 0 var(--ls-accent); }}
/* The phone scale comes from the shared strip too (the phone .gr .pnav a
   is in liveseasons.css beside .submenubarnav a), so the 14px/46px box that
   was here is gone with the rest of the segmented control. */
/* The chart's own labels.  The viewBox is fixed and the chart is drawn at
   very nearly the desktop size, so every label shrinks with the page when
   the phone scales it -- scale them back up, the same correction
   css/forecast.css makes under its phone query. */
@media (max-width: 600px){.gr svg.chart .ylab{ font-size: 17px; }}
@media (max-width: 600px){.gr svg.chart .xlab{ font-size: 19px; }}
@media (max-width: 600px){.gr svg.chart .striplab{ font-size: 17px; }}
@media (max-width: 600px){.gr svg.chart .aqilab{ font-size: 17px; }}
@media (max-width: 600px){.gr svg.chart .refline{ stroke-width: 1.6; }}
@media (max-width: 600px){.gr svg.chart .hiM.note,
.gr svg.chart .loM.note{ font-size: 19px; }}
/* THE SAME SIZE AS THE SPAN LINE IN THE HEAD, and written as the same
   0.95em off the same 30px phone root so the two cannot drift apart.
   Both are sentences about the window rather than labels on it -- "last
   27 hours" and "no rain in the last 27 hours" -- and at 21px this one
   was 7.3 real pixels against the other's 9.9.  (John, 2026-09-11: "make
   the 'no rain...' the same size as 'last 27 hours'.")
   It stays larger than the axis labels around it deliberately: those are
   ticks, this is the only thing in an otherwise empty frame. */
@media (max-width: 600px){.gr svg.chart .emptynote{ font-size: 0.95em; }}
@media (max-width: 600px){.gr .legend{ font-size: 0.66em; gap: .5rem 1.875rem; }}
/* THE READOUT DOES NOT FLOAT ON A PHONE.  A box that follows the pointer
   is a box under the finger that is scrubbing the chart, so here it is a
   static block beneath the chart instead.  scripts/graphs.js reads the
   computed position and skips its inline placement when it is static, so
   this rule alone moves it -- no second code path.  The min-height
   reserves the space so the page does not jump on first touch. */
@media (max-width: 600px){.gr .chartwrap .readout{
    position: static;
    display: flex;
    flex-wrap: wrap;
    gap: .25rem 1.875rem;
    align-items: baseline;
    min-height: 2.4em;
    margin-top: .625rem;
    font-size: 0.72em;
    border-left: 0;
    border-right: 0;
    border-top: .0625rem solid var(--ls-hair);
    border-bottom: 0;
    padding: .625rem .125rem 0;
}}
@media (max-width: 600px){.gr .chartwrap .readout b{ display: inline; margin-right: .75rem; }}
@media (max-width: 600px){.gr .chartwrap .readout .ro{ gap: .5rem; }}
@media (max-width: 600px){.gr .chartwrap .readout[hidden]{ display: flex; visibility: hidden; }}

