/* ============================================================
   nokken components — content-level rendering primitives shared
   across routes. Source: Round-3-Screens.html. Rules are lifted
   verbatim where possible; the only additions beyond the
   prototype are the ::before overlays that make cards and rows
   whole-clickable without nested <a> + <button>.

   Ordered roughly by visual weight: pills → cards → rows →
   chips/filter-bar → section heading.
   ============================================================ */

/* ---- Status pills -------------------------------------------- */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 11px 5px 9px;
  border-radius: var(--radius-pill);
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: -0.005em;
  white-space: nowrap;
  line-height: 1;
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.pill-lg { padding: 7px 13px 7px 11px; font-size: 13px; }
.pill-lg .dot { width: 8px; height: 8px; }

.status-too-low   { background: var(--status-too-low-bg);   color: var(--status-too-low-fg); }
.status-too-low   .dot { background: var(--status-too-low-dot); }
.status-low       { background: var(--status-low-bg);       color: var(--status-low-fg); }
.status-low       .dot { background: var(--status-low-dot); }
.status-good      { background: var(--status-good-bg);      color: var(--status-good-fg); }
.status-good      .dot { background: var(--status-good-dot); }
.status-high      { background: var(--status-high-bg);      color: var(--status-high-fg); }
.status-high      .dot { background: var(--status-high-dot); }
.status-very-high { background: var(--status-very-high-bg); color: var(--status-very-high-fg); }
.status-very-high .dot { background: var(--status-very-high-dot); }
.status-stale     { background: var(--warning-bg); color: var(--warning-fg); }
.status-stale     .dot { background: var(--warning-border); }
/* Neutral treatment for sections we can't classify — no thresholds
   yet, or no observation yet. Muted colours read as "we don't know"
   rather than imply a flow bucket. */
.status-unknown   { background: var(--surface-sunken); color: var(--text-muted); }
.status-unknown   .dot { background: var(--border-strong); }
/* No-gauge — a distinct absence from "unknown" (which implies a
   gauge we can't read). Same muted surface but a dashed dot
   communicates "intentionally not measured". */
.status-no-gauge  { background: var(--surface-sunken); color: var(--text-muted); }
.status-no-gauge  .dot { background: transparent; border: 1.5px dashed var(--border-strong); box-sizing: border-box; }

/* ---- Section card -------------------------------------------- */
.sec-card {
  position: relative;
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-2);
  padding: 18px 20px 16px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px 10px;
  border: 1px solid var(--border-subtle);
  transition: transform 0.12s ease, box-shadow 0.15s ease;
}
.sec-card:hover { transform: translateY(-1px); box-shadow: var(--elev-3); }
.sec-card .river {
  grid-column: 1 / 2;
  font-size: 11.5px;
  font-family: var(--font-mono);
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 500;
}
.sec-card .name {
  grid-column: 1 / 2;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--text-primary);
  line-height: 1.2;
  text-decoration: none;
}
/* Pull the title slightly up when the eyebrow sits above it — visual
   tightness between the two lines. Collapsed cards (no .river) leave
   .name at the card's padding-top, matching the distance from card
   edge to first-visible-text in the eyebrow case. */
.sec-card .river + .name { margin-top: -4px; }
.sec-card .name:hover { text-decoration: none; }
/* Overlay turns the whole card into the section link while the
   star button remains independently interactive via z-index. */
.sec-card .name::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
}
.sec-card .star {
  position: relative;
  z-index: 1;
  grid-column: 2 / 3;
  grid-row: 1 / 3;
  align-self: start;
  width: 32px;
  height: 32px;
  border: none;
  background: var(--surface-sunken);
  border-radius: var(--radius-pill);
  cursor: pointer;
  display: grid;
  place-items: center;
  color: var(--text-muted);
}
.sec-card .star.active { color: var(--status-low-dot); background: var(--status-low-bg); }
.sec-card .star svg { width: 16px; height: 16px; }
.sec-card .flow-row {
  grid-column: 1 / 3;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 14px;
  padding-top: 6px;
  border-top: 1px solid var(--border-subtle);
  margin-top: 2px;
}
.sec-card .flow-value {
  font-family: var(--font-mono);
  font-size: 30px;
  font-weight: 500;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text-primary);
  font-feature-settings: "tnum";
}
.sec-card .flow-value .unit {
  font-size: 13px;
  color: var(--text-muted);
  margin-left: 3px;
  font-weight: 400;
}
.sec-card .foot {
  grid-column: 1 / 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 11.5px;
  color: var(--text-muted);
  font-family: var(--font-mono);
}
.sec-card .fresh { display: inline-flex; align-items: center; gap: 6px; }
.sec-card .fresh::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--status-good-dot);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--status-good-dot) 25%, transparent);
}
.sec-card.is-stale .fresh { color: var(--warning-fg); }
.sec-card.is-stale .fresh::before {
  background: var(--warning-border);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--warning-border) 30%, transparent);
}
.sec-card.is-vhigh .fresh::before {
  background: var(--status-very-high-dot);
  box-shadow: 0 0 0 3px color-mix(in oklch, var(--status-very-high-dot) 25%, transparent);
}
/* Tertiary metadata chips — grade AND distance-badge share the
   same treatment so the card's foot-meta row reads as one
   vocabulary (small mono soft-chip). The badge sits next to the
   grade in `.foot-meta`; keeping their styling on a combined
   selector prevents typographic drift when either rule changes. */
.sec-card .grade,
.sec-card .distance-badge {
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  padding: 2px 7px;
  border-radius: var(--radius-xs);
  font-size: 11px;
  font-feature-settings: "tnum";
}

.sec-card .distance-badge[hidden] { display: none; }

/* foot-meta groups badge + grade so they sit together with a
   shared gap while the fresh span stays pinned to the left. */
.sec-card .foot-meta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* ---- Favorites grid wrapper (consumed by Home A) ------------- */
.fav-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
@media (max-width: 767.98px) {
  .fav-grid { grid-template-columns: 1fr; gap: 10px; }
}

/* ---- Compact row (WhatsUpList + Home A compact variant) ------ */
.nearby-list {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-subtle);
  overflow: hidden;
  box-shadow: var(--elev-1);
}
.row {
  position: relative;
  display: grid;
  grid-template-columns: 28px minmax(0,1.5fr) minmax(0,1fr) 130px 96px 120px 40px;
  align-items: center;
  gap: 14px;
  padding: 12px 20px;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border-subtle);
  font-size: 14px;
  transition: background 0.1s;
}
.row:hover { background: var(--brand-050); }
.row:last-child { border-bottom: none; }
.row .star-s {
  color: var(--text-disabled);
  display: grid;
  place-items: center;
  cursor: pointer;
  background: transparent;
  border: none;
  padding: 0;
  position: relative;
  z-index: 1;
}
.row .star-s.active { color: var(--status-low-dot); }
.row .star-s svg { width: 16px; height: 16px; }
.row .name-col {
  display: flex;
  min-width: 0;
}
/* One-line compound — river eyebrow + em-dash + section name.
   `.sec-s` stays the overlay-link so click target remains the
   whole row (see the ::before trick below). */
