/* Light and dark are both first-class: this is a page you check at 6am and at
 * 11pm. Colours live here as custom properties and are referenced by the tone-*
 * classes the chart emits, so config.js never names a colour. */

/* The palette aims at one register: HIGH CHROMA, LOW LUMINANCE CONTRAST. Hues are
 * distinct and vivid, but held at similar brightness and pulled slightly toward
 * the background, which is how telemetry and mission displays read -- saturated
 * without being neon, muted without going grey. Nothing here is a pure primary,
 * and nothing is allowed to shout: the dark theme is the one this page is mostly
 * looked at in, and every tone there sits within a narrow lightness band so no
 * single column grabs the eye first. */
:root {
  --bg: #fbfbfa;
  --panel-bg: #ffffff;
  --ink: #1a1a19;
  --dim: #6b6b68;
  --faint: #e6e7ea;
  --line: #d5d7dc;
  /* Column separators: structure, not decoration, and the one line on the page that has
   * twice been given MORE contrast rather than less.
   *
   * Chosen against every backdrop it can abut, which is the correction. It was set at
   * dE 26 from the PANEL BACKGROUND -- what you see beside an empty column, and what you
   * almost never see beside a full one, since every fill now runs edge to edge. Measured
   * against the fills themselves it was dE 3.2 from the cloud area and 3.7 from air
   * quality, the two columns that sit side by side: invisible exactly where two neighbours
   * both had ink at their edges.
   *
   * dE 14 is a floor against all of it -- panel, panel under night shading, and every
   * column's fill at both 0.5 and 0.22 over both -- and 14 rather than higher because 16
   * is unreachable on this hue in the dark theme without the separator going to 5.5:1
   * against its own panel, which would make the furniture the loudest thing on the page.
   * Within that floor these are the QUIETEST values available, not the strongest. */
  --edge: #949bac;
  /* Night must always DARKEN, in either theme. */
  --night: rgba(58, 74, 110, 0.085);
  --now: #d6402f;

  --warm: #d9663f;
  --snow: #8fa6bd;  /* must be unmistakable against --rain */
  --freeze: #a052c0; /* freezing rain: alarming, not merely cold */
  --rain: #3a7fc8;
  --cloud: #868c98;
  --ice: #7fb4d8;
  --sun: #cf9f2c;
  /* Pink, not violet. At #a862bd the burn line sat dE 6.5 from --freeze -- the page's
     tightest pair, and the one compromise the palette notes admitted -- and the air
     column then became a third violet. Rotated to oklch hue 340 at the same lightness
     it is dE 24 from --freeze, 38 from --air-dirty, and still 95 from the --sun fill it
     is drawn on top of. Nothing else on the page is pink. */
  --uv: #bc549e;
  --wind: #3f9e84;
  /* Instability briefly needed its own hue: as a 0.5 area fill it was dE 4.4 from
     the temperature band, two soft reds in two columns. The column is gone now --
     CAPE did not earn one -- and a small storm GLYPH cannot be mistaken for a
     temperature envelope, so red serves alerts, the glyph and nothing else. */
  --storm: #cc5a42;
  /* Lightning, and not `--sun`: the bolt sits in the precipitation column, and
     gold already means sunshine two columns over. Measured, no hue solves this one
     -- every candidate scored 1.3-1.4:1 against the rain bars it is drawn on top of
     in light mode -- which is what the halo below is for. */
  --thunder: #d8a318;
  /* Obscuration sits beside cloud now, and at #9a8d6f the two 0.5 fills were dE
     11.7 -- one grey-beige mass across the boundary. Darker and more olive is 17
     from cloud while holding ~20 from the sun fill in the next column over.
     Pushing it BRIGHTER yellow instead lands dE 6 from sun: the wrong escape. */
  --haze: #8d7a4a;
  /* The comfort scale's two poles: parched and oppressive. They are the dew point curve's own
   * two hues -- pattern for how far the air is from comfortable, hue for which way -- and
   * comfortable is the state that draws no line at all, so the scale needs no middle.
   *
   * ColorBrewer BrBG, with one departure. TEAL to BROWN and not blue to red, which is the
   * whole reason this pair can be read without a legend: brown is parched earth and teal is
   * water. It also keeps humidity out of temperature's lane -- the margin the curve runs
   * along is warm -- and it survives deuteranopia, which red/green does not and blue/red only
   * partly does. Measured under Machado 2009: dE 11.7 protan and 12.9 deutan, worse theme of
   * the two, against a floor of 8.
   *
   * The dry end sits between BrBG's ochre and its mid-brown. As a CURVE it has to be told
   * apart from what it is drawn over, which is the temperature row's own margin: 12.3 in dark
   * and 29.3 in light, with teal at 24.7 and 41.8. BrBG's deepest pair (#543005 and #003c30)
   * is two near-blacks that composite to the same muddy grey.
   *
   * Those five figures are OKLAB x100, the space with a published floor on it -- 8 to tell two
   * colours apart, 15 for a pair read quickly -- and the space `color-mix(in oklab)` resolves
   * in. Every other separation in this file is stated in CIELAB, whose number for the same
   * pair is two to three times larger. The two scales are not comparable. */
  --parched: #a86a1e;
  --muggy: #01665e;
  /* The two ends of the air-quality ramp, mixed in oklab for everything between. It is
     one hue at two SATURATIONS, not two hues: same oklab lightness at both ends, chroma
     0 to 0.200, so the mix is a pure chroma ramp and dirty air is the loud thing in the
     column while clean air is grey. Six category tones used to sit here, which made this
     the one column on the page that was a rainbow -- and put a wall between 99 and 101,
     two points of air either side of an advisory boundary.
     A green-to-red version came before this one and washed out: away from its ends it
     ran through khaki and tan, which is where the low-chroma middle of a warm ramp has
     to go, and those are --haze and --sun two columns over.
     Violet at oklch hue 306, which is a THIRD violet on the page and had to earn it:
     of the hues tried it is the one furthest from the two that were here already, at dE
     15.1 from --freeze and 26.0 from --uv in light, 29.6 and 27.1 in dark. It also only
     reaches full chroma above AQI 200, which is rare, while --freeze is a bar recolour
     three columns away and --uv a dashed hairline two.
     Cost, stated: the clean end is dE 15.5 (light) / 10.6 (dark) from --cloud, so a week
     of clean air puts a third grey column beside visibility and cloud. Which is the
     right picture -- clean air is unremarkable -- but it is a real collision. */
  --air-clean: #696969;
  --air-dirty: #863cc1;

  /* WHAT COVERS WHAT, back to front. Read this list as the order things paint in.
   *
   * These belong together because they are ONE ladder, whatever they are nested in. A
   * positioned box only holds its children's stacking inside itself when it has a z-index of
   * its own, and none of `.scroller`, `.grid` or `.row` does -- so a row's key, its readings
   * and the time strip above it all compete in the same scope as each other. Scattered through
   * the file as bare numbers, nothing said that, and two pairs came out in the wrong order:
   * a reading printed over the row's own name, and both of them over the hour labels.
   *
   * The rules the order encodes:
   *   - An alert's band says "these hours". The curve inside them is what it is about, so it
   *     goes under the drawing rather than washing it out.
   *   - The key is the one thing on a row that has to be legible at every hour -- it is sticky
   *     for exactly that reason, and the ruler already drops a tick rather than print one over
   *     it. So it covers a reading that scrolls under it.
   *   - The strip is the axis. A number over the hour it names leaves a chart of unlabelled
   *     shapes, which is the thing this page is arranged to prevent.
   *   - The header is over all of it, and holds its own stacking context, so `--z-popover` is
   *     scoped INSIDE the header and is not comparable with anything above it. */
  --z-band: 0;
  --z-chart: 1;
  --z-ruler: 2;
  --z-reading: 3;
  --z-key: 4;
  --z-legend: 6;
  --z-strip: 8;
  --z-topbar: 30;
  --z-popover: 20; /* inside the header's own context, above its buttons */

  color-scheme: light dark;
}

