/* Deck — the stylesheet.
 *
 * Written here rather than pulled in, and that is the whole point: what
 * replaced it was 658 KB of the Carbon Design System plus twenty-one
 * <script> tags to somebody else's CDN, in five different versions of the
 * same components. A weather station in a house should not need a company's
 * servers to be up in order to draw its own temperature.
 *
 * Everything below is one of four things: tokens, layout, components, or a
 * handful of overrides for the chart library. Nothing here needs a build
 * step -- it is a file, you can open it, change a number and reload.
 *
 * The dark theme is not a second stylesheet. Every colour is a token, and
 * the theme swaps the tokens. That is why it cannot half-apply: there is no
 * rule anywhere that names a colour directly.
 */

/* ---------------------------------------------------------------- tokens */

:root {
  /* One hue carries the whole page: a cold blue for the surface, warmed
   * slightly as it comes forward, so a card reads as nearer without a
   * heavy shadow doing the work. */
  --hue: 222;
  --accent-hue: 205;

  --bg: hsl(var(--hue) 30% 97%);
  --bg-sunk: hsl(var(--hue) 28% 94%);
  --surface: hsl(0 0% 100%);
  --surface-2: hsl(var(--hue) 30% 98%);
  --surface-hover: hsl(var(--hue) 34% 96%);

  --line: hsl(var(--hue) 20% 88%);
  --line-soft: hsl(var(--hue) 20% 92%);

  --ink: hsl(var(--hue) 32% 14%);
  --ink-2: hsl(var(--hue) 14% 38%);
  --ink-3: hsl(var(--hue) 12% 52%);

  --accent: hsl(var(--accent-hue) 82% 42%);
  --accent-ink: hsl(0 0% 100%);
  --accent-soft: hsl(var(--accent-hue) 84% 96%);
  --accent-line: hsl(var(--accent-hue) 70% 84%);

  /* The readings that carry meaning of their own. Used by the tiles and
   * handed to the charts, so a temperature is the same red in both. */
  --warm: hsl(8 72% 52%);
  --cool: hsl(205 78% 46%);
  --wet: hsl(200 72% 44%);
  --wind: hsl(168 58% 38%);
  --sun: hsl(42 92% 48%);
  --good: hsl(148 52% 38%);
  --warn: hsl(32 88% 46%);
  --bad: hsl(2 72% 50%);

  /* A place with no colour of its own. Grey and not `--accent`: eight
   * uncoloured places all painted in the UI accent read as eight *selected*
   * rows. The colours themselves are never tokens -- they live in
   * archives.toml and arrive as an inline `--place`. */
  --place-fallback: hsl(var(--hue) 12% 52%);

  --shadow-1: 0 1px 2px hsl(var(--hue) 30% 20% / 0.06),
              0 1px 1px hsl(var(--hue) 30% 20% / 0.04);
  --shadow-2: 0 2px 4px hsl(var(--hue) 30% 20% / 0.05),
              0 8px 24px -8px hsl(var(--hue) 30% 20% / 0.12);
  --shadow-3: 0 2px 6px hsl(var(--hue) 30% 20% / 0.06),
              0 20px 48px -16px hsl(var(--hue) 30% 20% / 0.2);

  /* A scale rather than a set of numbers: every gap on the page is one of
   * these, so nothing is 13px because it looked right that afternoon. */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;

  --r-1: 6px;
  --r-2: 10px;
  --r-3: 14px;
  --r-4: 20px;
  --r-full: 999px;

  /* The system stack. 556 KB of IBM Plex went with the rest: a font that
   * has to arrive before the page reads is a page that flashes, and every
   * machine already has a good one. */
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --font-num: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
              sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono",
               "Roboto Mono", Menlo, Consolas, monospace;

  --step--1: clamp(0.78rem, 0.76rem + 0.1vw, 0.84rem);
  --step-0: clamp(0.92rem, 0.89rem + 0.14vw, 1rem);
  --step-1: clamp(1.06rem, 1rem + 0.3vw, 1.2rem);
  --step-2: clamp(1.28rem, 1.16rem + 0.6vw, 1.6rem);
  --step-3: clamp(1.6rem, 1.36rem + 1.1vw, 2.2rem);
  --step-4: clamp(2rem, 1.5rem + 2.2vw, 3.2rem);

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --fast: 120ms;
  --slow: 260ms;

  --header-h: 3.5rem;
  --nav-w: 15rem;
  --page-max: 108rem;

  color-scheme: light;
}

/* The dark theme. Same names, different values, and that is the only
 * difference between the two -- which is why one cannot get half-applied
 * while the other does not.
 *
 * Three selectors, because there are three states: chosen light, chosen
 * dark, and "whatever the browser says", which is the default and stamps
 * no attribute at all. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: hsl(var(--hue) 24% 9%);
    --bg-sunk: hsl(var(--hue) 26% 7%);
    --surface: hsl(var(--hue) 20% 13%);
    --surface-2: hsl(var(--hue) 20% 16%);
    --surface-hover: hsl(var(--hue) 20% 18%);

    --line: hsl(var(--hue) 16% 24%);
    --line-soft: hsl(var(--hue) 16% 19%);

    --ink: hsl(var(--hue) 22% 94%);
    --ink-2: hsl(var(--hue) 12% 72%);
    --ink-3: hsl(var(--hue) 10% 58%);

    --accent: hsl(var(--accent-hue) 84% 64%);
    --accent-ink: hsl(var(--hue) 30% 10%);
    --accent-soft: hsl(var(--accent-hue) 50% 18%);
    --accent-line: hsl(var(--accent-hue) 50% 30%);

    --warm: hsl(10 78% 62%);
    --cool: hsl(205 82% 62%);
    --wet: hsl(198 74% 58%);
    --wind: hsl(168 52% 52%);
    --sun: hsl(44 88% 60%);
    --good: hsl(148 48% 52%);
    --warn: hsl(34 88% 58%);
    --bad: hsl(4 76% 62%);

    --place-fallback: hsl(var(--hue) 10% 68%);

    --shadow-1: 0 1px 2px hsl(0 0% 0% / 0.4);
    --shadow-2: 0 2px 4px hsl(0 0% 0% / 0.3), 0 8px 24px -8px hsl(0 0% 0% / 0.5);
    --shadow-3: 0 2px 6px hsl(0 0% 0% / 0.4), 0 20px 48px -16px hsl(0 0% 0% / 0.6);

    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --bg: hsl(var(--hue) 24% 9%);
  --bg-sunk: hsl(var(--hue) 26% 7%);
  --surface: hsl(var(--hue) 20% 13%);
  --surface-2: hsl(var(--hue) 20% 16%);
  --surface-hover: hsl(var(--hue) 20% 18%);

  --line: hsl(var(--hue) 16% 24%);
  --line-soft: hsl(var(--hue) 16% 19%);

  --ink: hsl(var(--hue) 22% 94%);
  --ink-2: hsl(var(--hue) 12% 72%);
  --ink-3: hsl(var(--hue) 10% 58%);

  --accent: hsl(var(--accent-hue) 84% 64%);
  --accent-ink: hsl(var(--hue) 30% 10%);
  --accent-soft: hsl(var(--accent-hue) 50% 18%);
  --accent-line: hsl(var(--accent-hue) 50% 30%);

  --warm: hsl(10 78% 62%);
  --cool: hsl(205 82% 62%);
  --wet: hsl(198 74% 58%);
  --wind: hsl(168 52% 52%);
  --sun: hsl(44 88% 60%);
  --good: hsl(148 48% 52%);
  --warn: hsl(34 88% 58%);
  --bad: hsl(4 76% 62%);

  /* The same value as the media block above, because there are three theme
   * states and the default stamps no attribute. A token added to one block
   * and not the other is right on the toggle and wrong at sunset, and no
   * test reads CSS. */
  --place-fallback: hsl(var(--hue) 10% 68%);

  --shadow-1: 0 1px 2px hsl(0 0% 0% / 0.4);
  --shadow-2: 0 2px 4px hsl(0 0% 0% / 0.3), 0 8px 24px -8px hsl(0 0% 0% / 0.5);
  --shadow-3: 0 2px 6px hsl(0 0% 0% / 0.4), 0 20px 48px -16px hsl(0 0% 0% / 0.6);

  color-scheme: dark;
}