.row .name-col .sec-s {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  font-size: 14.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-decoration: none;
  min-width: 0;
}
.row .name-col .river-s {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-weight: 500;
  flex-shrink: 0;
}
.row .name-col .sec-sep {
  color: var(--text-muted);
  font-weight: 400;
  flex-shrink: 0;
}
.row .name-col .sec-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.row .name-col .sec-s:hover { text-decoration: none; }
/* Same overlay trick as .sec-card: the name link covers the row. */
.row .name-col .sec-s::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
}
.row .region {
  color: var(--text-muted);
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row .status-col { display: flex; }
.row .flow-s {
  font-family: var(--font-mono);
  font-weight: 500;
  font-feature-settings: "tnum";
  font-size: 15px;
  text-align: right;
}
.row .flow-s .u {
  color: var(--text-muted);
  font-size: 11px;
  margin-left: 3px;
  font-weight: 400;
}
.row .grade-s {
  font-family: var(--font-mono);
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 600;
  text-align: center;
}
.row .updated-s {
  font-family: var(--font-mono);
  color: var(--text-muted);
  font-size: 11.5px;
}
.row .chev {
  color: var(--text-disabled);
  display: grid;
  place-items: center;
}
.row.header {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  padding: 10px 20px;
  background: var(--surface-base);
  cursor: default;
}
.row.header:hover { background: var(--surface-base); }

/* Optional distance column — activated by adding .with-distance to
   the list wrapper. Adds an 8th column between Updated and the
   chevron so rows and header stay aligned. When the wrapper is not
   .with-distance, the span is hidden (display:none) so the row's
   7-column default grid still matches its child count.

   Only body rows override typography (mono numeric). The header's
   own Distance span intentionally gets no overrides so it reads
   like "UPDATED" / "FLOW" — uppercase mono at the header's 10.5px,
   not the body row's 12px. See DESIGN_NOTES §"Status display order"
   for the table-header treatment the other columns share. */
.nearby-list.with-distance .row {
  grid-template-columns: 28px minmax(0,1.5fr) minmax(0,1fr) 120px 72px 96px 72px 28px;
}

/* `.no-star` — drops the leading star slot entirely. Rationale:
   the Home A favorites compact table is a list where every row is
   a favorite by definition, so a star toggle next to each row is
   redundant. The caller passes `hide_star=True` to
   `_section_row.render` and the corresponding header macro so the
   cell isn't emitted; this rule rewrites the track list to match
   the shrunken child count. Paired with the `.with-distance` rule
   for the Home+location scenario. */
.nearby-list.no-star .row {
  grid-template-columns: minmax(0,1.5fr) minmax(0,1fr) 130px 96px 120px 40px;
}
.nearby-list.no-star.with-distance .row {
  grid-template-columns: minmax(0,1.5fr) minmax(0,1fr) 120px 72px 96px 72px 28px;
}
.row .distance-s { display: none; text-align: right; }
.row:not(.header) .distance-s {
  font-family: var(--font-mono);
  color: var(--text-secondary);
  font-size: 12px;
  font-weight: 600;
  font-feature-settings: "tnum";
}
.nearby-list.with-distance .row .distance-s { display: inline; }

/* Sortable column header — inline with the existing `.row.header`
   typography. `::after` carries the state arrow so the markup
   stays clean (one selector per state, no JS-injected elements).
   See DESIGN_GAPS.md §"Sortable table-header" for the proper
   follow-up pattern; this is the inline version that ships with
   /WhatsUpList alongside the distance-sort pattern (#10 on the
   Nearby opt-in PR). */
.row.header .col-h-sortable {
  cursor: pointer;
  user-select: none;
}
.row.header .col-h-sortable:hover { color: var(--text-primary); }
.row.header .col-h-sortable:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
}
.row.header .col-h-sortable[data-sort-dir="asc"]::after {
  content: " ▲";
  font-size: 9px;
  color: var(--brand-500);
}
.row.header .col-h-sortable[data-sort-dir="desc"]::after {
  content: " ▼";
  font-size: 9px;
  color: var(--brand-500);
}

/* ---- Chips + filter bar -------------------------------------- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-secondary);
  cursor: pointer;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
}
.chip:hover { border-color: var(--border-strong); text-decoration: none; }
.chip.active {
  background: var(--brand-500);
  color: var(--text-on-brand);
  border-color: var(--brand-500);
}
.chip .dot { width: 7px; height: 7px; border-radius: 50%; }
.chip .count {
  font-family: var(--font-mono);
  font-size: 10.5px;
  opacity: 0.65;
  margin-left: 2px;
}

/* The bar itself is a horizontal row of groups that wraps when
   there isn't enough width. Each `.filt-group` is one flex item,
   keeping its heading + chip row bound together when the row
   reflows. At 1440px full-page, the Status group typically sits
   alone on row 1 and Grade + Gauge share row 2; at a 320px rail
   each group wraps to its own row; mobile reflows the same way
   as narrow-width desktop. No media queries — flex-wrap is load-
   bearing here. */
.filter-bar {
  display: flex;
  flex-flow: row wrap;
  gap: 14px 24px;
  padding: 12px 16px;
  align-items: flex-start;
}
.filt-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;  /* allow long chip rows to wrap inside the group */
}
.filt-head {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
}
.filt-row { display: flex; flex-wrap: wrap; gap: 6px; }
/* "Clear all filters" is a route-level footer of the filter
   panel, rendered only when at least one chip is active. Muted
   mono to read as a secondary affordance, not a chip itself. */
.filter-clear {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  text-decoration: none;
  display: inline-block;
}
.filter-clear:hover { color: var(--text-secondary); text-decoration: underline; }
/* The asymmetric bottom-padding belongs to the map-rail context: it
   visually separates the link from `.map-rail-list` underneath. In a
   flex action row (e.g. `.wul-action-row`), the same padding offsets
   the box center and misaligns the link with sibling controls; the
   per-page rule restores symmetry there. */
.map-rail-filters > .filter-clear { padding: 0 16px 10px; }

/* ---- Section heading ----------------------------------------- */
.h-section {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}
.h-section h2 {
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--text-primary);
}
.h-section h2 .meta {
  color: var(--text-muted);
  font-weight: 500;
  margin-left: 8px;
  font-size: 16px;
}
.h-section .right {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-muted);
  font-size: 12.5px;
  font-family: var(--font-mono);
}

/* ---- Search-mini dropdown ----------------------------------- */
/* Rendered by `templates/partials/search_results.html`, positioned
   absolutely against the `.search-mini` wrapper so it doesn't push
   other topbar chrome around. JS (static/js/search.js) toggles the
   `hidden` attribute based on query state + swap content. */
.search-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--elev-2);
  max-height: 360px;
  overflow-y: auto;
  z-index: 20;
}

.search-dropdown[hidden] { display: none; }

.search-dropdown-list {
  list-style: none;
  margin: 0;
  padding: 4px 0;
}

.search-dropdown-option {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 14px;
  text-decoration: none;
  color: var(--text-primary);
}

.search-dropdown-option:hover,
.search-dropdown-option.is-active {
  background: var(--surface-sunken);
  text-decoration: none;
}
.search-dropdown-option.is-active { background: var(--brand-050); }

.search-dropdown-section {
  font-size: 13px;
  font-weight: 500;
}

.search-dropdown-river {
  font-size: 11.5px;
  color: var(--text-muted);
  font-family: var(--font-mono);
}

.search-dropdown-empty {
  padding: 10px 14px;
  font-size: 12.5px;
  color: var(--text-muted);
  font-family: var(--font-sans);
}

.search-dropdown-q {
  color: var(--text-secondary);
  font-family: var(--font-mono);
}

/* ---- Typed search dropdown (Round 8) ---------------------------
   Three result kinds — river / section / gauge — grouped with mono
   headers. Design source: DESIGN.md §"Components — Search dropdown".
   Empty groups suppress entirely; a global zero-results block covers
   the all-empty case. Zero new tokens; reuses R1/R5 primitives. */

.search-dropdown-list .search-group {
  list-style: none;
  padding: 0;
}
.search-dropdown-list .search-group + .search-group {
  border-top: 1px solid var(--border-subtle);
  margin-top: 4px;
  padding-top: 4px;
}

.search-group-head {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  padding: 8px 14px 4px;
}

.search-result {
  display: grid;
  grid-template-columns: 22px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 8px 14px;
  text-decoration: none;
  color: var(--text-primary);
  cursor: pointer;
}
.search-result:hover,
.search-result.is-active { background: var(--surface-sunken); }
.search-result.is-active { background: var(--brand-050); }
.search-result.is-proxy { opacity: 0.88; }