/* Dark lives on an ATTRIBUTE, not in `@media (prefers-color-scheme: dark)`.
 *
 * A toggle needs an explicit dark the OS preference cannot override AND an explicit
 * light that overrides the OS -- which, with a media query, means writing the palette
 * twice: once under `@media ... :root:not([data-theme="light"])` and again under
 * `:root[data-theme="dark"]`. Two copies of a palette that is audited numerically is
 * exactly the duplication that goes stale on one side only.
 *
 * So the preference is resolved to an attribute instead. index.html stamps
 * `data-theme` before first paint, and header.js re-stamps it when the OS setting
 * changes while the toggle is on Auto. One dark block, one source of truth. Nothing
 * renders here without JavaScript anyway -- the rows are built by an ES module -- so
 * this adds no failure mode the page did not already have.
 */
:root[data-theme="dark"] {
  /* A blue-black, not a neutral grey: the whole point of the register. */
  --bg: #0b0e15;
  --panel-bg: #11141d;
  --ink: #e6e8ee;
  --dim: #8d93a3;
  --faint: #1d212c;
  --line: #2a3040;
  /* Not the light theme's value at another lightness: on this hue the separator collided
   * with --rain, which is also a blue, at dE 9.4 under a 0.5 fill. Rotated slightly off it
   * and desaturated instead -- the smallest move that clears the floor, dE 9.8 from where
   * it was. It runs 4.04:1 against its panel where the light theme's runs 2.79:1, and the
   * asymmetry is the dark palette's own doing: its tones sit closer together in lightness,
   * so a line that separates from all of them has further to travel. */
  --edge: #6e7590;
  /* DARKER than the panel, not lighter. Night reading as a brighter band was
   * backwards, and on a near-black page it was the brightest thing on it. */
  --night: rgba(0, 1, 6, 0.5);
  --now: #ff6a55;

  --warm: #f0805a;
  --snow: #cfdff0;
  --freeze: #b98ae0;
  --rain: #5a9ada;
  --cloud: #98a0b0;
  --ice: #8ec0e0;
  --sun: #e8bb55;
  --uv: #cd6aaf;
  --wind: #4fb89e;
  --storm: #dd6a58;
  --thunder: #f0c33c;
  --haze: #b59a5e;
  /* Dark's own ramp, not an inversion of the light one: a light centre fails the moment it
   * is lighter than the page. The lightness range is compressed at the extremes and the
   * chroma raised, and the middle sits just off the panel so a comfortable hour is a
   * whisper rather than a hole. */
  --parched: #c98a45;
  --muggy: #4db6ac;
  --air-clean: #8c8c8c;
  --air-dirty: #ab63ea;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 13px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

/* ------------------------------- header --------------------------------- */

.topbar {
  /* NOT `.bar`, which the chart's own bar marks wear: that rule sets `opacity: 0.85`, and
   * with both on one class name the whole header was painting at 85% -- the settings menu
   * with the chart's numbers showing through it, and the hour ticks through the units row.
   * The stylesheet already had this hazard written down for `.panel`; this is the second one.
   *
   * Its own stacking context, above the grid: the sticky column headers inside
   * .scroller paint after this element in DOM order, so the search dropdown was
   * appearing behind them. */
  position: relative;
  z-index: var(--z-topbar);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}

.place { display: flex; align-items: center; gap: 8px; position: relative; min-width: 0; }
/* A heading for the accessibility tree only. The place name is visible in the input. */
.offscreen {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Whose clock the axis is in. Dim and small: it is a caveat on the place name, and
 * it is absent whenever there is nothing to caveat. */
.tz { font-size: 11px; color: var(--dim); white-space: nowrap; }

/* The title and the search box are one control, so it has to read as a title at rest:
 * heading type, no border, no box. The FRAME is on the wrapper rather than the input, because
 * the box holds three things now -- the magnifier, the name, and the locator -- and a frame
 * around only the middle one would draw a box inside a box. */
.searchbox {
  display: flex;
  align-items: center;
  /* No shared gap: the three children want different spacing. The magnifier sits close to the
   * box's own edge and holds the name off it -- 3px to its left against 8px to its right, where
   * an even 6/4 read as a glyph drifting away from the frame and crowding the word. */
  gap: 0;
  min-width: 0;
  padding: 2px 5px 2px 3px;
  border: 1px solid transparent;
  border-radius: 6px;
}
.searchbox:hover { border-color: var(--line); }
.searchbox:focus-within {
  border-color: transparent;
  background: var(--panel-bg);
  outline: 2px solid var(--rain);
  outline-offset: -1px;
}
/* The magnifier. Dim, and 13px against the name's 15: it is the one thing in the header whose
 * job is to be recognised rather than read, and at the name's own weight it competed with it. */
.sb-icon { display: inline-flex; margin-right: 8px; color: var(--dim); }
.sb-icon .ico { width: 13px; height: 13px; }
/* Use my location, inside the box at its right end. A bare icon button: the bordered one it
 * replaces was a second box beside the first, and 28px of a header that has none to spare. */
.sb-btn {
  display: inline-flex;
  flex: none;
  margin-left: 5px;
  padding: 2px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--dim);
  cursor: pointer;
}
.sb-btn:hover { color: var(--ink); background: var(--faint); }
.sb-btn .ico { width: 14px; height: 14px; }
.placebox {
  /* Sized by its own `size` attribute, which app.js sets from the name: a fixed width
     leaves a gap after "Rome" and truncates "Provincetown". Shrinkable below that, so a
     narrow phone gives the name up rather than pushing the controls off the screen. */
  width: auto;
  /* Exactly as wide as what it holds, where the browser can do that. Sized by its `size`
   * attribute otherwise -- header.js sets that from the name -- which is measured in `ch`, the
   * width of a zero: 117px of box for 93px of "Brooklyn, NY", so the locator beside it sat 24px
   * out in space. The attribute stays as the fallback, and the floors below keep a short name
   * from becoming a box too small to aim at. */
  field-sizing: content;
  min-width: 6ch;
  flex-shrink: 1;
  padding: 1px 0;
  border: 0;
  background: none;
  color: inherit;
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
}
/* The native clear button would appear on hover over what looks like a title. Focusing
 * selects the name instead, so one keystroke already replaces it. */
.placebox::-webkit-search-cancel-button { display: none; }
/* The name is set in the title's own weight at rest and in ordinary type once it is being
 * typed in: a query is not a title. And it holds room to type in, which under `field-sizing`
 * is a floor rather than the `size` attribute's bump. */
.placebox:focus { font-weight: 400; min-width: 14ch; }
.placebox:focus-visible { outline: none; }
.placebox::placeholder { color: var(--dim); font-weight: 400; }

/* A box that hangs off the header, over the chart: the place list and the two menus. The
 * shadow and the radius are what make them read as the same kind of thing, so both read one
 * declaration of each. Each says for itself only where it hangs from and what it holds. */
.results, .menu-panel {
  position: absolute;
  z-index: var(--z-popover);
  background: var(--panel-bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.14);
}
/* Under the box it belongs to, at its left edge -- which is the page's left edge now that the
 * locator has moved inside the field. */
.results {
  top: 32px;
  left: 0;
  margin: 0;
  padding: 4px;
  list-style: none;
  min-width: 220px;
}
.results button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 5px 7px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
/* One highlight, whether the arrow keys or the pointer put it there: the two have to agree
 * about what Enter would take. `li.on` rather than `:hover` alone, so a list walked from the
 * keyboard highlights without the mouse having to be anywhere near it. */
.results li.on > button { background: var(--faint); }

/* Never squeezed: `.topbar` wraps whole items, so a shrinkable control group stays on the
 * first row and runs off the edge of a phone instead of moving to the second. */
.controls { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

.seg { display: flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
/* The header's two kinds of button, which differ only in their borders: the segmented
 * control shares its borders with its neighbours, and the ghost owns its own. */
.seg button, .ghost {
  padding: 4px 9px;
  background: var(--panel-bg);
  color: var(--dim);
  font: inherit;
  cursor: pointer;
}
.seg button {
  border: 0;
  border-right: 1px solid var(--line);
  line-height: 1.2;
}
.seg button:last-child { border-right: 0; }
.seg button.on { background: var(--ink); color: var(--bg); }

.ghost {
  border: 1px solid var(--line);
  border-radius: 6px;
}
.ghost:hover { color: var(--ink); }

/* Settings: everything you set once and stop thinking about. Units and theme were in
 * the header at all times, which is real estate spent on two decisions a reader makes
 * on their first visit. */
.menu { position: relative; }
/* `.panel` is taken: every chart SVG carries it. */
.menu-panel[hidden] { display: none; }
.menu-panel {
  top: calc(100% + 6px);
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
}
.menu-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.menu-label { font-size: 12px; color: var(--dim); }
/* The one row in the menu that DOES something, under a rule that says so. The 10px is the
 * panel's own gap again, so the rule sits midway between the settings and the action. */
.menu-act { border-top: 1px solid var(--line); padding-top: 10px; }
#refresh { padding: 2px 8px; }
#refresh .glyph { font-size: 14px; line-height: 1; }
/* The gear at 16px against the page's 13px body type, with the line box tightened to 1 so the
 * bigger glyph does not make a taller button: 26px against the view control's 26, where it used
 * to be 28 and drawn at body size. It is one of two things left in this corner of the header,
 * and the smaller of the two was the one that opens everything. */
#settings { padding: 4px 8px; font-size: 16px; line-height: 1; }

/* How many alerts are behind the bell. A count, not a dot: two of them is a different morning
 * from one, and the number is as cheap to draw as the dot would have been. */
.badge {
  min-width: 14px;
  margin-left: 4px;
  border-radius: 7px;
  background: var(--storm);
  color: var(--panel-bg);
  font-size: 10px;
  font-weight: 600;
  line-height: 14px;
  text-align: center;
}
/* Sized to sit level with the gear beside it: a 16px glyph paints its ink smaller than its em
 * box, so a 14px drawn icon is the same weight on the page. */
#bell { display: inline-flex; align-items: center; padding: 4px 7px; }
#bell .ico { width: 14px; height: 14px; }
#bell[hidden] { display: none; }

/* ------------------------------- alerts ---------------------------------
 *
 * One card, in either of the two places an alert is read: under the row it is about, opened by
 * that row's tile, or in the bell's panel for the alerts no row draws. Whole, not a head that
 * opens a body -- both places got here by a click already. */

.alerts { max-width: min(380px, 78vw); max-height: 60vh; overflow-y: auto; }
.alert {
  padding: 5px 0;
  background: none;
  font-size: 12px;
}
/* Between two of them in the bell's panel. Not below the last, and never in a row's own
 * dropdown, which holds exactly one. */
.alert + .alert { border-top: 1px solid var(--line); }
.alert-head { display: flex; gap: 10px; align-items: baseline; }
/* The severity, as the tint the row's tile wears for the same alert -- a warning is red and
 * everything else is the page's warm. On the NAME, since the card itself sits inside a panel
 * whose whole job is to be readable prose. And it has to be a colour that can be READ at 12px:
 * both tones are set for a fill, and against the light panel `--warm` measures 3.6:1 and
 * `--storm` 4.1:1 at 12px.
 *
 * Pulled a fifth of the way toward `--ink`, which is the right direction in EITHER theme
 * without a second rule: ink is dark on the light panel and near-white on the dark one, so the
 * mix darkens the hue in light and lightens it in dark. 4.9 and 5.5 in light, 8.1 and 6.7 in
 * dark, and the hue is the tile's own in both. */
.alert-head strong { color: color-mix(in srgb, var(--warm) 80%, var(--ink)); }
.alert.sev-severe .alert-head strong, .alert.sev-extreme .alert-head strong {
  color: color-mix(in srgb, var(--storm) 80%, var(--ink));
}
.alert-when { color: var(--dim); font-variant-numeric: tabular-nums; }
/* THE WINDOW, POPPED OUT SIDEWAYS from the tile that was pressed. In the key's own line, where
 * the event is already sitting: "Heat Advisory" and "Thu 11a → Fri 8p" are one statement, and a
 * row has no downward room to make it in -- a card under the key covers the rows below.
 *
 * Tabular figures, because two times either side of an arrow are read as a pair and proportional
 * digits make the arrow wander. Dim: the tile beside it is the alert, and this qualifies it. */
.awin {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  /* Ink, not dim: this sits inside the tinted section rather than on the key's own background,
   * and a grey that reads as "secondary" against panel reads as "washed out" against a 30% wash
   * of warm. The event beside it is bolder, which is what keeps the order between them. */
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.awin[hidden] { display: none; }
/* Pressing the time puts the alert away again, the same as pressing the tile: it is the widest
 * part of the section and the obvious thing a thumb goes for once the section is open. */
.awin-time { font-size: 10px; white-space: nowrap; cursor: pointer; }
/* The chevron that opens the words.
 *
 * ITS TARGET IS BIGGER THAN ITS BOX. A key is a 19px line -- 14px of type in 5px of padding -- and
 * a 24px control inside one makes it a 29px line, so pressing the tile grew the row's name by ten
 * pixels and left the temperature key visibly unlike the other six. The button is the line's own
 * 14px, and an `::after` reaches 6px past it on every side: 26px of thumb over 14px of layout,
 * which clears the 24px WCAG minimum without the key knowing about it. */
.awin-more {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 14px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--ink);
  cursor: pointer;
}
.awin-more::after {
  content: "";
  position: absolute;
  inset: -6px;
}
.awin-more .ico { width: 10px; height: 10px; }
.awin-more:hover { color: var(--ink); background: var(--faint); }
/* Turned to point back the way it came when the words are showing: the one thing on the page that
 * says "this is open" by direction rather than by weight. */
.awin-more[aria-expanded="true"] { color: var(--ink); }
.awin-more[aria-expanded="true"] .ico { transform: rotate(180deg); }

/* The one line of an alert that says something the chart cannot: the office's own number, and
 * a forecaster's rather than a model's. In the card's own ink at the head's own size, because it
 * is a reading and not a caveat on one -- the body below it is the dim, smaller voice. */
.alert-what {
  margin: 4px 0 0;
  color: var(--ink);
  font-size: 12px;
  line-height: 1.4;
}

/* The disclosure over the prose. Its own line rather than a control in the head, because the
 * head is three facts read together and a fourth thing to press between them is a fourth thing
 * to read.
 *
 * The padding is a TARGET, not a rhythm: 11px type sets a 13px line, and this is the control a
 * phone reaches for, so it is padded to clear the 24px WCAG minimum. At 5px and 3px it measured
 * 23, which is the kind of miss nobody sees and a thumb finds. */
.alert-more summary {
  padding: 6px 0 5px;
  color: var(--dim);
  font-size: 11px;
  cursor: pointer;
}
.alert-more summary:hover { color: var(--ink); }
/* The marker stays: a triangle that turns is the one disclosure affordance a reader does not
 * have to be taught, and it is the browser's own, so it points the right way in both states
 * and in both writing directions. */
.alert-more[open] summary { color: var(--ink); }
.alert-body {
  margin: 3px 0 0;
  /* Weather-service prose runs long: 553 characters at the median across the country and over a
   * thousand at the 90th percentile, which under a row's key covers the rows below it. The BODY
   * scrolls rather than the card, so the event, its hours and its one promoted line stay put --
   * they are what says which alert the paragraph belongs to. */
  max-height: 30vh;
  overflow-y: auto;
  color: var(--dim);
  font-size: 11.5px;
  line-height: 1.45;
  white-space: pre-wrap;
}

/* The count of issues collapsed into this row. Dim and small: it qualifies the
 * event's name rather than saying anything about the weather. */
.alert-copies { color: var(--dim); font-variant-numeric: tabular-nums; }

/* -------------------------------- rows ---------------------------------- */

.scroller {
  flex: 1;
  overflow: auto;
  position: relative;
  overscroll-behavior: contain;
}

/* One column, as wide as the week: the time strip, then one row per variable. It shrinks
 * to its own content rather than to the viewport, which is what gives the scroller
 * something to scroll. */
.grid {
  position: relative;
  width: max-content;
  /* A drag across a chart and a hold on one both select text and raise the iOS callout
   * otherwise, over a chart whose only text is numbers already on the screen. */
  user-select: none;
  -webkit-touch-callout: none;
}

.loading { padding: 24px; color: var(--dim); }
.loading.error { color: var(--storm); }

.row {
  position: relative;
  border-bottom: 1px solid var(--edge);
  /* The colour a reading falls back to when its series has no tone. On the row rather
   * than on the reading itself, where it would tie with the `tone-*` class -- both are
   * one class deep, and the loser of a tie is whichever comes first in this file. */
  color: var(--dim);
}
.row svg.chart { position: absolute; inset: 0; z-index: var(--z-chart); }
.strip-svg { display: block; }

/* NO RAIL. Every number this page prints is printed inside the row it belongs to, at the
 * moment it belongs to. A sticky rail of readings cost 113px of every screen -- a fifth of
 * a phone, and the axis is the one thing this orientation is short of -- to hold twenty
 * numbers that only matter where the reader is looking. What is left at the left edge are
 * two OVERLAYS, which float over the chart instead of taking width from it: the row's key,
 * and its ruler.
 *
 * The key: the row's name and one picture of what it is about. Sticky, so it is on screen at
 * whatever hour you have scrolled to.
 *
 * The BOX, holding two kinds of button -- the name, which opens the row's legend, and one tile
 * per alert out on the row, which opens that alert. It was itself the button, with the tiles as
 * spans inside it; a span in a button takes no focus, so the alerts could not be reached from a
 * keyboard once the strip that used to list them went. */
.key {
  position: sticky;
  left: 0;
  z-index: var(--z-key);
  display: flex;
  align-items: center;
  gap: 6px;
  width: max-content;
  max-width: 100%;
  padding: 2px 8px 3px 5px;
  border-bottom-right-radius: 5px;
  background: color-mix(in srgb, var(--panel-bg) 84%, transparent);
  font-size: 11px;
  line-height: 14px;
  color: var(--dim);
  white-space: nowrap;
}
.key-name {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.key .t { font-weight: 600; }
.key-name:hover .t, .key-name[aria-expanded="true"] .t { color: var(--ink); }

/* An alert out on this row EXTENDS ITS KEY: the box grows a tinted section holding the warning,
 * the event, and -- once pressed -- the hours it covers.
 *
 * Not a pill inside the box. A tinted pill within a tinted box states the same boundary twice,
 * with a padding and a radius each, and the inner one is what pays: the event and its window are
 * the longest things in a key, and they were reading in the narrower of two nested rooms. Flush,
 * the section bleeds through the key's own padding on the sides it touches and takes the box's
 * bottom-right corner as its own.
 *
 * Tinted by severity on the same two levels the strip used -- a warning is red, everything else
 * is the page's warm -- and it is the one part of a key that is not about what the row draws.
 *
 * Two alerts on one row are two sections, with the key's own gap showing between them: a 6px band
 * of the box's own colour is what says "and also", where one continuous tint would read as one
 * long alert. */
.alert-seg {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  /* Which hue this section is tinted in. Two strengths below read it from here: at rest, and
   * while this is the alert being read. A third severity level is one line, and it arrives
   * tinted for both states. */
  --sev: var(--warm);
  background: color-mix(in srgb, var(--sev) 30%, transparent);
  /* The key's own padding, cancelled where this section meets its edges. */
  margin: -2px 0 -3px 0;
  padding: 2px 5px 3px 5px;
}
.alert-seg:last-child {
  margin-right: -8px;
  padding-right: 8px;
  border-bottom-right-radius: 5px;
}
.alert-seg.sev-severe, .alert-seg.sev-extreme { --sev: var(--storm); }
/* The warning and the event: a button with nothing of its own to look at. What it is drawn in is
 * the section around it, which is the thing being pressed. */
.flag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  /* The key around it is dim, and a warning is the one thing in it that is not a caveat
   * on a reading. Ink against the tint measures 12.3:1 in light and 9.0:1 in dark. */
  color: var(--ink);
  cursor: pointer;
}
/* NOT filled. The triangle is an outline holding a bar and a dot, and both of those are
 * stroked in the same colour a fill would use -- so a filled triangle swallowed them and
 * the tile said "something" where it meant "warning". */
.flag .ico { width: 11px; height: 11px; }
/* The event, in the row's own type at one step down: it names the alert without
 * outweighing the row's name beside it. */
.flag .fl { font-size: 10px; }
/* A stronger tint, not the full tone. At full strength the label sat at 2.2:1 against its own
 * background in dark mode; 55% holds it at 5.2:1 and is still a clear step up from 30%.
 *
 * Hovered, or OPEN. A section whose alert is on screen has to look different from the one beside
 * it whose alert is not, and it is the same statement either way: this is the one being read. On
 * the SECTION, because the thing that changes is the room and not the button in it -- `:has`
 * rather than a class, so the state lives in one place, on the control that owns it. */
.alert-seg:hover, .alert-seg:has([aria-expanded="true"]) {
  background: color-mix(in srgb, var(--sev) 55%, transparent);
}

/* THE ROW'S OWN NAME DOES NOT CHANGE. Reading an alert used to wash the whole key in 34% warm --
 * "this one", stated where the row says its name -- and that was the only mark available when the
 * alert was a small pill and pressing it opened a card somewhere else.
 *
 * It is now the third statement of the same fact, and the one made in the wrong place. The alert's
 * own section brightens, on every row the alert is out on; its window pops out beside the event;
 * and the band shades the hours across the row. "Temperature" is the one thing in that key which
 * is not about the alert, and recolouring it said something about the row instead.
 *
 * With the wash gone there is nothing left for a `.flagged` class to do, so the row no longer
 * wears one: the band and the brightened section are the whole statement, and both are drawn
 * from the alert itself. */

/* And the hours it covers, shaded across the row. UNDER everything the row draws: an alert says
 * "these hours", and the curve inside those hours is what the alert is about. Over the drawing
 * it would wash out the reading it is pointing at.
 *
 * The page also prepends it, so the order holds without the z-index; declared as well, because
 * a band that was appended instead would cover the row and nothing would say why. */
.alert-band {
  position: absolute;
  top: 0;
  bottom: 0;
  z-index: var(--z-band);
  pointer-events: none;
  background: color-mix(in srgb, var(--warm) 22%, transparent);
  border-left: 1px solid var(--warm);
  border-right: 1px solid var(--warm);
}

/* The legend the key opens: one line per mark the row draws, under the key and over the
 * chart. */
.legend[hidden] { display: none; }
.legend {
  position: sticky;
  left: 0;
  /* Above the readings, which are later siblings: a tie goes to the later element, and the
   * numbers were printing through the prose. */
  z-index: var(--z-legend);
  width: max-content;
  max-width: min(420px, 100%);
  padding: 3px 8px 5px 6px;
  border-bottom-right-radius: 5px;
  /* OPAQUE, and it may overhang the row below. A legend is read as prose, and prose over a
   * curve is prose in two colours; a row is as deep as its drawing needs, which is not a
   * promise about how much explanation fits inside it. It is above every row's own furniture,
   * so what it covers stays covered until it is closed again. */
  background: var(--panel-bg);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
  font-size: 11px;
  line-height: 15px;
  color: var(--dim);
}
.lg { display: flex; align-items: center; gap: 6px; }
.lg .swatch { flex: none; overflow: visible; }
.lgn { white-space: normal; }
.lgu { flex: none; color: var(--dim); opacity: 0.75; }
/* `fill` here rather than on the icon itself, where a tone class would beat it: the tones
 * set fill and stroke together, and a filled outline comes out as a blob. */
.ico {
  width: 12px;
  height: 12px;
  flex: none;
  fill: none;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The ruler: the value scale, as numbers against the gridlines the chart already draws.
 * Sticky, because a scale you have scrolled away from is not a scale. */
.ruler {
  position: sticky;
  left: 0;
  z-index: var(--z-ruler);
  /* Zero height so it takes no room in the row and its ticks can be placed anywhere in it
   * -- and a WIDTH, because a sticky box as wide as its container has nowhere to stick: it
   * spans the whole scroll and never needs to shift, which left every tick at the far left
   * of the week rather than at the left of the screen. The numbers overflow it and show. */
  width: 30px;
  height: 0;
  pointer-events: none;
}
.tick, .rv {
  position: absolute;
  transform: translateY(-50%);
  white-space: nowrap;
  /* Both of these sit ON the drawing rather than beside it, so each one carries a halo
   * of the page's own background: a number over a fill is otherwise a number in two
   * colours. Two shadows because one is not opaque enough to cover a stroke. */
  text-shadow: 0 0 3px var(--panel-bg), 0 0 3px var(--panel-bg), 0 0 2px var(--panel-bg);
}
.tick { left: 3px; font-size: 10px; color: var(--dim); opacity: 0.9; }
/* A tick that names a threshold rather than a round number keeps full strength, whether
 * its colour comes from a tone or is mixed per value. */
.tick.threshold { opacity: 1; }

/* The readings themselves: at the moment being read, at the height of their own curve,
 * in that curve's colour. */
.rv {
  z-index: var(--z-reading);
  font-size: 12px;
  line-height: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* Lighter, for two kinds of reading that are not the row's subject: one that says its own name
 * (a day's rainfall, which is a depth where the row measures a rate) and one that qualifies the
 * curve above it (the dew point under the temperature, the feels-like as a difference from it).
 * Two sets of bold numbers on one row leave it with no headline. */
.rv.named, .rv.quiet { font-size: 11px; font-weight: 400; }
/* Smaller, for a reading that is three digits wide: an AQI and a wind speed at 12px are the
 * biggest numbers on the page and the least worth being it. This is the size the renderer's
 * own peak labels are drawn at -- the gust numbers on the same row -- so a wind row reads as
 * one set of numbers rather than two. */
.rv.small { font-size: 10px; line-height: 11px; }

/* The time strip: sticky at the top, because an axis you have scrolled away from leaves a
 * chart of unlabelled shapes. */
.strip {
  position: sticky;
  top: 0;
  z-index: var(--z-strip);
  background: var(--bg);
  border-bottom: 1px solid var(--edge);
}

/* ---------------------------- chart elements ----------------------------- */

.night { fill: var(--night); }
.hour-line { stroke: var(--faint); stroke-width: 1; }
.day-line { stroke: var(--line); stroke-width: 1; }
/* Now: the one line on the chart that is about the reader rather than the forecast, and it
 * only has to be findable, not loud. It was 1.5px at full strength, which made it the
 * heaviest stroke on a page whose data is drawn at 1.1 to 2.7 -- a piece of furniture
 * outshouting every reading it runs through. At 1px and 0.6 it is still the only red. */
.now-line { stroke: var(--now); stroke-width: 1; opacity: 0.6; }
/* Value gridlines: enough to judge a threshold against, never enough to compete
 * with the data drawn over them. */
.value-line { stroke: var(--faint); stroke-width: 1; }
/* A threshold belonging to one series rather than to the panel's own scale: tinted like
 * that series, and otherwise the same 1px solid line every other column's ruler is drawn
 * with. A dash would say "this line belongs to a series, not to the panel", which the
 * TINT already says. The air-quality column has no use for it at all, since all three of
 * its lines are thresholds and it has no round numbers to be told apart from. The column
 * whose ruler is its own legend is the last one whose gridlines should look unlike
 * gridlines.
 *
 * 0.35 puts a continuous line at about the salience of a dashed one at 0.5, since a `2 3`
 * dash covers 40% of its length. Measured against the panel in the light theme, the
 * air-quality boundary at 50 comes out at dE 18.5 and the dirty end of that ramp at 33.7,
 * with freezing at 13.6, against dE 8.5 for a plain round-number gridline. A threshold
 * sits above a round number because it is the line worth finding. The spread across the
 * three air-quality lines is the ramp's own, and it is the point of them: each is drawn in
 * the colour its own number gets. */
.value-line.threshold { fill: none; }
/* The dimming, and only where a colour pays for it: `tinted` is a declared line drawn in its
 * own tone or mixed per value. An untinted declared line keeps an ordinary gridline's weight,
 * because the number beside it is the whole meaning and there is no colour to find it by. */
.value-line.threshold.tinted { opacity: 0.35; }

/* Tones carry stroke, fill and colour, so one class serves a line, an area, a bar and a
 * piece of TEXT saying what one of them reads. The page prints its numbers inside
 * the rows, each in the colour of the curve it belongs to, and an HTML element takes its
 * text colour from `color`; `currentColor` is also how an untoned icon there follows the
 * text beside it.
 *
 * One class per tone some series names. A tone no series names is a colour the page
 * cannot show, so a column that goes takes its tones with it.
 *
 * Each class only NAMES its hue. A mark is stroked, filled or set in type depending on what
 * kind of mark it is, and every tone answers all three the same way, so all three are declared
 * once for every tone together.
 *
 * The attribute selector matches one class, like the tone classes it reads. That is what keeps
 * the kind rules below able to beat it: a line has to win `fill: none` against its own tone,
 * and later-and-equal-specificity is how it does. So the kind rules MUST stay after this
 * block. */
.tone-warm { --tone: var(--warm); }
.tone-snow { --tone: var(--snow); }
.tone-freeze { --tone: var(--freeze); }
.tone-rain { --tone: var(--rain); }
.tone-cloud { --tone: var(--cloud); }
.tone-ice { --tone: var(--ice); }
.tone-sun { --tone: var(--sun); }
.tone-uv { --tone: var(--uv); }
.tone-wind { --tone: var(--wind); }
.tone-thunder { --tone: var(--thunder); }
.tone-haze { --tone: var(--haze); }
.tone-parched { --tone: var(--parched); }
.tone-muggy { --tone: var(--muggy); }
.tone-air-clean { --tone: var(--air-clean); }
.tone-air-dirty { --tone: var(--air-dirty); }
[class*="tone-"] { stroke: var(--tone); fill: var(--tone); color: var(--tone); }

.line { fill: none; stroke-width: 1.6; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.line.thick { stroke-width: 3.4; stroke-linecap: round; }
/* The dew point's ladder: how far the air is from comfortable, in one step per point of the
 * comfort score. Four states, and a fifth that draws no line at all -- air that is neither wet
 * nor dry, where the quietest style available would still read as a reading.
 *
 * PATTERN carries the magnitude and HUE carries the direction (see `bandedLine`): two spacings
 * of dotted, then dashed, then solid, in ochre for parched and teal for muggy. The pattern has
 * to be the ordered channel, because hue is not one -- nobody can say which of two colours is
 * the larger.
 *
 * FOUR AND NOT FIVE. The fifth rung sat at a comfort score of 5, and that score meant opposite
 * things on the two sides of the scale: measured over a year of hourly dew points in twelve
 * cities it drew for 0.0% of the year on the wet side and 14-24% on the dry side. Cutting it
 * also bought the separation below -- see `comfortScore` for the rest of that recalibration.
 *
 * Ink is width x dash duty x opacity, and it is what the eye integrates along a curve. ROUND
 * caps, so a dash is drawn a full stroke-width longer than it is declared: the duty of the top
 * dotted rung is 0.47 rather than the 0.16 its numbers state, and a ladder pitched by the
 * declared duty alone would have two rungs that measure alike. Computed with the caps: 0.44 /
 * 0.75 / 1.29 / 2.20 -- steps of 1.70x, 1.71x and 1.71x, spanning the same 5.0x the five-rung
 * ladder did but over three steps instead of four, so the ladder is uniform where it used to
 * come down to 1.37x at its tightest. A step needs about 1.2x to be seen at all, and the test
 * measures ink rather than width because width alone is what missed the bug this was rebuilt
 * from: a `dotted` rung at 1.8px stepping up to a solid one at 1.6.
 *
 * The two dotted rungs differ in their GAP and barely in width, so they read as one kind of mark
 * at two spacings rather than as two kinds of line. The top is 2.2px; `thick` at 3.4 is the
 * air-quality index's weight and would dominate a column that also holds two temperature
 * curves. */
.line.away1 { stroke-width: 1.5; stroke-dasharray: 0.8 7; stroke-linecap: round; }
.line.away2 { stroke-width: 1.6; stroke-dasharray: 0.8 4.3; stroke-linecap: round; }
.line.away3 { stroke-width: 1.8; stroke-dasharray: 3.5 3.9; stroke-linecap: round; }
.line.away4 { stroke-width: 2.2; stroke-linecap: round; }
.line.dashed { stroke-width: 1.2; stroke-dasharray: 3 2.5; opacity: 0.8; }
/* Quieter still than dashed: a curve that is there to be read AGAINST, not
 * followed. Round caps so it reads as dots rather than a broken line. */
.line.casing {
  stroke: var(--panel-bg);
  stroke-width: 3.6;
  opacity: 0.9;
}
.area { stroke: none; opacity: 0.5; }
/* A fill that something else is drawn inside: chance behind rain bars, AQI
 * behind PM2.5. */
.area.faint { opacity: 0.22; }
/* The part of a threshold line where the reading is above it. Solid and round-capped
 * against the dashed stretch it runs into, so the lit section reads as a bracket down the
 * page rather than as another gridline. */
.span { fill: none; stroke-width: 1.8; stroke-linecap: round; opacity: 0.75; }
/* Was 2.8. The UV span is the only purple on the page and it ends in a labelled dot of
 * the same hue, so it is found by colour and by that connection, not by weight. */
.span.thick { stroke-width: 2.2; }
/* The lower of two thresholds on one series. Both are curved and the lower one is bounded
 * at the upper one's value, so this is the lower STRETCH of one continuous curve. Thinner,
 * dashed and dimmer than the segment it runs into, so the pair is a severity ramp read
 * along a single line: 1.4px dashed at 0.6 while the sun merely matters, 2.2px solid at
 * 0.75 past burning, a labelled dot at the peak. Monotonic like the dew point's five
 * weights: more of the thing is more ink at every crossing. Round caps are
 * dropped, since at this dash length they close the gaps back up. */
.span.dashed {
  stroke-width: 1.4;
  stroke-dasharray: 3 3;
  stroke-linecap: butt;
  opacity: 0.6;
}
/* A supporting curve: the edge of a fill something else is drawn inside, so it
 * stays findable without competing with the measurement on top of it. */
.line.minor { opacity: 0.5; stroke-width: 1.1; }
.bar { stroke: none; opacity: 0.85; }  /* a rect would inherit its tone's stroke */

.arrow path { stroke: var(--wind); stroke-width: 1.2; fill: none; }
/* A marked code, drawn on the curve it belongs to. Both forms carry a halo of the
 * panel background BEHIND the shape: a mark rides the curve it belongs to, which
 * means it lands on top of that series' own fill, and measured against the rain bars
 * every bolt colour considered scored 1.3-1.4:1 in light mode -- no hue would have
 * fixed that. The halo makes a mark legible on any fill without giving it a heavier
 * colour than it deserves.
 *
 * A filled shape gets its halo from paint-order, which strokes the outline of the
 * fill. A stroked shape has no fill to outline, so it is drawn twice instead -- the
 * background first and wider -- which is the same casing trick the PM2.5 line uses. */
.bolt {
  stroke: var(--panel-bg);
  stroke-width: 2.5;
  paint-order: stroke fill;
  stroke-linejoin: round;
}
.mark-line { fill: none; stroke-width: 1.4; stroke-linecap: round; }
.mark-casing {
  fill: none;
  stroke: var(--panel-bg);
  stroke-width: 3.4;
  stroke-linecap: round;
  opacity: 0.9;
}

/* Daily high and low, marked on the curve where they occur. */
/* Sunrise and sunset times, on the night side of their own boundary. Small and
 * held back: they name a line the night shading has already drawn. */
.daylight-caret { fill: var(--dim); stroke: none; opacity: 0.85; }
.daylight { fill: var(--dim); opacity: 0.85; }
.daylight { font-size: 10px; }
.extreme-dot { stroke: none; }
.extreme-label { font-size: 10px; font-weight: 600; }

/* EVERY NUMBER PRINTED ON THE DRAWING CARRIES A HALO of the page's own background -- a number
 * over a fill is otherwise a number in two colours. The rule was written for the ruler's ticks
 * and the row's readings, both of which are divs, and never reached the labels the renderer draws
 * inside the SVG: 16 numbers in the hourly view, where the UV peaks and the gust peaks are the
 * ones that survive, and 31 in the daily one. On the rain row in the daily view BOTH kinds appear
 * at once -- the day's peak rate from the renderer and its total from the page -- so one number
 * had a halo and the one beside it did not.
 *
 * `paint-order: stroke`, which is how `.bolt` and `.now-label` already do this: the same idea in
 * SVG's own spelling, and crisp at 10px where a blurred shadow is a smudge. The technique was
 * here, and these two labels were the gap in it.
 *
 * The stroke has to be NAMED here, because the tone classes set `stroke` along with `fill`: left
 * unstated, every peak is outlined in its own colour, which is why these rules said `stroke: none`
 * to begin with. Panel background and not `--bg`, because a peak is inside a row -- `.now-label`
 * uses the page's own background for the same reason, and it lives in the strip. */
.extreme-label, .daylight {
  paint-order: stroke;
  stroke: var(--panel-bg);
  stroke-width: 2.4;
  stroke-linejoin: round;
}

/* An empty column is kept and labelled, never removed -- and so is an empty STRETCH of
 * one, which is what .end-line marks: the row where a column's own feed stops short of
 * the axis. Dashed and dim, and the only dashed line among the page's furniture, so a
 * broken line in a panel means one thing: the data stops. */
.nodata { fill: var(--dim); font-size: 10px; text-anchor: middle; opacity: 0.7; }
.end-line { stroke: var(--dim); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.45; }

.day-label { fill: var(--ink); font-size: 11px; font-weight: 600; }
/* The hour clocks, in the strip's lower lane -- next to the chart they label. Each is
 * centred on its own line by the `text-anchor` attribute the strip writes, so no anchor is
 * stated here: a CSS declaration would outrank that attribute and shift every clock half
 * its own width off the line it names. */
.hour-label { fill: var(--dim); font-size: 10px; }
/* The time it marks, in the strip's clock lane and drawn last, so it covers the hour label
 * it lands on rather than sharing pixels with it. Cased in the page's own background, since
 * two times in the same 30px are neither of them readable and this is the one that answers. */
.now-label {
  fill: var(--now);
  font-size: 10px;
  font-weight: 600;
  stroke: var(--bg);
  stroke-width: 3;
  paint-order: stroke;
}