/* ----------------------------------------------------------------- reset */

*, *::before, *::after { box-sizing: border-box; }

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  min-height: 100dvh;
  font-family: var(--font);
  font-size: var(--step-0);
  line-height: 1.55;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, video, canvas { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; color: inherit; }
p, h1, h2, h3, h4 { overflow-wrap: break-word; }

h1, h2, h3, h4 {
  line-height: 1.2;
  font-weight: 640;
  letter-spacing: -0.015em;
}

a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { text-decoration-thickness: 2px; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-1);
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Every number on this page is compared with the one under it. Tabular
 * figures keep the columns straight, and the browser has them. */
.num, table td, table th, time {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

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

.header {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  height: var(--header-h);
  padding-inline: var(--s-4);
  background: hsl(from var(--surface) h s l / 0.86);
  backdrop-filter: saturate(1.6) blur(12px);
  -webkit-backdrop-filter: saturate(1.6) blur(12px);
  border-bottom: 1px solid var(--line);
}

/* `hsl(from ...)` is recent. Where it is not understood the whole
 * declaration is dropped, and an opaque header is the right fallback --
 * so it is stated first and the translucent one overrides it. */
@supports not (background: hsl(from red h s l)) {
  .header { background: var(--surface); }
}

.header__brand {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-weight: 640;
  color: inherit;
  text-decoration: none;
  white-space: nowrap;
}

.header__brand svg, .header__brand img { width: 1.5rem; height: 1.5rem; }
.header__station { color: var(--ink-2); font-weight: 500; }

.header__end { margin-inline-start: auto; display: flex; gap: var(--s-1); }

.icon-button {
  display: grid;
  place-items: center;
  width: 2.25rem; height: 2.25rem;
  border: 1px solid transparent;
  border-radius: var(--r-2);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.icon-button:hover { background: var(--surface-hover); color: var(--ink); }
.icon-button svg { width: 1.15rem; height: 1.15rem; fill: currentColor; }

/* ------------------------------------------------------------- units */

/* Two whole sets of these pages exist, one per unit system, and this says
   which one is showing and gets to the other. A pair of words rather than a
   toggle: a toggle has one visible state and leaves somebody guessing which
   way it points, and this has to read the same on the page it lands on. */
.unit-switch {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: var(--r-full, 999px);
  overflow: hidden;
  font-size: var(--step--1);
  font-weight: 620;
  line-height: 1;
}

.unit-switch > * {
  display: inline-flex;
  align-items: center;
  padding: .4em .7em;
}

.unit-switch__here {
  background: var(--accent);
  color: var(--accent-ink);
}

.unit-switch__other {
  color: var(--ink-2);
  text-decoration: none;
}

.unit-switch__other:hover { background: var(--surface-hover); color: var(--ink); }
.unit-switch__other:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The toggle shows the theme you would switch *to*, not the one you are
 * in -- a button says what it does. */
[data-theme="dark"] .theme-toggle__dark,
.theme-toggle__light { display: none; }
[data-theme="dark"] .theme-toggle__light { display: block; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .theme-toggle__dark { display: none; }
  :root:not([data-theme="light"]) .theme-toggle__light { display: block; }
}
:root[data-theme="light"] .theme-toggle__light { display: none; }
:root[data-theme="light"] .theme-toggle__dark { display: block; }

.header__menu { display: none; }

/* ------------------------------------------------------------ side panel */

.layout {
  display: grid;
  grid-template-columns: var(--nav-w) minmax(0, 1fr);
  align-items: start;
}

.sidenav {
  position: sticky;
  top: var(--header-h);
  max-height: calc(100dvh - var(--header-h));
  overflow-y: auto;
  padding: var(--s-5) var(--s-3);
  border-inline-end: 1px solid var(--line);
}

.sidenav__group + .sidenav__group { margin-top: var(--s-5); }

.sidenav__title {
  padding-inline: var(--s-3);
  margin-bottom: var(--s-2);
  font-size: var(--step--1);
  font-weight: 640;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.sidenav a {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-2);
  color: var(--ink-2);
  text-decoration: none;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.sidenav a:hover { background: var(--surface-hover); color: var(--ink); }

.sidenav a[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 560;
}

.sidenav svg { width: 1.05rem; height: 1.05rem; fill: currentColor; flex: none; }

/* The archive. One disclosure holding every year, rather than one per year
 * stacked under each other -- a station recording since 2016 had eleven of
 * them, and the tenth is no more worth a line of its own than the first.
 *
 * The summary is shaped like the links above it, so the panel reads as one
 * list. The marker is drawn here: a browser's own triangle is the only thing
 * in this panel that looks like 1997. */
.sidenav__archive > summary {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-2);
  color: var(--ink-2);
  cursor: pointer;
  list-style: none;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.sidenav__archive > summary::-webkit-details-marker { display: none; }
.sidenav__archive > summary:hover { background: var(--surface-hover); color: var(--ink); }

/* Turns when it opens, which is the whole of what a disclosure has to say. */
.sidenav__archive > summary::after {
  content: "";
  margin-inline-start: auto;
  width: 0.4rem;
  height: 0.4rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--fast) var(--ease);
}
.sidenav__archive[open] > summary::after { transform: rotate(45deg); }

.sidenav__year { margin-top: var(--s-2); padding-inline-start: var(--s-3); }
.sidenav__year > a { font-weight: 560; }

/* Twelve months as a grid rather than twelve rows. Three characters each,
 * so four fit across the panel and a year takes three lines instead of
 * twelve -- which is what made the old list worth collapsing in the first
 * place. */
.sidenav__months {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 2px;
  padding: var(--s-1) 0 var(--s-2) var(--s-3);
}
.sidenav__months a {
  justify-content: center;
  padding: var(--s-1) var(--s-2);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
}

.main { min-width: 0; padding: var(--s-6) var(--s-5) var(--s-8); }
.wrap { max-width: var(--page-max); margin-inline: auto; }

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

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-3) var(--s-5);
  margin-bottom: var(--s-6);
}