.search-result .sr-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--brand-500);
}
.search-result .sr-river-glyph {
  width: 16px;
  height: 10px;
  color: var(--brand-500);
}
.search-result .sr-section-glyph {
  width: 14px;
  height: 20px;
}
.search-result .sr-gauge-glyph {
  width: 14px;
  height: 14px;
  display: block;
}
.search-result .sr-gauge-glyph polygon { stroke: #fff; stroke-width: 1; }
.search-result .sr-gauge-glyph[data-status="good"] polygon     { fill: var(--status-good-fg); }
.search-result .sr-gauge-glyph[data-status="low"] polygon      { fill: var(--status-low-fg); }
.search-result .sr-gauge-glyph[data-status="high"] polygon     { fill: var(--status-high-fg); }
.search-result .sr-gauge-glyph[data-status="veryhigh"] polygon { fill: var(--status-very-high-fg); }
.search-result .sr-gauge-glyph[data-status="unknown"] polygon  { fill: var(--text-disabled); }

.search-result .sr-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.search-result .sr-eyebrow {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 1px;
}
.search-result .sr-label {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result .sr-label .sr-river {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-right: 6px;
}
.search-result .sr-label .proxy-mark { color: var(--text-muted); }
.search-result .sr-meta {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-result .sr-meta .dot {
  width: 7px;
  height: 7px;
  display: inline-block;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: baseline;
}
.search-result .sr-meta .dot[data-status="good"]     { background: var(--status-good-fg); }
.search-result .sr-meta .dot[data-status="low"]      { background: var(--status-low-fg); }
.search-result .sr-meta .dot[data-status="high"]     { background: var(--status-high-fg); }
.search-result .sr-meta .dot[data-status="veryhigh"] { background: var(--status-very-high-fg); }
.search-result .sr-meta .dot[data-status="unknown"]  { background: var(--text-disabled); }
.search-result .sr-meta .fresh.is-stale { color: var(--status-low-fg); }
.search-result .sr-chev {
  color: var(--text-disabled);
  font-size: 16px;
  line-height: 1;
}

.search-zero {
  padding: 14px 16px;
  list-style: none;
  text-align: center;
  color: var(--text-muted);
  font-family: var(--font-sans);
}
.search-zero-line {
  font-size: 13px;
  color: var(--text-secondary);
}
.search-zero-q {
  color: var(--text-primary);
  font-family: var(--font-mono);
}
.search-zero-hint {
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--text-muted);
  font-family: var(--font-mono);
}

/* ---- Hide-rail toggle (Round 8) --------------------------------
   Shared chrome for the rail-collapse button on `.river-grid` and
   `.features-grid`. Grid-column collapse rules live in the page-
   scoped CSS (river.css / section.css) because each grid carries
   its own list-child selector. Design source: DESIGN.md §"Components
   — Hide-rail". Mobile suppresses entirely — below 900 px the grid
   is already stacked. */

.rail-toggle {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 500;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid var(--border-default);
  color: var(--text-secondary);
  cursor: pointer;
  box-shadow: var(--elev-1);
  padding: 0;
}
.rail-toggle:hover {
  color: var(--brand-700);
  border-color: var(--brand-500);
}
.rail-toggle[aria-pressed="true"] {
  background: var(--brand-050);
  border-color: var(--brand-500);
  color: var(--brand-700);
}
.rail-toggle:focus-visible {
  outline: 2px solid color-mix(in srgb, var(--brand-500) 50%, transparent);
  outline-offset: 2px;
}
.rail-toggle .rail-toggle-glyph {
  width: 14px;
  height: 14px;
  display: block;
}
.rail-toggle .glyph-hide { display: block; }
.rail-toggle .glyph-show { display: none; }
.rail-toggle[aria-pressed="true"] .glyph-hide { display: none; }
.rail-toggle[aria-pressed="true"] .glyph-show { display: block; }

/* Below 900 px both grids stack (no rail to hide). Toggle hides
   entirely — DESIGN_NOTES §"Hide-rail and gauges chip". The
   Round-7 mobile gauges chip takes the top-right slot alone. */
@media (max-width: 899px) {
  .rail-toggle { display: none; }
}

/* ============================================================
   Mobile-specific variants

   Consumed by routes that render mobile-oriented markup (Home A
   mobile, WhatsUpList mobile). Classes are lifted verbatim from
   design/Round-3-Screens.html; the prototype itself has no
   breakpoint that switches between .row and .mb-row, so this
   file doesn't invent one either. Route templates pick the
   markup they need and pair it with visibility control at the
   page level.
   ============================================================ */

/* Favorites stack — drop-in replacement for `.fav-grid` at mobile
   widths. Wraps the same `.sec-card` markup; only the parent
   layout differs. */
.mb-fav-stack {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 20px;
}

/* Nearby list container — mobile counterpart to `.nearby-list`,
   wrapping `.mb-row` children. */
.mb-nearby-list {
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  box-shadow: var(--elev-1);
}

/* Mobile row — tighter than the desktop `.row`. Four children:
   rv (river + optional region/distance), nm (section name),
   fm (flow · grade · freshness), and the status pill. No star,
   no chevron, no ellipsis cell. */
.mb-row {
  padding: 12px 14px;
  border-bottom: 1px solid var(--border-subtle);
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 3px 10px;
  align-items: center;
  color: var(--text-primary);
  text-decoration: none;
}
.mb-row:last-child { border-bottom: none; }
.mb-row:hover { text-decoration: none; background: var(--brand-050); }
.mb-row .rv {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.mb-row .nm {
  font-weight: 600;
  font-size: 14.5px;
  letter-spacing: -0.01em;
  color: var(--text-primary);
}
.mb-row .fm {
  font-family: var(--font-mono);
  color: var(--text-muted);
  font-size: 11.5px;
}
.mb-row .fm strong { color: var(--text-primary); font-weight: 600; }
.mb-row .pill { grid-column: 2 / 3; grid-row: 1 / 3; align-self: center; }

/* ---- Home prompts (.home-prompt) ----------------------------
   Round 4 pattern — single inline band for Home A opt-in
   affordances. See DESIGN.md §"Components — Prompt pattern
   (.home-prompt)". */
.home-prompt-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  margin: var(--space-5) 0 var(--space-7);
}
.home-prompt-row.single { grid-template-columns: 1fr; }

.home-prompt {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  column-gap: var(--space-4);
  padding: 12px 14px 12px 16px;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-left: 3px solid var(--brand-500);
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font-size: 14px;
  min-height: 52px;
}
/* Expandable variant — the location prompt stacks its main row
   + the inline fallback (HTTPS warning, manual-entry input)
   vertically. The main row keeps its four-column grid via
   `.home-prompt-main`; the fallback slot sits below it.
   DESIGN_GAPS.md §"Home prompt expanded state" documents this
   Round 4 pattern extension — triggered by browser denial or
   insecure origin, never by a click on the main row. */
.home-prompt-expandable {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  /* Main-row padding now lives on the inner `.home-prompt-main`
     grid — the wrapper provides only the border-radius + left
     accent. */
  padding: 0;
  /* Suppress the `align-items: center` inherited from `.home-prompt`.
     On the base (grid) variant that rule vertically centres the
     row's children; here the wrapper is `display: flex;
     flex-direction: column`, so cross-axis is horizontal — and
     `center` there leaves `.home-prompt-main` shrink-to-fit
     instead of stretching to the wrapper's full width. Result:
     at stacked single-column widths the location prompt's row
     sat centred with whitespace on both sides while the
     favorites prompt (plain grid) correctly used the full band.
     `stretch` aligns the two variants. */
  align-items: stretch;
}
.home-prompt-expandable .home-prompt-main {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  column-gap: var(--space-4);
  padding: 12px 14px 12px 16px;
  min-height: 52px;
}
.home-prompt-fallback {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: 0 16px 14px;
  border-top: 1px dashed var(--border-subtle);
  padding-top: var(--space-4);
}
.home-prompt-fallback[hidden] { display: none; }
.home-prompt-fallback-lead {
  margin: 0;
  font-size: 13.5px;
  color: var(--text-primary);
}
.home-prompt-insecure {
  background: var(--warning-surface, var(--surface-sunken));
  border: 1px solid var(--warning-border, var(--border-default));
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  color: var(--warning-fg, var(--text-primary));
  font-size: 12.5px;
  line-height: 1.4;
}
.home-prompt-insecure strong { display: inline-block; margin-right: 6px; }
/* The main row already has grid-column layout so direct
   children (glyph, copy, action, dismiss) are unaffected — but
   `.home-prompt-main` wraps them in a single flex child of the
   column-flex wrapper. Without this, the main row collapsed onto
   one column when the wrapper became a flex container. Applies
   only to the `.home-prompt` descendants since the main wrapper
   itself owns the padding + border. */
.home-prompt .glyph {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: grid;
  place-items: center;
  background: var(--brand-050);
  color: var(--brand-700);
  flex-shrink: 0;
}
.home-prompt .copy { display: grid; row-gap: 2px; min-width: 0; }
.home-prompt .lede {
  font-weight: 500;
  letter-spacing: -0.005em;
  line-height: 1.3;
}
.home-prompt .sub {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  line-height: 1.3;
}
.home-prompt.no-dismiss .prompt-dismiss { display: none; }

.prompt-action {
  padding: 6px 12px;
  height: 30px;
  border-radius: 8px;
  background: transparent;
  color: var(--brand-700);
  border: 1px solid var(--brand-500);
  font-family: var(--font-sans);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: -0.005em;
  cursor: pointer;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}
.prompt-action:hover { background: var(--brand-050); text-decoration: none; }
.prompt-action:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--brand-500) 50%, transparent);
  outline-offset: 2px;
}

/* R10 slice 2 closing — signed-out sign-up CTA in the prompt
 * row uses two buttons (Sign up + Sign in) where every other
 * prompt has one. ``.prompt-action-pair`` wraps the two anchors
 * in a flex row so they live in the same grid slot the single-
 * action prompts use. The pair shrinks to wrap on narrow
 * mobile widths via flex-wrap. */
.prompt-action-pair {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.prompt-dismiss {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  background: transparent;
  border: 0;
  color: var(--text-muted);
  cursor: pointer;
  display: grid;
  place-items: center;
  flex-shrink: 0;
  font: inherit;
}
.prompt-dismiss:hover {
  background: var(--surface-sunken);
  color: var(--text-secondary);
}
.prompt-dismiss:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--brand-500) 50%, transparent);
  outline-offset: 2px;
}

/* Narrow-viewport prompt layout: collapse the row to one column
   and drop the `.hide-sub-mobile` sub line at the same breakpoint
   the topbar/tabbar swaps (see chrome.css §"Responsive swap").
   Below 960px the two-column prompt-row leaves each prompt too
   narrow to hold a glyph + lede + sub + action + dismiss without
   wrapping the lede to multiple lines and the sub to one word
   per line; that cramped intermediate state was the shape the
   user saw "shrink to a tall state on narrow, stay tall when
   widened" in Safari during a continuous drag. Stacking the
   prompts earlier keeps each one's copy column wide enough that
   the lede sits on one line from the topbar's natural minimum
   all the way down to 320px. Full sub text stays reachable via
   the dismiss button's `title` attribute for screen readers and
   hover. */
@media (max-width: 959.98px) {
  .home-prompt-row { grid-template-columns: 1fr; gap: var(--space-3); }
  .home-prompt.hide-sub-mobile .sub { display: none; }
}
@media (max-width: 559.98px) {
  .home-prompt { padding: 10px 12px 10px 14px; min-height: 46px; column-gap: 10px; }
  .home-prompt-expandable { padding: 0; }
  .home-prompt-expandable .home-prompt-main {
    padding: 10px 12px 10px 14px;
    min-height: 46px;
    column-gap: 10px;
  }
  .home-prompt-fallback { padding: var(--space-4) 14px 12px; }
  .home-prompt .lede { font-size: 13.5px; }
  .prompt-action { height: 28px; padding: 5px 10px; font-size: 12.5px; }
  /* Expanded state on mobile: show the manual-entry input and
     HTTPS warning stacked full-width; keep the input big enough
     to avoid iOS font-size < 16px zoom (matching the pattern in
     _search_mini + home_nearby manual-entry). */
  .home-prompt-fallback .nearby-manual-input input { font-size: 16px; }
}

/* ---- "What's up in Norway" empty state ---------------------- */
.whatsup-empty {
  padding: 20px 22px;
  background: var(--surface-sunken);
  border: 1px dashed var(--border-default);
  border-radius: 12px;
  color: var(--text-secondary);
  font-size: 14px;
  max-width: 640px;
  display: flex;
  gap: 14px;
  align-items: center;
}
.whatsup-empty .ic {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  display: grid;
  place-items: center;
  color: var(--text-muted);
  flex-shrink: 0;
}
.whatsup-empty .headline {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-primary);
  letter-spacing: -0.01em;
}
.whatsup-empty .meta {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
  margin-top: 2px;
  letter-spacing: 0.02em;
}

/* ---- Buttons (.btn) -----------------------------------------
   Canonical button vocabulary — see DESIGN.md
   §"Components — Button system (.btn)". Three sizes x three
   variants; `.prompt-action` above is the scoped fourth variant
   used only inside `.home-prompt`. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 32px;
  padding: 6px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-family: var(--font-sans);
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: -0.005em;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.btn-sm { height: 28px; padding: 5px 10px; font-size: 12.5px; }
.btn-lg { height: 44px; padding: 10px 20px; font-size: 15px; }

.btn-primary {
  background: var(--brand-600);
  color: var(--text-on-brand);
  border-color: var(--brand-600);
}
.btn-primary:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--brand-700);
  border-color: var(--brand-700);
  /* Preserve text-on-brand on hover for <a class="btn-primary"> —
     base.css's global `a:hover { color: var(--link-hover) }` would
     otherwise tint the label to --link-hover (#155a8a), which is
     identical to --brand-700 and renders the text invisible. */
  color: var(--text-on-brand);
  text-decoration: none;
}

.btn-secondary {
  background: var(--surface-raised);
  color: var(--text-primary);
  border-color: var(--border-default);
}
.btn-secondary:hover:not(:disabled):not([aria-disabled="true"]) {
  background: var(--surface-sunken);
  color: var(--text-primary);
  text-decoration: none;
}

.btn-tertiary {
  background: transparent;
  color: var(--brand-700);
  border-color: transparent;
}
.btn-tertiary:hover:not(:disabled):not([aria-disabled="true"]) {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

.btn:focus-visible {
  outline: 3px solid color-mix(in srgb, var(--brand-500) 50%, transparent);
  outline-offset: 2px;
}
.btn:active:not(:disabled):not([aria-disabled="true"]) {
  transform: translate(0, 1px);
}
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ---- Gauge pin (triangle) ----------------------------------- */
/* Used by river_map.js and section_map.js; the SVG is built by
   gauge_pin.js. Lives in components.css because two routes need
   it; route-specific scoping (e.g. .rv-wrap show/hide toggle)
   stays in the route's own stylesheet. */
.gauge-pin {
  background: none;
  border: none;
  filter: drop-shadow(0 1px 2px rgba(14, 26, 38, 0.24));
  overflow: visible;
  position: relative;
}
.gauge-pin svg { width: 22px; height: 22px; display: block; overflow: visible; }
.gauge-pin .tri-fill   { fill: var(--gauge-status-fg, var(--text-secondary)); }
.gauge-pin .tri-stroke { fill: none; stroke: #ffffff; stroke-width: 1.75; }
.gauge-pin .tri-outer  { fill: none; stroke: var(--text-primary); stroke-width: 0.9; }
.gauge-pin.is-proxy .tri-outer { stroke-dasharray: 2.5 2.5; }
.gauge-pin[data-status="good"],
.gauge-pin-status-good     { --gauge-status-fg: var(--status-good-fg); }
.gauge-pin[data-status="low"],
.gauge-pin-status-low      { --gauge-status-fg: var(--status-low-fg); }
.gauge-pin[data-status="high"],
.gauge-pin-status-high     { --gauge-status-fg: var(--status-high-fg); }
.gauge-pin[data-status="veryhigh"],
.gauge-pin-status-veryhigh { --gauge-status-fg: var(--status-very-high-fg); }
.gauge-pin[data-status="unknown"],
.gauge-pin-status-unknown  { --gauge-status-fg: var(--text-disabled); }

/* Halo ring on hover/lock — center-anchored circle behind the
   triangle. Shape-agnostic counterpart to .sd-map-pin::before /
   .rv-map-pin::before. Lifted from river.css so the section page
   gets the same affordance. */
.gauge-pin::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 44px;
  height: 44px;
  margin: -22px 0 0 -22px;
  border-radius: 50%;
  border: 3px solid var(--gauge-status-fg, var(--brand-500));
  background: color-mix(in srgb, var(--gauge-status-fg, var(--brand-500)) 15%, transparent);
  box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9);
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 140ms ease, transform 140ms ease;
  pointer-events: none;
  z-index: -1;
}
.gauge-pin.is-hover::before,
.gauge-pin.is-locked::before {
  opacity: 1;
  transform: scale(1);
}
.gauge-pin.is-locked::before {
  animation: gauge-pin-pulse 1.6s ease-in-out infinite;
}
@keyframes gauge-pin-pulse {
  0%, 100% {
    transform: scale(1);
    box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9);
  }
  50% {
    transform: scale(1.14);
    box-shadow: 0 0 0 1.5px rgba(255, 255, 255, 0.9),
                0 0 0 6px color-mix(in srgb, var(--gauge-status-fg, var(--brand-500)) 20%, transparent);
  }
}