.page-head h1 { font-size: var(--step-3); }

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s-3);
  margin: var(--s-7) 0 var(--s-4);
}

.section-head:first-child { margin-top: 0; }
.section-head h2 { font-size: var(--step-2); }

/* ------------------------------------------------------------------ grid */

/* `auto-fit` and `minmax`, not twelve columns. A tile says how narrow it
 * may get and the browser decides how many fit -- which is the same answer
 * at every width, including the ones nobody thought to test. */
.grid {
  display: grid;
  gap: var(--s-4);
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

.grid--tiles {
  grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr));
}

.grid--wide {
  grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
}

/* ------------------------------------------------------------------ card */

.card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-1);
  overflow: clip;
}

.card__head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-4) var(--s-4) 0;
}

.card__head h3 {
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink-2);
}

.card__head svg { width: 1.1rem; height: 1.1rem; fill: var(--ink-3); flex: none; }
.card__body { padding: var(--s-4); }
.card__foot {
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--line-soft);
  background: var(--surface-2);
  font-size: var(--step--1);
  color: var(--ink-3);
}

/* ------------------------------------------------------------------ tile */

/* The reason the page exists: one reading, big enough to read across a
 * kitchen, with its context underneath in a size that does not compete. */
.tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  padding: var(--s-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-1);
  transition: box-shadow var(--slow) var(--ease),
              transform var(--slow) var(--ease),
              border-color var(--slow) var(--ease);
}

.tile:hover {
  box-shadow: var(--shadow-2);
  border-color: var(--line-soft);
  transform: translateY(-1px);
}

/* A tinted tile: the colour is set from the reading itself, so the tint is
 * the value rather than a decoration. */
.tile[style*="--tint"], .stat-tile[style*="--tint"] {
  background:
    linear-gradient(160deg,
      hsl(from var(--tint) h s l / var(--tint-strength, 0.14)),
      transparent 62%),
    var(--surface);
}

/* ---------------------------------------------------------------- charts */

.chart {
  width: 100%;
  height: clamp(16rem, 30vh, 24rem);
}

.chart-card .card__body { padding: var(--s-3) var(--s-2) var(--s-2); }

/* ----------------------------------------------------------------- table */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--step--1);
}

thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--s-3) var(--s-4);
  text-align: start;
  font-weight: 600;
  color: var(--ink-2);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

tbody td {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line-soft);
  vertical-align: baseline;
}

/* A row header is a cell of its row, not a heading floating in it. Without
 * this it keeps the browser's own `th`: centred, unpadded and with no rule
 * under it -- so the name of a row sat a hundred pixels in from the column
 * heading above it, on a line the eye could not follow across. Measured on
 * the comparison table: 1px of padding against the 12px 16px every figure
 * beside it had. */
tbody th {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--line-soft);
  text-align: start;
  vertical-align: baseline;
  font-weight: 560;
  color: var(--ink);
}

tbody tr:last-child td,
tbody tr:last-child th { border-bottom: none; }
tbody tr:hover td { background: var(--surface-hover); }

td.num, th.num { text-align: end; }
td.name { font-weight: 560; color: var(--ink); white-space: nowrap; }
td .sub { display: block; color: var(--ink-3); font-size: 0.92em; }

/* A sortable column says so, and says which way it is sorted. */
th[aria-sort] { cursor: pointer; user-select: none; }
th[aria-sort]:hover { color: var(--ink); }
th[aria-sort]::after {
  content: "";
  display: inline-block;
  width: 0.6em; height: 0.6em;
  margin-inline-start: 0.4em;
  opacity: 0.3;
  background: currentColor;
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
th[aria-sort="descending"]::after { transform: rotate(180deg); opacity: 1; }
th[aria-sort="ascending"]::after { opacity: 1; }

/* ------------------------------------------------------------------ tabs */

.tabs {
  display: flex;
  gap: var(--s-1);
  padding: var(--s-1);
  margin-bottom: var(--s-4);
  background: var(--bg-sunk);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-full);
  width: fit-content;
  max-width: 100%;
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar { display: none; }

.tabs button {
  padding: var(--s-2) var(--s-4);
  border: none;
  border-radius: var(--r-full);
  background: none;
  color: var(--ink-2);
  font-weight: 560;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.tabs button:hover { color: var(--ink); }

.tabs button[aria-selected="true"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}

[role="tabpanel"][hidden] { display: none; }

/* ----------------------------------------------------------------- badge */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 0.15em var(--s-2);
  border-radius: var(--r-full);
  font-size: var(--step--1);
  font-weight: 560;
  line-height: 1.6;
  background: var(--surface-2);
  color: var(--ink-2);
  border: 1px solid var(--line);
}

/* ---------------------------------------------------------------- notice */

.notice {
  display: flex;
  gap: var(--s-3);
  padding: var(--s-4);
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--r-2);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

.notice--warn { border-inline-start-color: var(--warn); }
.notice--bad { border-inline-start-color: var(--bad); }
.notice svg { width: 1.2rem; height: 1.2rem; flex: none; fill: var(--accent); }
.notice--warn svg { fill: var(--warn); }
.notice--bad svg { fill: var(--bad); }
.notice__title { font-weight: 620; }
.notice__body { color: var(--ink-2); font-size: var(--step--1); }

/* --------------------------------------------------------------- tooltip */

/* `<abbr>` with a title works everywhere and needs no script. This adds a
 * readable bubble on top of it for pointers, and leaves the native one for
 * everybody else. */
.hint {
  position: relative;
  border-bottom: 1px dotted var(--ink-3);
  cursor: help;
  text-decoration: none;
}

.hint::after {
  content: attr(data-hint);
  position: absolute;
  bottom: calc(100% + var(--s-2));
  left: 50%;
  translate: -50% 0;
  z-index: 30;
  width: max-content;
  max-width: 18rem;
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-2);
  background: var(--ink);
  color: var(--bg);
  font-size: var(--step--1);
  font-weight: 500;
  line-height: 1.4;
  box-shadow: var(--shadow-2);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--fast) var(--ease), visibility var(--fast);
}