/* ---- Elevation strip (R9 slice 4) -----------------------------
   Inline SVG below the river map showing a continuous elevation
   curve along the per-river track polyline, with status-tinted
   section bands behind the curve. River-scale (96 px tall, the
   default) is rendered on `/river/<id>`; section-scale (72 px,
   `.section-scale` modifier) is reserved for slice 5 on
   `/section/<id>` — shipped now so the file isn't reopened. The
   SVG viewBox is fixed at 1000×96 with preserveAspectRatio="none"
   so server-side path math is percentage-stable; CSS stretches
   the SVG across the container. */

.elevation-strip {
  position: relative;
  background: var(--surface-raised);
  border-top: 1px solid var(--border-subtle);
  padding: 8px 12px 18px;
  /* Reserve space for tick labels under the SVG so the axis row
     doesn't collide with the curve at the bottom. */
  /* Grid / flex items default to ``min-width: auto`` (= min-content),
     which here is the intrinsic width of the caption row's title.
     On narrow viewports that pushes the strip — and through it the
     entire river card — wider than the screen. Pinning ``min-width:
     0`` lets the strip honour its container's width and the title's
     ellipsis-truncation rule actually fire. */
  min-width: 0;
  /* The R9 follow-up first landed ``touch-action: pan-x`` on the
     SVG so iOS wouldn't arbitrate the horizontal scrubber toward
     vertical scroll. The first round of THIS follow-up bumped
     ``pan-x`` up to the strip parent so it would govern touches on
     the R11 band-labels overlay too — but ``pan-x`` *forbids*
     vertical pan, and the strip is tall enough on mobile that
     scoping it to the whole strip locked vertical page scroll
     anywhere a thumb landed in that region.
     ``pan-y`` is the right primitive: vertical pan is permitted
     (page scrolls normally), horizontal motion is NOT a permitted
     pan so iOS dispatches it as pointer events to the strip
     handler. The chain combines via intersection, so any
     descendant declaring ``pan-x`` would resolve to the empty set
     (= ``none``); we therefore drop the SVG's old ``pan-x`` and
     let the strip parent govern. */
  touch-action: pan-y;
}

/* ---- Slice 4 PR D: caption row above the SVG ----
   "Elevation profile — {km} km · {max} m → {min} m" on the left,
   "© Kartverket Høydedata · CC BY 4.0" attribution on the right.
   Pattern lifted from Round-9-Screens.html L165-176.
   Fixed height so the absolutely-positioned band-labels and axis
   below can offset by a known amount and not collide with the
   caption. The total vertical reservation is
   ``--elev-head-h`` (head text) + 6px margin-bottom = 22px. */
.elevation-strip {
  --elev-head-h: 16px;
  --elev-head-mb: 6px;
}
.elevation-strip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  height: var(--elev-head-h);
  margin-bottom: var(--elev-head-mb);
  /* Same reasoning as ``.elevation-strip``: without this, the
     flex container's min-width is the title's intrinsic content
     width, blocking the title's ``min-width: 0`` from doing
     anything. */
  min-width: 0;
}
.elevation-strip-title {
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* The strip's head is a fixed-height row; without nowrap +
     ellipsis, a long ``Elevation profile — 38 km · 940 m → 260 m``
     wraps onto a second line at narrow viewports and the wrapped
     line overflows down into the SVG. Truncate the title (the
     dynamic km/elevation suffix is the lossy part) and let the
     attribution keep its full width. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
.elevation-strip-attribution {
  font-family: var(--font-mono);
  font-size: 9.5px;
  color: var(--text-disabled);
  white-space: nowrap;
  flex-shrink: 0;
}

.elevation-strip-svg {
  display: block;
  width: 100%;
  height: 96px;
  overflow: visible;
  /* ``touch-action`` is governed by the ``.elevation-strip`` parent
     (``pan-y``) so the chain combines cleanly. Declaring ``pan-x``
     here intersected with the parent's ``pan-y`` to the empty set
     and broke vertical scroll on the strip; declaring ``pan-y``
     here would be redundant. */
}

.elevation-strip.section-scale .elevation-strip-svg { height: 72px; }

.elevation-strip-band {
  fill: currentColor;
  fill-opacity: 0.12;
  /* `currentColor` is set per status slug below; section-scale
     overrides via the .section-scale modifier to a single 0.28
     opacity (the brief: section-scale = whole background = one
     section band at 0.28). */
}
.elevation-strip-band[data-status="good"]     { color: var(--status-good-fg); }
.elevation-strip-band[data-status="low"]      { color: var(--status-low-fg); }
.elevation-strip-band[data-status="toolow"]   { color: var(--status-too-low-fg); }
.elevation-strip-band[data-status="too-low"]  { color: var(--status-too-low-fg); }
.elevation-strip-band[data-status="high"]     { color: var(--status-high-fg); }
.elevation-strip-band[data-status="veryhigh"] { color: var(--status-very-high-fg); }
.elevation-strip-band[data-status="unknown"]  { color: var(--text-disabled); }
.elevation-strip-band.is-hover,
.elevation-strip-band.is-locked { fill-opacity: 0.28; }

/* Section-scale strips render no band fill — the put-in / take-out
   ``band_edges`` strokes (vertical lines inset 4 px from the strip
   top/bottom) are the only border treatment. The section's status
   colour reads from the page's status pill / gauge pin, not the
   strip background, per operator review on slice 5. The hover /
   locked promotion is also suppressed: a single-section strip has
   nothing to "select" — the cursor's vertical line + chip already
   show the lock state, so a band fill toggling underneath would
   only add noise. River-scale's hover/locked rule above
   (``.elevation-strip-band.is-hover, .is-locked { fill-opacity:
   0.28 }``) is unaffected because the modifier scoping makes this
   rule strictly more specific. */
.elevation-strip.section-scale .elevation-strip-band,
.elevation-strip.section-scale .elevation-strip-band.is-hover,
.elevation-strip.section-scale .elevation-strip-band.is-locked {
  fill-opacity: 0;
}

.elevation-strip-area {
  fill-opacity: 1;
  pointer-events: none;
}
.elevation-strip-line {
  fill: none;
  stroke: var(--brand-700);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}

/* ---- Round 11 item 5: band-boundary strokes ----
   Adjacent same-status bands rendered as a single fill blob; one
   stroke per unique band edge pulls the seam back. Round 11
   bumps the stroke from --border-subtle (slice 4 PR D) to a
   dedicated --strip-band-divider-fg token at 1.25px, with the
   line inset 4px top and bottom (in template y-coords) so the
   divider reads as a tick rather than a wall.
   ``vector-effect: non-scaling-stroke`` so the line keeps its
   intrinsic width regardless of the SVG's pixel-to-viewBox
   stretch. */
.elevation-strip-band-edge {
  stroke: var(--strip-band-divider-fg);
  stroke-width: 1.25;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}

/* ---- Slice 4 PR D: Y-axis static guides ----
   Dashed reference rules at the elevation_max (top) and
   elevation_min (bottom) levels of the curve. Pattern lifted
   from Round-9-Screens.html L183-184 (``prof-axis-y`` /
   ``prof-tick``). The tick labels sit just inside the SVG box
   on the left, in the same font/size as the axis km labels. */
.elevation-strip-y-axis {
  stroke: var(--border-default);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}
.elevation-strip-y-tick {
  font-family: var(--font-mono);
  font-size: 9.5px;
  fill: var(--text-muted);
  pointer-events: none;
}

.elevation-strip-cursor-line,
.elevation-strip-cursor-dot {
  pointer-events: none;
  opacity: 0;
  transition: opacity 100ms ease;
}
.elevation-strip-cursor-line {
  stroke: var(--brand-500);
  stroke-width: 1.2;
  vector-effect: non-scaling-stroke;
}
.elevation-strip-cursor-dot {
  fill: var(--brand-500);
  stroke: #ffffff;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.elevation-strip[data-cursor-mode="hover"] .elevation-strip-cursor-line,
.elevation-strip[data-cursor-mode="hover"] .elevation-strip-cursor-dot,
.elevation-strip[data-cursor-mode="locked"] .elevation-strip-cursor-line,
.elevation-strip[data-cursor-mode="locked"] .elevation-strip-cursor-dot {
  opacity: 1;
}

/* ---- Round 11 item 3: section-label lane overlays the SVG ----
   Lane width matches the SVG (both inset 12px from the strip's
   horizontal padding); height matches the SVG so each label can
   centre vertically within the *band region* (top: 50%;
   translateY(-50%)) instead of baseline-anchoring to the band
   top. Section-scale uses a 72px SVG so the lane shrinks to
   match.

   Item 4: the per-label inline ``max-width`` (server-side, from
   each band's percent-of-strip width minus 11px — 3px left
   offset + 8px effective right margin) takes care of horizontal
   truncation. The label clips with an ellipsis when the band is
   too narrow for its name. */
.elevation-strip-band-labels {
  position: absolute;
  top: calc(8px + var(--elev-head-h) + var(--elev-head-mb));
  right: 12px;
  left: 12px;
  height: 96px;
  pointer-events: none;
}
.elevation-strip.section-scale .elevation-strip-band-labels { height: 72px; }
.elevation-strip-band-label {
  position: absolute;
  /* ``--label-anchor-y`` is set per-label by elevation_strip.js
     (a percentage of the band region), defaulting to 50% so the
     label sits in the band's vertical mid when the curve doesn't
     pass through. The JS picks 22% or 78% on bands where the
     elevation curve crosses the centre, anchoring the label to
     the half furthest from the curve so the white pill no longer
     overlaps the line. */
  top: var(--label-anchor-y, 50%);
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  white-space: nowrap;
  /* Item 4 truncation: the inline ``max-width`` is the band's
     pixel-width-as-percent minus 11px (3px left offset to clear
     the band's left divider + 8px effective right margin). This
     rule makes truncation behave: when the label exceeds its
     band's usable width, an ellipsis stands in for the
     overflow. */
  overflow: hidden;
  text-overflow: ellipsis;
  /* Item 3 white halo pill — the label sits on top of a tinted
     band, so a translucent white background + small radius +
     blur lets the type stay legible at 9.5px without a heavy
     chip treatment. ``--band-status-fg`` is set inline per-label
     so the text colour matches the underlying band's status.
     ``--band-pill-bg`` defaults to the light-mode white halo and
     is overridden in the dark-mode rules below to a dark surface
     tint so the dark-tuned status-fg colours read clearly against
     it. D4 follow-up — the previous fixed white pill kept the
     pill near-light against dark status-fg hues, dropping the
     section-label contrast on /river/<id> in dark mode below
     legibility. */
  background: var(--band-pill-bg, rgba(255, 255, 255, 0.78));
  border-radius: 3px;
  padding: 2px 5px;
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  color: var(--band-status-fg, var(--text-secondary));
}

/* D4 follow-up — dark-mode pill background for the section-name
   labels on the elevation strip. Status-fg tokens are already
   tuned for dark surfaces by tokens.css, but the pill background
   was hard-locked to translucent white which left near-white text
   on a near-white pill. Switch the pill to a translucent dark-
   surface tint so the dark-tuned status-fg reads against it.
   Mirrors the gate pattern used elsewhere: media query gated on
   :not([data-color-scheme="light"]) so an explicit "light" beats
   the OS, plus a parallel manual-override rule for
   data-color-scheme="dark". */
@media (prefers-color-scheme: dark) {
  html:not([data-color-scheme="light"]) .elevation-strip-band-label {
    --band-pill-bg: rgba(11, 20, 32, 0.78);
  }
}
html[data-color-scheme="dark"] .elevation-strip-band-label {
  --band-pill-bg: rgba(11, 20, 32, 0.78);
}

.elevation-strip-axis {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 4px;
  height: 12px;
  pointer-events: none;
}
.elevation-strip-tick {
  position: absolute;
  transform: translateX(-50%);
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.elevation-strip-tick:first-child { transform: translateX(0); }
.elevation-strip-tick:last-child  { transform: translateX(-100%); }

/* ---- Round 11 item 2: visible elevation tooltip chip ----
   Lifted verbatim from Round-11-Screens.html L128-157. Rendered
   as an absolutely-positioned div inside the strip (not the SVG)
   so the chip can carry CSS shadows/backdrop without battling
   SVG overflow. Hidden by default; the JS toggles ``hidden`` and
   updates ``transform: translate(...)`` for px-precise placement
   relative to the strip's bounding rect. ``flip-right`` flips the
   tail anchor when cursor.x > strip.width - 60. ``locked`` swaps
   to the brand-700 treatment on click-lock per spec L1347. */
.elev-tip {
  position: absolute;
  /* Pin the absolute origin to the strip's top-left corner so the
     JS ``transform: translate(tx, ty)`` is relative to (0, 0).
     Without this, ``top: auto`` defaults the origin to the chip's
     static-flow position — which lands at the bottom of the strip's
     children, far below where the chip should render. The previous
     "anchor at strip top" treatment used a fixed negative ty that
     happened to put the chip inside the strip from that flow
     position; the curve-follow treatment uses positive ty values
     up to the SVG's bottom, which translated the chip clean out
     of the strip's box. */
  top: 0;
  left: 0;
  background: var(--elevation-tooltip-bg);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  padding: 5px 8px;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  z-index: 6;
  box-shadow: 0 1px 4px rgba(14, 26, 38, 0.18);
  /* Smooth pointer-driven motion AND the flip-left/right /
     flip-up/down anchor jumps. 90 ms cubic-bezier gives a brisk
     glide that doesn't lag the cursor. The JS adds
     ``.no-transition`` for one frame on first appearance so the
     chip snaps in from its destination instead of sliding in
     from its last stale position. */
  transition: transform 90ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.elev-tip.no-transition,
.elev-tip.no-transition::before,
.elev-tip.no-transition::after { transition: none; }
.elev-tip[hidden] { display: none; }
/* Two tails: ``::after`` is the down-pointing tail at the chip's
   bottom edge (chip above dot, the default); ``::before`` is the
   up-pointing tail at the chip's top edge (chip below dot, used
   when the dot sits too high on the strip for the chip body to
   render upward). Both share ``--tail-x`` so they slide
   horizontally with the cursor; on a vertical flip the chip body
   transitions through the dot via ``transform``, while the two
   tails cross-fade — giving an opacity-blended "morph" instead
   of the discrete tail-edge swap. */
.elev-tip::before,
.elev-tip::after {
  content: "";
  position: absolute;
  width: 0; height: 0;
  border: 4px solid transparent;
  left: var(--tail-x, 12px);
  transition:
    left 90ms cubic-bezier(0.2, 0.8, 0.2, 1),
    opacity 90ms ease-out;
}
.elev-tip::after {
  bottom: -8px;
  border-top-color: var(--elevation-tooltip-bg);
  opacity: 1;
}
.elev-tip::before {
  top: -8px;
  border-bottom-color: var(--elevation-tooltip-bg);
  opacity: 0;
}
.elev-tip.flip-down::after { opacity: 0; }
.elev-tip.flip-down::before { opacity: 1; }
.elev-tip.locked {
  background: var(--brand-700);
  box-shadow: 0 0 0 2px var(--brand-100), 0 1px 4px rgba(14, 26, 38, 0.22);
}
.elev-tip.locked::after { border-top-color: var(--brand-700); }
.elev-tip.locked::before { border-bottom-color: var(--brand-700); }

/* On lock, dim non-locked bands so the locked focus visibly pops.
   Spec L1351: "consistent with R5/R9 cursor pattern". The locked
   band keeps its own .is-locked highlight (fill-opacity 0.28); the
   rest of the bands tone back to ``opacity: 0.34``. Targets only
   when the strip itself is in locked mode AND the band is not the
   locked one — labels follow the same dim so they don't pop forward
   when their band recedes. */
.elevation-strip[data-cursor-mode="locked"] .elevation-strip-band:not(.is-locked) {
  opacity: 0.34;
  transition: opacity 120ms ease;
}
.elevation-strip[data-cursor-mode="locked"] .elevation-strip-band-label:not(.is-locked) {
  opacity: 0.34;
  transition: opacity 120ms ease;
}

/* Item-4 tap-to-reveal: only labels that actually need the chip
   (truncated by the inline ``max-width``) opt back into pointer
   events, via an ``is-truncated`` class set by the JS at init.
   Untruncated labels stay ``pointer-events: none`` so they never
   block strip hover or steal a click that would otherwise lock
   the cursor on the band underneath. */
.elevation-strip-band-label.is-truncated {
  pointer-events: auto;
  cursor: pointer;
}

/* ---- Round 11 item 1: out-of-viewport bookends ----
   When the map is zoomed in past either end of the river, the strip
   renders muted preview bands at its left/right edges to convey
   "there's more river off the edges." The .prof-out rect fills the
   bookend region with --surface-sunken @ 0.7; the .prof-out-line
   dashed boundary marks the seam between bookend and the in-viewport
   reach. Both are rendered inside the strip's SVG by JS; CSS owns
   the styling. Lifted from artboard L124-126. */
.elevation-strip .prof-out {
  fill: var(--surface-sunken);
  opacity: 0.7;
  pointer-events: none;
}
.elevation-strip .prof-out-line {
  fill: none;
  stroke: var(--text-disabled);
  stroke-width: 1;
  stroke-dasharray: 2 2;
  opacity: 0.55;
  pointer-events: none;
  vector-effect: non-scaling-stroke;
}

/* ---- Round 11 item 1: viewport-extent chip ----
   Top-right of the strip when the map is zoomed in past full extent.
   Hidden when the viewport covers the full river. Mobile narrow
   viewports tuck the chip under the strip header to preserve
   horizontal width (per brief §1 "Mobile"). Lifted from artboard
   L160-184. JS toggles ``hidden`` and writes the visible km range
   into ``.range``. Reset link clears ``store.viewportTarget`` and
   unwinds the clip. */
.vp-chip {
  position: absolute;
  top: 8px;
  right: 14px;
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-secondary);
  padding: 3px 9px 3px 6px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  z-index: 5;
  box-shadow: 0 1px 2px rgba(14, 26, 38, 0.04);
}
.vp-chip[hidden] { display: none; }
.vp-chip .reset {
  color: var(--brand-700);
  text-decoration: none;
  font-weight: 600;
  margin-left: 2px;
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
}
.vp-chip .reset:focus-visible {
  outline: 2px solid var(--brand-500);
  outline-offset: 2px;
  border-radius: 2px;
}
.vp-chip .ind {
  width: 8px;
  height: 8px;
  background: var(--brand-500);
  border-radius: 1px;
}

/* Mobile: chip moves below the strip header so it doesn't fight the
   narrow strip's right-edge chrome. The header row has fixed height
   ``--elev-head-h`` (16px) + ``--elev-head-mb`` (6px) + 8px top
   padding = 30px, so anchoring at top:30px tucks the chip directly
   beneath. The header's title is set to nowrap+ellipsis, so the
   chip never collides with the title text on the same row. */
@media (max-width: 720px) {
  .elevation-strip .vp-chip {
    top: calc(8px + var(--elev-head-h) + var(--elev-head-mb));
    right: 8px;
  }
}

/* ---- River-path section polylines (status-colored solid strokes) ----
   Class is emitted by both river_map.js (one polyline per `'ok'`
   section_trace row on /river/<id>) and section_map.js (the section's
   own polyline on /section/<id> — R9 slice 3c). Hoisted from
   river.css in slice 3c so both surfaces share one definition. */

.river-path {
  stroke-width: 4.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0.9;
  fill: none;
  pointer-events: none;
}
.river-path[data-status="good"]     { stroke: var(--status-good-fg); }
.river-path[data-status="low"]      { stroke: var(--status-low-fg); }
.river-path[data-status="toolow"]   { stroke: var(--status-too-low-fg); }
.river-path[data-status="high"]     { stroke: var(--status-high-fg); }
.river-path[data-status="veryhigh"] { stroke: var(--status-very-high-fg); }
.river-path[data-status="unknown"]  { stroke: var(--text-disabled); }
.river-path.is-hover,
.river-path.is-locked {
  stroke-width: 6;
  opacity: 1;
  filter: drop-shadow(0 0 6px currentColor);
}

/* ---- Catchment weather card (Round 12 · Proposal B) -----------------
   Mounts on /section/<id> and /river/<id> via partials/_put_in_card.html.
   File path stays `_put_in_card.html` for backwards compat; CSS hooks
   were renamed from `.put-in-card-*` to `.wxc-*` / `.wxB-*` per
   design/round-12-catchment-weather.md (operator-locked rename — no
   alias kept). Body shape is yr.no-style: hourly precip bars + temp
   line on a single dual-axis chart for the next +66h, then a 7-day
   strip below using 6-hourly data folded to daily lo/hi + total mm.

   Token additions live in tokens.css (--wx-tile-bg, --wx-grid,
   --wx-axis, --wx-glyph-stroke). Tokens reused otherwise — brand
   ramp + status-low scale + spacing/radius primitives. */
.wxc {
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-1);
  overflow: hidden;
  font-family: var(--font-sans);
  color: var(--text-primary);
  /* Operator review 2026-05-03 (Round 12 follow-up): the parent
     ``.sd-wrap`` / ``.rv-wrap`` flex containers already supply
     uniform 18px gap between sibling cards. The pre-fix
     ``margin-block`` here stacked on top of that gap and made the
     catchment card sit further from the features / river-grid
     card above than other cards on the page. Drop the override
     so card-to-card rhythm is uniform. */
}
.wxc-head {
  padding: 14px 18px 12px;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 6px 12px;
  border-bottom: 1px solid var(--border-subtle);
}
.wxc-head.is-stale {
  background: var(--status-low-bg);
}
.wxc-title {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.wxc-title h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--text-primary);
}
.wxc-title .horizon {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.wxc-attr {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  background: var(--surface-sunken);
  border-radius: 5px;
  border: 1px solid var(--border-subtle);
  white-space: nowrap;
}
/* Methodology trigger (round-12 follow-up · item 6). Inline next
   to the H3 heading; opens the ``#wxc-method-dialog`` native
   <dialog>. Visually a hairline circle with an "i" — matches the
   icon language of the dark-mode toggle. */
.wxc-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 50%;
  transition: color 120ms ease, background 120ms ease;
}
.wxc-info:hover,
.wxc-info:focus-visible {
  color: var(--brand-700);
  background: var(--surface-sunken);
  outline: none;
}
.wxc-info:focus-visible {
  box-shadow: 0 0 0 2px var(--brand-100);
}

.wxc-method-dialog {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  color: var(--text-primary);
  padding: 0;
  max-width: 460px;
  width: calc(100vw - 32px);
  box-shadow: 0 8px 32px rgba(14, 26, 38, 0.18);
}
.wxc-method-dialog::backdrop {
  background: rgba(14, 26, 38, 0.4);
}
.wxc-method-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 18px 20px 16px;
  margin: 0;
}
.wxc-method-form h3 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}
.wxc-method-form p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-secondary);
}
.wxc-method-form p strong {
  color: var(--text-primary);
  font-weight: 600;
}
.wxc-method-caveat {
  border-left: 2px solid var(--brand-400);
  padding-left: 10px;
  font-style: italic;
}
.wxc-method-foot {
  font-size: 11.5px !important;
  color: var(--text-muted) !important;
}
.wxc-method-foot a {
  color: var(--link);
}
.wxc-method-close {
  align-self: flex-end;
  margin-top: 4px;
  padding: 6px 14px;
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  background: var(--surface-base);
  color: var(--text-primary);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.wxc-method-close:hover,
.wxc-method-close:focus-visible {
  background: var(--surface-sunken);
  border-color: var(--brand-400);
  outline: none;
}
.wxc-attr .gauge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand-500);
}
.wxc-attr .proxy-flag {
  margin-left: 4px;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--status-low-bg);
  color: var(--status-low-fg);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-weight: 600;
}
.wxc-foot {
  padding: 8px 18px 10px;
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  letter-spacing: 0.04em;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  border-top: 1px solid var(--border-subtle);
  background: var(--surface-base);
}
.wxc-foot .stale {
  color: var(--status-low-fg);
  font-weight: 600;
}
.wxc-foot strong {
  color: var(--text-secondary);
  font-weight: 600;
}