.hint:hover::after, .hint:focus-visible::after { opacity: 1; visibility: visible; }

/* ---------------------------------------------------------------- dialog */

dialog {
  width: min(40rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-3);
}

dialog::backdrop {
  background: hsl(var(--hue) 30% 10% / 0.5);
  backdrop-filter: blur(2px);
}

dialog .card__head { justify-content: space-between; padding: var(--s-4); }

/* ------------------------------------------------------------ almanac art */

/* The sun and moon panels: a strip that shows where in the day we are,
 * drawn from two numbers rather than as a picture. */
.daylight {
  position: relative;
  height: 0.5rem;
  border-radius: var(--r-full);
  background: var(--bg-sunk);
  overflow: hidden;
}

/* ------------------------------------------------------------------ misc */

.muted { color: var(--ink-3); }
.stack > * + * { margin-top: var(--s-4); }
.row { display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; }
.spacer { flex: 1; }

.footer {
  margin-top: var(--s-8);
  padding: var(--s-5) 0;
  border-top: 1px solid var(--line);
  color: var(--ink-3);
  font-size: var(--step--1);
}

.footer .row { gap: var(--s-4); }

/* --------------------------------------------------------------- narrow */

@media (max-width: 64rem) {
  :root { --nav-w: 0px; }

  .layout { grid-template-columns: minmax(0, 1fr); }

  .header__menu { display: grid; }

  /* Off-canvas rather than hidden: the links are the same ones, they just
   * arrive from the side. `inert` while closed keeps them out of the tab
   * order without a script deciding that separately. */
  .sidenav {
    position: fixed;
    inset-block: var(--header-h) 0;
    inset-inline-start: 0;
    z-index: 45;
    width: min(20rem, 82vw);
    padding-block: var(--s-4);
    background: var(--surface);
    border-inline-end: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    translate: -100% 0;
    transition: translate var(--slow) var(--ease);
  }

  .sidenav[data-open] { translate: 0 0; }

  .scrim {
    position: fixed;
    inset: var(--header-h) 0 0;
    z-index: 44;
    background: hsl(var(--hue) 30% 10% / 0.4);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--slow) var(--ease);
  }

  .scrim[data-open] { opacity: 1; pointer-events: auto; }

  .main { padding: var(--s-5) var(--s-4) var(--s-7); }
  .header__station { display: none; }
}

@media (max-width: 40rem) {
  .main { padding-inline: var(--s-3); }
  .card__body, .card__head { padding: var(--s-3); }
}

/* ----------------------------------------------------------------- print */

@media print {
  .header, .sidenav, .scrim, .tabs { display: none; }
  .layout { grid-template-columns: 1fr; }
  .card, .tile { break-inside: avoid; box-shadow: none; }
  body { background: white; }
}

/* ------------------------------------------------- the skin's own names */

/* Below here the selectors are the class names the templates already
 * carried -- `stat-tile`, `value-min`, `diagram-tile`. They were being
 * styled by 658 KB of Carbon; they say what they are, so they are worth
 * keeping and worth addressing directly.
 *
 * The structure of a tile, which the selectors below depend on:
 *
 *   .stat-tile
 *     div > div > p.label            the reading's name
 *                 p.value            icon + span.stat-title-obs-value
 *     div > .value-min > p.stat-label
 *                        p.stat-value > span.value
 *           .value-max > ...
 *
 * `.value` appears at both levels. The figure is addressed as a direct
 * child of the tile's first block rather than as `.stat-tile .value` --
 * that made every minimum and maximum as large as the reading itself and
 * pushed the tiles off the side of the page.
 */

.stat-tile {
  gap: var(--s-3);
  padding: var(--s-4);
  min-width: 0;
}

.stat-tile > div { min-width: 0; }

.stat-tile .label {
  margin: 0;
  color: var(--ink-2);
  font-size: var(--step--1);
  font-weight: 560;
  letter-spacing: 0.01em;
}

/* The figure, sized against the tile rather than against the page. A
 * container query, because the tile is what it has to fit inside and the
 * page can be any width at all. */
[class*="stat-tile-wrap-"] { container-type: inline-size; }

.stat-tile > div > div > p.value {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-1) 0 0;
  font-size: clamp(1.15rem, 9.5cqi, 2.4rem);
  font-weight: 660;
  letter-spacing: -0.03em;
  line-height: 1.1;
  min-width: 0;
}

@supports not (width: 1cqi) {
  .stat-tile > div > div > p.value { font-size: var(--step-2); }
}

.stat-tile > div > div > p.value > svg,
.stat-tile > div > div > p.value > img {
  width: 1.4rem;
  height: 1.4rem;
  fill: var(--ink-3);
  flex: none;
}

.stat-title-obs-value {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--s-2);
  min-width: 0;
  /* The figure and its unit are one thing. Without this, `1014.5 mbar`
     put `mbar` on a line of its own the moment the tile got narrow. The
     words that follow -- "Total", "Avg" -- may still wrap, which is what
     `flex-wrap` above is for. */
  white-space: nowrap;
}

.stat-title-obs-value .raw { white-space: nowrap; }

/* "Total", "Sum", the compass point: whatever the template puts beside the
 * figure. Not the figure, so not the figure's size -- at 2.4rem it made
 * `0.30 mm Gesamt` wider than any tile could be and the grid overflowed. */
.stat-title-obs-value > span:not(.raw),
.stat-title-obs-value > .stat-detail {
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--ink-3);
  letter-spacing: 0;
  white-space: nowrap;
}

/* Everything under the figure: minimum, maximum, total, and when. Side by
 * side, so a tile with three of them is not three times as tall as its
 * neighbour. */
.stat-tile > div:has(.value-min),
.stat-tile > div:has(.value-max),
.stat-tile > div:has(.value-total-week),
.stat-tile > div:has(.value-rain-rate-max),
.stat-tile > div:has(.value-rain-rate-current) {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2) var(--s-5);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line-soft);
}

/* The forecast shares these, so the same kind of thing is the same size and
   the same grey wherever it appears. Widened rather than copied: two rules
   that mean "a small grey label" drift apart the first time one is touched. */