.wxc-empty {
  padding: 22px 18px;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
  border-top: 1px solid var(--border-subtle);
  background: var(--surface-base);
}
.wxc-empty .em-glyph {
  /* Round 12 follow-up — a11y / Lighthouse color-contrast: --text-disabled
     (#aab9cc on --surface-base #f4f6f9) renders at 1.84:1, well below the
     WCAG AA 4.5:1 floor. Bump to --text-muted (5.18:1 light, 4.67:1 dark)
     so the empty-state glyph clears AA without altering structure. */
  color: var(--text-muted);
  margin-bottom: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.wxc-empty p {
  margin: 0;
}

/* ---- Proposal B body ------------------------------------------------- */
.wxB-body {
  padding: 14px 18px 12px;
}
.wxB-summary {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  gap: 12px 16px;
  align-items: center;
  padding-bottom: 10px;
  margin-bottom: 8px;
}
.wxB-summary .now-temp {
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.025em;
  font-variant-numeric: tabular-nums;
}
.wxB-summary .now-cond {
  font-size: 13px;
  color: var(--text-secondary);
  line-height: 1.4;
  max-width: 520px;
}
.wxB-summary .now-cond b {
  color: var(--text-primary);
  font-weight: 600;
}
.wxB-summary .stat {
  display: flex;
  flex-direction: column;
  gap: 1px;
  align-items: flex-end;
}
.wxB-summary .stat .k {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.wxB-summary .stat .v {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.wxB-chart-wrap {
  background: transparent;
  border: 1px solid var(--border-subtle);
  border-radius: 8px;
  /* Round 12 follow-up — top padding bumped 12 → 16 so the absolutely-
     positioned ``.wxB-axis-head`` "mm/h" / "°C" labels (translated up
     by their own height from the SVG top) get a small gap from the
     wrap border instead of sitting flush against it. */
  padding: 16px 8px 6px;
  position: relative;
  /* Operator review 2026-05-03 (item 5) — signal hover affordance.
     The chart now hydrates a tooltip on pointer / focus / tap; a
     pointer cursor over the chart area matches the elevation +
     flow charts which both readout-on-hover. */
  cursor: crosshair;
}
.wxB-chart {
  /* Round 12 follow-up — chart wrapper positions absolutely-placed
     axis text overlays. SVG fills width via preserveAspectRatio=none;
     axis labels ride on this layer at their server-computed
     x_pct / y_pct so glyphs render at intrinsic pixel size. */
  position: relative;
}
.wxB-chart svg {
  display: block;
  width: 100%;
  height: 200px;
  /* Bars take the same fill via a CSS class so the hover state can
     swap fill without re-emitting the <rect>. ``is-zero`` rects are
     pointer-only hit areas — invisible in the SVG (height=0) but
     still focusable for keyboard tooltip walks. */
}
/* Axis labels — HTML overlay (Round 12 follow-up). Same font /
   colour tokens as ``.elevation-strip-tick``; positioning math
   server-side via tick.y_pct (top%) + per-axis edge anchor
   (left/right: 0). transform: translateY(-50%) centres the line
   on its gridline. */
.wxB-y-tick {
  position: absolute;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  white-space: nowrap;
  transform: translateY(-50%);
  pointer-events: none;
}
.wxB-y-tick--left { left: 0; }
.wxB-y-tick--right { right: 0; }
.wxB-axis-head {
  position: absolute;
  top: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  color: var(--text-muted);
  letter-spacing: 0.02em;
  pointer-events: none;
  transform: translateY(-100%);
}
.wxB-axis-head--left { left: 0; color: var(--brand-700); }
.wxB-axis-head--right { right: 0; color: var(--text-secondary); }
.wxB-zero-marker {
  position: absolute;
  right: 0;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 600;
  color: var(--brand-700);
  letter-spacing: 0.02em;
  pointer-events: none;
  transform: translateY(-100%);
  padding-right: 4px;
}
.wxB-bar {
  /* Pointer-events on individually so each <rect> can dispatch
     mouseenter / focus events. The whole <g> share one fill. */
  pointer-events: all;
}
.wxB-bar.is-active {
  fill: var(--brand-700);
  opacity: 1;
}
.wxB-cursor.is-active {
  opacity: 0.6;
}
/* Cursor dot — HTML overlay div, sibling to the chart SVG. Was
   originally an SVG ``<circle>`` but under ``preserveAspectRatio="none"``
   the radius (a viewBox unit) stretched non-uniformly with x vs y, so
   the dot rendered as an oval. As a CSS-styled div it stays a true
   pixel circle regardless of how the SVG box scales. JS sets ``left``
   / ``top`` (CSS px) on activation and toggles the ``hidden``
   attribute. Visual language mirrors the elevation strip cursor dot:
   brand-700 fill, surface-coloured ring so it reads against the
   curve. */
.wxB-cursor-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--brand-700);
  border: 1.5px solid var(--surface-raised, #ffffff);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 3;
}
.wxB-cursor-dot[hidden] { display: none; }
/* Isolated temperature dots — HTML overlay markers (Round 12 follow-up
   B). The 6-hourly tail past the dense hourly band lands as isolated
   valid samples (no neighbour to draw a polyline segment to). The path
   used to emit a zero-length "M x y L x y" pair to coax stroke-linecap
   into rendering a dot — barely visible and off-centre from the JS
   cursor dot (path used bar-left x, cursor used bar-centre x). HTML
   overlay div solves both: pixel-circle regardless of SVG scaling,
   placed at the SAME (bar_cx, temp_y) the cursor dot reads. Sized one
   step smaller than the cursor dot so the active hover dominates
   visually. */
.wxB-temp-point {
  position: absolute;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--brand-700);
  border: 1px solid var(--surface-raised, #ffffff);
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 2;
}
/* Tooltip chip — same visual language as ``.elev-tip`` (mono,
   rounded, drop-shadow). Positioned by JS with transform: translate. */
.wxB-tip {
  position: absolute;
  top: 0;
  left: 0;
  background: var(--elevation-tooltip-bg);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 500;
  padding: 5px 8px;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  z-index: 4;
  box-shadow: 0 1px 4px rgba(14, 26, 38, 0.18);
  transition: transform 90ms cubic-bezier(0.2, 0.8, 0.2, 1);
  line-height: 1.35;
}
.wxB-tip[hidden] { display: none; }
.wxB-tip.locked {
  background: var(--brand-700);
  box-shadow: 0 0 0 2px var(--brand-100), 0 1px 4px rgba(14, 26, 38, 0.22);
}
.wxB-tip .tip-time {
  display: block;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.wxB-tip .tip-row {
  display: block;
  opacity: 0.9;
}
.wxB-x {
  /* Round 12 follow-up — switch from a flex/grid row to absolutely-
     positioned ticks, matching the elevation-strip pattern. The
     server emits ``style="left: x_pct%"`` per tick (computed from
     the same plot_left..plot_right axis as the SVG bars), so the
     ticks line up with their gridline columns regardless of width. */
  position: relative;
  height: 14px;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.wxB-x-tick {
  position: absolute;
  transform: translateX(-50%);
  white-space: nowrap;
}
.wxB-x-tick:first-child { transform: translateX(0); }
.wxB-x-tick:last-child  { transform: translateX(-100%); }
.wxB-x-tick.now-mark {
  color: var(--brand-700);
  font-weight: 700;
}

.wxB-daily {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 6px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px dashed var(--border-subtle);
}
.wxB-day {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 4px;
  border-radius: 6px;
  text-align: center;
  background: transparent;
}
.wxB-day.is-wettest {
  background: var(--brand-050);
}
.wxB-day .d {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.wxB-day .range {
  font-size: 12.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.wxB-day .range .lo {
  color: var(--text-muted);
  font-weight: 500;
}
.wxB-day .pp {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--brand-700);
}
.wxB-day .pp.zero {
  color: var(--text-disabled);
}

.wxB-tiles-compat {
  display: none;
}

/* Mobile reflow at 375 / 390 — spec §"Proposal B mobile reflow":
   summary 4 cols → 2 cols, chart x-labels reduce visually via flex
   wrap, daily 7 cols → 4 cols. */
@media (max-width: 480px) {
  .wxB-summary {
    grid-template-columns: auto 1fr;
    gap: 8px 12px;
  }
  .wxB-summary .now-cond {
    grid-column: 1 / -1;
  }
  .wxB-summary .stat {
    align-items: flex-start;
  }
  .wxB-chart svg {
    height: 160px;
  }
  .wxB-x-tick:nth-child(odd):not(:first-child):not(:last-child) {
    display: none;
  }
  /* Wrap 7 day tiles into 2 rows of 4 (last row has 3 tiles
     aligned start) instead of hiding days 5-7. Operator
     feedback 2026-05-03 — full week visible on mobile. */
  .wxB-daily {
    grid-template-columns: repeat(4, 1fr);
    grid-auto-rows: auto;
  }
}