.stat-tile .stat-label,
.forecast-tile .stat-label {
  margin: 0;
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--ink-3);
  white-space: nowrap;
}

.stat-tile .stat-value,
.forecast-tile .stat-value {
  margin: 0;
  font-size: var(--step-0);
  font-weight: 620;
  color: var(--ink);
  white-space: nowrap;
}

.stat-tile .stat-value .value,
.forecast-tile .stat-value .value { font-size: inherit; font-weight: inherit; }

@supports not selector(:has(*)) {
  .value-min, .value-max, .value-total-week {
    display: inline-block;
    margin-inline-end: var(--s-5);
  }
}

.stat-wind-dir {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--ink-3);
}

.stat-wind-dir svg { width: 0.9rem; height: 0.9rem; fill: var(--wind); }
.value-detail { font-size: var(--step--1); color: var(--ink-2); font-weight: 560; }
.stat-detail { font-size: var(--step--1); color: var(--ink-3); }

.stat-tile-link { text-decoration: none; color: inherit; display: block; height: 100%; }
.stat-tile .icon-link {
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  width: 0.85rem;
  height: 0.85rem;
  fill: var(--ink-3);
}

[class*="stat-tile-wrap-"] { position: relative; display: flex; min-width: 0; }
[class*="stat-tile-wrap-"] > * { flex: 1; min-width: 0; }

/* A tile that names its own reading gets that reading's colour on the
 * icon. One rule per family rather than a class per observation. */
.stat-tile-wrap-outTemp p.value > svg,
.stat-tile-wrap-appTemp p.value > svg,
.stat-tile-wrap-heatindex p.value > svg { fill: var(--warm); }
.stat-tile-wrap-dewpoint p.value > svg,
.stat-tile-wrap-windchill p.value > svg,
.stat-tile-wrap-outHumidity p.value > svg { fill: var(--cool); }
.stat-tile-wrap-rain p.value > svg,
.stat-tile-wrap-rainRate p.value > svg,
.stat-tile-wrap-cloudbase p.value > svg { fill: var(--wet); }
.stat-tile-wrap-windSpeed p.value > svg,
.stat-tile-wrap-windGust p.value > svg,
.stat-tile-wrap-windrun p.value > svg { fill: var(--wind); }
.stat-tile-wrap-UV p.value > svg,
.stat-tile-wrap-radiation p.value > svg { fill: var(--sun); }

/* --------------------------------------------------------- diagram tile */

.diagram-tile {
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-4);
  min-width: 0;
}

.diagram-tile > div { min-width: 0; }

.diagram-tile .label {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  font-size: var(--step-0);
  font-weight: 600;
  color: var(--ink-2);
}

.diagram-tile .label svg,
.diagram-tile .label img {
  width: 1.1rem;
  height: 1.1rem;
  fill: var(--ink-3);
  flex: none;
}

.diagram-tile .value { display: block; margin: 0; font-size: inherit; }

/* The chart element. Sized here and never inline, because ECharts measures
 * the box it is handed. */
.diagram, .chart { width: 100%; height: clamp(15rem, 26vh, 20rem); }
.diagram.windrose { height: clamp(20rem, 34vh, 26rem); }
/* A calendar and a month grid are each as tall as their own contents --
   the number of years, the orientation -- so the height is set by the
   drawing code and this is only what stands before it has run. */
.diagram.calendar { height: 15rem; }

/* -------------------------------------------------------------- gauges */

.gauge-tile { padding: var(--s-3); }
.gauge-tile .diagram { height: 10rem; }
.gauge-tile .label { text-align: center; justify-content: center; }

/* -------------------------------------------------------- almanac tiles */

.almanac-wrap { display: flex; min-width: 0; }
.almanac-wrap > * { flex: 1; min-width: 0; }

.almanac-tile { display: grid; gap: var(--s-3); padding: var(--s-4); }

/* The tile's heading: the body it is about, with its symbol. */
.almanac-tile > div > div > .label {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  font-size: var(--step-0);
  font-weight: 620;
  color: var(--ink);
}

.almanac-tile > div > div > .label svg {
  width: 1.15rem;
  height: 1.15rem;
  fill: var(--ink-3);
}

/* Rise, transit, set: three of the same thing, so a row of three rather
   than a column of six alternating labels and times. */
.almanac-tile > div > div > div {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-5);
  margin-top: var(--s-3);
}

.almanac-tile .value {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  font-size: var(--step-1);
  font-weight: 620;
  white-space: nowrap;
}

.almanac-tile .value > svg {
  width: 1.1rem;
  height: 1.1rem;
  fill: var(--ink-3);
  flex: none;
}

.almanac-tile .value-col { display: grid; gap: 2px; }

.almanac-tile .value-col .label {
  margin: 0;
  font-size: var(--step--1);
  font-weight: 500;
  color: var(--ink-3);
}

.almanac-tile .value-col .value {
  font-size: var(--step-1);
  font-weight: 640;
  color: var(--ink);
}

.almanac-tile .pyephem { color: var(--ink-3); font-size: var(--step--1); }

/* --------------------------------------------------------------- lists */

/* A grid that wraps, never a row that scrolls.
 *
 * This was a flex row with `overflow-x: auto`, and it was wrong in both
 * directions at once: on a wide screen seven fixed-width days left a third of
 * the card empty, and on a phone the week and the day became two scrollbars
 * stacked on top of each other with arrow buttons -- the one interaction
 * nobody performs on a weather page. Sideways scrolling hides content behind
 * a gesture; wrapping puts it on the screen.
 *
 * `auto-fit` with a `1fr` maximum does both jobs: the tracks stretch to fill
 * whatever width there is, and how many fit is the browser's arithmetic
 * rather than a breakpoint somebody has to keep in step with the font. */
.forecast-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.forecast-list > * { min-width: 0; text-align: center; }

/* An hour needs a time, a picture and a number and nothing else, so it fits
   in half the width of a day -- and twenty-four of them are four rows on a
   phone rather than a scrollbar. */
/* Eight three-hour tiles for a day, not twenty-four one-hour ones. At
   5.5rem that is four and four on a phone and a single row on a desktop --
   and the columns land near the day above them, which is what makes the two
   rows read as one thing. */
.forecast-list.hourly {
  /* Capped, not stretched. `1fr` shares whatever is left between however
     many tiles there are, so late in the evening the two hours still to come
     each took half the row and stood twice the height of a full day's. A
     maximum keeps a tile a tile, and what is left over goes to the right of
     them rather than into them. */
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 9rem));
  justify-content: start;
  margin-top: var(--s-4);
  padding-top: var(--s-4);
  border-top: 1px solid var(--line-soft);
}

.forecast-hour--none { grid-column: 1 / -1; text-align: start; }

.forecast-list p { margin: 0; }

.forecast-day, .forecast-hour {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-1);
  padding: var(--s-2) var(--s-1);
  border-radius: var(--r-2);
}

.forecast-hour { gap: 0; padding-inline: 0; }

/* A day is the control that chooses which hours are shown below it, so it is
   a button. Stripped back to the tile it already was: a button that looks
   like a button here would be seven buttons in a row where the page has
   none. */
button.forecast-day {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: center;
  transition: background-color .12s ease;
}

button.forecast-day:hover { background: var(--surface-hover); }
button.forecast-day:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The chosen day, and the hour we are in. The same sunk surface the rest of
   the skin uses for "this one", not a colour invented here. */
.forecast-day[aria-selected="true"],
.forecast-list.daily:not([role]) .forecast-day:first-child,
.forecast-hour:first-child {
  background: var(--bg-sunk);
}

/* Which day is being shown, said in ink as well as in a background: a
   difference in shade alone is not a difference for everyone. */
.forecast-day[aria-selected="true"] .forecast-day__name {
  color: var(--accent);
}

.forecast-day__name, .forecast-hour__time { white-space: nowrap; }
.forecast-day__name { font-weight: 620; color: var(--ink-2); }

.forecast-day__text {
  font-size: var(--step--2);
  line-height: 1.25;
  color: var(--ink-3);
  /* Two lines at most. "Thunderstorm with slight hail" is a real answer and
     it must not make one card twice the height of its neighbours. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-clamp: 2;
  overflow: hidden;
}

.forecast-day__temps {
  display: flex;
  gap: var(--s-2);
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
}

.forecast-day__max { color: var(--warm); }
.forecast-day__min { color: var(--cool); font-weight: 500; }
.forecast-hour__temp { font-size: var(--step--1); font-weight: 620; }

.forecast-day__wet, .forecast-day__wind, .forecast-hour__wet {
  display: inline-flex;
  align-items: center;
  gap: .25em;
  white-space: nowrap;
}

.forecast-hour__wet { color: var(--wet); }

/* The one thing that made this section unreadable in the dark: an SVG with
   no `fill` of its own is black, and these came from a set that leaves it
   out. Every other icon in this skin is given a colour explicitly -- see
   `.card__head svg` and the stat tiles -- and the reading's own colour is
   the same one its tile uses, so the same quantity is the same hue wherever
   it appears. */
.forecast-icon svg { width: 2rem; height: 2rem; fill: var(--ink-2); }
.forecast-hour .forecast-icon svg { width: 1.5rem; height: 1.5rem; }
.forecast-day__wet svg { width: 1em; height: 1em; fill: var(--wet); }
.forecast-day__wind svg { width: 1em; height: 1em; fill: var(--wind); }

/* A wet sky is drawn wet and a clear one is drawn clear, so the row reads
   before any of the words do. */
.forecast-icon[data-symbol^="clear"] svg,
.forecast-icon[data-symbol^="mostly-clear"] svg { fill: var(--sun); }
.forecast-icon[data-symbol$="-night"] svg { fill: var(--ink-3); }
.forecast-icon[data-symbol^="drizzle"] svg,
.forecast-icon[data-symbol^="rain"] svg,
.forecast-icon[data-symbol^="heavy-rain"] svg,
.forecast-icon[data-symbol^="showers"] svg,
.forecast-icon[data-symbol^="heavy-showers"] svg { fill: var(--wet); }
.forecast-icon[data-symbol^="thunderstorm"] svg { fill: var(--warn); }

.forecast-issued { margin: 0; }

/* ------------------------------------------------------------- warnings */

/* Above the numbers, not beside them: a warning is the one thing on the page
   somebody needs to read before anything else. A station with none shows
   nothing at all rather than an empty box saying so. */
.forecast-warnings { display: grid; gap: var(--s-3); margin-block: var(--s-5); }

.forecast-warning__when {
  margin-inline-start: var(--s-2);
  font-weight: 400;
  font-size: var(--step--1);
  color: var(--ink-3);
  white-space: nowrap;
}

.forecast-warning__area { color: var(--ink-3); }
.forecast-warning__more summary { cursor: pointer; color: var(--accent); }
.forecast-warning__more p { margin-top: var(--s-2); }
.forecast-warning__instruction { font-weight: 620; color: var(--ink-2); }

.custom-content { margin-block: var(--s-5); }
.section-header { margin: var(--s-7) 0 var(--s-4); }
.section-header h2 { font-size: var(--step-2); }
.section-header:first-child { margin-top: 0; }

.noaa-report-link { display: inline-flex; gap: var(--s-2); align-items: center; }

.statistics-day-select {
  display: flex;
  gap: var(--s-3);
  flex-wrap: wrap;
  align-items: end;
  margin-bottom: var(--s-5);
}

.statistics-day-select input,
.statistics-day-select select {
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--surface);
}

.small { font-size: var(--step--1); color: var(--ink-3); }
.cell-value { font-variant-numeric: tabular-nums; }
.cell-label { font-weight: 560; }

/* Nothing on the page may push it sideways. A chart that has not been laid
 * out yet, a table wider than its column: they scroll inside their own box,
 * and the page does not. */
.main, .wrap, .grid, .card, .tile { min-width: 0; }
body { overflow-x: clip; }

/* A table wider than the screen scrolls inside its own box. The page
 * cannot scroll sideways, so without this a fourteen-column year table is
 * simply cut off at the edge of a phone. */
.table-scroll {
  position: relative;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  margin-bottom: var(--s-4);
  border-radius: var(--r-3);
}

/* A shadow at the edge while there is more to the right, and none once
 * there is not: a table that scrolls with no sign of it reads as a table
 * with three columns. */
.table-scroll::after {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  width: 2rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
  background: linear-gradient(to left,
              hsl(from var(--surface) h s l / 0.95), transparent);
}

.table-scroll[data-more]::after { opacity: 1; }

/* The first column stays while the rest moves, so a row keeps its name.
 * Only where there is room for it to be worth doing. */
@media (min-width: 26rem) {
  .table-scroll td:first-child,
  .table-scroll th:first-child {
    position: sticky;
    inset-inline-start: 0;
    z-index: 2;
    background: var(--surface);
  }

  .table-scroll thead th:first-child { background: var(--surface-2); }

  .table-scroll[data-scrolled] td:first-child,
  .table-scroll[data-scrolled] th:first-child {
    box-shadow: 1px 0 0 var(--line);
  }
}

/* A bare table on a page needs the same frame a card has, or it floats
 * between the tabs above it and the tiles below with nothing holding it. */
.main table:not(.card table) {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  margin-bottom: var(--s-4);
}

.table-scroll > table {
  margin-bottom: 0;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

.table-scroll:has(> table) {
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: var(--shadow-1);
}

/* The panels the tabs switch between. */
[role="tabpanel"] { margin-top: var(--s-2); }
[role="tabpanel"] > .grid { margin-top: var(--s-4); }

/* --------------------------------------------------------- fact lists */

/* "Here is a thing, here is its value", a few times over: the rain
 * statistics, the almanac details. A table needs a header row to mean
 * anything and offers to sort itself, neither of which applies. */

.factlist { padding: var(--s-4); }

.factlist__title {
  margin: 0 0 var(--s-3);
  font-size: var(--step-0);
  font-weight: 620;
  color: var(--ink-2);
}

.factlist dl {
  display: grid;
  grid-template-columns: minmax(12rem, auto) 1fr;
  gap: var(--s-2) var(--s-5);
  margin: 0;
}

.factlist dt {
  color: var(--ink-2);
  font-size: var(--step--1);
}

.factlist dd {
  margin: 0;
  font-weight: 560;
  font-variant-numeric: tabular-nums;
}

/* One column when there is no room for two: the label above its value
 * rather than a value squeezed into three characters. */
@media (max-width: 42rem) {
  .factlist dl { grid-template-columns: 1fr; gap: var(--s-1); }
  .factlist dd { margin-bottom: var(--s-3); }
}

/* The row under a stat table is a different kind of thing from the years
 * above it: not another year, but what they come to. */
tr.summary td {
  border-top: 2px solid var(--line);
  background: var(--surface-2);
  font-weight: 600;
}

/* -- live ---------------------------------------------------------------- */

/* Whether a reading is actually live, and how old it is.
 *
 * The skin can take live updates over MQTT: `live-updates.js` rewrites the
 * values in place as packets arrive. What it does not do is say so, and a
 * page with a broker that has gone away looks exactly like one with a broker
 * that is working. A dashboard that is quietly wrong is worse than one that
 * is visibly stale.
 *
 * The colours are the skin's own `--good` and `--bad`, so both themes carry
 * and a temperature stays the same red everywhere. */

.live-badge {
  display: inline-flex;
  align-items: center;
  gap: .25em;
  margin-inline-start: .5em;
  padding: .05em .4em;
  border-radius: 1em;
  font-size: .6rem;
  font-weight: 700;
  letter-spacing: .06em;
  line-height: 1.5;
  text-transform: uppercase;
  vertical-align: middle;
  white-space: nowrap;
  /* Never the thing the eye goes to first. The reading is what somebody
   * came for; this only has to be findable once they wonder. */
  opacity: .85;
}

.live-badge::before,
.live-indicator__dot {
  content: "";
  width: .45em;
  height: .45em;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.live-badge--live,
.live-indicator--live {
  color: var(--good);
  background: color-mix(in srgb, var(--good) 14%, transparent);
}

.live-badge--stale, .live-badge--off,
.live-indicator--stale, .live-indicator--off {
  color: var(--bad);
  background: color-mix(in srgb, var(--bad) 14%, transparent);
}

/* Connected, nothing yet, and it is early. Neither claim is true, so the
 * badge makes neither -- grey rather than a red that turns green ten seconds
 * later and teaches people to ignore it. */
.live-badge--waiting,
.live-indicator--waiting {
  color: var(--ink-3);
  background: color-mix(in srgb, var(--ink-3) 12%, transparent);
  letter-spacing: 0;
}

/* The dot breathes only while it is actually live. A page full of animation
 * is a page people stop looking at. */
.live-badge--live::before,
.live-indicator--live .live-indicator__dot {
  animation: live-pulse 2.4s ease-in-out infinite;
}

@keyframes live-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .35; }
}

.live-indicator {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4em;
  margin-inline-end: var(--s-2);
  padding: .2em .6em;
  border-radius: 1em;
  font-size: .75rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.6;
  white-space: nowrap;
  /* Tabular numerals and a floor on the width, so "3 s ago" becoming
   * "12 s ago" does not shift the header sideways once a second. */
  min-width: 6.5em;
}

/* On a phone the header is already tight. The dot alone still answers the
 * question the indicator exists for -- is this live -- and the exact age is
 * in the title. */
@media (max-width: 42rem) {
  .live-indicator { min-width: 0; padding: .2em .4em; }
  .live-indicator__label { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .live-badge--live::before,
  .live-indicator--live .live-indicator__dot { animation: none; }
}

/* ============================================================== places ==
 *
 * Everything below is for a site that shows several measurement series. At
 * one place none of this markup is produced, so none of it applies.
 *
 * **A place's own colour is never a token.** It arrives as an inline
 * `style="--place: #d1642a"` on the row, the chip, the link. Two reasons,
 * both load-bearing: `token()` in charts.js reads computed style off
 * `document.documentElement` only, so a container-scoped place colour would
 * be invisible to `palette()` and the chart would draw one colour while the
 * row beside it showed another; and the colours live in `archives.toml`, so
 * a stylesheet holding a copy is a second place to keep in step.
 */

/* --------------------------------------------------------- place mark */

/* One shape in three places: the board row, the switcher and the chips. A
 * place looks the same everywhere it appears, which is what makes its colour
 * on the comparison chart already familiar by the time somebody gets there. */
.place-dot {
  display: inline-block;
  width: .65em;
  height: .65em;
  border-radius: var(--r-full);
  background: var(--place, var(--place-fallback));
  flex: none;
  /* A ring, not a second colour for dark mode. A dark place colour on a dark
   * card is a hole without it, and computing a lightened twin in Python is
   * arithmetic nobody can check by looking. The charts already run every line
   * through `visible()`, which does the same job at that end. */
  box-shadow: 0 0 0 1px var(--line);
}

/* ------------------------------------------------------------- board */

.now-board { width: 100%; border-collapse: collapse; }
.now-board tbody tr {
  border-inline-start: 3px solid var(--place, var(--place-fallback));
}

.board-place { white-space: nowrap; }
.board-place a { font-weight: 620; color: var(--ink); text-decoration: none; }
.board-place a:hover { text-decoration: underline; }
.board-code {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--ink-3);
  margin-inline: var(--s-2) var(--s-1);
}
.board-where {
  display: block;
  color: var(--ink-3);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
}

.board-cell { text-align: end; }

/* Tabular figures on every number here. A column whose values jitter by a
 * digit's width as they update is a column the eye cannot run down, which is
 * the only thing this table is for. */
.board-now {
  display: block;
  font-size: var(--step-1);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.board-range {
  display: block;
  color: var(--ink-3);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.board-range .hi { color: var(--warm); }
.board-range .lo { color: var(--cool); }

.board-age {
  text-align: end;
  color: var(--ink-3);
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.board-age[data-stale] { color: var(--warn); }
/* A console that has records and has stopped. Not one that has never
 * written a record: that row carries no flag, keeps the ordinary muted
 * colour and says "no readings yet" in words. Red for an unconfigured place
 * is a check that goes off because somebody else's hardware was never
 * plugged in, and `live-status.js` sets and clears this same attribute from
 * the live document by the same rule the renderer used. */
.board-age[data-silent] { color: var(--bad); font-weight: 560; }

/* A board in a sideways scroll box hides its values behind a gesture nobody
 * performs on a weather page. Below 46rem each row becomes a card and each
 * cell a labelled line instead -- in CSS alone, no script and no breakpoint
 * anybody has to keep in step with a font size.
 *
 * `display: block` drops the implicit table roles in some engines, which is
 * why `role=` is written into the markup unconditionally: redundant at full
 * width, load-bearing here. */
@media (max-width: 46rem) {
  .now-board thead { display: none; }
  .now-board tbody tr {
    display: block;
    margin-bottom: var(--s-4);
    border: 1px solid var(--line);
    border-inline-start: 3px solid var(--place, var(--place-fallback));
    border-radius: var(--r-3);
    background: var(--surface);
  }
  .now-board tbody td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: var(--s-3);
    border-bottom: 1px solid var(--line-soft);
  }
  .now-board tbody td::before {
    content: attr(data-label);
    color: var(--ink-3);
    font-size: var(--step--1);
    text-align: start;
  }
  .now-board tbody th.board-place { display: block; }
}

/* ----------------------------------------------------------- unusual */

/* Deliberately not `.notice`. A framed, tinted, icon-bearing alert per line
 * turns four lines into a wall and makes the fourth look as urgent as the
 * first. Flat lines with the figure in colour is a newspaper's answer and it
 * stays readable at four. */
.unusual { list-style: none; margin: 0 0 var(--s-5); padding: 0; }
.unusual li {
  display: flex;
  gap: var(--s-3);
  align-items: baseline;
  padding: var(--s-2) 0;
  border-bottom: 1px solid var(--line-soft);
}
.unusual li:last-child { border-bottom: none; }
.unusual__figure {
  font-variant-numeric: tabular-nums;
  font-weight: 620;
  color: var(--warn);
  white-space: nowrap;
}
.unusual--silent .unusual__figure { color: var(--bad); }
.unusual__place {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-weight: 560;
  color: var(--ink);
}

/* ---------------------------------------------------------- switcher */

/* Built from the tabs' look, but as links, so nothing about it needs a
 * script -- the same argument the unit switch already carries. `flex-wrap`,
 * not `overflow-x: auto`: eight places must wrap, not hide the eighth. */
.place-switch {
  display: flex;
  gap: var(--s-1);
  margin-top: var(--s-3);
  padding: var(--s-1);
  background: var(--bg-sunk);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-full);
  width: fit-content;
  max-width: 100%;
  flex-wrap: wrap;
}
.place-switch a {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-full);
  color: var(--ink-2);
  font-weight: 560;
  font-size: var(--step--1);
  text-decoration: none;
  white-space: nowrap;
}
.place-switch a:hover { color: var(--ink); background: var(--surface-hover); }
.place-switch a[aria-current="page"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-1);
}
.place-switch__all { color: var(--ink-3); }

/* ------------------------------------------------------------- chips */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-bottom: var(--s-5);
}
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--step--1);
  font-weight: 560;
  white-space: nowrap;
  cursor: pointer;
}

/* Off: the dot hollows out and the label strikes through. Opacity alone is a
 * state that low-contrast screens and colour-blind readers lose, and the
 * question being asked is "which two are on". */
.chip[aria-pressed="false"] { color: var(--ink-3); }
.chip[aria-pressed="false"] .place-dot {
  background: none;
  box-shadow: inset 0 0 0 2px var(--place, var(--place-fallback));
}
.chip[aria-pressed="false"] span:last-child { text-decoration: line-through; }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* -------------------------------------------------- comparison figures */

.compare-figures { margin-bottom: var(--s-5); }
.compare-figures thead th .place-dot { margin-inline-end: var(--s-1); }
/* A column of figures and the heading over it read as one column only if
 * they line up. Left as headings start and figures end, "KIR" sat at the
 * left of a 200px column and its readings at the right of it, and which
 * place a number belonged to was a guess the reader had to make four times
 * a page. Every column but the first holds figures. */
.compare-figures thead th + th { text-align: end; }
.compare-value, .compare-spread {
  text-align: end;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* The spread is not another place's reading, so it does not sit in the run
 * of them. A rule rather than a colour: at four places the eye counts
 * columns to find the last one, and a line is what stops the count. */
.compare-figures thead th:last-child,
.compare-figures td.compare-spread {
  border-inline-start: 1px solid var(--line);
}
/* Which aggregate a row is, said rather than assumed: a mean and a total are
 * different questions and a column of numbers does not say which it
 * answered. */
.compare-how { color: var(--ink-3); margin-inline-start: var(--s-2); }
.compare-spread__figure { font-weight: 620; }
.compare-spread__ends {
  display: block;
  color: var(--ink-3);
  font-size: var(--step--1);
  font-family: var(--font-mono);
}

/* --------------------------------------------------------- chart note */

/* What a comparison chart holds, written by `chartdata` into the file: the
 * aggregation, the unit, the places drawn and anything left out. A chart
 * drawn from records beside a tile drawn from `archive_day_*` looks like a
 * bug unless the chart says which it is. */
.chart-note { margin-top: var(--s-2); font-size: var(--step--1);
              color: var(--ink-3); }

/* ------------------------------------------------------ live, wrong */

/* A console sending Fahrenheit into a page written in Celsius. Its own state
 * because "wrong unit" and "nothing arriving" are different things to do
 * about, and one text in one colour for both is the warning that stands
 * everywhere and is therefore not one. */
.live-badge--wrong { background: var(--warn); color: var(--bg); }

/* ---------------------------------------------------- places in the nav */

/* `.sidenav svg { fill: currentColor }` already covers every icon in the
 * panel. This one overrides it so a place's mark carries the place's own
 * colour -- and it has to exist, because an SVG with no `fill` of its own is
 * black, which on a dark page is nothing at all. */
.sidenav__places > summary svg,
.sidenav__places a[style*="--place"] svg {
  fill: var(--place, var(--place-fallback));
}

@media print {
  .place-switch, .chips { display: none; }
  .now-board tbody tr { break-inside: avoid; }
}
