/* Ocean Oracle - shared styles for the picker and forecast pages. */

:root {
  --navy: #0a2540;
  --deep-blue: #0f3460;
  --ocean-blue: #1b6ca8;
  --ocean-blue-light: #2e8bc0;
  --bg: #f4f7fa;
  --card-bg: #ffffff;
  --border: #e2e8ef;
  --text: #16233a;
  --text-muted: #64748b;
  --high: #1b7a3d;
  --high-bg: #e3f5e9;
  --good: #0c5f7a;
  --good-bg: #e0f2f7;
  --moderate: #8a6300;
  --moderate-bg: #fdf3d9;
  --low: #a13a1f;
  --low-bg: #fbe6e0;
  --uncertain: #7a1f2b;
  --uncertain-bg: #f8dde0;
  /* 2026-08-05 rework, per Caleb ("new labels, palette, and tiers" for
     confidence, then "saturate the colors to be a bit more darker,
     and make Good Confidence to be a bit more green"): confidence now
     gets its OWN 6-color namespace instead of reusing --high/--good/
     --moderate/--low/--uncertain above. Those older vars stay
     untouched because two unrelated things quietly depend on their
     OLD values - .error-banner (a reddish rust tone for errors, not a
     confidence concept at all) and .narrative p:last-of-type's
     "Recommendation" callout (originally a pale blue tint, back when
     "Good Confidence" itself was blue) - repointing them at these new,
     differently-hued confidence colors would have silently changed
     both of those unrelated to anything Caleb asked for here. */
  --conf-excellent: #0f5940;
  --conf-excellent-bg: #c2e3d6;
  --conf-high: #1f6b34;
  --conf-high-bg: #c9e8d3;
  --conf-good: #4a7a12;
  --conf-good-bg: #dcecc0;
  /* 2026-08-06, per Caleb ("Saturday confidence trend color... looks
     more brown than yellow"): #765405 was a low-lightness gold, and at
     that darkness any yellow hue reads as brown/olive regardless of
     saturation. Lightened to a goldenrod (#b8860c, Caleb's pick from 3
     options) that stays in the same warm-gold family but sits high
     enough in lightness to actually read as yellow. Border shade below
     recomputed at the same ~0.65 ratio every other --conf-*-border
     uses; -bg tint left as-is since it was already a light, compatible
     tint of this same hue. */
  --conf-moderate: #b8860c;
  --conf-moderate-bg: #f6e6b8;
  --conf-low: #8a3712;
  --conf-low-bg: #f5d6c3;
  --conf-none: #6e1a26;
  --conf-none-bg: #f3ced2;
  /* 2026-08-06, per Caleb ("add dark borders to the other pop ups on
     the page... darker version of the color the confidence badge is
     showing"): each badge's own text color (above) pushed 35% further
     toward black - severity._interpolate_hex(0.35, conf-color,
     '#000000') - for a border that's a visibly darker shade of that
     same tier's color, not a flat neutral gray shared across every
     tier. */
  --conf-excellent-border: #0a3a2a;
  --conf-high-border: #144622;
  --conf-good-border: #304f0c;
  --conf-moderate-border: #785708;
  --conf-low-border: #5a240c;
  --conf-none-border: #481119;
  --hazard-bg: #fdf3d9;
  --hazard-text: #7a5400;
  /* 2026-08-04: a genuinely bright, warm accent - deliberately outside
     the blue/navy family every other color in this palette lives in,
     so the AI Estimate column's border reads as "different on
     purpose" against a chart that's otherwise all shades of blue. */
  --accent-gold: #f5a623;
  /* 2026-08-06, per Caleb ("I like the current color but don't want it
     to be the same as the headers... I love the color of the water in
     the Bahamas, can we use this as inspiration?"): the AI Estimate
     bar's own fill used to be a flat var(--navy) - identical to the
     MPH/FT header boxes, so the one column meant to stand out as the
     app's own synthesized answer visually disappeared into the same
     navy as everything around it. First tried a Bahamas-inspired
     turquoise (#0f8a94), but Caleb's follow-up ("still not crazy how
     the color looks similar to a color we already have established")
     caught that the page's own model-bar gradients already fade
     through blue/teal/violet, and the wind severity spectrum already
     has its own teal tier - so ANY blue/teal/green pick was always
     going to echo something already on screen somewhere. Landed on a
     desaturated slate charcoal instead - the one muted, colorless bar
     on an otherwise fully-saturated page, distinct by NOT competing
     with any existing hue family rather than by picking a new one.
     Renamed from --estimate-teal (no longer accurate) to this neutral
     name so it doesn't mislead the next color swap either.

     2026-08-06, same-day follow-up ("I'm thinking of making the AI
     estimate bar color gold/shade of yellow... let's go with R"):
     swapped again, this time to a muted brass gold - picked from a set
     of 4 gold/yellow mockups Caleb reviewed. Flagged before he chose
     it: this shade sits fairly close to the Moderate Confidence tier's
     own amber (originally #765405, severity.py's
     CONFIDENCE_COLOR_STOPS_SCORE - since lightened to #b8860c, see
     that var's own comment above) - a deliberate tradeoff Caleb made
     anyway at the time, picking the most muted/
     subdued of the 4 gold options over louder ones that would have
     stayed further from that amber. White icon/label text (unchanged
     below) still reads fine at this shade - unlike the brighter true-
     gold options in that same mockup set, which would have needed a
     text-color swap to navy for contrast. */
  --estimate-accent: #8a6a1f;
  /* Aesthetic pass (2026-07-29): one shared shadow so every card - data
     cards, per-model charts, trend charts, the outlook table, the
     narrative - reads as the same family of surface instead of some
     having a shadow and others not. */
  --shadow-card: 0 1px 3px rgba(10, 37, 64, 0.06), 0 1px 2px rgba(10, 37, 64, 0.04);

  /* 2026-08-17 (Phase 7e layout/density pass, continued) - the 8/03
     round below deliberately stopped at the page's biggest structural
     rhythm and left "component-internal spacing (data-grid gaps, card
     padding...) untouched for now." This round picks that up: page-
     section 32px -> 24px, chart-section 20px -> 16px (--space-6 stays
     24px but isn't actually consumed by any rule right now - kept
     rather than renumbered, so this diff doesn't ripple into values
     nothing currently reads), plus .data-card padding, .data-grid gap,
     .outlook-table cell padding, and .narrative padding all tightened
     alongside this (see each rule's own updated value below). Chart
     heights, the hourly table, and the By Model chart frame are
     deliberately left alone again - all three went through several
     rounds of live-tuned bug fixes (sticky-column z-index, gradient
     clamping, gridline bleed) earlier this project, so they're a
     separate, higher-risk pass, not part of this spacing-only round. */
  /* Spacing scale (Phase 7e layout/density pass, 2026-08-03) - the
     page's structural vertical rhythm (section gaps, card gaps,
     section-header spacing) had grown as a set of ad hoc pixel values
     picked feature-by-feature (36, 24, 20, 16, 14, 10...) with no
     documented relationship between them. Named here as a real scale
     so future spacing decisions pull from it instead of inventing a
     new number, per Build Sequence's Phase 7e call for an actual
     "typography system," not just more one-off polish. This first
     round only touches the page's biggest structural rhythm - section
     and chart-section spacing - not every margin in the file;
     component-internal spacing (data-grid gaps, card padding, icon
     sizes, button padding) stays untouched for now, on purpose, so
     this stays a small, reviewable change rather than a blind
     full-file pass. Two deliberate tightenings included as part
     of adopting this scale, not just a renaming exercise: page-section
     36px -> 32px, chart-section 24px -> 20px - the "This Week" section
     in particular stacks 3-4 full bordered/shadowed blocks (Wind
     Trend, Seas Trend, Confidence Trend, 7-Day Outlook) back to back,
     and that's the densest, most repetitive part of the page; a
     slightly tighter rhythm there reads less like 4 separate floating
     cards and more like one coherent section, without going so far as
     to change any card's own internal padding or the trend charts'
     heights. */
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 16px;
  --space-6: 24px;
  --space-7: 24px;
}

* { box-sizing: border-box; }

/* Typography: tabular figures for every real data readout (7e branding
   pass, 2026-08-02) - wind/seas values, chart values, the AI estimate,
   confidence scores, and the outlook table. Without this, digits in
   the system font are proportionally spaced (a "1" is narrower than a
   "8"), so numbers in a column - the outlook table especially - don't
   line up and columns visually "jitter" row to row. Every marine
   forecast site this project's been benchmarked against (PredictWind,
   Windfinder) uses fixed-width digits for exactly this reason. Applied
   directly via font-variant-numeric rather than a class, since it's a
   property of WHAT the content is (a number), not a reusable visual
   style - every current and future numeric readout should get this
   automatically by being one of these selectors, not by remembering
   to add a class. */
.data-card .value,
.chart-value,
.estimate-range,
.confidence-badge,
.outlook-table td {
  font-variant-numeric: tabular-nums;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
  margin: 0;
  line-height: 1.55;
}

.site-header {
  background: linear-gradient(135deg, var(--navy), var(--deep-blue));
  color: #fff;
  padding: 22px 24px 30px;
}

.site-header .inner {
  max-width: 720px;
  margin: 0 auto;
}

.site-header .brand-row {
  display: flex;
  align-items: center;
  gap: 9px;
}

/* 2026-08-04, per Caleb: swapped the placeholder circle/diamond SVG
   for his real AI-generated compass-and-wave mark (static/logo-icon.png,
   the navy-background variant, background color-keyed to transparent
   so it blends into this header's own navy gradient without a visible
   square edge). It's a raster illustration with real detail (dashed
   compass ticks, shaded wave layers) rather than a flat currentColor
   glyph, so sized up from the placeholder's 22px to 38px - big enough
   for that detail to actually read, still modest next to the 22px
   wordmark text next to it. */
.site-header .brand-icon {
  width: 38px;
  height: auto;
  flex-shrink: 0;
}

/* 2026-08-17, Phase 7e wordmark treatment: previously the wordmark was
   just bold system-sans text - identical typeface to every data
   readout and label on the page, so nothing marked it as a designed
   brand mark rather than a heading. First version also split "Ocean"/
   "Oracle" into an upright-white + italic-accent-blue two-tone pair -
   Caleb's live reaction: the italic read as a second, different font
   rather than a deliberate accent, and he wanted one consistent color.
   Simplified to just the one real change that actually reads as
   "designed wordmark": a real display serif (Fraunces, loaded in each
   template's <head>) instead of the system sans, both words upright,
   same weight, same white (matches the rest of the header's text
   color). Body text, data values, and every other label on the page
   are still untouched - this is a logotype pairing, not a site-wide
   font swap. The markup keeps its two <span class="brand-word"> split
   (harmless, no visual difference now) rather than collapsing back to
   one text node, in case a future distinct-but-not-italic treatment
   wants that hook again. */
.site-header .brand {
  font-family: 'Fraunces', Georgia, 'Times New Roman', serif;
  font-size: 25px;
  font-weight: 600;
  letter-spacing: 0.1px;
}

.site-header .tagline {
  font-size: 14px;
  color: #cfe0f0;
  margin-top: 2px;
}

/* 2026-09-29, per Caleb: "add in somewhere small on the website view
   and mobile view to show when the data was last updated as well as
   next expected refresh." Placed inside the header's own .inner (same
   element .brand-row/.tagline already live in), not the map/scrollable
   content below - the LAST time something sat below the fold on this
   page (the old legend/last-refreshed strip, removed 2026-09-25) it got
   cut off on an iPhone with the page unable to scroll to reach it. The
   header always renders at its natural size regardless of viewport
   height (body.map-page-body's flex:0 0 auto rule below already proves
   this for map.html), so this can never repeat that bug on either page.
   Deliberately smaller/dimmer than .tagline (11px vs 14px, a more muted
   blue) - genuinely secondary status text, not a second tagline
   competing for the same visual weight. */
.site-header .data-refresh-note {
  font-size: 11px;
  color: #9ab6d1;
  margin-top: 5px;
  line-height: 1.4;
}

/* 2026-09-25 (mobile round 3), per Caleb: "take the 'List View' text
   option and change it by creating a small pop up button with a back
   arrow similar to how PredictWind is set up, and place it in the dark
   blue banner where it labels Ocean Oracle." Scoped to the map page's
   own header (site-header-map, set only in map.html) rather than the
   shared .site-header rule itself - forecast.html/index.html's headers
   don't have anything to go back TO from themselves, so this button
   only makes sense here. */
/* 2026-09-25 (mobile round 4, same day), real bug Caleb caught live:
   "the new back button is in the same location as the Ocean Oracle
   label and overlaps." The button sits absolutely positioned at the
   header's own top-left corner (left:18px below), which is exactly
   where .brand-row's icon+wordmark also start (.site-header's own
   24px left padding puts them almost in the same spot) - the button
   was floating ON TOP of the logo instead of beside it. Fixed by
   pushing this header variant's own left padding out past the
   button's full width+gap (40px wide + 18px inset + ~8px clearance),
   so .inner's content starts clear of the button instead of behind
   it - a spacing fix, not a stacking/z-index one, since the real
   problem was two things occupying the same horizontal space, not one
   incorrectly rendering above/below the other. */
.site-header-map {
  position: relative;
  padding-left: 66px;
}

.map-header-back {
  position: absolute;
  top: 18px;
  left: 18px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  text-decoration: none;
  z-index: 2;
}

.map-header-back:active {
  background: rgba(255, 255, 255, 0.3);
}

/* Small wave divider (2026-07-29 aesthetic pass) between the header
   and the page - a light, literal nod to "marine forecast app"
   without adding any actual illustration weight. The svg's path fill
   is set to the page background color in the rule below it, so the
   header's gradient reads as if it dips into scalloped waves right at
   the page's own background, not a hard rule. */
.header-wave {
  display: block;
  line-height: 0;
  margin-top: -1px;
}

.header-wave svg { display: block; width: 100%; height: 16px; }
.header-wave path { fill: var(--bg); }

.page {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px 24px 48px;
}

/* Picker page */

.location-list { margin-top: 8px; }

.location-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  padding: 20px 22px;
  margin-bottom: 14px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  text-decoration: none;
  color: var(--text);
  box-shadow: var(--shadow-card);
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}

.location-card:hover {
  border-color: var(--ocean-blue-light);
  box-shadow: 0 4px 14px rgba(15, 52, 96, 0.1);
  transform: translateY(-1px);
}

/* 2026-08-17 (Phase 7e mobile refinement) - real bug Caleb caught live
   on his phone: longer location names ("The Nipple (Pensacola
   Offshore)", "USS Oriskany (Offshore Pensacola)") rendered with the
   pin star sitting on top of their own text, and "Buoy 42028 (Offshore
   Navarre Beach)" - long enough to wrap to a second line - looked
   broken/taller than every other card instead of just cleanly
   wrapping. Root cause: .pin-btn below is position:absolute at a fixed
   right:56px, completely outside this row's own flexbox calculation
   (name/arrow only) - nothing ever told .name to leave room for it, so
   at narrow widths (where names are more likely to run close to the
   card's full width) the text runs right underneath the star instead
   of wrapping before it. padding-right reserves that space: the star's
   icon+padding box spans roughly right:41px-71px from the card's own
   right edge, so 40px of padding-right here pushes .name's own wrap
   boundary comfortably past the star's far edge - name text now always
   wraps (or ends) clear of the star, on every card, not just the ones
   with names long enough to make it obvious. */
.location-card .name { font-size: 18px; font-weight: 600; padding-right: 46px; }
.location-card .arrow { color: var(--ocean-blue); font-size: 20px; }

/* Pinned locations (7d-7, 2026-08-02) - the star sits absolutely
   positioned over the card's right edge, just left of the arrow, as a
   sibling of the <a> rather than nested inside it (a <button> nested
   inside an <a> is invalid HTML and behaves unpredictably across
   browsers). Because it's stacked above the anchor via z-index, clicks
   on the star never reach the link underneath - no preventDefault
   gymnastics needed beyond what pinned.js already does for safety. */
.location-row { position: relative; }

/* 2026-09-25 (mobile refinement round 2) - real tap-target gap found
   auditing against Apple/Google's own 44px minimum touch-target
   guideline: this button's actual hit area was only 18px icon + 6px
   padding on each side = 30x30px, well under that minimum, on the one
   control someone's most likely tapping one-handed on a moving boat.
   Bumped padding to 13px (44x44px total, right at the guideline) -
   grows the button's LEFT edge further into the card (right:56px stays
   fixed, so the box only grows leftward, not into the arrow's space on
   the right) - .name's own padding-right above bumped 40px -> 46px as
   a small extra safety margin against that, though the two were never
   a tight pixel-exact collision calculation to begin with (see that
   rule's own 2026-08-17 comment - .pin-btn is absolutely positioned
   outside the flex layout .name/.arrow actually participate in, so
   padding-right there is a buffer, not a live collision boundary). */
.pin-btn {
  position: absolute;
  top: 50%;
  right: 56px;
  transform: translateY(-50%);
  z-index: 2;
  background: none;
  border: none;
  padding: 13px;
  margin: 0;
  cursor: pointer;
  color: var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 0;
}

.pin-btn .pin-icon {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
  transition: fill 0.15s ease, stroke 0.15s ease, transform 0.15s ease;
}

.pin-btn:hover .pin-icon { stroke: var(--ocean-blue); transform: scale(1.1); }

.pin-btn.pinned .pin-icon {
  fill: var(--ocean-blue);
  stroke: var(--ocean-blue);
}

.location-section-heading {
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-muted);
  margin: 20px 0 10px;
}

.location-section-heading:first-child { margin-top: 0; }

.error-banner {
  color: var(--low);
  background: var(--low-bg);
  padding: 12px 16px;
  border-radius: 8px;
  margin-bottom: 20px;
  font-size: 14px;
}

.note {
  color: var(--text-muted);
  font-size: 13px;
  margin-top: 28px;
}

/* 2026-09-25, map view only, per Caleb ("a banner that just says 'last
   refreshed (last refresh time)'"): deliberately smaller/tighter than
   .note above - this replaces map.html's old disclaimer paragraph, and
   part of the point of the swap was reclaiming vertical space for the
   mobile no-scroll goal, not just changing the words. */
.refresh-banner {
  color: var(--text-muted);
  font-size: 12px;
  margin-top: 10px;
  text-align: center;
}

/* Forecast page */

.back-link {
  display: inline-block;
  color: var(--ocean-blue);
  text-decoration: none;
  font-size: 14px;
  margin-bottom: 14px;
}

.forecast-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px 16px;
  margin-bottom: 24px;
}

.forecast-header h1 {
  font-size: 28px;
  margin: 0;
}

.confidence-badge {
  display: inline-block;
  padding: 7px 16px;
  border-radius: 999px;
  font-weight: 600;
  font-size: 14px;
}

/* Sits inline in .forecast-header now (location name + confidence
   badge on one row) rather than stacked below the heading - the two
   most important "so what" facts on the page read together instead of
   the badge feeling like an afterthought underneath. */
.forecast-header .confidence-badge { margin-bottom: 0; }

/* 2026-09-23, per Caleb's manual Refresh button (Phase 8) - a quiet
   secondary control sitting next to the confidence badge, not
   competing with it for attention. Uses the same ocean-blue accent
   the rest of the page's interactive elements already use. */
.refresh-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  border-radius: 999px;
  border: 1.5px solid var(--ocean-blue);
  background: var(--card-bg);
  color: var(--ocean-blue);
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.refresh-btn:hover:not(:disabled) {
  background: var(--ocean-blue);
  color: #ffffff;
}

.refresh-btn:disabled {
  cursor: default;
  opacity: 0.7;
}

.refresh-btn-loading {
  opacity: 0.85;
}

/* 2026-08-05 rework - 6 tiers now (was 5): conf-excellent is new (the
   scale's new top band), conf-uncertain is renamed conf-none (matching
   confidence.py's label rename "Highly Uncertain" -> "No Confidence").
   All 6 now read from the dedicated --conf-* vars above, not the
   generic --high/--good/--moderate/--low/--uncertain ones.

   2026-08-06, per Caleb ("add dark borders to the other pop ups on the
   page... darker version of the color the confidence badge is
   showing"): each tier now also draws a 1.5px border in its own
   darker --conf-*-border shade (see those vars' own comment above) -
   previously these badges had no border at all, just a flat color
   fill. */
.conf-excellent { background: var(--conf-excellent-bg); color: var(--conf-excellent); border: 1.5px solid var(--conf-excellent-border); }
.conf-high { background: var(--conf-high-bg); color: var(--conf-high); border: 1.5px solid var(--conf-high-border); }
.conf-good { background: var(--conf-good-bg); color: var(--conf-good); border: 1.5px solid var(--conf-good-border); }
.conf-moderate { background: var(--conf-moderate-bg); color: var(--conf-moderate); border: 1.5px solid var(--conf-moderate-border); }
.conf-low { background: var(--conf-low-bg); color: var(--conf-low); border: 1.5px solid var(--conf-low-border); }
.conf-none { background: var(--conf-none-bg); color: var(--conf-none); border: 1.5px solid var(--conf-none-border); }

/* 2026-08-18, per Caleb ("decrease the size of the Wind/Seas boxes just
   a hair, then add Precipitation next to them in the top row"): 3
   columns instead of 2 now that Precipitation is a top-level .data-grid
   child alongside Wind/Seas (see forecast.html's own comment on that
   move). Tried 5fr/5fr/3fr first (Wind/Seas keeping most of their old
   width, Precipitation taking a narrower third column), but Caleb
   ("can we have the three boxes up top match the dimensions of the
   ones below? That way it looks symmetrical") wanted this top row's 3
   boxes the same width as .currents-row's own 3 boxes below - even
   1fr/1fr/1fr instead. For the two rows' column edges to actually line
   up, not just each independently be "3 equal columns," .currents-row's
   own gap had to drop from 14px to this rule's 12px too (see that rule
   below) - same box count, same total row width, same gap now produces
   identical column widths whether split via grid-template-columns here
   or flex:1 there. .currents-row below still spans all 3 columns via
   grid-column:1/-1, unaffected by the column split above it (it lays
   out its own children with flexbox, not this grid). */
.data-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 12px;
  margin-bottom: 18px;
}

/* Groups the page into clearly labeled sections (2026-07-29 aesthetic
   pass) - "Right Now" (current wind/seas/current cards + per-model
   charts) and "This Week" (trend charts + outlook table). The page had
   grown into one long undifferentiated stack of chart-sections as
   Phase 7c added more content; these headers give the eye a place to
   land and make it obvious at a glance which content answers "right
   now" vs. "over the coming week." */
.page-section { margin-bottom: var(--space-7); }

.page-section-header {
  margin-bottom: var(--space-4);
  padding-bottom: var(--space-3);
  border-bottom: 2px solid var(--border);
}

.page-section-header .eyebrow {
  display: block;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--ocean-blue);
  margin-bottom: 2px;
}

.page-section-header h2 {
  font-size: 21px;
  font-weight: 700;
  color: var(--navy);
  margin: 0;
}

.data-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  box-shadow: var(--shadow-card);
}

/* Right Now Wind/Seas cards, severity-tinted (2026-08-03, per Caleb -
   "make the color scheme similar to what the confidence box shows...
   filled with a much lighter/faded version of the text showing the
   actual number"). forecast.html now puts the SAME sev-* class already
   used on the inner .value/.sev-dot (severity.py's wind_speed_band()/
   seas_height_band(), the exact bands driving every other severity
   color on this page) onto the outer .data-card div too - one class,
   two places, no separate color decision to keep in sync. Backgrounds
   are each band's real severity color lightened 85% toward white
   (hand-computed with the same lighten_hex() math severity.py already
   uses for the Trend charts' "headroom" tint, just run once here since
   these are static CSS colors, not per-request server data) - a light
   wash of the SAME hue as the bold number, not an unrelated pastel.
   Border is a plain dark neutral gray for "clear distinction" per
   Caleb's ask, not per-severity-colored - reuses #7d8797, the same
   dark gray already established as this page's divider/gridline color
   elsewhere (hourly strip, Trend chart column dividers). Only applies
   when a card actually HAS real data (forecast.html only adds the
   class when wind_max_kt/seas_max_ft is not None) - the "not enough
   sources" fallback card stays plain, nothing to color-code. */
/* 2026-08-04 rework - wind and seas moved to two independent tier
   systems (7 wind tiers, 5 seas tiers - see WIND_BANDS_MPH/
   SEAS_BANDS_FT in severity.py), so each metric now has its own class
   set instead of sharing one sev-good/more-severe/severe-high/worst
   palette. Tints are each tier's own base hex (see .value.sev-wind-*/
   .value.sev-seas-* below) lightened toward white by the same ~0.85
   blend severity.lighten_hex() already uses elsewhere on this page -
   several of these tints intentionally reuse the exact old sev-good/
   more-severe/severe-high/worst values since those tiers kept the same
   blue/orange/red/grey base colors. */
/* 2026-08-04, per Caleb (live catch): the original #dde9f2 tint here
   was nearly indistinguishable from the page's own --bg (#f4f7fa) -
   same hue family, both very light, so the "Excellent" card read as
   plain white even though the class/rule were both wired correctly
   (the .value text/icon color a few hundred lines down still showed
   the right blue - only this background lacked contrast). Bumped to a
   more saturated blue (previewed against the page bg alongside two
   options; Caleb picked this one, "option B") that reads clearly
   filled, roughly matching how distinctly sev-wind-great's mint
   already stood out. */
/* 2026-08-05, live bug fix (Caleb: "the wave height is not colored
   correlating to what the sea reading is"): seas' "Excellent" tier
   used to share this exact blue with wind's own "Excellent" tier -
   both #c3ddf3 bg / #1b6ca8 text - but #1b6ca8 is also literally
   --ocean-blue, the same hue used everywhere else on the page as
   generic UI chrome (nav links, buttons, arrows). On the Seas card
   specifically that made the "colored" text read as just more of the
   page's regular blue rather than a distinct severity signal - unlike
   Wind, which never has this problem because its own "Excellent" isn't
   the tier its typical readings land in (its live default look uses
   the very different-looking teal "Great" tier instead). Split seas
   off its own, more saturated variant (previewed against 2 other
   options, Caleb picked this one - same choose-from-options process as
   the original #c3ddf3 bump documented below) so it reads as clearly
   colored on its own rather than piggybacking on wind's rule. Wind's
   own #c3ddf3/#1b6ca8 pair is untouched. */
.data-card.sev-wind-excellent { background: #c3ddf3; border-color: #7d8797; }
.data-card.sev-seas-excellent { background: #a9d1ef; border-color: #7d8797; }
.data-card.sev-wind-great                                    { background: #ddf0ee; border-color: #7d8797; }
/* 2026-08-06, per Caleb - the new Fair band (10-12 mph, Favorable
   tier; see severity.py's WIND_BANDS_MPH comment): a green tint
   sitting between Great's mint and Moderate's yellow-green, keeping
   the calm->rough hue progression readable at a glance. */
.data-card.sev-wind-fair                                     { background: #e2f2e4; border-color: #7d8797; }
.data-card.sev-wind-moderate, .data-card.sev-seas-moderate    { background: #ebf5e2; border-color: #7d8797; }
.data-card.sev-wind-caution                                   { background: #fcf6e3; border-color: #7d8797; }
.data-card.sev-wind-rough, .data-card.sev-seas-rough          { background: #faebdd; border-color: #7d8797; }
.data-card.sev-wind-hazardous, .data-card.sev-seas-hazardous  { background: #f6dfdd; border-color: #7d8797; }
.data-card.sev-wind-extreme, .data-card.sev-seas-extreme      { background: #dfdfdf; border-color: #7d8797; }

.data-card .label-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.data-card .label {
  font-size: 11px;
  text-transform: uppercase;
  /* 2026-08-03, per Caleb: darken to match the same dark text color
     already used on the 7-Day Trend charts' axis labels (charts.js's
     TICK_COLOR) and the hourly strip's row labels (var(--text),
     "#16233a") - was var(--text-muted) (gray), reading washed-out next
     to the rest of the page's now-consistently-dark text. */
  color: var(--text);
  letter-spacing: 0.6px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Consistent iconography per data type (7e branding pass, 2026-08-02) -
   Wind/Seas/Current each get a small, simple line icon (wind lines,
   stacked wave, flowing arrow) matching the same stroke-based visual
   language as the header brand mark, rather than staying plain text
   labels. currentColor so each inherits .label's existing muted color -
   no separate color decision to keep in sync. */
.label-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

/* 2026-08-06, per Caleb ("incorporate a graphic that shows the current
   moon phase inside of the box that shows the ocean current speed and
   direction?"): the moon block sits beside the Current card's original
   content, not inside its .label-row.

   Went through a few layouts before landing here: inline in the label-
   row at 30px -> a separate two-column layout at 96px (grew the card
   taller than every other Right Now card) -> back to one inline label-
   row at 44px (kept the card's original height, but that same "inline"
   placement is what capped how big the icon could get - anything
   bigger inflates the row, and the card, again).

   2026-08-06 final update, per Caleb ("increase the size of the moon,
   while keeping the...box the same size as it currently is"): back to
   a two-column row (.currents-card > .currents-main + .moon-phase),
   which is what actually breaks that tradeoff - .currents-main's own
   label-row + value define the card's height by themselves (the same
   content that determined it before the moon feature existed at all),
   and .moon-phase sits next to that column instead of inside it, so
   the icon can grow without touching that height calculation. 72px is
   sized to land just under .currents-main's own natural height.

   2026-08-06 update, per Caleb ("align the moon phase and last quarter
   text to the top of the box in line with the 'Current' icon and
   text"): both columns were align-items:center (vertically centered
   against each other, which centered .moon-phase - the taller of the
   two - down past .currents-main's own top). Switched both the row and
   .moon-phase itself to flex-start so "MOON PHASE:" starts at the same
   line as "Current" instead of the row's vertical middle.

   2026-08-06 update, per Caleb ("format this [precipitation]
   information...to be in the center of the current and moon phase
   box... keep the box the same size as it is now"): switched from a
   2-item flex row to a 3-column CSS grid (1fr auto 1fr). Plain
   flexbox with a 3rd item inserted between the other two wouldn't put
   it at the box's true center - .currents-main's flex:1 absorbs all
   the row's free space itself, so a middle sibling just ends up
   wherever that leaves it, not at the geometric middle of the card.
   Grid's outer 1fr columns each flex equally regardless of their own
   content width, so the middle `auto` column (.weather-mini) is
   always the real horizontal center of the box, whether or not
   Current/Moon Phase's own content is exactly symmetric. align-items:
   start carries over unchanged from the flex-start fix above - same
   "top-aligned, not vertically centered" behavior, just expressed as
   grid's equivalent value. */
/* 2026-08-06 update, per Caleb ("the ocean current speed/direction AND
   the weather icon/percent/label...the same distance from the bottom
   of the...window as the three heading text are from the top"): was
   align-items:start, which top-aligns each column at its own natural
   height - moon-phase's 80px icon is the tallest, so it alone reaches
   .data-card.currents-card's padding-bottom, while the shorter Current/
   Precipitation columns' own content stopped well short of it (nothing
   pushing them down to match). align-items:stretch makes every column
   fill the row's full height (still set by the 80px moon icon), so
   Current's/Precipitation's own two-piece column layout (header top,
   content bottom - see their justify-content:space-between below) can
   push their bottom content flush to that same shared floor, the same
   distance from padding-bottom that the headers already sit from
   padding-top. */
/* 2026-08-06, per Caleb ("instead of one large box...reformat these
   three readings into three separate boxes...leave the pop up window
   sizing the same as is"): what used to be ONE .data-card.span-2.
   currents-card (an internal 3-column grid holding all three readings)
   is now a plain flex row (.currents-row - no border/background/
   padding of its own, still grid-column:1/-1 to span both .data-grid
   columns the same way .span-2 did) around three separate .data-card.
   currents-mini-card boxes. Flexbox's default cross-axis behavior
   (align-items:stretch, unset here so it falls through to that
   default) makes all three boxes exactly as tall as the tallest one
   (Moon Phase's 80px icon + this class's own 12px top/bottom padding),
   which is what keeps the overall footprint identical to the old
   single box's - same total height, same total width (flex:1 on each
   splits that width three ways, gap:14px between them matching
   .data-grid's own card gap). */
.currents-row {
  grid-column: 1 / -1;
  display: flex;
  /* 2026-08-18, per Caleb ("have the three boxes up top match the
     dimensions of the ones below... symmetrical"): dropped from 14px to
     match .data-grid's own 12px gap above - see that rule's comment for
     why the two rows' gaps have to match for their column widths to
     actually line up, not just each be "3 equal columns" independently. */
  gap: 12px;
}

.currents-mini-card {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* Same gray tint/border/reduced padding .data-card.currents-card
     used to apply once around all three readings - restated per-box
     now (see git history for that old combo-class rule). */
  background: #e7e9ec;
  border-color: #7d8797;
  padding-top: 12px;
  padding-bottom: 12px;
}

.currents-main {
  display: flex;
  flex-direction: column;
  /* 2026-08-06: was justify-content:center - with the column now
     stretched to the full row height (see .currents-mini-card above),
     center would float the label-row/value pair in the middle instead
     of pinning the value to the bottom to match the other two
     columns' own bottom-aligned content. space-between pushes the
     label-row to the top (flush with the other headers) and the value
     to the bottom (flush with Precipitation's icon/text and Moon
     Phase's icon). flex:1 makes this fill its own .currents-mini-card
     box's full stretched height, so there's actually room for
     space-between to distribute.
     2026-08-06 later update, per Caleb ("space the 'Heading: From
     110°' text to be the same distance from the speed measurement as
     Wind/Seas own subtext"): value and sub used to be TWO separate
     direct children here, so space-between put a gap both above AND
     below the value line, stretching the value-to-sub gap way past
     Wind's/Seas's own plain-margin spacing. They're now wrapped
     together as one .currents-value-group child (see forecast.html),
     so space-between only ever opens a gap above that group - the
     value-to-sub gap inside it is back to the shared .data-card
     .value/.sub margins, matching Wind/Seas exactly.
     2026-08-06 final update, per Caleb ("move this grouped text up so
     the top of the speed text is the same distance from 'CURRENT' as
     the speed text is from 'WIND'"): space-between was still opening a
     gap ABOVE .currents-value-group too (pushing it down away from
     .label-row to land at the box's bottom, flush with Moon Phase's
     icon) - that's what made the label-to-value gap bigger than
     Wind's/Seas's own plain margin-top gap. flex-start removes that
     gap entirely, so .currents-value-group now sits directly under
     .label-row with only its own .data-card .value margin-top:6px in
     between - identical gap to Wind's/Seas's own header-to-value
     spacing. The stretched box's leftover height (from matching Moon
     Phase's 80px icon) now just sits as blank space below the value
     group instead of being distributed into it - see .weather-mini-body
     below for the matching change that keeps Precipitation's icon/text
     at this same new higher position. */
  justify-content: flex-start;
  flex: 1;
  min-width: 0;
}

/* 2026-08-06, per Caleb (see .currents-main's own comment above): pure
   grouping wrapper, no layout rules of its own needed - value/sub stack
   as plain block children via their own .data-card .value/.sub
   margin-top, exactly like they already do inside Wind's/Seas's own
   (non-flex) .data-card. Existing only so .currents-main's space-between
   treats value+sub as one unit instead of two. */
.currents-value-group {
  min-width: 0;
}

/* 2026-08-06, per Caleb ("center the moon [icon] in the box the same
   way we just centered the icon and information in the precipitation
   box"): was a single ROW (text column beside the icon). Now a COLUMN,
   same shape as .weather-mini - .moon-phase-header stays top-left
   (matching Current's/Precipitation's own headers), .moon-phase-body
   below holds the icon + phase name and gets centered as its own
   group (see that rule). */
.moon-phase {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  flex: 1;
}

/* 2026-08-17, per Caleb ("bring the moon icon to the left so it is in
   line with every other pop up"): was centered (both horizontally via
   justify-content:center and vertically via flex:1 - see the removed
   comment above for that history), which read fine back when Moon
   Phase was a narrow column squeezed between Current and Precipitation
   in one shared box, but stood out once it became its own full-width
   standalone card next to Wind/Seas/Current, all of which sit flush
   left under their own headers. .weather-mini-body (right above) hit
   this exact same problem with Precipitation earlier and was already
   fixed the same way - flex-start instead of center, and flex:1
   dropped so the icon+name group sits at its own natural height
   directly under .moon-phase-header (matching .weather-mini's own
   header-to-body gap) instead of floating in the vertical center of
   whatever leftover height the box happens to have. align-items:center
   is kept, same as .weather-mini-body - that only centers the icon
   against the phase-name text within their own short row, not the
   whole box, so it stays harmless. */
.moon-phase-body {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  align-self: stretch;
  gap: 10px;
}

/* 2026-08-06, per Caleb ("format this information and design to be in
   the center of the current and moon phase box"): the precipitation
   mini-widget, centered in .currents-card's middle grid column. Icon-
   over-text stack (not the label-row/value/sub shape the standalone
   Right Now card used) - compact enough to fit inside the box's
   existing height budget (set by the taller of Current's own content
   and the 80px moon icon) without growing the card. */
.weather-mini {
  display: flex;
  flex-direction: column;
  /* Header stays left-aligned (matching Current's/Moon Phase's own
     headers) - only .weather-mini-body below gets centered now (see
     that rule's own align-self:stretch + justify-content:center). */
  align-items: flex-start;
  gap: 6px;
  /* 2026-08-06 update, per Caleb ("increase the size of the sun icon/
     precipitation %/label, and center it in its...box"): now that
     Precipitation is its own separate box (not squeezed between
     Current and Moon Phase anymore - see .currents-mini-card above),
     there's real room to make the icon+text group the visual focus of
     the box instead of just matching Current's/Moon Phase's own
     top+bottom-pinned two-line shape. Header stays flush at the top
     (unchanged, still matches its siblings' headers); justify-content:
     flex-start (was space-between) lets .weather-mini-body's own
     flex:1 do the vertical centering instead (see that rule), rather
     than pinning it to the box's bottom edge. */
  justify-content: flex-start;
  /* Fills its own .currents-mini-card box's full stretched height -
     same reasoning as .currents-main's own flex:1 above. */
  flex: 1;
}

/* 2026-08-06, per Caleb ("include a small rain icon and header that
   says 'PRECIPITATION' or 'PRECIP' depending on the room available"):
   restates .data-card .label's own uppercase/bold/11px/letter-spacing
   values directly (rather than relying on that class's cascade,
   which only reaches elements inside a real .label-row) so this
   header matches "Current"'s look exactly. */
.weather-mini-header {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  text-transform: uppercase;
  color: var(--text);
  letter-spacing: 0.6px;
  font-weight: 600;
  white-space: nowrap;
}

/* Full "Precipitation" by default; swaps to "Precip" below the
   .currents-card-tight breakpoint (see the media query at the bottom
   of this file) once three columns squeezed into one row start
   running short on width. */
.weather-mini-heading-short {
  display: none;
}

/* 2026-08-06, per Caleb ("take the '3%' and 'Clear' text and place it
   to the right of the weather icon... in line with the floor and
   ceiling of the icon"): icon + text side by side, not stacked - same
   "icon beside its own text" shape as Moon Phase's icon+.moon-phase-
   text.

   2026-08-06 update, per Caleb ("make the '4%' and 'Clear' text come
   before the sun"): DOM order swapped in forecast.html (text span
   first, icon span second) - this row's own CSS (row-direction flex,
   no explicit ordering) didn't need to change, just which element
   comes first in markup. */
/* 2026-08-06, per Caleb ("increase the size...and center it in its
   respective box"): was pinned to the box's bottom-left corner (a
   leftover from when this had to coexist with Current/Moon Phase in
   one shared box). Now that Precipitation is its own box, this row:
   1. Fills the leftover vertical space below the header via flex:1
      (inherited from .weather-mini's own justify-content:flex-start
      above giving this room to grow into), with align-items:center
      centering the icon+text vertically within that space.
   2. Fills the box's full WIDTH via align-self:stretch, then
      justify-content:center centers the icon+text horizontally too -
      align-self:stretch is what makes centering possible at all here;
      without it this row would just shrink-wrap its own content width
      (inherited from .weather-mini's align-items:flex-start) and
      "centering" inside that tight wrapper would do nothing visible.
   2026-08-06 later update, per Caleb ("align the weather icon and text
   in line with the water droplet icon in the header"): swapped
   justify-content from center back to flex-start - align-self:stretch
   is kept (still needed so this row spans the box's full width, same
   reason as above) but content now starts flush at that row's own left
   edge instead of centering within it, which lines the icon up with
   .weather-mini-header's own raindrop icon directly above it (both
   start at .weather-mini's shared left edge).
   2026-08-06 final update, per Caleb ("ensure the sun icon and
   percentage text stay in line with the speed text in the ocean
   current box" - after Current's own value moved up flush under its
   header, see .currents-main's own comment): dropped flex:1, which
   used to grow this row into ALL the box's leftover stretched height
   so align-items:center could vertically center it there. Without
   flex:1, this row just sits at its own natural content height
   directly below .weather-mini-header (separated only by .weather-
   mini's own gap:6px) - the same "flush under the header" position
   Current's value now sits at, so the sun icon/%/label line up
   horizontally with "0.6 mph" again. align-items:center is kept
   (harmless now - just centers the icon against the text within this
   row's own short height, not the whole box) since removing it would
   change nothing visible. */
/* 2026-08-18, per Caleb ("lower the '11%' and cloud icon to be in line
   with the larger wind speed/sea height text... take 'Overcast' and
   make it in line with direction/swell interval text"): was a single
   flex ROW (align-items:center) holding the icon vertically centered
   beside a 2-line pct/condition text stack (.weather-mini-text) - now a
   flex COLUMN of two separate blocks (.weather-mini-value-row, then
   .weather-mini-condition directly), the same two-block shape as
   Wind's/Seas's own .data-card .value + .sub, so each block's own
   margin-top can be tuned to match its Wind/Seas counterpart's real
   vertical position instead of just centering as one group. */
.weather-mini-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  align-self: stretch;
}

/* Pct + icon on one line now, mirroring Wind's/Seas's own value+sev-icon
   line (see .sev-icon above) - margin-top:6px matches .data-card
   .value's own offset under its header exactly, so "11%" starts at the
   same height "6-11 mph"/"1-3 ft" do under their own (same-shaped)
   headers. */
.weather-mini-value-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
}

/* 2026-08-06, per Caleb ("increase the size of the sun icon"): grew
   through several rounds up to 44px when this icon was the standalone
   visual focus of its own centered icon+text group.
   2026-08-18 update, per Caleb (see .weather-mini-body's own comment):
   now that the icon sits inline directly beside the 22px pct text
   (Wind's/Seas's own shape - see .sev-icon svg's 20px), 44px would
   swamp that line - shrunk to 22px, matching the pct text's own
   font-size 1:1, the same proportion Wind/Seas keep between their
   value text and .sev-icon.
   2026-08-18 later update, per Caleb ("increase the size of the cloud
   icon"): 22px read a little small next to the bold 22px pct text -
   bumped to 28px, still comfortably inside the value row's own real
   line-height (22px font's inherited 1.55 line-height is ~34px) so it
   doesn't overhang above/below the text or push the row taller. */
.weather-mini-icon svg {
  width: 28px;
  height: 28px;
  display: block;
}

/* 2026-08-06, per Caleb ("increase the size of the...precipitation
   %"): was 15px, matched to the old cramped shared-box layout - bumped
   to 22px, the same size as .data-card .value uses for Current's own
   headline number, so Precipitation's own headline reads with equal
   visual weight now that it has a full box to itself.
   2026-08-18 update, per Caleb (see .weather-mini-body's own comment):
   dropped the line-height:1 override - .data-card .value (Wind's/
   Seas's own 22px line) never had one, so keeping it here was shrinking
   this line's real height below theirs and throwing off the vertical
   rhythm .weather-mini-condition's own margin-top depends on below.
   Same "line-height:1 quietly steals leading a sibling's margin-top
   assumes is still there" issue .wind-direction-sub-text's own comment
   already documents - see that rule for the fuller explanation. */
.weather-mini-pct {
  font-size: 22px;
  font-weight: 700;
  color: var(--navy);
  white-space: nowrap;
}

/* 2026-08-06, per Caleb ("increase the size of the...day label" -
   "Clear"/"Rain"/etc.): was 10px - bumped to 12.5px, matching
   .data-card .sub's own size (the "Swell period: 6s" line under Seas),
   so this reads as a normal sub-label instead of fine print.
   2026-08-18 update, per Caleb ("take 'Overcast'... make it in line
   with direction/swell interval text"): dropped the line-height:1.3
   override (same "quietly steals leading" issue as .weather-mini-pct's
   own comment above - Seas's own plain .sub, which this needs to match,
   has no line-height override either) and added margin-top:3px,
   matching .data-card .sub's own margin-top exactly. With
   .weather-mini-pct above no longer shrunk by its own line-height
   override either, this now lands at the same vertical position
   Seas's "Swell period: 4s" sits at. */
.weather-mini-condition {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
  margin-top: 3px;
}

/* 2026-08-06, per Caleb ("add a small crescent moon icon to the left
   of the text 'MOON PHASE'"): the icon + "Moon Phase" label's own row -
   same shape as .weather-mini-header.

   2026-08-06 later update, per Caleb ("center the moon in the box"):
   used to sit nested inside .moon-phase-text (a right-aligned column
   that also held "Last Quarter" beside the icon) - now a direct child
   of .moon-phase itself, top-left of the box, matching Current's/
   Precipitation's own headers exactly (that old .moon-phase-text
   wrapper is gone; "Last Quarter" moved into .moon-phase-body, see
   forecast.html). */
.moon-phase-header {
  display: flex;
  align-items: center;
  gap: 4px;
}

/* 2026-08-06, per Caleb ("unbold the text... ensure the font, color,
   and boldness match on all three heading labels"): was font-weight:
   700 with no letter-spacing - drifted heavier/tighter than Wind/Seas/
   Current/Precipitation's shared .data-card .label look (600 weight,
   0.6px letter-spacing). Now restates those same exact values, same
   approach .weather-mini-header already uses for the same reason. */
.moon-phase-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
}

/* 2026-08-06, per Caleb ("move the 'Last Quarter' text to fit evenly
   in the...box"): now lives in .moon-phase-body next to the icon
   (was beside it in a separate right-aligned text column before).
   Font-size bumped 11px -> 12.5px to match .weather-mini-condition's
   own size - both are the "sub-label under/beside the headline icon"
   text in their respective boxes, so they read as the same visual
   weight across both. */
.moon-phase-name {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-muted);
  white-space: nowrap;
}

/* 2026-08-18, per Caleb ("reduce the size of the moon icon just a hair,
   so that the bottom row's box dimensions match that of the top"): was
   80px - .currents-row's own height is set by whichever mini-card's
   content is naturally tallest (align-items:stretch), and at 80px Moon
   Phase's icon+header+padding grew past Wind/Seas/Precipitation's own
   natural height in the top row above, so the two rows no longer lined
   up. Back down to 72px, the same value forecast.html's own history
   comment (see the .moon-phase svg markup) says was originally chosen
   specifically to "land just under .currents-main's own natural
   height" - i.e. under Current's own label-row+value+sub content shape,
   which Wind/Seas/Precipitation's own top-row content is now built the
   same way as. If Tide's own sparkline height (aspect-ratio driven, see
   .tide-svg) ends up the real tall pole in .currents-row instead once
   this renders, that would need its own separate trim - flag it to
   Caleb if the two rows still don't match after this. */
.moon-icon {
  width: 72px;
  height: 72px;
  flex-shrink: 0;
  /* Night side + rim of the Weather Icons glyph (fill="currentColor"). */
  color: #16233a;
}

/* 2026-08-06: the 12px top/bottom padding this used to set (was
   .data-card.currents-card, back when Current/Precipitation/Moon
   Phase shared one big box) now lives directly on .currents-mini-card
   above, since each reading is its own separate .data-card now - see
   that rule's own comment for the full "reformat into three boxes"
   story and why 12px specifically. */

/* 2026-08-17, per Caleb ("add another pop up similar to the moon and
   precipitation, and add tide... a visual that shows the tide rise and
   fall for the day"): another .currents-mini-card, same header-then-body
   column shape as .moon-phase/.weather-mini (header stays top-left
   matching every other card's header; the body below is the new part).
   flex:1 fills the box's own stretched height the same way .moon-phase
   does, so the sparkline gets real vertical room instead of just
   sitting at its own small intrinsic height.

   2026-08-18 update: was the 4th sibling in .currents-row until
   Precipitation moved out into the top row (see .data-grid's own
   comment above) - back to 3rd of 3, meaningfully more width per card
   than the squeezed 4-across arrangement had. */
.tide-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  flex: 1;
  align-self: stretch;
  min-width: 0;
}

.tide-header {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Same weight/size/letter-spacing as .moon-phase-label/.weather-mini-
   heading - every mini-card header reads as the same visual weight. */
.tide-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--text);
  white-space: nowrap;
}

/* The sparkline itself. width:100% + a fixed aspect-ratio (matching
   app.py's own VIEW_W/VIEW_H) instead of an explicit height - this
   box's real width varies (different viewport widths, and this is 1 of
   3 flex:1 siblings in .currents-row - see that rule above, and its own
   2026-08-18 update comment for why it's back to 3 instead of 4), and
   aspect-ratio is what keeps the drawn curve and its dots looking like
   circles/proportional text at any of those widths instead of
   stretching non-uniformly the way a plain width:100%/height:100% +
   preserveAspectRatio="none" SVG would. flex:1 lets it fill the
   leftover height under .tide-header, same role .moon-phase-body's own
   flex:1 plays.
   2026-08-18, per Caleb ("increase the size of the tide graphic" then
   later "shrink the size of the graph... keep the boxes the same size
   across everything"): went 92 -> 130 -> 95 - app.py's own VIEW_H is
   the source of truth (see _build_tide_card()'s comment for the full
   back-and-forth), this ratio just has to match it exactly or the
   curve would stretch/squash against the box's real shape. */
.tide-svg {
  width: 100%;
  aspect-ratio: 300 / 95;
  display: block;
  flex: 1;
  overflow: visible;
}

.tide-curve {
  fill: none;
  stroke: #2f6f8f;
  stroke-width: 2.4;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.tide-point {
  fill: var(--navy);
  stroke: #ffffff;
  stroke-width: 1;
}

/* High/low dots share the same navy fill as most of this page's other
   direction-arrow/marker glyphs - deliberately NOT colored by severity
   the way Wind/Seas bands are (severity.py's own docstring is explicit
   that tide predictions are informational context, not a forecast
   input scored like wind/seas), so there's no red/green/amber high-vs-
   low distinction to invent here. */
.tide-point-high,
.tide-point-low {
  fill: var(--navy);
}

/* Small time-only labels at each real high/low point (2026-08-17, per
   Caleb: "just the high/low points on the tide is fine") - the exact
   height in feet is still available on tap/hover via this same point's
   title attribute (see forecast.html), same "precise number is one tap
   away, the card itself stays uncluttered" convention .moon-phase's own
   title attribute already established. font-size is in the SVG's own
   300-wide viewBox units, not real px - looks roughly like other
   charts' smallest labels (Trend charts' 8px unit line, the Hourly
   strip's 9.5px period line) once scaled down to this card's real,
   much smaller on-screen width. */
/* 2026-08-18 update, per Caleb ("incorporate the High and Low FT/M
   measurement somewhere in the box"): this now carries "5:40 AM ·
   1.8 ft" (time + height combined into one label) instead of just the
   time - see forecast.html's own comment for why a second stacked line
   was tried and reverted (fought against "keep the same box
   dimensions"/"increase the size of the graph"). Font size unchanged -
   the combined string is longer, not taller, so no size adjustment was
   needed here, just a longer line for text-anchor:middle to center.

   2026-08-18 further update, per Caleb ("still overlapping... I want
   the text to clearly be distinguished from the graph line"): on top of
   widening the label's own offset from its dot (see forecast.html),
   added a solid halo behind the text so it stays legible on ANY day's
   curve shape, not just today's - paint-order:stroke fill draws the
   stroke UNDER the fill instead of centered on top of it (the SVG
   default, which would eat into the letterforms), and the stroke color
   matches .currents-mini-card's own background (#e7e9ec) exactly so it
   reads as the card's own surface poking through around the letters,
   not a mismatched white patch, wherever the curve happens to pass
   near or under a label. */
.tide-point-label {
  font-size: 15px;
  font-weight: 700;
  fill: var(--text);
  paint-order: stroke fill;
  stroke: #e7e9ec;
  stroke-width: 4px;
  stroke-linejoin: round;
}

/* 2026-08-18, per Caleb ("make the bottom text the same distance below
   the curve as the top text is above it"): SVG <text> positions by its
   own BASELINE by default, and a line of text sits mostly ABOVE that
   baseline (ascenders/cap-height) with only a sliver below it
   (descenders) - so the SAME y-offset from a dot reads as very
   different real clearance depending on which side of the dot the
   text's baseline lands on (see forecast.html's own comment for the
   fuller "why" and the math). dominant-baseline swaps which edge of the
   text box that y-offset actually measures from:
   text-after-edge anchors by the text's own BOTTOM edge (the edge
   nearest the dot for a label sitting ABOVE it), text-before-edge
   anchors by its TOP edge (the edge nearest the dot for a label sitting
   BELOW it) - so both labels now measure their real gap from the same
   "nearest edge to the dot," not from an invisible baseline offset by
   an inconsistent amount on either side. */
.tide-point-label-high {
  dominant-baseline: text-after-edge;
}

.tide-point-label-low {
  dominant-baseline: text-before-edge;
}

.unit-toggle {
  display: flex;
  /* 2026-08-03, per Caleb: "outline the toggle button... in a dark
     line" - was var(--border) (the page's plain pale outline color),
     nearly invisible against the card's now-tinted severity background
     above. Reuses #7d8797, the same dark gray already established as
     this page's divider/border color elsewhere (hourly strip, Trend
     chart column dividers, the Wind/Seas data-card's own new border). */
  border: 1px solid #7d8797;
  border-radius: 6px;
  overflow: hidden;
}

.unit-btn {
  border: none;
  background: #fff;
  /* 2026-08-03, per Caleb (same "darken the font" ask as .data-card
     .label above) - the inactive unit text was also sitting at
     var(--text-muted). */
  color: var(--text);
  font-size: 11px;
  font-weight: 600;
  padding: 3px 8px;
  cursor: pointer;
}

.unit-btn + .unit-btn { border-left: 1px solid #7d8797; }
.unit-btn.active { background: var(--ocean-blue); color: #fff; }

.data-card .value { font-size: 22px; font-weight: 700; margin-top: 6px; color: var(--navy); }
/* 2026-08-03, per Caleb: same darkening as .data-card .label above -
   "Swell period: 5s"/"not enough sources" were also at var(--text-muted). */
.data-card .sub { font-size: 12.5px; color: var(--text); margin-top: 3px; }

/* 2026-08-06, per Caleb ("add an arrow next to the 'Direction: SE' in
   the right now wind box"): same tip-up/+180 "blowing toward" arrow as
   everywhere else on the page, sized to sit comfortably inline with
   this text's own 12.5px font instead of the 16-20px used elsewhere. */
.data-card .wind-direction-sub {
  display: flex;
  align-items: center;
  /* 2026-08-06, per Caleb ("space the arrow and the direction text out
     a bit more"): widened from 4px now that the arrow trails the text
     instead of leading it.
     2026-08-06 update, per Caleb ("space them a bit closer together"):
     pulled back in from 8px. */
  gap: 4px;
  /* 2026-08-18, per Caleb ("bring 'Direction: NW' and the arrow down so
     it's the same height as the swell period text in the Seas box"):
     .wind-direction-sub-text's own line-height:1 (see that rule's
     comment below) strips out the ~3.4px of top leading a plain .sub
     line normally gets from body's 1.55 line-height - exactly the
     leading Seas's own "Swell period: 4s" (a plain .sub, no line-height
     override) still has, which is what was pushing Wind's line visibly
     higher than Seas's. Restoring that gap here as real margin instead
     of leading - inherited .data-card .sub's 3px margin-top plus 3.4px
     ~ 6px total - keeps .wind-direction-sub-text's own line-height:1
     intact (still needed for the arrow-centering fix below) while
     landing the text back at the same vertical position Seas's sub
     line sits at. Selector bumped to .data-card .wind-direction-sub
     (matching .data-card .sub's own specificity) so this actually wins
     over the inherited 3px instead of losing to it.
     2026-08-18 update, per Caleb ("it may be one pixel above where the
     other is"): 6px was a hair short - nudged to 7px. */
  margin-top: 7px;
}

/* 2026-08-06, per Caleb ("make sure the arrow is in line with the top
   and bottom of the direction text"): the raw text node's default
   line-height (~1.4-1.5x the 12.5px font) added extra leading above/
   below the glyph itself, so centering the arrow against that whole
   line box landed it visually lower than the actual "SE" lettering.
   Wrapping the text in its own span with line-height:1 shrinks that
   box down to the font's own natural height, so align-items:center on
   the row now centers the arrow against the text's real ink, not
   invisible padding around it. */
.wind-direction-sub-text {
  line-height: 1;
}

.wind-direction-sub-arrow {
  /* 2026-08-06: viewBox was tightened from "0 0 24 24" to the path's
     real bounds ("7 2 10 12") to fix vertical centering against this
     text - but that removed the ~2x "zoom out" the old 24-unit viewBox
     was providing at this same 12px CSS box, so the glyph rendered
     much bigger than before. Shrinking width/height here to the SE
     text's own cap-height (12.5px font -> ~9px) restores a size that
     lines up with the top/bottom of the text instead of towering over
     it, while keeping the tight viewBox that fixed the centering. */
  /* 2026-08-06 update, per Caleb ("the bottom of the arrow is below the
     bottom of the SE text"): the viewBox moved from the path's own
     bounding box to a bounding CIRCLE around its rotation center (see
     forecast.html for the full explanation), so this box is now a
     square sized to that same ~9px cap-height instead of the old
     7.5x9 rectangle - keeps the arrow visually the same size, but now
     contained on every side at any rotation, not just the bearing this
     screenshot happened to show.
     2026-08-06 update, per Caleb ("increase the size... as big as
     possible while still staying in line with the top and bottom of
     the direction text"): because the box is now a bounding CIRCLE
     (not a bounding box), any size we pick here is guaranteed to
     contain the glyph at every rotation, not just this one - so we can
     size it directly to the text's own cap-height with no overhang
     risk. Bumped from 9px to 10px, just under the 12.5px font's full
     line height. */
  width: 10px;
  height: 10px;
  fill: var(--navy);
  flex-shrink: 0;
}

/* 2026-08-06, per Caleb ("match the format of the wind and sea boxes...
   in the same format as 'Direction: SE (Arrow)'"): identical rules to
   .wind-direction-sub/-text/-arrow above, just restated under their own
   class names since this sits in the separate .currents-mini-card box
   rather than Wind's box. Kept as its own copy (not a shared class on
   both elements) so either box's spacing/sizing can be tuned
   independently later without side effects on the other. */
.currents-direction-sub {
  display: flex;
  align-items: center;
  gap: 4px;
}

.currents-direction-sub-text {
  line-height: 1;
}

.currents-direction-sub-arrow {
  width: 10px;
  height: 10px;
  fill: var(--navy);
  flex-shrink: 0;
}

/* Currents card (Phase 7c-7) spans both columns - it's a single value
   with no unit toggle/severity dot like wind/seas, so it reads better
   as one full-width row than squeezed into half the grid.

   2026-08-03, per Caleb: a faded background, matching the same tinted-
   card treatment the Wind/Seas cards just got - but currents have no
   severity band in this app (no sev-* class gets added to this card at
   all, see forecast.html), so unlike Wind/Seas this is a plain STATIC
   color, not tied to the current speed the way the other two cards
   react to wind/seas severity - explicitly what Caleb asked for
   ("doesn't need to change at all with different current speeds").

   2026-08-04: originally reused the Wind card's light-blue sev-good
   tint, but once blue became a real severity color on the Wind/Seas
   spectrum (the "Excellent" tier), this card's blue could misread as
   implying a severity - Caleb asked for a different fill for that
   reason. Switched to a neutral slate-grey wash instead (previewed
   alongside a violet and a sand option; slate was the pick) - close to
   the page's own #7d8797 border/divider color family, so it reads as
   "not a severity color" at a glance rather than borrowing a hue from
   the wind/seas scale. Same dark #7d8797 border as the other two cards
   too, for the same "clear distinction" reasoning.

   2026-08-06: this combo class (.data-card.span-2) is no longer used
   anywhere - the one card it applied to (Current/Precipitation/Moon
   Phase) split into three separate .currents-mini-card boxes (see that
   rule above), which restates this same background/border-color
   directly rather than relying on .span-2. The bare .span-2 class
   still exists on .currents-row (its own rule sets grid-column:1/-1
   directly too), just no longer paired with .data-card. */

/* 7-Day Outlook table (Phase 7c-7) - the first place the multi-period
   cached series data (built across 7c-1 through 7c-6) actually reaches
   the screen. Deliberately a plain table, not a chart yet - Phase
   7c-8 (Chart.js) is where the full-resolution series gets plotted. */
/* Wrapper handles the rounded corners/border/shadow/horizontal scroll
   so the table itself can stay a plain border-collapsed table - on a
   narrow phone screen the 4 columns can get tight, so this scrolls
   instead of squeezing text unreadably small. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
}

/* 2026-08-04, per Caleb - first step in bringing the 48-hour table in
   line with today's By Model chart work ("I want all of the tables to
   look similar to this... let's start with making the outline border
   for the entire window"): matches .model-chart-wrap's own outer
   border exactly (var(--navy) instead of the plain light
   var(--border) every other card on the page still uses).

   2026-08-04, same-day follow-up ("the bottom corners look white or
   gapped... can we make this a smooth single border line"): the
   border-color override alone lived on .table-scroll itself, the SAME
   element that also has overflow-x:auto for the horizontal scroll.
   Combining a rounded border with overflow-x:auto on one element is
   exactly the setup that trips this up - the native scrollbar track
   Chrome draws at the bottom of a horizontally-scrollable box doesn't
   reliably respect that same element's own border-radius, so the
   rounded corners look cut off / mismatched right where the scrollbar
   sits. .model-chart-wrap never had this problem because it never
   needed to scroll - it could just be overflow:hidden.

   Fixed by splitting into the same two-layer pattern used for
   .model-chart-wrap/.model-chart earlier this session: a new OUTER
   wrapper (.hourly-table-frame) owns the border/radius/shadow and is
   overflow:hidden (real clipping, not fighting a scrollbar), while
   .table-scroll goes back to being ONLY the scrolling element inside
   it - no border/radius/shadow of its own left to conflict with its
   own scrollbar. Scoped to just this table for now (a new wrapper
   class, not a change to .table-scroll's own base rule) since
   .table-scroll is shared with the 7-Day Outlook table too, and Caleb
   asked to review the hourly table on its own first. */
.hourly-table-frame {
  position: relative;
  border: 1px solid var(--navy);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.hourly-table-frame .table-scroll {
  border: none;
  border-radius: 0;
  box-shadow: none;
}

/* 2026-08-17, per Caleb ("looks like it isn't formatting correctly" -
   the 7-Day Outlook table's Confidence column was cut off at the
   right edge on his phone): confirmed live - it wasn't a real bug,
   the table was already correctly, deliberately horizontally
   scrollable (.table-scroll's own overflow-x:auto, plus a sticky Day/
   hour column specifically so scrolling right doesn't lose track of
   which row you're on - see .outlook-day-cell's own comment). Nothing
   on screen ever hinted that swiping reveals more, though - a real,
   previously-flagged gap ("horizontal-scroll fade/edge affordance for
   the Hourly and Outlook tables," noted early in this mobile pass but
   not built yet).
   This ::after lives on the OUTER, non-scrolling frame (not
   .table-scroll itself, which would just scroll the hint away with
   the rest of the content) - a thin translucent-navy gradient at the
   right edge, reading as "more content this direction" against either
   the navy header row or the light body rows beneath it without
   needing to color-match either one exactly. table_scroll.js toggles
   an "at-scroll-end" class on this same frame that fades this hint
   out once there's genuinely nothing left to scroll to (or hides it
   immediately if the table never needed to scroll in the first place
   - a wide enough phone, or desktop). position:relative is new here
   (this frame previously only needed overflow:hidden) - required so
   the ::after's own position:absolute anchors to this box instead of
   whatever positioned ancestor happens to be further up the page. */
.hourly-table-frame::after,
.outlook-table-frame::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 28px;
  background: linear-gradient(to right, transparent, rgba(10, 37, 64, 0.16));
  pointer-events: none;
  z-index: 5;
  opacity: 1;
  transition: opacity 0.15s ease;
}

.hourly-table-frame.at-scroll-end::after,
.outlook-table-frame.at-scroll-end::after {
  opacity: 0;
}

/* 2026-08-05, per Caleb - the last table left out of the navy/gridline
   language every other table on the page already carries (By Model
   charts, Trend charts, the 48-hour table). Same two-layer
   frame/table-scroll split as .hourly-table-frame above, for the same
   reason (rounded border + a horizontally-scrolling child on one
   element mismatches the corners). */
.outlook-table-frame {
  position: relative;
  border: 1px solid var(--navy);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.outlook-table-frame .table-scroll {
  border: none;
  border-radius: 0;
  box-shadow: none;
}

/* 2026-08-17, per Caleb ("shorten the width of Day boxes, which allows
   the table to fit correctly on mobile view so the user doesn't have
   to swipe over"): 460px was sized around the OLD single-line Day cell
   ("Tuesday 8/18," the widest real weekday name plus date on one
   line). Stacking a short 3-letter day above a short "8/18" (see
   .outlook-date below) needs roughly 60px less than that did, so the
   floor comes down by the same amount - still a real floor, not
   removed outright, since the other 4 columns' own content widths
   (Wind's range+unit+direction arrow, the Confidence badge, etc.)
   are unchanged and still need genuine room. .table-scroll's own
   overflow-x:auto plus the scroll-fade hint (.outlook-table-frame::
   after) stay in place as a safety net for whatever narrow phone this
   still isn't quite enough for. */
/* 2026-08-18, per Caleb ("make all of the text in the rows the same
   size... match the same text size as the day/date label"): was
   13.5px - the main wind/seas numbers and Seas's own "FT" unit have
   no font-size override of their own, so they inherited this value
   directly; dropping the base itself to 10px (matching .outlook-date-
   day, the smaller of the Day column's own two sizes, and the same
   10px already used for MPH/direction/period per the last round of
   feedback) reaches both of them in one change without needing a
   separate override on each. .outlook-badge and .outlook-precip-pct
   both set their own explicit font-size below and needed their own
   matching update, since an inherited base change doesn't touch a
   property a more specific rule already sets. */
.outlook-table {
  width: 100%;
  min-width: 400px;
  border-collapse: collapse;
  background: var(--card-bg);
  /* 2026-08-18, per Caleb ("the text we've been editing is maybe one
     size smaller than the text in the Day headers... increase it by
     one"): was 10px (matching .outlook-date-day, the smaller of the
     Day column's own two sizes) - bumped to 11px to match .outlook-
     date-num instead, the larger one, per this ask. Same +1px bump
     applied to every other row-content rule below that had matched
     the old 10px (.outlook-badge, .outlook-precip-pct, .outlook-wind-
     unit, .outlook-direction-label, .outlook-period) so the whole row
     moves together, not just whatever inherits from this base rule. */
  font-size: 11px;
}

/* 2026-08-18, per Caleb ("bold the text to reflect the day/date
   labels" - .outlook-date-day/-num, both font-weight:700): a separate
   rule rather than folding this into .outlook-table td's existing
   font-variant-numeric rule above, since that rule is shared with
   .chart-value/.estimate-range/.confidence-badge elsewhere on the
   page - bolding through that shared selector would reach those too,
   not just this table. font-weight is inherited, so this one rule on
   the cell itself reaches every plain span inside it (the wind/seas
   numbers, .outlook-unit, .outlook-direction-label, .outlook-period,
   .outlook-precip-pct - none of which set their own font-weight to
   block it) without needing a change on each individually. The one
   exception is .outlook-badge just below, whose own .confidence-badge
   base class already sets an explicit font-weight:600 of its own -
   inheritance never overrides an element's own explicit declaration,
   so that one still needs its own explicit bump. */
.outlook-table td {
  font-weight: 700;
}

.outlook-table th, .outlook-table td {
  padding: 8px 10px;
  /* 2026-08-18, per Caleb ("take the text inside of the headers, and
     center them inside the header boxes, rather than starting from
     the left... take the text/content in the table below these
     headers, and make them in line with the centered header text"):
     was text-align:left on both th and td together, so this one
     change centers every column - Day/Wind/Seas/Precip/Confidence -
     top to bottom, headers included, in one shot. The stacked-line
     cells (.outlook-cell-stack, .outlook-date) are flex columns with
     default align-items:stretch, so each line already spans the
     cell's full width - text-align:center on that full-width line
     centers it correctly with no extra flex changes needed. The one
     exception is .outlook-direction (the wind arrow row), which needs
     its own justify-content:center below since it's laying out an
     icon next to text, not just text alone. */
  text-align: center;
  /* Was var(--border) (light gray) - now the same navy-family divider
     tint (#3d5a7a) the By Model/Trend/hourly tables already use, so row
     dividers read as one consistent language across every table. */
  border-bottom: 1px solid #3d5a7a;
}

.outlook-table th {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  /* Was var(--bg)/var(--text-muted) (light gray fill, muted gray text) -
     now the same navy/white header treatment as .hourly-row-label and
     .trend-axis-label, so this table's header row matches every other
     table's header language instead of being the one plain holdout. */
  background: var(--navy);
  color: #ffffff;
  font-weight: 700;
}

/* 2026-08-18, per Caleb ("the dividing line separating the Header
   boxes is gone now" / later confirmed still wanted after the Day
   column's own divider - see .outlook-day-head/-cell below - was
   fixed separately): first two attempts here (border-right, then
   box-shadow) confirmed live via DevTools as matching/applied, never
   actually painted on screen. Root cause, once isolated:
   `border-collapse: collapse` on .outlook-table (needed for the
   table's own border-bottom row dividers to render as crisp single
   hairlines rather than doubled ones) silently blocks `box-shadow` on
   `<th>`/`<td>` cells in every major browser - the same bug that had
   also been silently breaking .outlook-day-head/-cell's own
   pre-existing divider this whole time. Same fix as that one: a
   ::after pseudo-element instead, ordinary positioned content inside
   the cell rather than a cell border/shadow, so the collapsed-border
   table model has no say over it - confirmed working live once
   applied to the Day column, now applied here too. */
.outlook-table th:not(:last-child):not(.outlook-day-head) {
  position: relative;
}

.outlook-table th:not(:last-child):not(.outlook-day-head)::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 1px;
  background: #3d5a7a;
}

.outlook-table tr:last-child td { border-bottom: none; }

/* 2026-08-06, per Caleb ("add the date to follow right after the day
   label... then bold the day/date label text"). Only this one cell
   gets bolded - the rest of the row (wind/seas numbers, units,
   direction) was just unified to plain body text a moment ago, so
   this is a deliberate, singular exception marking the Day column as
   the row's own label rather than reopening that decision. */
.outlook-day-cell {
  font-weight: 700;
  background: var(--card-bg);
}

/* 2026-08-17, per Caleb ("abbreviate the day labels the same as the
   other tables, and then stack the date number underneath the day
   text, rather than next to it... it can be in a smaller font like the
   48 hour table is"): same always-two-lines flex-column wrapper as the
   Hourly strip's .hourly-date/-weekday/-day (style.css, above) - a
   short abbreviated weekday on top, the bare "M/D" date underneath,
   both small/bold/uppercase-for-the-weekday instead of one long
   "Tuesday 8/18" line. Narrowing what this cell actually needs to
   display is what lets .outlook-day-head/-cell's own width below (and
   the table's overall min-width just above) come down - this is the
   real fix, the width numbers are just following it. */
/* 2026-08-18, per Caleb ("the gap between Wind and Seas text looks
   larger than the gap between the day label and the date... I want
   things symmetrical"): added an explicit 2px gap and line-height:1
   here (previously this stack had neither - it relied purely on
   whatever leading each line's own font-size pulled in from body's
   ambient 1.55 line-height, which happened to look tight mostly by
   coincidence since 10px/11px text doesn't carry much extra leading).
   .outlook-cell-stack below picks up the exact same treatment so both
   stacks now build their spacing the same explicit way instead of two
   different ones that only sometimes matched by luck. */
.outlook-date {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1;
}

.outlook-date-day {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  font-weight: 700;
  white-space: nowrap;
}

.outlook-date-num {
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}

/* Pins the Day column to a real, narrow width instead of letting it
   grow to fit whatever the widest OTHER column's content happens to
   push it toward - 58px comfortably fits "TODAY" (the one label that's
   deliberately NOT abbreviated, see app.py's _outlook_day_label_short,
   and also the widest of the two stacked lines - every real 3-letter
   abbreviation and every "M/D" date are shorter) at 10px bold uppercase
   with real breathing room inside the cell's own 10px side padding,
   close to the Hourly strip's own 46px .hourly-col-head min-width for
   the same two-line date shape, just a bit wider to cover "TODAY"
   specifically. */
.outlook-day-head,
.outlook-day-cell {
  width: 58px;
  min-width: 58px;
}

/* 2026-08-17 (Phase 7e mobile refinement) - the 48-hour table
   (.hourly-row-label above) already pins its own row-label column
   while its hours scroll underneath; this table never got the same
   treatment, so on a phone (.outlook-table's own min-width forces
   horizontal scroll on anything narrower than ~460px) scrolling right
   to see Confidence loses track of which day's row you're even on.
   .outlook-day-head picks up its background/color for free from the
   existing .outlook-table th rule (navy/white) above; .outlook-day-
   cell sets its own explicit var(--card-bg) since sticky positioning
   needs a real background to occlude what scrolls behind it, not an
   inherited/transparent one. */
.outlook-day-head,
.outlook-day-cell {
  position: sticky;
  left: 0;
  z-index: 3;
}

/* 2026-08-18, per Caleb ("I want one vertical line that separates the
   Day header, and goes all the way down to the bottom of the table"):
   this divider originally used `box-shadow: 1px 0 0 #3d5a7a` (chosen
   over border-right at the time specifically because "border-collapse:
   collapse shares a collapsed border's space with the adjacent
   scrolling cell and lets a hairline of its background bleed
   through"), but a live round of debugging a different divider on
   this same table (see .outlook-table th's own comment above)
   surfaced that box-shadow on a `<th>`/`<td>` cell doesn't paint AT
   ALL once its table has border-collapse:collapse, in every major
   browser - so this one had been silently invisible the whole time
   too, not just the newer attempt. Switched to a ::after pseudo-
   element instead: ordinary positioned content living inside the
   cell, not a cell border/shadow, so the collapsed-border table model
   has no say over it. Applied to BOTH .outlook-day-head (the header
   cell) and every row's .outlook-day-cell - since border-collapse
   leaves zero gap between stacked cells, each cell's own full-height
   divider segment lines up seamlessly with the next row's, reading as
   one continuous line top to bottom exactly as asked. */
.outlook-day-head::after,
.outlook-day-cell::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 1px;
  background: #3d5a7a;
}

/* 2026-08-18, per Caleb's "same size as the day/date label" ask above -
   was 12px, its own explicit override that the .outlook-table base
   font-size change alone doesn't reach. */
/* font-weight:700 here specifically because .confidence-badge's own
   base rule already sets an explicit 600 - see .outlook-table td's
   own comment above for why the rest of the row gets there by simple
   inheritance but this one badge needs its own explicit override. */
.outlook-table .outlook-badge { margin-bottom: 0; padding: 4px 10px; font-size: 11px; font-weight: 700; white-space: nowrap; }
/* 2026-09-29: phone-only short confidence label - see forecast.html. */
.outlook-badge-short { display: none; }

/* 2026-08-17, per Caleb ("the text under Wind/Seas looks unformatted
   and all over the place... make sure all the text lines up nice and
   neat"): before this, the Wind/Seas cells were just plain inline
   text that wrapped wherever a row's particular number happened to
   run out of room - "9-14 MPH" (too wide to fit alongside its column)
   wrapped between the number and unit, while a shorter "2 FT" fit on
   one line right next to it, so two rows of the same column read as
   two different shapes. This wrapper forces each cell's pieces into
   real, deliberate block-level lines instead - always the same shape
   every row, matching the Hourly strip's own established wind/seas
   layout exactly (see .hourly-wind-value-overlay/-unit-overlay and
   .hourly-seas-value-overlay/-period-overlay above). */
/* 2026-08-18, per Caleb ("the gap between Wind and Seas text looks
   larger than the gap between the day label and the date... symmetrical"):
   was gap:1px with no line-height control - the wind value line's
   bigger 13.5px font (kept unchanged, see .outlook-wind-unit's own
   comment above) inherited body's 1.55 line-height same as everything
   else, and a bigger font at 1.55 leading contributes noticeably more
   whitespace above/below itself than a small one does, so the visual
   gap under a 13.5px number read as much bigger than the plain 1px
   gap value suggested, even though both stacks used a "gap." Matched
   line-height:1 + gap:2px exactly to .outlook-date's own new values
   above so both stacks build spacing the identical explicit way
   regardless of what font-size any one line happens to be. */
.outlook-cell-stack {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1;
}

/* 2026-08-18, per Caleb ("ensure the '-' between the number ranges has
   equal spaces on both sides of the dash" - e.g. Today's "9-14" vs
   Wed's "7-13" not looking symmetric): checked the actual generated
   text first (severity.format_range) rather than assuming - both are
   byte-for-byte the same shape, plain "low-high" with zero spaces
   either side, no stray character anywhere. So this isn't a data bug,
   it's font kerning: different digit shapes (a "7"'s diagonal stroke
   vs a "9"'s closed bowl) sit differently next to a bare hyphen glyph
   even under tabular-nums (which only equalizes DIGIT widths, not the
   hyphen's own spacing against its neighbors). A small letter-spacing
   forces a truly equal fixed gap after every character, hyphen
   included, which is the actual fix for uneven-looking kerning rather
   than a string-formatting change to something that was never
   actually inconsistent. */
.outlook-value-line {
  white-space: nowrap;
  letter-spacing: 0.4px;
}

/* 2026-08-06, per Caleb ("add unit of measure texts after the numbers
   in the different rows... switch to MPH/KT and FT/M depending on
   what the toggles are set to"): unit label next to each row's own
   wind/seas number - started at the Hourly strip's small/muted/bold
   MPH-FT visual language (.hourly-wind-unit-overlay), but Caleb's
   follow-up ("make the font the same size and color as the number
   text") asked for it to blend in as part of the same value instead
   of reading as a separate badge - now just inherits .outlook-table
   td's own 13.5px/var(--text), no size/weight/color overrides left. */
.outlook-unit {
  margin-left: 3px;
}

/* 2026-08-17: Wind's unit now sits alone on its own .outlook-value-line
   (see .outlook-cell-stack above) instead of right after the number on
   a shared line - the 3px margin above was written for that shared-
   line case (still true for Seas, where the unit follows its number on
   one line) and would just read as a stray indent when the unit is the
   line's only content. Seas's own number+unit span pair still matches
   this selector's specificity fight correctly since there ARE two
   children on that line, so :only-child only strips the margin for
   Wind's standalone case. */
.outlook-value-line > .outlook-unit:only-child {
  margin-left: 0;
}

/* 2026-08-06, per Caleb ("add the wind direction text/arrow formatted
   the same way as the other tables"): same bold/navy/small label +
   bounding-circle-viewBox arrow language as .chart-col-wind-arrow-
   label/.hourly-direction-label - originally inline right after the
   unit, now (2026-08-17, see .outlook-cell-stack above) its own
   deliberate block line underneath, matching the Hourly strip's own
   .hourly-direction-overlay row exactly instead of flowing inline and
   wrapping unpredictably. */
.outlook-direction {
  display: flex;
  align-items: center;
  /* 2026-08-18, per Caleb's header/column centering ask above: a plain
     text-align:center on the cell doesn't reach this row, since it's
     laying out a label+arrow pair via flexbox, not free-flowing text -
     text-align only centers inline content, and flex items ignore it
     in favor of justify-content. Without this, the label+arrow pair
     would stay flush left inside an otherwise-centered cell. */
  justify-content: center;
  gap: 3px;
}

/* 2026-08-06 update, per Caleb ("increase the size of the wind
   direction text/arrow to be the same size/inline with the number and
   unit of measure text"): label bumped from 10px to the row's own
   13.5px (.outlook-table's base size), arrow bumped from 8px to a
   matching ~10px - safe to size right up near that thanks to the
   bounding-circle viewBox (see forecast.html's arrow comments), which
   contains the glyph at any rotation regardless of box size.

   2026-08-06 update, per Caleb ("ensure all text on the page is the
   same size, font, color, and boldness" - scoped to just this table):
   dropped the bold/navy treatment this label borrowed from the By
   Model/Hourly arrows (a deliberate accent there, since those sit in
   dedicated chart headers) - here it now reads as plain body text,
   matching the Day/Wind/Seas numbers and unit labels around it.
   .outlook-direction-arrow's navy fill is left alone - it's a graphic
   icon, not text, so it's outside this specific ask's scope. */
/* 2026-08-18, per Caleb ("MPH, Direction, and Arrow to be a bit
   smaller and match the text to the left" - the Day/Date labels,
   .outlook-date-day/-num above, at 10px/11px - leaving the wind speed
   number itself untouched): was 13.5px (.outlook-table's own base
   size), now 10px to match .outlook-date-day specifically - both read
   as short secondary labels next to a bigger primary value (the day
   abbreviation next to the date number, MPH/direction next to the
   wind speed range), so matching the smaller of the two Day/Date
   sizes keeps this reading as "label," not "another data value." */
.outlook-direction-label {
  font-size: 11px;
  color: var(--text);
  line-height: 1;
  white-space: nowrap;
}

/* Same ask, same 2026-08-18 comment above - shrunk proportionally
   with the text (was 10px, ~74% of the old 13.5px label size) rather
   than by a flat pixel amount, so the arrow shrinks in step with the
   label it sits next to instead of suddenly looking mismatched in
   scale against it. */
.outlook-direction-arrow {
  width: 8px;
  height: 8px;
  fill: var(--navy);
  flex-shrink: 0;
  transition: transform 0.2s ease;
}

/* 2026-08-18, per Caleb ("can we apply this to the text that displays
   second interval for waves as well?") - the swell period line
   ("at 6s") in the Seas cell, same 10px match-the-Day-label treatment
   as Wind's MPH/direction/arrow just above. No existing rule set this
   cell's font-size at all before now, so it was inheriting the plain
   13.5px .outlook-table td base size. */
.outlook-period {
  font-size: 11px;
}

/* Same ask, same 2026-08-18 comment above - the MPH/KT unit text
   specifically (see forecast.html's outlook-wind-unit comment for why
   this needs its own class rather than reusing plain .outlook-unit,
   which Seas's FT/M unit also uses and which Caleb wants left alone). */
.outlook-wind-unit {
  font-size: 11px;
}

/* Hourly forecast strip (Phase 7f, 2026-08-03) - the "Windfinder
   style" hour-by-hour view. Built as a plain <table> (not synced
   Chart.js canvases) specifically so every row's columns share exact
   pixel alignment for free via normal table layout, with zero risk of
   drift between the direction/wind/seas rows the way three separate
   canvases could have - see app.py's _build_hourly_forecast()
   docstring for the full reasoning. Reuses table-scroll for the same
   horizontal-scroll-on-mobile behavior the 7-Day Outlook table
   already has. */
.hourly-table {
  border-collapse: collapse;
  background: var(--card-bg);
  font-size: 12px;
}

/* Two real bugs Caleb caught while scrolling (2026-08-03):
   1. Wind/Seas value text was bleeding OVER the sticky "Wind"/"Seas"
      label column during horizontal scroll. The overlaid value/arrow
      elements in each data cell have z-index:1 (needed locally, so
      they paint above that cell's own gradient fill) - but that tied
      z-index:1 on this sticky column too, and ties can resolve in the
      data cells' favor depending on DOM/paint order. Bumped this to a
      clearly higher z-index so the sticky column always wins,
      regardless of what's scrolling underneath it.
   2. A thin sliver of a data cell's colored fill (the "small red
      line") was visible right at the sticky column's edge - a known
      rendering artifact of combining position:sticky with
      border-collapse:collapse, where collapsed borders share space
      with the adjacent (scrolling) cell and can let a hairline of its
      background bleed through. Switched the divider from a
      border-right (part of the collapsed border geometry) to a
      box-shadow (paints on top, not part of that geometry at all). */
/* 2026-08-04, per Caleb (live follow-up): "change the color scheme of
   the header boxes for the 48 hour outlook graph to match that of the
   7 day outlook graphs - the dark blue with white font." Matches the
   Trend charts' own navy axis-label box (style.css's .trend-axis-label,
   background: var(--navy), color: #fff) - both the sticky row-label
   column (Date/Wind/Seas) and the scrolling column headers below now
   use that same treatment, so the two tables read as one consistent
   "header" visual language across the page instead of the 48-hour
   table's plain light var(--bg) looking like a different, older
   pattern next to the now-navy Trend charts. */
/* 2026-08-18, per Caleb ("take all the text in their respective header
   boxes, and center the text to their box rather than starting at the
   left"): was text-align:left - no separate mobile-scoped override of
   this property exists anywhere else, so this one change reaches both
   desktop and mobile (Caleb was fine either way: "I'd like mobile to
   reflect this too, but can address that once we get to mobile tweaks
   if it makes things easier" - turned out not to need a separate
   pass). */
.hourly-row-label {
  position: sticky;
  left: 0;
  z-index: 3;
  background: var(--navy);
  padding: 8px 12px;
  text-align: center;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: #ffffff;
  font-weight: 700;
  white-space: nowrap;
  /* Was var(--border) (#e2e8ef) - all but invisible against the new
     navy fill, the same problem the Trend charts' own interior lines
     hit against navy before switching to DIVIDER_COLOR. Reuses that
     same lighter-navy tint here for the identical reason. */
  box-shadow: 1px 0 0 #3d5a7a;
  border-bottom: 1px solid #3d5a7a;
}

/* 2026-08-04 (same day, live follow-up): reverted back to the plain
   light fill - Caleb liked the navy treatment on the STICKY row-label
   column (.hourly-row-label, Date/Wind/Seas) but wanted the actual
   date/hour boxes themselves left as they were - "light filled for the
   box with the date change, and regular white/no fill on the hours."
   So only .hourly-row-label above picked up the navy/white treatment;
   these two rules are unchanged from before that round. */
.hourly-col-head {
  padding: 6px 3px;
  text-align: center;
  min-width: 46px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  /* 2026-08-04, per Caleb (live follow-up): "make the interior
     gridlines on the 48 hour table the same color as they are on the 7
     day outlook tables" - was #7d8797 (this page's older general-
     purpose divider gray), now the Trend charts' own DIVIDER_COLOR
     (charts.js, #3d5a7a) so both tables' interior lines read as the
     same navy-family divider language instead of two different grays.
     Same swap applied to .hourly-data-cell's vertical lines and the
     Wind/Seas horizontal separators below - the strong per-day
     boundary (.hourly-day-start, var(--navy)) is deliberately left
     alone, same as how the Trend charts keep their outer frame darker
     than their own interior dividers. */
  border-right: 1px solid #3d5a7a;
  font-weight: 600;
  color: var(--text);
}

.hourly-col-head.hourly-day-start {
  background: #dde2e9;
}

/* Date/hour header text: one uniform color (2026-08-03, per Caleb -
   "make the font all one color, black looks best"). .hourly-date used
   to sit at var(--text-muted) (gray) while .hourly-hour inherited
   .hourly-col-head's var(--text) (near-black) - the mismatch read as
   the date label being de-emphasized/secondary, which wasn't the
   intent once the header stopped being data-colored. Both now use the
   same var(--text) already set on .hourly-col-head, so this rule only
   needs to stop overriding it. */
/* 2026-08-06, per Caleb ("the text format changes from Thursday to
   Friday in the date header boxes"): first attempt added white-space:
   nowrap here so "THU 8/6"/"FRI 8/7" never accidentally line-wrapped
   at different points (they'd been sharing a column just narrow enough
   that one day name's pixel width tipped it into wrapping and the
   other's didn't). Caleb's next message ("I liked the way it looked
   stacked... reformat it to reflect how Thursday looked") reversed
   course entirely - he preferred the WRAPPED two-line look, just
   wanted it applied deliberately and consistently instead of by
   accident. So .hourly-date is now a plain flex column wrapper around
   two always-separate child lines (see .hourly-date-weekday/-day
   below) rather than a single text node that may or may not wrap -
   "always 2 lines" instead of "wraps to 2 lines sometimes". */
.hourly-date {
  display: flex;
  flex-direction: column;
  margin-bottom: 2px;
}

.hourly-date-weekday,
.hourly-date-day {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  font-weight: 700;
  white-space: nowrap;
}

.hourly-hour { font-size: 11.5px; white-space: nowrap; }

/* A new calendar day gets a left divider through every row in that
   column, so the strip reads as "today | tomorrow | day after"
   without needing a separate date row spanning multiple columns. This
   is meant to be a stronger, bigger boundary than the light per-hour
   gridlines below - a different KIND of divider, not just another
   hour.

   2026-08-03 (live feedback): it wasn't reading that way at all -
   var(--border) (#e2e8ef) is actually LIGHTER than the per-hour
   gridlines' #7d8797, so the one divider meant to stand out the most
   was rendering as the faintest line in the whole table (Caleb saw it
   as a near-white line). Switched to var(--navy), the same dark color
   already used for headings and values elsewhere on the page - now
   genuinely the darkest, most prominent line in the table, matching
   what this was always supposed to be. */
.hourly-day-start { border-left: 2px solid var(--navy); }

/* Per-hour gridlines (2026-08-03, per Caleb). First version used a
   translucent blue tint - looked "sloppy" live, because a translucent
   line blends differently over every different fill color underneath
   it (looks like a different shade over yellow than over pink), so
   the grid read as uneven instead of clean. Switched to a plain SOLID
   dark gray - same visible color everywhere regardless of what's
   behind it. Zero padding (per Caleb - "squeeze the bars together...
   any space between the dividers can be filled by the bars") - the
   colored block below fills the cell edge to edge, so the only visual
   gap between adjacent hours is this 1px line, not a padded margin. */
.hourly-data-cell {
  padding: 0;
  text-align: center;
  border-right: 1px solid #3d5a7a;
}

/* Wind/Seas separator (2026-08-03, revised same day per Caleb's live
   look at the two-tone version above): a blue wash for Wind and a
   green wash for Seas read as two different DATA colors, not a
   section boundary - easy to misread as meaning something (especially
   since severity.py's real wind/seas gradients also use blue-to-red).
   Replaced with a single horizontal rule, styled identically to the
   per-hour vertical gridlines just above (same rgba, same weight), so
   "new hour" (vertical) and "wind block ends / seas block starts"
   (horizontal) read as the same KIND of light divider - consistent
   with each other, not a second, competing color language. Row
   background goes back to the table's plain default (var(--card-bg)
   for data cells, the row-label's existing var(--bg) for the sticky
   column) - no per-group tint at all now, only the two grids of
   lines. */
/* 2026-08-03, per Caleb (live follow-up): the STICKY LABEL COLUMN'S
   version of this line ("Wind" box to "Seas" box) was reading fainter
   than the data columns' version of the same boundary, and fainter
   than the Date/Wind divider just above it - the exact same border-
   collapse conflict as that Date/Wind fix: .hourly-row-label sets a
   `border-bottom: 1px solid var(--border)` (pale) on EVERY row-label
   cell, including "Wind"'s, which competes with this rule's border-top
   on "Seas"'s row-label cell for the same shared edge. The data <td>
   columns never had this problem (.hourly-data-cell sets no border-
   bottom at all, so there was nothing to compete with there) - only
   the sticky column did. Bumped to 2px, same fix/same reasoning as
   .hourly-wind-group's border-top above, so both dividers in the
   sticky column now look identical, as asked. */
.hourly-seas-group td, .hourly-seas-group .hourly-row-label {
  border-top: 2px solid #3d5a7a;
}

/* Header/Wind separator, ORIGINALLY (2026-08-03, per Caleb - now that
   every column header carries a real date+hour instead of just the
   day-start ones, he asked for the same kind of divider marking
   "header block ends / Wind block starts" as already exists between
   Wind and Seas above.

   First attempt used the identical 1px solid #7d8797 as the Wind/Seas
   divider, but it rendered faint - Caleb caught this live. Root cause:
   with border-collapse:collapse, this boundary has TWO borders
   competing for the same shared edge - .hourly-col-head's own
   `border-bottom: 1px solid var(--border)` (the pale #e2e8ef header
   underline) sits right where this row's border-top does. The Wind/
   Seas boundary just below has no such competition (nothing else
   claims that edge), so it painted correctly the first time - this
   boundary needed the tie broken explicitly. Per CSS2.1's border-
   collapse conflict resolution, a WIDER border always wins regardless
   of style/color, so bumping this one to 2px (same trick already used
   by .hourly-day-start's border-left for "the strongest divider in the
   table") guarantees it paints over the header's pale underline instead
   of leaving the outcome up to a same-width tie-break.

   2026-08-06, per Caleb ("shift the weather row up... between WIND and
   DATE"): Weather is now the first tbody row, so this rule's own job
   changed from "header/Wind separator" to "Wind/Weather separator" -
   the declaration itself didn't need to change (still 2px solid
   #3d5a7a, still wins the same border-collapse conflict against
   whatever pale border sits on the shared edge above it), just which
   boundary it happens to sit on. See .hourly-weather-group directly
   below for its new replacement as the actual header/first-row
   divider. */
.hourly-wind-group td, .hourly-wind-group .hourly-row-label {
  border-top: 2px solid #3d5a7a;
}

/* Header/Weather separator (2026-08-06, per Caleb - see the comment on
   .hourly-wind-group above): Weather took over the "first data row
   under the header" spot, so it now needs the same border-collapse-
   winning 2px navy top border that job requires - identical reasoning/
   values as that rule, just applied to Weather's own cells/row-label
   instead of Wind's. */
.hourly-weather-group td, .hourly-weather-group .hourly-row-label {
  border-top: 2px solid #3d5a7a;
}

.hourly-direction-arrow {
  /* 2026-08-06, per Caleb ("make sure the top and bottom of the arrow
     is in line with the direction text"): shrunk from 14px to the
     9px .hourly-direction-label's own cap-height (~7px), same
     bounding-circle-viewBox approach as .wind-direction-sub-arrow and
     .chart-col-wind-arrow svg - contained at any rotation, not just
     today's bearings.
     2026-08-06 update, per Caleb ("increase the arrow size while
     still keeping it in line with the direction text"): because the
     viewBox is a bounding CIRCLE (not a bounding box), any size here
     is overhang-safe at any rotation - bumped from 7px up to 9px, the
     label's own full line height (line-height:1 on a 9px font), still
     never crossing above/below the text. */
  width: 9px;
  height: 9px;
  fill: var(--ocean-blue);
  transition: transform 0.2s ease;
}

/* Bars fill the column's own width (2026-08-03, per Caleb - "in line
   with the speed/size text") instead of a fixed narrow 16px, AND now
   fill the full CELL edge-to-edge with no inset (padding moved to 0
   on .hourly-data-cell above) - "squeeze the bars together... any
   space between the dividers can be filled by the bars."

   The fill itself went through three revisions (2026-08-03): flat
   single color (matching the Windfinder screenshot Caleb sent), then
   a two-layer flat-color track + flat-color fill with a hard edge
   between them (per Caleb's follow-up wanting a real "bar graph
   showing severity" back), then this - a single smooth CSS gradient
   (app.py's _hourly_bar_gradient()) fading from the full-saturation
   color at the bottom up into the lightened tint, centered on the
   real height_pct. Caleb's own words: "make the colors a faded
   gradient instead... it looks fuzzy right now" - the hard edge
   combined with the text's white halo was creating a messy seam right
   where they crossed; a smooth fade has no seam to cross. Height still
   encodes severity (a high value pushes the saturated portion further
   up) - just blended now instead of a stark line. */
.hourly-bar-track {
  position: relative;
  width: 100%;
}

.hourly-wind-track { height: 120px; }
.hourly-seas-track { height: 92px; }

/* 2026-08-06, per Caleb ("add the wind direction text to the left of
   the arrows in these columns"): the absolute positioning/centering
   that used to live directly on the arrow svg (via
   .hourly-direction-arrow-overlay) now lives on this wrapper, which
   holds the arrow AND its new direction-letter label as one centered
   flex row.

   2026-08-06 update, per Caleb ("center them together in the middle
   of the bars"): left:0/right:0/margin:0 auto only centers a box with
   a definite width - .hourly-value-overlay/.hourly-unit-overlay right
   below solve this the same way, with an explicit width:fit-content,
   which was missing here and left the row hugging the left edge
   instead of centering.
   Also, per Caleb ("close the gap between the direction text and the
   arrows"): gap tightened from 3px to 2px.
   2026-08-06 update, per Caleb ("swap the locations of the wind
   direction text/arrow and the number/unit... number/unit at the top,
   direction text and arrow underneath"): moved from top:6px (where
   the value used to start) down to top:34px, clearing the value
   (top:6, ~11px tall) and unit (top:20, ~8px tall) now stacked above
   it. */
.hourly-direction-overlay {
  position: absolute;
  top: 34px;
  left: 0;
  right: 0;
  margin: 0 auto;
  width: fit-content;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 2px;
}

/* .hourly-direction-arrow-overlay itself no longer carries any styles
   of its own - kept as a class on the svg purely so nothing else
   referencing that selector (including test_wind_direction_arrow.py's
   HOURLY_ROTATE_RE regex) breaks. */
.hourly-direction-label {
  font-size: 9px;
  font-weight: 700;
  color: var(--navy);
  line-height: 1;
  white-space: nowrap;
}

/* Value/period text sits directly on the bar, no background chip.
   Even with a smooth gradient instead of a hard edge, text can still
   land on a more saturated stretch of it depending on the hour's
   value, so a fixed text color alone isn't reliably legible
   everywhere. Uses a thin white stroke (crisp outline, not a blurred
   glow - the earlier stacked text-shadow read as "fuzzy") plus a
   tight shadow as a non-Chrome fallback. */
.hourly-value-overlay {
  position: absolute;
  left: 0;
  right: 0;
  margin: 0 auto;
  width: fit-content;
  z-index: 1;
}

/* 2026-08-04, per Caleb ("move [the wind speed/wave height text] to
   the top of the column rather than where it is right now at the
   bottom"): both used to anchor to `bottom` (wind under the direction
   arrow, seas under the swell-period line). Now `top`-anchored instead
   - wind stacks right below its own direction arrow (arrow at top:6px,
   ~14px tall, so the value starts at 22px to clear it with a small
   gap), seas simply moves to the top of its own track (no arrow to
   share space with there). The swell-period line (.hourly-period-
   overlay, below) stays at the bottom - Caleb only asked about the
   primary value text moving, not the secondary period line under it.

   2026-08-06 update, per Caleb ("swap the locations of the wind
   direction text/arrow and the number/unit... number/unit at the top,
   direction text and arrow underneath"): wind value moved from 22px
   back up to 6px (where the direction row used to sit), unit follows
   14px below it as before, and the direction row (below, in its own
   rule) now sits under both. */
.hourly-wind-value-overlay { top: 6px; }
.hourly-seas-value-overlay { top: 8px; }

/* 2026-08-04, per Caleb ("add FT or M that toggles... if you can
   squeeze it next to the numbers great"): unlike the wind unit label
   (its own line underneath, .hourly-wind-unit-overlay below - the
   direction arrow already took the space directly next to that value)
   there's nothing competing for space beside the seas number, so this
   fits inline instead. align-items:baseline keeps the smaller unit
   letters sitting on the same text baseline as the number rather than
   vertically centered against its full height (which would look
   slightly high next to a number with no descenders). Still centered
   as a whole within the track via the box's own left:0/right:0/
   margin:auto/width:fit-content (unchanged) - flex row content inside
   a fit-content box centers the same way a single line of text did. */
.hourly-seas-value-overlay {
  display: flex;
  align-items: baseline;
  gap: 2px;
}

.hourly-seas-unit-inline {
  font-size: 8px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  color: var(--navy);
  white-space: nowrap;
}

/* 2026-08-05, per Caleb (live bug report - "I changed the wave height
   to M and it looks like the text messed up"): meters values like
   "0.2-0.4" are more than twice as wide as feet's "0-1"/"2-4" -
   squeezed inline next to the number (the layout above, which feet's
   short values fit fine) they were overflowing the 46px-wide hourly
   columns and colliding with neighboring columns and the swell-period
   line below. Rather than shrink the already-approved feet-mode look
   to accommodate a problem only meters has, units.js now toggles
   `seas-unit-m` on <body> when meters is active - this table falls
   back to stacking the unit label underneath the number instead of
   beside it, the same "underneath" treatment .hourly-wind-unit-overlay
   already uses, and only for the unit that actually needs it. */
/* 2026-08-05 (live follow-up, per Caleb - "match the gap... to the
   spacing of the number text and unit measurement text for the wind
   chart above it"): the first pass stacked value/unit via flex-column,
   which relies on the two spans' own default line-heights for spacing
   - not the same gap the wind row uses, just visually similar. Wind's
   real gap is a tuned, explicit 14px delta between two independently
   absolutely-positioned lines (.hourly-wind-value-overlay top:22 ->
   .hourly-wind-unit-overlay top:36). Reusing that exact mechanism
   here instead: .hourly-seas-value-overlay is already position:absolute
   (from .hourly-value-overlay), so it's already a valid containing
   block for an absolutely-positioned child - pulling the unit span out
   of the flex row and re-anchoring it 14px below the value's own top
   reproduces the wind row's spacing exactly rather than approximating
   it. Period line follows at another 14px down, same delta again. */
body.seas-unit-m .hourly-seas-unit-inline {
  position: absolute;
  top: 14px;
  left: 0;
  right: 0;
  margin: 0 auto;
  width: fit-content;
}

/* 2026-08-05 (live follow-up again, per Caleb - "move the swell
   interval text up to be spaced evenly as well"): a flat 14px top-
   delta for both steps (value->unit, unit->period) looked uneven live
   because the two gaps consume different amounts of that 14px - the
   unit line's own text height (8px font, ~9-10px line box) is smaller
   than the value line's (11px font, ~13px line box), so the value-
   >unit gap reads as a near-touch while the unit->period gap left a
   visibly bigger leftover sliver. Tightened just this second gap
   (32px, not 36) so the two gaps read the same size instead of the
   second one looking bigger for no visual reason. */
body.seas-unit-m .hourly-period-overlay {
  top: 32px;
}

/* 2026-08-04, per Caleb ("add text under the [wind] numbers that says
   either MPH or KT, whichever the user's toggle is set to"): reuses
   the exact same data-wind-value/data-mph-text/data-kt-text mechanism
   as every other unit-toggle-aware element on the page (units.js's
   applyUnit() - see that file's own comment) rather than anything
   custom - "MPH"/"KT" are the two literal strings, no unit math
   involved, same pattern as the By Model chart's own merged-header
   "MPH"/"FT" label earlier today. */
.hourly-unit-overlay {
  position: absolute;
  left: 0;
  right: 0;
  margin: 0 auto;
  width: fit-content;
  z-index: 1;
}

.hourly-wind-unit-overlay {
  /* 2026-08-06, per Caleb ("swap the locations... number/unit at the
     top, direction text and arrow underneath"): kept the same 14px
     delta below the value it's always had (was 22->36, now 6->20). */
  top: 20px;
  font-size: 8px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  color: var(--navy);
  white-space: nowrap;
}

/* No stroke/shadow (2026-08-03, reverted same day per Caleb - "I
   don't like the white fuzzy look"). The white text-stroke meant to
   help legibility against the gradient's more saturated stretch was
   reading as a fuzzy halo around the numbers rather than a crisp
   outline, especially at this font size. The gradient itself (smooth
   fade, no hard edge) is the part Caleb confirmed he likes - plain
   text on top of it, no extra effect. */
.hourly-value {
  font-size: 11px;
  font-weight: 700;
  color: var(--navy);
  white-space: nowrap;
}

/* 2026-08-04, per Caleb ("raise the swell interval text up to be
   underneath the numbering as well"): moved from bottom:6px to
   top-anchored, right below .hourly-seas-value-overlay (top:8px, an
   11px value ending around 21px) - same "stack right under the
   number" treatment .hourly-wind-unit-overlay already uses under the
   wind value. */
.hourly-period-overlay {
  position: absolute;
  left: 0;
  right: 0;
  top: 22px;
  margin: 0 auto;
  width: fit-content;
  z-index: 1;
}

/* Color matches .hourly-value now (2026-08-03, per Caleb - "change
   the seas second interval font color so it shows up like the text
   above") - the muted gray was nearly invisible against the darker/
   red end of the seas gradient (the real bug he was flagging, visible
   in his screenshot's "5s" labels disappearing into red bars). Font
   stays smaller/lighter-weight than .hourly-value so it still reads
   as a secondary line, just no longer unreadable. */
.hourly-period {
  font-size: 9.5px;
  color: var(--navy);
  white-space: nowrap;
}

/* Trend line charts (Phase 7c-8) - Chart.js canvases need an
   explicitly sized wrapper (responsive+maintainAspectRatio:false
   still needs a parent with real height, or the canvas collapses to
   0px). Confidence gets a shorter wrapper - one line, less visual
   weight needed than the multi-model wind/seas charts.

   2026-08-04, per Caleb - restyled to read more like the 48-hour
   table: a flex row now, split into a real HTML "header box" for the
   y-axis unit label (.trend-axis-label below, styled like the hourly
   table's sticky Date/Wind/Seas row-label cells) plus a canvas-wrap
   for the chart itself, instead of Chart.js's own rotated axis-title
   text floating with no box around it. */
/* Border color (2026-08-05, per Caleb - "I'd like to have a dark navy
   border that matches the other around the entire pop up window"):
   was the plain light var(--border) every other floating card uses -
   .model-chart-wrap already made this exact same swap (see its own
   comment: "border around the entire window that matches the color of
   the header to the left"), so this brings the Trend chart cards in
   line with that same precedent instead of being the one card style
   still on the neutral outline. */
.trend-chart-wrap {
  display: flex;
  align-items: stretch;
  height: 220px;
  background: var(--card-bg);
  border: 1px solid var(--navy);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.trend-chart-wrap.small { height: 150px; }

/* The y-axis "header box" - navy sidebar next to each chart.
   2026-08-04, per Caleb (live follow-up #1/#2): fill went through
   var(--bg) -> the #dde2e9 hourly-day-start tint -> landed on
   var(--navy) (#0a2540), the exact same dark color the chart's own
   outer frame (ooChartFrame) uses, so the box reads as a real
   extension of that frame.
   2026-08-05, per Caleb ("merge the navy box and the white column
   showing the numbers into one navy header box... arrange the
   measurement numbers and 'MPH'/'FT' text on the bottom horizontally
   so it lines up with the date labels"): this box's own unit-label
   text (was vertical-rl/rotated, spanning the box's full height) moved
   into the canvas itself - charts.js's new ooAxisGutter plugin now
   paints the SAME navy behind the canvas's own y-axis tick-number
   gutter (previously left white/card-bg, visually a separate box from
   this one) and draws "MPH"/"FT"/"SCORE" horizontally there instead,
   lined up with the day-label row. With no text of its own left to
   style, this box goes back to being a plain empty navy spacer -
   background:navy is now the only rule left that still matters; the
   rest is dead weight from when this div held rotated text directly
   (same "empty navy spacer, nothing left to fight over" resolution
   .model-chart-frame's own axis-label box reached earlier). Still kept
   at 32px here for .model-chart-wrap's own use (the By Model chart,
   plain HTML/CSS bars, not canvas) - see the Trend-chart-specific
   override just below for why THAT usage collapses to 0 instead. */
.trend-axis-label {
  flex: 0 0 32px;
  background: var(--navy);
}

/* 2026-08-05, per Caleb (live follow-up - "center the text? So both
   number and unit of measure will be centered in the column"): unlike
   .model-chart-wrap's own axis-label-text (real HTML, can float via
   position:absolute with a negative left offset to borrow width from
   this sibling spacer), the Trend charts' tick numbers and unit label
   are drawn with canvas fillText() (ooAxisGutter/native Chart.js
   ticks) - canvas content is hard-clipped to the canvas element's own
   pixel bounds, no CSS trick lets it render into a sibling HTML
   element's space. So as long as this spacer kept its own 32px next to
   the canvas, centering math done relative to the canvas's OWN gutter
   width (ooAxisGutter's gutterWidth/2, Chart.js's ticks.crossAlign:
   "center") would only be centered against PART of the visible navy
   block, not the whole thing - visibly off-center. Collapsing this
   spacer to 0 width for Trend charts specifically makes the canvas's
   own drawn gutter the ENTIRE visible navy header, so that same
   centering math now lines up against everything the user actually
   sees. Scoped to .trend-chart-wrap (Wind/Seas/Confidence Trend) only
   - .model-chart-wrap's own identical-looking spacer (By Model chart)
   is untouched, still 32px, since it holds real positioned HTML text
   that still needs it. */
.trend-chart-wrap > .trend-axis-label {
  flex: 0 0 0;
}

/* 2026-08-04, per Caleb - full history of this label's positioning:
   (1) "shift the FT/Seas text to be in line with the model text at the
   bottom" - moved from centered-in-the-whole-box to the bottom 34px
   band. (2) "get the MPH/FT text to read horizontally instead of
   vertically" - dropped the vertical-rl/rotate(180) pairing the Trend
   charts' own axis labels still use. (3) "center the MPH and FT text
   in their boxes" - widened to the full 66px merged column instead of
   just this box's own original 32px. (4) "the text is cut off... the
   2nd column we combined is filled over the layer with the text" -
   .model-axis-label-text was a child of THIS box (.trend-axis-label),
   extending 34px right into its sibling .model-chart's territory;
   .model-chart is later in the DOM and paints on top by default, so
   its .model-chart-frame background covered the overflowing half of
   the text. Fixed with z-index:3 on this box. (5) Same-day, right
   after that fix shipped: "the numbers and gridlines are messed up" -
   z-index:3 solved the label's overflow into .model-chart's territory,
   but .model-chart-axis-ticks/.model-chart-header-gridlines do the
   EXACT same thing in the opposite direction (extending left into
   THIS box's territory, see their own comments) - so raising this
   box's z-index above .model-chart fixed one overflow direction by
   breaking the other. No single z-index ordering between two sibling
   boxes can win a shared boundary in both directions at once.

   Real fix: stop having two sibling elements each reach into the
   other's space. The label text now lives inside .model-chart-frame
   itself (see forecast.html - a direct child, sibling to
   .model-chart-axis-ticks/.model-chart-header-gridlines), using the
   exact same left:-32px/width:66px footprint those two already use
   safely - safely because, as children of .model-chart-frame, they
   always paint above ITS OWN background regardless of z-index or
   which direction they overflow, with no sibling-boundary fight
   possible. .trend-axis-label goes back to being an empty navy
   spacer (still needed to occupy 32px of real width in the flex
   layout), no text, no z-index tricks, nothing left to fight over. */
.model-chart-frame > .model-axis-label-text {
  position: absolute;
  left: -32px;
  width: 66px;
  bottom: 0;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: #ffffff;
  z-index: 2;
  pointer-events: none;
}

/* 2026-08-04, per Caleb (live follow-up): "there's a larger gap above
   the bars vs below... balance this out." This box's own CSS padding
   was asymmetric top/bottom (14px/6px) on top of Chart.js's own
   internal layout padding, compounding into a real, visible imbalance
   - not just a Chart.js-side issue. Symmetrized to 14px/14px here,
   matched by symmetrizing Chart.js's layout.padding to 18/18 in
   charts.js, so the two layers add up to the same total space on both
   edges instead of silently stacking two different asymmetries. */
/* 2026-08-05, per Caleb ("expand the graphs so they touch the top and
   bottom of the entire table pop up seamlessly", comparing this card
   to the By Model chart's own flush edges): this box's own 14px/16px/
   14px CSS padding was stacking ON TOP OF Chart.js's own internal
   layout.padding (18/18 top/bottom, charts.js), so the real gap
   between the card's edge and the actual bars was 32px, not 18 - the
   .model-chart-wrap comparison chart has ZERO CSS-side padding of its
   own for exactly this reason (its own history: "no padding... like
   .trend-chart-canvas-wrap's own padding" used to be the model chart's
   own comparison point, before THAT padding was removed there too).
   Dropped to 0 here so Chart.js's own 18/18 (still untouched - it's
   the real headroom the bar-value labels above each bar need so they
   don't clip against the canvas edge) is the ONLY layer of spacing
   left, same single-layer approach the By Model chart already uses. */
.trend-chart-canvas-wrap {
  position: relative;
  flex: 1;
  min-width: 0;
}

/* 2026-08-05, per Caleb (live follow-up - after layout.padding was
   already 0/18 and a hard refresh still showed the same top gap):
   the real cause was never the Chart.js-side padding at all - a
   <canvas> element is `display: inline` by default (same as <img>),
   so even sitting inside a zero-padding wrapper it still picks up the
   inline box model's default `vertical-align: baseline`, which
   reserves a few pixels for text descenders below/above the element
   depending on line-height - the classic "phantom gap under an img/
   canvas" CSS issue. There was no CSS rule targeting the canvas
   element itself anywhere on this page, so every Chart.js canvas (all
   3 Trend charts) had this gap; the By Model chart never showed it
   because it's plain HTML/CSS bars, not a canvas, so it was never
   subject to this rule in the first place - explains why "looks the
   same" after the layout.padding fix. */
.trend-chart-canvas-wrap canvas {
  display: block;
}

.hazard-banner {
  padding: 12px 16px;
  border-radius: 8px;
  margin-bottom: 24px;
  font-size: 14px;
  font-weight: 600;
  border-left: 4px solid transparent;
}

/* Hazard severity - reflects NOAA's own designation (Small Craft
   Advisory < Gale/Storm Warning < Hurricane/Tropical Storm Warning),
   not an invented scale. See severity.py for the text matching. */
.haz-generic  { background: var(--hazard-bg); color: var(--hazard-text); border-left-color: #d9a441; }
.haz-moderate { background: #fdf3d9; color: #7a5400; border-left-color: #e0a825; }
.haz-severe   { background: #fbe3d4; color: #8a3a10; border-left-color: #d9611c; }
.haz-extreme  { background: #f7d6d6; color: #7a1010; border-left-color: #b31414; }

/* Wind/seas severity icon badges (2026-08-04, replacing the old plain
   color dot) - a small ocean-themed glyph next to the value, colored
   via currentColor so the same SVG markup (see forecast.html) just
   inherits whichever tier color is set here. Wind gets a signal-
   pennant icon (loosely modeled on real small-craft-advisory/storm-
   warning flag conventions - single pennant -> double pennant ->
   square flag -> flag with center dot as conditions worsen); seas
   gets a wave-crest icon that gets visibly more turbulent (more
   peaks, sharper crests) as seas build. Per Caleb: "Some sort of Icon
   or Badge that is ocean themed and correlates to the severity." */
.sev-icon {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  vertical-align: middle;
}

.sev-icon svg {
  width: 20px;
  height: 20px;
}

/* Same bands applied to the value text itself, so the number reads
   at a glance the way PredictWind/Windfinder's colored tables do -
   the icon badge next to it inherits this same color via currentColor.
   Wind and seas are independent tier systems as of the 2026-08-04
   rework (see WIND_BANDS_MPH/SEAS_BANDS_FT in severity.py); each tier
   here uses its own base hex from the spectrum, matching the card
   tint above. Caution's amber text reuses the same darker amber
   (#8a6300) already used for the Confidence badge's Moderate tier,
   rather than the spectrum's literal pale yellow - a pale yellow
   reads poorly as body text against a white/light card. */
/* Split from wind's rule (2026-08-05, see the .data-card.sev-seas-
   excellent comment above for the full reasoning) - same #a9d1ef/
   #0c4a78 pair as that card background, so the number text and its own
   card tint stay a matched, deliberately-chosen pair the way every
   other tier's already is. */
.value.sev-wind-excellent { color: #1b6ca8; }
.value.sev-seas-excellent { color: #0c4a78; }
.value.sev-wind-great                                 { color: #1f9e8b; }
/* Fair (2026-08-06): a true green between Great's teal and Moderate's
   yellow-green - same in-between position it occupies on the scale. */
.value.sev-wind-fair                                  { color: #4aa662; }
.value.sev-wind-moderate, .value.sev-seas-moderate     { color: #7ab93f; }
.value.sev-wind-caution                                { color: #8a6300; }
.value.sev-wind-rough, .value.sev-seas-rough           { color: #e07b1f; }
.value.sev-wind-hazardous, .value.sev-seas-hazardous   { color: #c1291f; }
.value.sev-wind-extreme, .value.sev-seas-extreme       { color: #2b2b2b; }

/* Deterministic boating recommendation tier (2026-07-30) - sits above
   the AI narrative in Forecast Summary as a guaranteed safety signal
   the AI's own prose can't soften or override (see
   severity.boating_recommendation_tier()). Same sev-* classes as the
   Right Now dots/values, so a given color means the same risk level
   everywhere on the page - just styled as a banner instead of a dot,
   since this is meant to be the first thing read, not a small accent. */
.boating-tier {
  padding: 14px 16px;
  border-radius: 8px;
  margin-bottom: 16px;
  border-left: 4px solid transparent;
}

.boating-tier-label {
  font-weight: 700;
  font-size: 15px;
}

.boating-tier-guidance {
  font-size: 13px;
  font-weight: 400;
  margin-top: 4px;
  opacity: 0.9;
}

.boating-tier.sev-good        { background: #e2f0f8; color: #1b6ca8; border-left-color: #1b6ca8; }
.boating-tier.sev-more-severe { background: #fbe9d6; color: #9a5313; border-left-color: #e07b1f; }
.boating-tier.sev-severe-high { background: #fbdad7; color: #8a1d14; border-left-color: #c1291f; }
.boating-tier.sev-worst       { background: #e2e3e5; color: #2b2b2b; border-left-color: #2b2b2b; }

/* Model comparison bar chart - one bar per forecast source, each
   colored by ITS OWN value (not the group's worst case), so you can
   see at a glance how the individual models actually differ instead
   of one blended range. The AI Reasoning Engine's estimate gets its
   own bar with a distinct striped pattern - visually marked as a
   synthesized result, not just another model in the row. */
.chart-section { margin-bottom: var(--space-5); }

.chart-section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-3);
}

.chart-section-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Wind direction arrow - one overall indicator (circular mean of all
   available sources), not one per model bar, so the chart stays
   readable. Rotated via CSS transform since the underlying value is
   just a 0-360 degree number from true north (0deg = straight up).

   2026-08-06 update, per Caleb ("I want the arrow to be pointed
   towards the direction the wind is blowing, not pointed at where the
   wind is coming from"): the arrow now points where the wind is
   blowing TOWARD, not a weather-vane-style FROM indicator anymore -
   see forecast.html, where the template adds 180deg on top of the raw
   wind_direction_deg value at the point each arrow is drawn (the value
   itself is untouched, since "Direction: SE" and everything else on
   the page still needs the real FROM bearing). */
.wind-direction {
  display: flex;
  align-items: center;
}

/* 2026-08-06, per Caleb ("place the 'N' above the arrow so people
   aren't confused where North is... include all N,S,E,W directions
   around the arrow"): replaces the old single "N" letter sitting next
   to the arrow with a small fixed compass rose - 4 labels pinned to
   the top/right/bottom/left of a circle, the arrow centered inside and
   rotating relative to them, the same way a real compass face works. */
.compass-rose {
  position: relative;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--border);
}

.compass-label {
  position: absolute;
  font-size: 9px;
  font-weight: 700;
  color: var(--text-muted);
  line-height: 1;
}

.compass-n { top: 2px; left: 50%; transform: translateX(-50%); }
.compass-s { bottom: 2px; left: 50%; transform: translateX(-50%); }
.compass-e { top: 50%; right: 2px; transform: translateY(-50%); }
.compass-w { top: 50%; left: 2px; transform: translateY(-50%); }

/* Centered via fixed-pixel absolute positioning (not transform), so
   the inline `transform: rotate(...)` set per-request in forecast.html
   is free to do nothing but rotate - no translate() to stack with it. */
.wind-direction-arrow {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 16px;
  height: 16px;
  margin: -8px 0 0 -8px;
  fill: var(--ocean-blue);
  transition: transform 0.2s ease;
}

.chart-section-header h2 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  font-weight: 700;
  margin: 0;
}

/* 2026-08-04, per Caleb (live follow-up): "make the Wind and Sea by
   model tables look the same as the other tables we've been working
   on... keep the blended bar color, but reformat the rest." Matches
   the Trend charts' own two-layer wrap: an OUTER card that "hovers"
   over the page (light var(--border) outline, rounded corners, real
   box-shadow - .trend-chart-wrap's own look) holding the navy unit-
   label box (reuses .trend-axis-label directly, see forecast.html) and
   the bars themselves.

   2026-08-04, same-day follow-up ("the exterior border... should have
   the shadow and look like it's floating rather than having the dark
   dividing line"): .model-chart originally also carried its own 2px
   navy border, meant as an inner frame around just the bars - but with
   .model-chart filling 100% of .model-chart-wrap's remaining space
   (no gap between them), that navy border sat flush against the wrap's
   own edge instead of inset from it, so the two borders fused into one
   heavy dark outline right where the light "floating card" edge was
   supposed to be the only thing visible. Removed entirely - the
   "dark grid" now comes only from the #3d5a7a interior column dividers
   below (.chart-col's border-right, .chart-track's own border-bottom),
   which read clearly as a grid on their own without a redundant outer
   frame competing with .model-chart-wrap's shadow/light-border edge. */
/* 2026-08-04, same-day follow-up ("border around the entire window
   that matches the color of the header to the left"): was the plain
   light var(--border) every other floating card on the page uses -
   swapped to var(--navy), the same color as the MPH/FT header box
   (.trend-axis-label) sitting right inside it, so the card's outer
   edge and its own header read as one deliberate color pairing instead
   of a neutral grey outline around a navy box. */
.model-chart-wrap {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--navy);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

/* 2026-08-04, same-day follow-up ("remove the exterior border you just
   added, leave that box looking like it's floating... the bars and
   text [should be] fully enclosed by the dividing lines, while the
   entire window itself looks like it's floating"): my previous attempt
   put the #3d5a7a frame directly on .model-chart, flush against
   .model-chart-wrap's own edge with zero gap - reading as one fused
   double-border again, the exact problem the earlier navy version had.
   Split into two layers instead, mirroring the Trend charts' own
   structure exactly: .model-chart is now just breathing room (plain
   padding, no border - like .trend-chart-canvas-wrap's own padding),
   and the actual enclosing frame lives on the NEW .model-chart-frame
   INSIDE it (like chartFramePlugin's drawn rectangle, inset from the
   canvas-wrap's edges by that same padding) - so there's real visible
   gap between the floating card's own light edge and the dark frame
   around the bars, instead of the two touching. */
/* 2026-08-04, same-day follow-up ("I want the entire table to look
   like the graph, not a graph placed on top of a floating table - the
   same way the 48-hour table looks"): compared this against
   .table-scroll + .hourly-table directly. That pair has exactly ONE
   boundary - .table-scroll's own border/shadow - and .hourly-table
   itself carries NO border and NO wrapping padding; its cells
   (.hourly-row-label, .hourly-col-head, .hourly-data-cell) each have
   their own small cell-level padding but their backgrounds/borders run
   flush to .table-scroll's inner edge with zero gap, which is exactly
   why it reads as "the table IS the card" instead of "a table floating
   inside a card."

   .model-chart had drifted from that: .model-chart's own 14px padding
   plus .model-chart-frame's own #3d5a7a border created a second,
   visually separate box nested inside .model-chart-wrap - the "graph
   placed on a floating table" look Caleb is calling out. Removed both:
   .model-chart no longer has padding, and .model-chart-frame no longer
   has its own border. .model-chart-wrap's border/shadow is now the
   ONLY boundary, exactly matching .table-scroll, and the axis
   ticks/bars/dividers run flush to it. */
/* 2026-08-04, same-day follow-up ("the numbers column looks
   unorganized... it's floating"): the numbers gutter had a divider on
   its RIGHT (.model-chart-frame::before, separating it from the bars)
   but nothing marking where it starts on the left against the navy
   MPH/FT box - so it read as loose space rather than a real column.
   This border-right is scoped to .model-chart only (not
   .trend-axis-label itself, which is shared with the Trend charts) so
   it doesn't affect anything else on the page. */
/* 2026-08-04, per Caleb ("combine [the navy MPH/FT header] with the
   column that's white showing numbers, so both columns are one dark
   navy column, and the numbers show up as white text"): this
   border-left used to mark the seam between .trend-axis-label (navy)
   and the numbers gutter (white) as two visually distinct boxes.
   Removed now that both are meant to read as one continuous navy
   block - see .model-chart-frame's own background (below) for the
   actual navy fill, and .model-chart-tick for the now-white number
   text. */
.model-chart {
  flex: 1;
  min-width: 0;
}

/* #3d5a7a interior dividers between each model column (the same
   DIVIDER_COLOR the Trend charts and 48-hour table both already use)
   in place of the old flex `gap` spacing - the gap is dropped since a
   real border-right IS the divider now, the same trade made for the
   Trend charts' own bars earlier this session. left stays 34px since
   that's real content space for the tick numbers, not slack.

   2026-08-04, same-day follow-up ("still not seamlessly fitting... the
   ceiling number is cut off"): the 2px top padding I'd left as
   clipping insurance for the top tick's centered text wasn't enough to
   stop it clipping, and it was still the last bit of dead space
   keeping this from truly touching the card's top/right edges. Dropped
   to 0 - the top tick no longer needs that insurance now that it has
   its own non-overlapping treatment below (.model-chart-tick-top),
   same fix already proven for the "0" tick and the divider line. */
/* 2026-08-04, per Caleb ("combine the navy header with the numbers
   column, both one dark navy column, numbers in white"): this used to
   be one flat var(--card-bg) covering the whole frame (gutter + bars
   alike). A hard-stop gradient splits it in two instead - solid navy
   for exactly the 34px numbers gutter (matching .trend-axis-label's
   own navy so the two read as one unbroken block, per .model-chart's
   own comment above), then the normal card-bg for the bars area,
   unchanged from before. The mobile media query below repeats this
   stop at 30px, matching that breakpoint's narrower gutter. */
.model-chart-frame {
  position: relative;
  display: flex;
  align-items: flex-end;
  background: linear-gradient(to right, var(--navy) 0, var(--navy) 34px, var(--card-bg) 34px, var(--card-bg) 100%);
  padding: 0 0 0 34px;
}

/* Divider between the bars and the value/label text below, drawn as
   one continuous line across the bar columns - rather than per-column
   .chart-track border-bottoms (which never lined up as one continuous
   boundary on their own).

   2026-08-04, same-day follow-up ("remove the horizontal line under
   the numbers... extend the vertical line down to the bottom"): this
   used to start at left:0, running under the axis-number gutter too -
   but the Trend charts' own reference (ooColumnDividers) only ever
   draws this line across chartArea, which itself starts AFTER the
   y-axis numbers, never under them. The numbers column isn't "bars
   then text" the way the model columns are, so a bars/text divider
   running under it never made sense. Now starts at left:34px, right
   where the numbers gutter ends - paired with the new full-height
   vertical divider below marking that same boundary. */
.model-chart-frame::after {
  content: "";
  position: absolute;
  left: 34px;
  right: 0;
  bottom: 34px;
  height: 1px;
  background: #3d5a7a;
  z-index: 2;
}

/* The vertical counterpart - separates the numeric axis gutter from
   the bar columns, running the FULL height of the frame (top to
   bottom, through both the bars and the text row below), same "row
   label column" treatment .trend-axis-label and .hourly-row-label
   already use elsewhere on the page (a hairline on their own right
   edge, floor to ceiling). Since .model-chart-frame's own padding is
   now 0 on every side but the left, top:0/bottom:0 here reaches all
   the way to the card's real edges, not just the bars' own bounds. */
/* 2026-08-05, per Caleb ("can we can the vertical dividing lines on
   these two graphs to match the thickness and darkness of the 7 day
   graphs?"): matches the Trend charts' own COLUMN_DIVIDER_COLOR
   (charts.js, #23405d - darkened the same day from the shared
   #3d5a7a) so this gutter/bars boundary reads the same as the Trend
   charts' own gutter/Today divider (ooColumnDividers' leading line,
   added earlier the same day).
   2026-08-05, same-day follow-up ("looks a bit too bold now"): first
   pass also bumped this to 2px, assuming it should match the header
   dividing lines' own earlier 1px->2px change - but ooColumnDividers'
   real lineWidth is 1, not 2 (only the HORIZONTAL header lines in
   ooAxisGutter got thickened; the vertical day dividers only ever got
   darkened, never thickened). Reverted to 1px - color is the only
   change that actually matches. */
.model-chart-frame::before {
  content: "";
  position: absolute;
  left: 34px;
  top: 0;
  bottom: 0;
  width: 1px;
  background: #23405d;
  z-index: 2;
}

/* 2026-08-04, per Caleb ("the colored bar seems to go a bit over the
   border grid line into the AI Estimate column, top right corner"):
   .chart-track already has its own overflow:hidden, which should clip
   .chart-bar to that track's own bounds - but flex layouts split
   fractional pixel widths across columns and round each one for
   painting, so a tall bar's content edge and its column's own
   border-right can land a hair off from each other on some displays,
   letting a sliver of color peek past the line right at the top. Added
   overflow:hidden here too (a second clip at the column level, not
   just the track) as a safety net, since that's the actual boundary
   the color shouldn't cross. */
/* 2026-08-05, per Caleb ("can we can the vertical dividing lines on
   these two graphs to match the thickness and darkness of the 7 day
   graphs?"): same swap as .model-chart-frame::before above - the
   Trend charts' own darker COLUMN_DIVIDER_COLOR (#23405d), replacing
   the original #3d5a7a, so every model column's own divider
   (NOAA | Buoy | GFS | ... | AI Estimate) matches the Trend charts'
   between-day dividers.
   2026-08-05, same-day follow-up ("looks a bit too bold now"): reverted
   the width back to 1px (see .model-chart-frame::before's own comment
   just above for why 2px was wrong here - ooColumnDividers' real
   lineWidth is 1, this only needed the color change). */
.chart-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  /* 2026-08-17 removed: this overflow:hidden was redundant with
     .chart-track's own (a real bug fix comment right below confirms
     "chart-track already has its own overflow:hidden, which should
     clip .chart-bar to that track's own bounds" - this was only ever
     a second safety net for that SAME bar-bleed issue, not something
     the footer text below the bar relied on). Real cost of keeping it:
     Caleb caught live on his phone that at mobile column widths,
     .chart-value's text (e.g. "10.7 mph") can wrap onto a 3rd line
     inside .chart-col-footer's fixed 34px height - and with THIS
     overflow:hidden also active at the column level, that 3rd line
     doesn't just look cramped, it makes the value number vanish
     completely (clipped off before it ever became visible). Removing
     this still leaves .chart-track's own overflow:hidden doing 100% of
     the actual bar-clipping job it was added for - nothing regresses -
     while the footer text now stays visible (just visually tight) in
     the rare case it still needs a 3rd line, instead of disappearing. */
  border-right: 1px solid #23405d;
  /* 2026-08-06, per Caleb ("add these arrows into the top of each of
     the wind model columns"): anchors .chart-col-wind-arrow's own
     position:absolute below, same technique .chart-col-estimate
     already used for its star badge. */
  position: relative;
}

.chart-col:last-child { border-right: none; }

/* 2026-08-04, per Caleb ("the AI Estimate text no longer looks
   centered... I'm sure because the border is larger"): the column
   right before AI Estimate used to draw its own 1px #3d5a7a
   border-right at the exact same physical line where the gold overlay
   (.chart-col-estimate::after) also drew its own 2px left edge - the
   two stacked on top of each other, making that one side ~3px instead
   of the 2px gold alone on the other three sides, which read as the AI
   Estimate column's content sitting off-center. This rule dropped the
   plain divider here so the gold overlay would be the only line on
   that boundary.

   2026-08-06, per Caleb ("get rid of the yellow/gold border") ->
   same-day follow-up ("it looks like the other dark dividing lines
   were removed though. Can we add those back?"): the gold overlay
   this rule was avoiding a collision with is gone now (see
   .chart-col-estimate's own comment, further up) - so this override
   was just leaving that one boundary with NO line at all instead of
   restoring the plain divider every other column pair already has.
   Removed entirely; .chart-col's own border-right: 1px solid #23405d
   applies here again uniformly, same as every other internal column
   boundary. */

/* 2026-08-04, per Caleb: "add numbers to the left like the other
   graphs" - a real numeric axis, same spirit as the Trend charts' own
   y-axis ticks. Positioned absolutely (not a flex sibling) so it can
   be pinned to exactly the 150px .chart-track region regardless of how
   tall the value/label text below the tracks happens to render -
   bottom:34px matches .chart-col-footer's own fixed min-height, so
   this box's bottom edge always lines up with the real tracks'.

   2026-08-04, same-day follow-up ("make the lines go all the way
   across the graph"): each .chart-track used to draw its OWN copy of
   this gridline pattern (N separate repeating-linear-gradients, one
   per column, plus a further separate copy in this gutter) - fine
   until Caleb asked for one that visibly spans the entire chart, since
   N independent copies give no guarantee of staying pixel-aligned with
   each other. Replaced all of them with ONE shared layer,
   .model-chart-gridlines (see forecast.html - a plain div, first child
   inside .model-chart-frame). This box itself no longer draws its own
   copy of the gridline pattern - see that div's own comment. z-index:2
   keeps these numbers (and their own halo, below) painted above that
   shared layer regardless of DOM order, so the halo trick still works. */
/* 2026-08-04, per Caleb ("center the numbers in the middle of the
   column" - the merged navy column, i.e. the old 32px label box PLUS
   this 34px gutter, now that there's no seam between them): this box
   used to be positioned/sized to exactly the 34px gutter alone, so
   left:50% centered ticks only within that narrower slice, off-center
   against the full 66px navy column now visible behind them. Since
   this box lives inside .model-chart-frame (which starts AFTER the
   label box, not at the card's real left edge), reaching left over
   the label box's own 32px means a negative left offset - position:
   absolute allows that, and nothing along the way clips it (only
   .model-chart-wrap clips overflow, and this stays inside its
   bounds). width grows by that same 32px so left:50% inside it now
   centers on the full merged column, not just this element's old
   narrower box. */
.model-chart-axis-ticks {
  position: absolute;
  left: -32px;
  bottom: 34px;
  width: 66px;
  height: 150px;
  z-index: 2;
}

/* 2026-08-04, same-day follow-up ("make the lines go all the way
   across the graph"): each .chart-track used to draw its OWN copy of
   this pattern, so it only ever showed in that one bar's own headroom
   - a short bar showed plenty of line, a tall bar showed almost none,
   and none of them visually connected to the numbers gutter's own
   copy. Replaced with ONE shared layer spanning the full frame width
   (gutter included), given a real z-index so it paints ON TOP of the
   bars/tracks instead of being hidden behind their opaque fills - the
   same "continuous ruler line" every column now shares regardless of
   how tall its own bar is. Color is a translucent tint (not the old
   solid #d7dde5) specifically so it still reads as a light reference
   line crossing colored bars rather than an opaque band cutting them
   off. z-index:2 on .model-chart-axis-ticks/frame's own divider
   pseudo-elements keeps the numbers and the frame's own structural
   lines painting above this layer, not fighting it for visibility.

   2026-08-04, same-day follow-up ("make the lines go all the way
   across, even if the colored column doesn't cover that line"): the
   light grey-blue tint above (rgba 215/221/229) was nearly invisible
   against each column's own pale headroom fill - similar lightness, so
   low contrast there - even though it showed up fine crossing the
   saturated bar colors. Switched to a translucent navy instead: dark
   tints read clearly against both a pale headroom AND a saturated bar,
   so the line is now visibly continuous everywhere along its length,
   not just where a vivid bar color happens to be underneath it.

   2026-08-04, same-day follow-up ("a bit lighter, so it doesn't get
   confused with the bar dividers"): 0.22 read close enough in weight
   to the solid #3d5a7a column/frame dividers that the two started
   blending together as one visual language instead of two distinct
   ones (bold structural dividers vs. faint reference lines). Dropped
   to 0.12 - still visible against both pale and saturated fills, just
   clearly the lighter of the two now.

   2026-08-04, same-day follow-up ("change the numbers so the
   gridlines don't dissect them"): the halo-behind-the-number trick
   (.model-chart-tick's own background + z-index) was still letting the
   line show through in practice, and after several rounds trying to
   tune it, that approach just isn't reliable enough to depend on.
   Simpler and guaranteed to work regardless of any stacking/paint-order
   subtlety: don't draw a line under the numbers gutter AT ALL. Now
   starts at left:34px - same boundary .model-chart-frame::after
   already uses - so the lines only ever exist within the bar columns,
   where there's no text for them to cross in the first place. The
   numbers still sit right at that same boundary, so they still read as
   labeling those heights, just without a line physically running
   through the digits. */
/* 2026-08-04, per Caleb ("bring the gridlines over into the new merged
   header box") - reverted back to left:34px. The -32px attempt drew
   the lines correctly, but they were invisible once they crossed into
   the navy header: this tint is rgba(10, 37, 64, 0.12), i.e.
   var(--navy) itself at 12% opacity - a translucent copy of navy
   painted on top of a SOLID navy background is indistinguishable from
   that same background, so extending this particular layer into the
   header was invisible, not absent. See .model-chart-header-gridlines
   below for a second, separate layer using a color that actually shows
   up against navy, covering just that region instead. */
/* 2026-08-04, per Caleb ("make the lines solid" -> then, same day,
   after seeing it live: "that also darkened the other gridlines... I
   don't like how this looks, make those look the same as they did"):
   the solid #3d5a7a version above was too heavy against the bars -
   reverted back to the original translucent navy tint (0.12 opacity),
   which is what actually read well here across pale/saturated bar
   colors alike (see the fuller history above this rule for why that
   specific color/opacity was chosen in the first place). The "solid"
   ask stands for .model-chart-header-gridlines below, which Caleb
   didn't flag - only this bars-side layer gets reverted. */
.model-chart-gridlines {
  position: absolute;
  left: 34px;
  right: 0;
  bottom: 34px;
  height: 150px;
  background-image: repeating-linear-gradient(to top, rgba(10, 37, 64, 0.12) 0, rgba(10, 37, 64, 0.12) 1px, transparent 1px, transparent 25%);
  z-index: 1;
  pointer-events: none;
}

/* 2026-08-04, per Caleb ("extend the faded lines...over to the navy
   header as well"): a second, separate gridline layer just for the
   merged navy header region (same -32px/66px footprint
   .model-chart-axis-ticks uses, so it lines up exactly with where the
   numbers now sit). Needs its own color - a translucent WHITE instead
   of .model-chart-gridlines' navy tint - since that navy tint is
   invisible against this box's own solid navy background (see that
   rule's own updated comment). White at a modest opacity shows clearly
   against the solid navy fill here, the same way the navy tint next
   door shows clearly against the bars' pale/saturated fills - each
   layer using whichever color actually contrasts with what's behind
   it, rather than one color trying to do both jobs. */
/* 2026-08-04, same-day follow-up ("make the lines solid"): matching
   .model-chart-gridlines' own switch away from a translucent tint -
   full-opacity white instead of the old 0.18-opacity version, so this
   half of the ruler reads just as solid against the navy header as the
   other half now reads against the bars. */
/* 2026-08-05, per Caleb ("can we do the same color and thickness
   adjustments to the two by model table header dividing lines that we
   just did to the 7 day trend charts?" - referring to charts.js's
   ooAxisGutter plugin, updated the same day to use DIVIDER_COLOR
   (#3d5a7a, the 48-hour table's own gridline blue) at 2px instead of
   solid white at 1px): matching both changes here too, so the Trend
   charts' and By Model charts' header dividing lines read as the same
   treatment. The tick NUMBERS (.model-chart-tick) keep their own white
   text unchanged - same "line color changes, number stays white for
   contrast" split ooAxisGutter made. */
.model-chart-header-gridlines {
  position: absolute;
  left: -32px;
  width: 66px;
  bottom: 34px;
  height: 150px;
  background-image: repeating-linear-gradient(to top, #3d5a7a 0, #3d5a7a 2px, transparent 2px, transparent 25%);
  z-index: 1;
  pointer-events: none;
}

/* 2026-08-04, same-day follow-up ("make the numbers clean so they're
   not getting intersected by the lines"): centering the old full-width
   text-align:center span put the digits right on top of the gridline
   at their own height, reading as the line running straight through
   them. Switched to a shrink-to-fit label (left:50% + translateX(-50%)
   instead of a full-width centered span) with its own small
   var(--card-bg) patch behind it, so it visually "erases" the segment
   of line directly behind each number instead of crossing through it -
   the same halo-behind-axis-label trick most chart libraries use. */
/* 2026-08-04, same-day follow-up ("clean up the numbers, I don't like
   the lines intersecting"): the halo above only had horizontal padding
   (0 3px) - no vertical breathing room above/below the text's own
   line-box, so the gridline's 1px still peeked past the digits' actual
   ink at the top/bottom of the glyphs. Added real vertical padding and
   a tight line-height so the halo is visibly taller than the text
   itself on every side, guaranteeing full coverage instead of a
   pixel-tight fit that depends on exact glyph metrics. */
/* 2026-08-04, per Caleb: now that the gutter itself is solid navy
   (see .model-chart-frame's own comment) instead of white, this tick's
   own halo - originally var(--card-bg)/white, so gridlines wouldn't
   visibly cut through the number - has to match that navy instead, or
   every tick would show as a small white box floating on the navy
   strip. Text flips from the old var(--text) (dark, meant for a white
   background) to white, for the same reason. */
/* 2026-08-04, per Caleb ("shift the numbers up so they're just on top
   of the gridlines they represent"): every tick used to straddle its
   own line (translateY(50%), centered ON it) except two special-cased
   exceptions - the "0" tick (which used to collide with
   .model-chart-frame::after's own line at that same height) and the
   top tick (which had nowhere to go but past the card's top edge).
   Caleb asked for that "0" treatment - text sitting flush above its
   line, not straddling it - as the default for every tick, not just
   one. That happens to make the old .model-chart-tick-zero override
   below redundant (it's now identical to the base rule) and the old
   .model-chart-tick-top override moot (the top tick isn't rendered at
   all anymore - see forecast.html's axis-ticks loop, `tick.pct != 100`
   - Caleb's own call on how to handle a top number with nowhere left
   above it to go: drop it, and let the tallest bar's own height near
   the top of the frame imply that ceiling value instead of printing
   it). Both overrides removed as dead code.

   2026-08-04, same-day follow-up ("extend the numbers up a bit more to
   sit on top of the lines completely"): translate(-50%, 0%) put the
   tick's own bottom edge flush AT the line, with no breathing room -
   Caleb wants clear daylight between the number and the line it
   labels, not the two touching. An extra -4px nudges it up off the
   line while keeping the same "labels its line from above, doesn't
   straddle it" relationship the rest of this rule's history is about. */
.model-chart-tick {
  position: absolute;
  left: 50%;
  transform: translate(-50%, calc(0% - 4px));
  background: var(--navy);
  padding: 2px 4px;
  line-height: 1;
  font-size: 10px;
  font-weight: 700;
  color: #ffffff;
  white-space: nowrap;
}

.chart-track {
  position: relative;
  width: 100%;
  /* 2026-08-04, per Caleb: "make the bars taller" - was 90px, which
     left the new blended gradient (violet -> ... -> red/grey) mostly
     compressed into a thin sliver for typical Gulf conditions (single-
     digit to teens mph/ft, a small fraction of the 0-50 mph / 0-10 ft
     scale) - taller bars give that gradient more real pixel height to
     actually read as a blend instead of a near-flat color. Since then,
     the axis itself was also made to scale to the real values shown
     (severity.nice_axis_max()) rather than a fixed 50 mph/10 ft
     ceiling, so a typical bar now uses most of these 150px on its own
     merits, not just because the track got taller.
     background-color comes from app.py per-bar now (a faded tint of
     THAT bar's own severity color, "bar.light_color" - see the
     .chart-bar comment below).

     2026-08-04, same-day follow-up ("make the lines go all the way
     across the graph"): this used to also carry its own copy of the
     gridline background-image, visible only within its own headroom.
     That's now .model-chart-gridlines instead - one shared layer for
     the whole chart, painted above every track via z-index rather than
     tucked behind each one individually - so this element goes back to
     just its own background-color and nothing else. */
  height: 150px;
  overflow: hidden;
}

/* Bars sit flush against each other, square corners throughout now -
   was rounded on the chart's outer edges only; squared to match the
   Trend charts' own square bars. */

/* 2026-08-04, per Caleb: "make the remaining parts in the bar that are
   left over from the actual measurement a faded color of what's shown
   in the bar rather than a light grey" - .chart-track's own
   background-color (set inline per-bar in forecast.html, from app.py's
   new bar.light_color) now supplies that faded tint instead of the old
   flat #eef1f5 every bar shared regardless of its value. Model bars
   are still colored via an inline gradient/color computed in app.py
   (severity.py's continuous WIND_COLOR_STOPS_MPH/SEAS_COLOR_STOPS_FT
   scale), not a fixed CSS class - so two values in the same safety
   tier still render visibly different colors. */
.chart-bar {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
}

/* 2026-08-04, per Caleb ("add a faded fill for the rest of the top of
   that column as well"): was a flat neutral #f2f6fa, unlike every
   other bar's own headroom (a faded tint of THAT bar's real color, via
   severity.lighten_hex()). Matches that same pattern using the
   estimate bar's own color, hardcoded here rather than piping a new
   value through app.py for what's already a fixed, known color (unlike
   the model bars, whose headroom tint depends on each bar's own
   value).

   2026-08-06: recomputed after the estimate bar's own color changed
   from var(--navy) to a Bahamas-teal (see .chart-bar-estimate's own
   comment) - severity.lighten_hex('#0f8a94', 0.75) worked out to
   #c3e2e4, replacing the old navy-derived #c2c8cf.

   2026-08-06, same-day follow-up ("still not crazy how the color
   looks similar to a color we already have established... any
   suggestions on how to make this one bar unique?"): the teal got
   swapped again for a neutral slate charcoal (var(--estimate-accent),
   #2b2f38) - recomputed once more, severity.lighten_hex('#2b2f38',
   0.75) worked out to #cacbcd.

   2026-08-06, same-day follow-up ("making the AI estimate bar color
   gold/shade of yellow... let's go with R"): recomputed a third time
   for the muted brass gold (#8a6a1f) - severity.lighten_hex('#8a6a1f',
   0.75) works out to #e2dac7, so this headroom tint keeps tracking
   whatever the bar's own current color actually is. */
.chart-col-estimate .chart-track { background-color: #e2dac7; }

/* 2026-08-04, per Caleb ("stick out compared to the other models" ->
   picked accent border + badge -> then, same day, after seeing it
   live: "the fill for the rest of the columns no longer goes up" +
   "the new floating barrier conflicts with the original boxes
   designed" + "revert to the way it was before, then just make the
   border a bright color that contrasts").

   Root cause of complaint #1: the first attempt used a real `border`
   plus `margin: 4px 4px 0`. margin is never absorbed by border-box,
   so it added actual height to this ONE flex item inside
   .model-chart-frame (align-items: flex-end, auto height = sized to
   its tallest child). That made the frame itself grow ~8px taller
   than the fixed 150px track + 34px footer every other column
   actually uses, leaving a blank strip above the whole row - not just
   this column - since the axis ticks/gridlines/bars are all still
   pinned to that original 184px box. Complaint #2 was the
   rounded-corner, inset-margin "card" look itself, which breaks the
   flush, edge-to-edge grid language every other part of this chart
   (and the 48-hour table) was deliberately built to match.

   Fix: no margin, no border-radius, no real `border` on the column
   itself (border-box would keep it out of layout height, but a
   rounded/margined "chip" is what's being reverted here, not just the
   height bug).

   2026-08-04, same-day follow-up ("extend the gold to circle the
   entire column on all 4 sides"): an inset box-shadow was tried first
   since it paints with zero layout footprint - but this column's own
   children (.chart-track, the bar inside it, .chart-col-footer) are
   all opaque and in normal flow, so they paint on top of their
   parent's box-shadow and covered it everywhere except the thin gaps
   between them, which is why only the footer's outline showed up
   live. Switched to the same technique already used elsewhere in this
   chart for exactly this problem (.model-chart-frame::before/::after,
   the shared column/row dividers) - a ::after overlay, absolutely
   positioned to the column's exact bounds and painted above
   everything in it (z-index higher than the bar, badge, and
   gridlines), so the border shows on all 4 sides regardless of what's
   underneath. Still zero effect on the column's own layout size. */
.chart-col-estimate {
  position: relative;
}

/* 2026-08-06, per Caleb ("can we get rid of the yellow/gold border
   surrounding the AI Estimate column?"): the ::after overlay above
   existed solely to paint that gold ring (a ::after rather than a
   plain `border` because .chart-col-estimate's own children are all
   opaque/in-flow and would have covered a real border or box-shadow -
   see the long history above this rule for why). With the border
   itself gone, an empty inset:0 pseudo-element paints nothing, so the
   whole rule is removed rather than left as dead CSS. .chart-col-
   estimate keeps its position:relative (below) - that's what the star
   badge's own position:absolute still anchors to, unrelated to the
   border this removes. */

/* Small star badge in the column's own top-right corner - kept from
   the earlier round (not part of what Caleb flagged when the border
   was removed).

   2026-08-06, per Caleb ("can we make the star the same color as the
   bar?"): was var(--accent-gold), the original bright gold that used
   to pair with the (now-removed) border - a fixed color independent
   of whatever the bar itself was currently using, so once the bar
   moved through teal and charcoal it stopped matching the star at all.
   Switched to var(--estimate-accent) - the same variable the bar
   itself reads (currently the muted brass gold) - so the star always
   tracks the bar's real color instead of needing its own separate
   update every time the bar's color changes again. */
.chart-col-estimate-badge {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 14px;
  height: 14px;
  color: var(--estimate-accent);
  z-index: 3;
}

/* 2026-08-06, per Caleb ("add these arrows into the top of each of the
   wind model columns in the graph"): started as a white circle badge
   for contrast against the bar's own color underneath it.

   2026-08-06 update, per Caleb ("remove the white circle/halo around
   the arrow, and increase the arrow size to match that of the one
   above it [the compass-rose header arrow, 16px]. Also, color the
   arrow as dark navy to match the header box"): dropped the circle
   backdrop entirely - just the bare arrow now, centered horizontally,
   sized and colored to match .wind-direction-arrow above it exactly
   (var(--navy) - "the header box," i.e. the dark navy MPH/axis gutter
   this chart already uses, not var(--ocean-blue) the compass arrow
   itself happens to be). */
/* 2026-08-06, per Caleb ("add the direction label next to the arrows
   in the by model boxes... just put 'SE'"): the arrow's own container
   became a small flex row (arrow + letter) instead of just the bare
   arrow - still centered as one unit via the same left:50%/
   translateX(-50%) pairing. */
.chart-col-wind-arrow {
  position: absolute;
  top: 4px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  align-items: center;
  /* 2026-08-06, per Caleb ("space the by model arrows and the text a
     bit more... match that spacing with the spacing of the arrow in
     the 'Right Now' box"): widened from 2px to match
     .wind-direction-sub's own 8px gap.
     2026-08-06 update, per Caleb ("space them a bit closer together"):
     pulled back in to match .wind-direction-sub's own 4px gap. */
  gap: 4px;
}

.chart-col-wind-arrow svg {
  /* 2026-08-06: same fix as .wind-direction-sub-arrow above - the
     viewBox tightening (24x24 -> path's real "7 2 10 12" bounds) made
     this render much larger at the old 16px box than intended. Sized
     down to the adjacent 10px bold direction-letter's own cap-height
     (~7px) so the arrow lines up with the top/bottom of that text. */
  /* 2026-08-06 update, per Caleb ("the bottom of the arrow is below
     the bottom of the SE text"): same bounding-circle viewBox fix as
     .wind-direction-sub-arrow - square box at the same ~7px
     cap-height, now containing the glyph at any rotation instead of
     just the bearing shown in that screenshot.
     2026-08-06 update, per Caleb ("increase the size... as big as
     possible while staying in line with the text"): same reasoning as
     .wind-direction-sub-arrow - the bounding-circle viewBox makes any
     box size here overhang-safe, so bumped from 7px to 8px, just
     under the 10px label's full line height. */
  width: 8px;
  height: 8px;
  fill: var(--navy);
  flex-shrink: 0;
  transition: transform 0.2s ease;
}

.chart-col-wind-arrow-label {
  font-size: 10px;
  font-weight: 700;
  color: var(--navy);
  line-height: 1;
  white-space: nowrap;
}

/* AI Estimate bar: fills from the bottom just like the model bars
   (so it reads as a complete bar, not a floating sliver). The thin
   marker line across it shows where the low end of the estimate's
   range sits within the fill.

   2026-08-04, per Caleb ("make the color the same dark color as the
   FT and MPH header boxes"): stripe/border color swapped from the
   old accent-blue (var(--ocean-blue), #1b6ca8) to var(--navy)
   (#0a2540) - the same dark navy already used for the MPH/FT header
   box and the chart's own outer border, so the AI Estimate bar now
   visually ties back to that same "this is the app's own synthesized
   answer" navy language instead of a separate blue accent.

   2026-08-04, same-day follow-up ("solid color of darker navy... with
   a compass inside in white, 'Ocean Oracle Estimate' underneath"):
   the diagonal stripe pattern is gone entirely - flat fill now, with
   .chart-bar-estimate-content (the compass + label, see
   forecast.html) absolutely positioned and centered inside it - this
   element's own position:absolute already makes it a valid containing
   block for that child, no extra property needed. That content only
   renders when the bar is tall enough to hold it (app.py's
   estimate_compact, computed from the real fill percentage) - a short
   bar just stays solid.

   2026-08-06, per Caleb ("I like the current color but don't want it
   to be the same as the headers"), then ("I love the color of the
   water in the Bahamas, can we use this as inspiration?"), then
   ("still not crazy how the color looks similar to a color we already
   have established... any suggestions on how to make this one bar
   unique?"): first swap was a Bahamas-teal, but it landed too close
   to the model bars' own gradient (which already fades through blue/
   teal/violet) and the wind severity spectrum's own teal tier - so it
   still read as "one more teal on an already-teal-heavy chart," not
   as distinct. Settled on var(--estimate-accent) (see that variable's
   own comment above for its full history, including its most recent
   swap to a muted brass gold, per Caleb's "gold/shade of yellow" ask)
   - whatever color that variable currently holds is this bar's real
   fill; check there first before assuming this comment's history is
   still current. */
.chart-bar-estimate {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  background: var(--estimate-accent);
  border: 1.5px solid var(--estimate-accent);
  border-bottom: none;
}

.chart-bar-estimate-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  padding: 0 4px;
}

.estimate-compass {
  width: 22px;
  height: 22px;
  color: #ffffff;
  flex-shrink: 0;
}

.chart-bar-estimate-label {
  margin-top: 4px;
  font-size: 7.5px;
  font-weight: 700;
  letter-spacing: 0.2px;
  text-transform: uppercase;
  color: #ffffff;
  text-align: center;
  line-height: 1.2;
}

/* 2026-08-04, same-day follow-up ("what is the white line... not a
   fan of it"): .chart-bar-estimate-marker (the low-end-of-range
   indicator line) removed entirely, along with its two references in
   forecast.html - the compass + "Ocean Oracle Estimate" content is
   now the bar's whole visual story, no secondary marker competing
   with it. */

/* 2026-08-04: fixed height (not just min-height) so every column's
   footer is EXACTLY the same size regardless of whether its value/
   source text wraps to one line or two - this is what lets the
   .chart-col border-right dividers (and .model-chart-axis-ticks'
   bottom:34px, and .model-chart-frame::after's own bottom:34px) all
   line up on one shared, consistent bottom edge instead of drifting
   per-column with whatever that column's text happened to need.
   justify-content centers shorter one-line content within the fixed
   box instead of leaving it stuck to the top.

   2026-08-04, same-day follow-up ("still doesn't look centered - the
   text directly under the bars"): align-items:center centers this
   footer's children as a shrink-wrapped box, which depends on the
   browser's own intrinsic-width guess for text that CAN wrap - it
   doesn't reliably land the text on the same center line as the bar
   above it. Switched to width:100% (matching .chart-track's own
   width, so the footer spans the exact same column width as the bar)
   with align-items:stretch, so .chart-value/.chart-label each get the
   FULL column width to center their own text within via text-align,
   instead of centering a shrink-wrapped box that may not line up. */
/* 2026-08-04, same-day follow-up ("more room above the text than
   below"): .chart-value's own margin-top:6px was baked INSIDE the
   group that justify-content:center balances - so the centered group
   was really "6px + value + label," not just "value + label," which
   pushed the two visible lines of text down off true-center (more
   slack above the group as a whole, on top of that extra leading 6px,
   than below it). Moved that spacing to a `gap` on this flex container
   instead - gap sits BETWEEN items, not before the first one, so the
   actual text content (not a margin) is what gets centered now. */
.chart-col-footer {
  height: 34px;
  width: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: 2px;
}

/* 2026-08-04, per Caleb ("still looks too low... too close to the
   bottom border"): confirmed via follow-up - not a row-alignment issue
   with the other columns (their footer text sits at the identical
   position, since .chart-col-footer's own rule above is unscoped and
   applies the same to every column) and not the compass/label inside
   the bar. It's specifically that this ONE column now has a visible
   gold frame around it (.chart-col-estimate::after) drawing the eye
   right to its edges, and the value/label text sits flush against that
   bottom line with zero breathing room - something every other,
   unbordered column never had to account for. A small bottom padding
   here (border-box, so it eats into the existing 34px rather than
   growing it - no effect on the column's overall height/alignment)
   nudges the centered text up off that line without touching anything
   else about this column's layout.

   2026-08-04, same-day follow-up ("AI Estimate looks like a smaller
   font than the numbers above and other columns"): the actual
   font-size/weight rules (.chart-value 11px, .chart-label 9px) are
   completely unscoped by column - every column, including this one,
   uses the exact same values, so nothing actually shrank. 5px of
   bottom padding was just enough dead space below the text, next to a
   border now drawing attention to that edge, to make the label read
   as smaller/lighter by contrast (classic effect of extra surrounding
   whitespace) - even though it measures identically to "NOAA" or
   "GFS" next to it. Dialed back to 2px: still enough to lift the text
   off the gold line per the original ask, without enough empty space
   below it to trigger that illusion. */
.chart-col-estimate .chart-col-footer {
  padding-bottom: 2px;
}

/* 2026-08-04, per Caleb ("center the model label and number text
   inside their respective boxes"): .chart-col-footer's align-items:
   center handles horizontal position for single-line text fine, but
   .chart-value had no text-align of its own - on a narrow column where
   a longer value (e.g. "5.8-11.5 mph") wraps to two lines, the wrapped
   lines default to left-aligned within the box instead of centered
   like .chart-label already is below it. */
/* 2026-08-18, per Caleb ("add the numbers to the top of the colored
   bars rather than in the boxes with the source text for the by-model
   graphs, the same way we did for mobile"): position:absolute/bottom/
   left/width/z-index below used to live ONLY inside the <=480px mobile
   media query (see the "By Model mobile text-overlay" comments further
   down, "option 3") - moved up here so it applies at every width, not
   just mobile. This works unchanged at desktop because .chart-track's
   150px height and .chart-col-footer's 34px height (the two numbers
   the "42px + fillpct*1.5px" formula below is built from) are BOTH
   already unscoped, universal values - never mobile-specific to begin
   with - so the exact same anchor math mobile already proved lines
   this up correctly here too. .chart-col-footer now only ever holds
   .chart-label (the source name) in normal flow; that rule's own
   flex/justify-content:center (also already universal) centers a
   single line fine with no changes needed there.
   line-height:1.1 is new here (desktop never needed one before, since
   the value used to sit in-flow in the footer where wrapping wasn't a
   height-budget problem) - same tight value the mobile-only rule
   already proved, so a wrapped 2-line value doesn't eat into its own
   clearance above the bar any more than it has to. The mobile-only
   rule further down still exists but is now mostly redundant with this
   (font-size/line-height match) - kept rather than deleted so it's an
   easy place to diverge mobile's overlay from desktop's again later if
   that's ever needed. */
.chart-value {
  position: absolute;
  bottom: calc(42px + (var(--bar-fill-pct, 0) * 1.5px));
  left: 0;
  width: 100%;
  z-index: 3;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.1;
  color: var(--navy);
  text-align: center;
}

/* 2026-08-04: darkened + bolded to match the day-label/hourly-date
   treatment the rest of the page's tables already use (was
   var(--text-muted) at regular weight, reading de-emphasized next to
   those bold dark labels). */
.chart-label {
  font-size: 9px;
  color: var(--text);
  font-weight: 700;
  text-align: center;
  line-height: 1.15;
}

/* 2026-08-04, same-day follow-up ("the number the same color as the
   text below it... fine with a darker shade of blue [so it still
   stands out]"): unified to a single color instead of two different
   ones (was ocean-blue value / var(--text) label). var(--deep-blue)
   (#0f3460, already in the palette - the header gradient's second
   stop) sits between var(--ocean-blue) and var(--navy): still reads
   as clearly blue (so this column keeps standing out from every other
   column's plain var(--text) source name/value), just darker and more
   legible than the original ocean-blue was. */
.chart-col-estimate .chart-value,
.chart-col-estimate .chart-label { color: var(--deep-blue); }

/* Full "AI Estimate" by default; swaps to just "Estimate" below the
   480px mobile breakpoint (see that media query) - same full/short
   swap already proven for .weather-mini-heading-full/-short above,
   for the same reason: this is the one column whose label alone needs
   2 lines at mobile width (every model code - HRRR, ECMWF, ICON - fits
   on 1), which was pushing this column past what its footer box can
   show cleanly. The gold background + compass icon already mark this
   column as the AI estimate without the word "AI" spelled out too. */
.chart-estimate-label-short {
  display: none;
}

/* 2026-08-17: same full/short pattern as .chart-estimate-label-full/
   -short just above - full (1-decimal, "5.8-11.5") shown by default;
   swaps to short (whole-number, "6-12" - matching the 48-hour table's
   own severity.round_half_down() rounding, see app.py's
   _bar_number_text_short()) below the 480px breakpoint instead, so
   the mobile overlay text is short enough to never wrap. */
.chart-value-number-short {
  display: none;
}

/* 2026-08-18, per Caleb ("round the NOAA reading the same way we did on
   the mobile version as well"): NOAA is the one By Model column whose
   own real reading is a range ("11.5-17.3 mph"), not a single-model
   point value, so it's the one column visibly wrapping to 2 lines now
   that the value overlays the bar (a wider space than the old footer
   box, but not infinite either). Applied to that column's own
   .chart-value div (see forecast.html), this flips the normal full/
   short visibility for JUST that element, at every width, not only
   below the 480px breakpoint - same rounded whole-number text
   (.chart-value-number-short, already computed for every column by
   app.py, just normally hidden above 480px) mobile already shows. */
.chart-value-always-short .chart-value-number-full {
  display: none;
}

.chart-value-always-short .chart-value-number-short {
  display: inline;
  white-space: nowrap;
}

/* 2026-08-04, same-day follow-up ("go back to not all capitalized, but
   try spacing the number and AI label closer together"): reverted the
   uppercase/letter-spacing treatment above - Caleb prefers "AI
   Estimate" back in its normal mixed case, and wants the fix aimed at
   tightening the gap between the value ("2-9 mph") and the label
   below it instead. .chart-col-footer's own `gap: 2px` (shared by
   every column) is what separates them; tightened to 0 for just this
   column so the two lines read as one closer-knit unit without
   touching the shared rule everyone else still uses. */
.chart-col-estimate .chart-col-footer {
  gap: 0;
}

/* 2026-08-06, per Caleb ("add dark borders to the other pop ups on
   the page... border that matches the text inside for the 'View AI
   Estimate' pop ups"): border swapped from the generic neutral
   var(--border) to var(--ocean-blue) - the same color the button's
   own text already is, so the border reads as an intentional match
   instead of the plain gray outline every other bordered control on
   the page already uses. */
.estimate-btn {
  border: 1px solid var(--ocean-blue);
  background: #fff;
  color: var(--ocean-blue);
  font-size: 12.5px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 6px;
  cursor: pointer;
}

.estimate-btn:hover { background: var(--bg); }

.estimate-dialog {
  border: none;
  border-radius: 12px;
  padding: 22px 24px;
  max-width: 320px;
  box-shadow: 0 12px 32px rgba(10, 37, 64, 0.25);
}

.estimate-dialog::backdrop { background: rgba(10, 37, 64, 0.35); }

.estimate-dialog h3 { margin: 0 0 10px; font-size: 15px; color: var(--navy); }
.estimate-dialog .estimate-range { font-size: 24px; font-weight: 700; color: var(--navy); margin: 0 0 10px; }
.estimate-dialog .estimate-explanation { font-size: 13.5px; color: var(--text-muted); margin: 0 0 16px; }

.dialog-close {
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  padding: 6px 14px;
  border-radius: 6px;
  cursor: pointer;
}

/* 2026-08-06, per Caleb ("a smaller pop up by-model graph... linked to
   the specific hour" -> landed on one "View 48-Hour Analysis" button
   after discussion, see app.py's _build_hourly_disagreement_analysis
   docstring): scoped wrapper so ONLY the Hour-by-Hour section's header
   gets the flex/space-between treatment needed to sit a button next to
   its title - .page-section-header itself (used by every other
   section on the page) is untouched. */
.page-section-header-with-action {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 12px;
}

/* The dialog itself: same .estimate-dialog base (border-radius/shadow/
   backdrop/close-button styling) as the Wind/Seas "View AI Estimate"
   dialogs, but this one needs real room - it can hold several flagged
   hours' worth of notes, not always just one short paragraph, so it
   gets its own wider max-width and a capped/scrollable height instead
   of growing to fill the viewport on a long list. */
.hourly-analysis-dialog {
  max-width: 440px;
  width: 90vw;
}

.hourly-analysis-list {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  max-height: 340px;
  overflow-y: auto;
}

.hourly-analysis-list li {
  padding: 10px 0;
  border-top: 1px solid var(--border);
}

.hourly-analysis-list li:first-child { border-top: none; padding-top: 0; }

.hourly-analysis-item-time {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--navy);
  margin-bottom: 4px;
}

.hourly-analysis-item-note {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0 0 4px;
}

.hourly-analysis-item-note:last-child { margin-bottom: 0; }

.narrative {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px 22px;
  box-shadow: var(--shadow-card);
}

.narrative h1, .narrative h2, .narrative h3 {
  font-size: 16px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--ocean-blue);
  margin: 22px 0 8px;
}

.narrative h1:first-child, .narrative h2:first-child, .narrative h3:first-child { margin-top: 0; }
.narrative p { margin: 0 0 14px; }
.narrative ul { padding-left: 20px; margin: 0 0 14px; }
.narrative strong { color: var(--navy); }

/* The AI narrative's fixed output format (see reasoning.py's
   SYSTEM_PROMPT) always ends with one "**Best Boating Window:** ..."
   line - the single most actionable sentence on the whole page, so it
   gets pulled out visually as a callout instead of blending into the
   rest of the prose. Targeted by position (last paragraph), not a
   class, since the AI's markdown output has no hooks to add one to -
   safe because the prompt's output format is fixed and always ends
   with this exact line. */
.narrative p:last-of-type {
  background: var(--good-bg);
  border-left: 4px solid var(--ocean-blue);
  border-radius: 8px;
  padding: 14px 16px;
  margin-top: 18px;
  font-size: 15px;
}

.narrative p:last-of-type::before {
  content: "Recommendation";
  display: block;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  color: var(--ocean-blue);
  margin-bottom: 4px;
}

/* 2026-07-30, per Caleb: this callout's color was fixed blue no matter
   what the recommendation actually said - the same problem the
   .boating-tier badge above it was built to fix, just missed here
   because this box is carved out of the AI's markdown output by
   position (:last-of-type), not a template-controlled element. Reuses
   the EXACT same hex values as .boating-tier.sev-* below, via a
   data-tier attribute set on .narrative from the same boating_tier
   the badge already uses - one color decision, shown in two places,
   never two competing color choices for the same conditions. Falls
   back to the original fixed blue (via the base rule above) when
   boating_tier is None (not enough wind/seas data yet). */
.narrative[data-tier="sev-good"] p:last-of-type        { background: #e2f0f8; border-left-color: #1b6ca8; }
.narrative[data-tier="sev-more-severe"] p:last-of-type { background: #fbe9d6; border-left-color: #e07b1f; }
.narrative[data-tier="sev-severe-high"] p:last-of-type { background: #fbdad7; border-left-color: #c1291f; }
.narrative[data-tier="sev-worst"] p:last-of-type       { background: #e2e3e5; border-left-color: #2b2b2b; }

.narrative[data-tier="sev-good"] p:last-of-type::before        { color: #1b6ca8; }
.narrative[data-tier="sev-more-severe"] p:last-of-type::before { color: #9a5313; }
.narrative[data-tier="sev-severe-high"] p:last-of-type::before { color: #8a1d14; }
.narrative[data-tier="sev-worst"] p:last-of-type::before       { color: #2b2b2b; }

/* Legal disclaimer (2026-07-31, per Caleb) - deliberately template-
   rendered, not part of the AI narrative, so it always appears exactly
   the same way regardless of what the AI generates that day. Sits
   directly under the narrative/recommendation box, since that's the
   highest-stakes text on the page (a go/no-go call), not tucked away
   in a footer where it'd be easy to miss. Muted styling on purpose -
   present and legible, not competing visually with the severity-
   colored boating tier badge or recommendation callout above it. */
.disclaimer {
  margin-top: 14px;
  padding: 10px 14px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-muted);
  border-top: 1px solid var(--border);
}

.disclaimer strong { color: inherit; }

/* 2026-08-06, per Caleb ("a percentage chance of rain... and an icon
   that shows a sun for sunny, partially covered sun for partly
   cloudy..."): precipitation chance + condition icon, wired into all 3
   placements he chose (Right Now card, Hour-by-Hour row, 7-Day Outlook
   column) - see forecast.html's weather_icon() macro for the actual
   SVG shapes. */

/* Right Now placement: originally its own standalone data-card (see
   git history) - moved 2026-08-06, per Caleb ("format this
   information...to be in the center of the current and moon phase
   box"), into a centered column of .currents-card instead. See
   .weather-mini/.weather-mini-icon/.weather-mini-pct/.weather-mini-
   label, defined alongside .currents-card/.moon-phase above. */

/* Hourly strip row - no bar-track here (precip chance has no "worse is
   bigger" severity scale the way wind/seas do), so this is just a
   centered icon + percentage, the same height-agnostic treatment as a
   plain data cell. */
.hourly-weather-cell {
  padding: 8px 6px;
}

.hourly-weather-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
}

.hourly-weather-icon svg {
  width: 20px;
  height: 20px;
}

.hourly-weather-pct {
  font-size: 11px;
  font-weight: 700;
  color: var(--navy);
  white-space: nowrap;
}

/* 7-Day Outlook column - inline icon + percentage, matching the
   table's existing inline treatment for wind direction
   (.outlook-direction) rather than a stacked layout, since this table
   reads as a dense row of short inline facts, not a card grid.

   2026-08-18, per Caleb ("the icons under precip are a bit scattered
   - have the Icon start at the same position for all rows... so the
   Icon's position doesn't change if the percentage is double or
   triple digits"): this box was inline-flex with no width of its own,
   shrink-wrapped to exactly icon+gap+text - since the Precip cell
   centers its content (the table-wide text-align:center change
   above), a SHORTER shrink-wrapped box (like "7%") centers narrower
   than a WIDER one ("100%"), so the icon (first in the row) landed at
   a different x-position row to row even though it's always the
   first element. Giving the box itself a fixed width, with its
   content left-aligned inside that fixed width, means the box's own
   footprint stays constant regardless of the number inside it - only
   the fixed box as a whole gets centered in the cell, so the icon at
   its left edge lands in the exact same spot on every row. 46px is
   room enough for the 18px icon + 4px gap + up to "100%" (the
   widest realistic value) at 11px bold without crowding. */
.outlook-precip {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  width: 46px;
  gap: 4px;
  /* 2026-08-18, per Caleb ("the weather icon and percentage text are a
     hair above the center line of the rows... in line with the Day,
     Wind, and Seas text"): .outlook-precip-cell-inner's flexbox
     centering (below) is working correctly now, but a plain geometric
     center isn't always the same as the visual/optical one - these
     weather SVGs (sun/cloud/rain shapes) tend to carry more visual
     weight in their upper half, so a perfectly centered bounding box
     still reads as sitting a hair high. A small nudge down on the
     group as a whole (not just the text, so the icon moves with it -
     the earlier per-element nudges got de-synced from each other,
     which was the actual complaint a few rounds back). */
  margin-top: 2px;
}

/* 2026-08-18, per Caleb's "center them in the middle of the rows" ask
   - see .outlook-precip-pct's own comment for the full story. Table
   cells default to vertical-align:middle already (every other column
   in this table has been relying on that same default all along), so
   this was never actually missing - made explicit here just so it's
   not an invisible browser default this cell happens to depend on. */
.outlook-precip-cell {
  vertical-align: middle;
}

/* 2026-08-18, per Caleb's "center them in the middle of the rows" ask,
   same-day follow-up after vertical-align:middle above still didn't
   visibly center this cell - see forecast.html's own comment on this
   wrapper div for the full reasoning. height:100% resolves against
   .outlook-precip-cell's own rendered height (a real, definite value
   here since it's a table cell whose row height is already
   established by its taller siblings - Wind/Seas/Day), then plain
   flexbox centers the icon+text (or "--") within that height, fully
   independent of the table-cell vertical-align model this table has
   already shown itself to be unreliable about once this session. */
.outlook-precip-cell-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.outlook-precip-icon svg {
  width: 18px;
  height: 18px;
}

/* 2026-08-18, per Caleb's "same size as the day/date label" ask - see
   .outlook-table's own base-rule comment above - was 13.5px, its own
   explicit override the base font-size change alone doesn't reach. */
/* 2026-08-18, per Caleb ("make sure the percentage number is centered
   with the Icon"): was missing its own line-height, so it inherited
   body's ambient 1.55 - at 11px that's a ~17px line box wrapping an
   ~11px glyph, and .outlook-precip's align-items:center centers each
   flex item by its rendered box height, not its actual glyph height,
   so the text's inflated line box threw its visual center off from
   the 18px icon's real center by a couple pixels. Same root cause
   already diagnosed and fixed this session for .outlook-direction-
   label's own icon+text row - line-height:1 makes this element's box
   match its real glyph height, so align-items:center now centers
   what's actually visible instead of an invisible padding margin. */
/* 2026-08-18, per Caleb ("group the icon and percentage text together
   so one doesn't change without the other, then center them in the
   middle of the rows"): the margin-top nudges tried above (-1px, then
   -2px, then -3px) fixed the icon-vs-text relationship but each one
   also shrank .outlook-precip's own rendered height from the top,
   since margin-top on a flex item pulls it (and the space it
   occupies) upward without anything filling in below - that quietly
   dragged the WHOLE icon+text group's centered position within the
   row upward too, which is what read as "too close to the top" once
   the internal alignment itself looked right. Switched to
   position:relative + top instead: this repaints the text a few
   pixels higher WITHOUT changing the box's own height/footprint in
   the layout, so .outlook-precip's real height stays true to its
   actual content and the table's default vertical-align:middle on
   <td> (unchanged, already relied on by every other column) centers
   the icon+text group correctly within the full row height again. */
.outlook-precip-pct {
  font-size: 11px;
  line-height: 1;
  position: relative;
  top: -2px;
  color: var(--text);
}

/* 2026-08-06, per Caleb ("PRECIPITATION or PRECIP depending on the
   room available"): a dedicated breakpoint just for this label swap,
   separate from the existing 480px mobile breakpoint below (which
   restacks .data-grid to one column per row - past that point,
   .currents-card gets the full row width back, so the squeeze this is
   actually solving for is the medium-width range above 480px but
   below full desktop width, where 3 columns still share one row). */
@media (max-width: 640px) {
  .weather-mini-heading-full { display: none; }
  .weather-mini-heading-short { display: inline; }
}

/* 2026-09-29, per Caleb ("the ribbon at the top showing the contributors
   is way too big"): map credits (Esri basemap + OpenFreeMap/OpenStreetMap
   coastline). Both providers ask for their credits roughly as written, so
   this shrinks the box rather than the wording. Desktop: small, tight,
   mostly-transparent strip, still always visible. Phones (max-width 480px
   below): collapsed behind the round "i" button map.html adds, expanded
   on tap. #ocean-oracle-map in the selector so these outrank Leaflet's own
   `.leaflet-container .leaflet-control-attribution` rule, which loads
   after this file. */
#ocean-oracle-map .leaflet-control-attribution {
  font-size: 10px;
  line-height: 1.4;
  padding: 1px 6px;
  color: #475569;
  background: rgba(255, 255, 255, 0.6);
  border-bottom-left-radius: 4px;
}
#ocean-oracle-map .attribution-toggle {
  display: none;
}

@media (max-width: 480px) {
  .data-grid { grid-template-columns: 1fr; }
  .page { padding: 24px 16px 48px; }
  .page-section-header h2 { font-size: 18px; }
  .forecast-header h1 { font-size: 24px; }
  .trend-chart-wrap { height: 190px; }
  .trend-chart-wrap.small { height: 130px; }
  /* 2026-08-17, per Caleb ("still running into the issue of the graph
     not expanding to the full pop up window size compared to the
     other pop ups"): this 10px all-sides padding was undoing 2026-08-
     05's own desktop fix for the exact same complaint - see .trend-
     chart-canvas-wrap's own base-rule comment ("expand the graphs so
     they touch the top and bottom of the entire table pop up
     seamlessly, comparing this card to the By Model chart's own flush
     edges" - that rule went to effectively zero CSS-side padding
     specifically so this card's canvas fills it exactly like .model-
     chart-wrap's own zero-padding approach). No comment anywhere
     explains why mobile should differ, and .model-chart-wrap (the "by
     model" chart Caleb's comparing this against) has no equivalent
     mobile padding re-add of its own - reads as a leftover/oversight,
     not an intentional design choice, especially since removing it
     doesn't risk clipping the value/day labels: Chart.js's own
     internal layout.padding (18/18 top/bottom, charts.js) is a
     separate, already-proven-adequate layer for that, independent of
     this CSS wrapper's own padding - the desktop version already
     relies on exactly that same protection with zero wrapper padding
     of its own. Removed entirely, matching desktop's own 0. */
  .trend-axis-label { flex-basis: 26px; }
  .model-chart-frame { padding: 0 0 0 30px; background: linear-gradient(to right, var(--navy) 0, var(--navy) 30px, var(--card-bg) 30px, var(--card-bg) 100%); }
  .model-chart-frame::after { left: 30px; }
  .model-chart-frame::before { left: 30px; }
  .model-chart-axis-ticks { left: -26px; width: 56px; }
  .model-chart-gridlines { left: 30px; }
  .model-chart-header-gridlines { left: -26px; width: 56px; }
  .model-chart-frame > .model-axis-label-text { left: -26px; width: 56px; }
  .model-chart-tick { font-size: 9px; }
  .narrative { padding: 20px 18px; }
  /* 2026-08-17, real bugs Caleb caught live on his phone: the By Model
     chart squeezes every source (NOAA/Buoy/GFS/HRRR/ECMWF/ICON/AI
     Estimate) into one non-scrolling row (.model-chart-wrap is
     deliberately overflow:hidden, not a .table-scroll like the Hourly/
     Outlook tables have - it was never expected to need scrolling), so
     each column gets very little width on a phone.
     The arrow/star collision on the WIND AI Estimate column - his
     first fix attempt here (hiding the arrow's own label at this
     breakpoint) is gone now that the star itself is gone from that
     column's markup instead (see forecast.html's chart-col-estimate
     comment) - his actual call once he saw both options was to keep
     the arrow AND its direction label, drop the star, not the other
     way around.
     .chart-value's text can still need a 3rd wrapped line inside
     .chart-col-footer's fixed 34px height at these narrower widths
     (see .chart-col's own updated comment - removing that class's
     redundant overflow:hidden already stops the value from being
     clipped invisible, and overflow:visible below is the belt-and-
     suspenders fallback if a 3rd line still happens). Shrinking the
     value/label font here reduces how often that 3rd line is needed at
     all - but "AI Estimate" is a genuinely longer label than any model
     code (HRRR, ECMWF, ICON...), the one column still needing 2 lines
     for its LABEL alone even after the shrink, which combined with a
     2-line value pushes that specific column to 4 total lines instead
     of everyone else's 3 - past what even the visible-overflow
     fallback can do gracefully (the extra line pokes up into the
     divider line above it, "cut off" in a different, worse way).
     Same fix already proven for Precipitation's own heading (full text
     on desktop, a short form below 640px) - .chart-estimate-label-full/
     -short below swap "AI Estimate" for a single word at this
     breakpoint, back to the same 1-line-label budget every other
     column already has. */
  /* 2026-08-17, per Caleb ("still looking cramped in the text boxes...
     more columns in the wind box compared to seas, which allows seas
     more room for the text to wrap... making the text labels on the
     by-model graph the same as the hourly graph, which doesn't have a
     separate box at the bottom for measurement text, it just places
     the text above the colored bar"): confirmed - Wind splits 7
     columns (NOAA/Buoy/GFS/HRRR/ECMWF/ICON/AI Estimate) across the same
     row width Seas splits across only 5 (NOAA/Open-Meteo/WaveWatch
     III/ECMWF-Wave/AI Estimate), so Wind's columns are narrower and
     wrap .chart-value's text ("7-11 mph") more readily inside the same
     fixed 34px .chart-col-footer every column shares, even after the
     "AI Estimate" -> "Estimate" swap right below fixed that one
     column's specific overflow.
     Caleb's own proposed fix, already proven on the Hourly table's
     .hourly-value-overlay/.hourly-unit-overlay: don't fit the value
     into the cramped 34px footer at all - lift it out and overlay it
     directly on the bar/track above, which has a real 150px of height
     to wrap into instead of 34px. Only the short source name (a model
     code, or "Estimate" after the swap below - never more than one
     line) stays in the footer.
     This anchors to .chart-col itself (position:relative, see that
     rule above), not .chart-track - .chart-value lives in the DOM as
     a .chart-col-footer child, but CSS position:absolute walks up
     past non-positioned ancestors to the nearest positioned one
     regardless of DOM nesting, so this needs no markup change.
     top:20px clears .chart-col-wind-arrow's own ~14px-tall row
     (top:4px + its own content height) with a few px of breathing
     room on wind columns; seas columns have no arrow to clear, so
     they just get a bit of harmless extra headroom above the value
     instead. z-index:3 matches .chart-col-estimate-badge's own tier -
     enough to paint above .model-chart-gridlines/-axis-ticks (z-index
     1-2) so the value stays legible over the bar/gridline backdrop.
     Once absolutely positioned, .chart-value leaves normal flow
     entirely - .chart-col-footer's flex layout no longer reserves any
     space for it, so the footer (still the same fixed 34px, still
     keeping every .chart-col divider/tick/gridline aligned to that
     shared bottom:34px edge untouched) now holds only .chart-label,
     with plenty of room to spare.

     2026-08-17, same-day follow-up, per Caleb's live screenshot after
     the first version of this shipped: "the text doesn't go in line
     with the colored bars" + "the colored bars may get too close to
     the top to allow for space for the text" + noticing the 48-hour
     table has no left-side axis numbers and wondering if that's
     related. All three were right, confirmed by reading the actual
     scale code (not guessed): the By Model chart's bar heights come
     from severity.nice_axis_max() - sized to whatever's ~35% above
     the tallest REAL value that day (see that function's own updated
     comment for why 35, was 15), so a real, meaningful left-axis
     makes sense. The 48-hour table's bars, by contrast, scale against
     a fixed generous 50 mph/10 ft ceiling a normal day never gets
     close to - guaranteed headroom always, which is also exactly why
     IT has no left-axis (ticks against bars that only ever fill the
     bottom quarter of the box wouldn't say anything useful). Two
     charts, two deliberately different, both-correct scaling choices
     - which is why the overlay technique "just worked" on one and
     not the other.
     Fixed top:20px put every column's text at the same height
     regardless of how tall THAT column's own bar happened to be -
     fine on the 48-hour table (every bar is short relative to its
     fixed ceiling, so a fixed offset from the top always cleared
     every bar), wrong here (bars are real, proportional, and often
     near the top of the track by design). --bar-fill-pct (set inline
     per .chart-value div in forecast.html - a plain, unitless number,
     that column's own bar.height_pct or {wind,seas}_chart.
     estimate_fill_pct, whichever this particular value belongs to)
     lets this position itself 20px above THAT bar's own real fill
     top instead: 150px (the track height) minus 1.5px per fill
     percent (150/100) gives the unfilled headroom above the fill in
     px, then another 20px off that for clearance. Combined with the
     wider 35% axis headroom above, the tallest bar of the day now
     keeps ~39px of clearance instead of the old ~20px, and every
     shorter bar gets its text sitting just above its OWN top instead
     of floating at a fixed height unrelated to its own fill. */
  /* 2026-08-17, same-day follow-up #2, per Caleb's live screenshot:
     "the speed text is a bit below the bars" - the per-bar anchoring
     above is correctly landing each column's text at 20px above that
     bar's own fill top for a SINGLE line, but a `top`-anchored,
     height:auto box grows DOWNWARD as its text wraps to 2-3 lines
     (e.g. "5.8-11.5 mph" -> 3 lines) - so the box's fixed top edge
     stayed put but its bottom edge (the last line) kept sliding
     further down as it wrapped, eating right back into the 20px
     clearance and touching the fill boundary on exactly the columns
     that most needed the room.
     Anchoring via `bottom` instead of `top` fixes this the same way a
     caption sitting above a photo usually does: with height:auto and
     no `top` set, the browser computes the box's top edge as
     (container height - bottom - box's own height) - so the BOTTOM
     edge is what stays fixed at the anchor point, and extra wrapped
     lines push the top edge further up into the (normally ample)
     space above instead of pushing the bottom edge down into the bar.
     Same anchor point as before, just measured from .chart-col's own
     bottom edge instead of its top: .chart-col is 184px tall (150px
     track + 34px footer), so the fill top's distance from .chart-col's
     BOTTOM is 34px (the footer) plus 1.5px per fill percent (the
     unfilled headroom, same conversion as before) - then the same
     20px of clearance added on top of that.

     2026-08-17, same-day follow-up #3, per Caleb's live screenshot:
     still cramped - NOAA's "5.8-11.5 mph" (the one genuine range
     value, wider than every other column's single number) wraps to 3
     lines and was overflowing PAST THE TOP of the column instead
     (bottom-anchoring fixed the downward collision, but a box that
     grows upward still needs somewhere to grow INTO). Found the real
     numbers instead of guessing further: NOAA's real headroom above
     its own bar this run was ~64px (150px track, bar fillpct ~57.5%
     against the wind axis's real axis_max=20 - axis_step is 5, see
     _build_model_chart()'s own wind call). 3 lines needed to fit in
     that 64px - but .chart-value had no line-height of its own, so it
     was inheriting body's 1.55 (see body's own rule far above) - at
     11px font-size that's ~17px PER LINE, ~51px for 3 lines, plus the
     20px clearance = 71px needed against only 64px available. A
     small, real deficit, not a huge one - line-height:1.1 (still
     perfectly readable at this size, just not stretched for
     paragraph text) brings 3 lines down to ~36px, and trimming
     clearance from 20 to 16px brings the total to 52px against 64px
     available - comfortable margin instead of a near-miss. */
  /* 2026-08-17, final follow-up, per Caleb ("I want the text inside
     the column of the by model table to reflect how it looks in the
     48 hour table. Same format, font, layout, etc."): the earlier
     11px-vs-12px guessing missed the real difference - the 48-hour
     table's .hourly-value is 11px too, it just never wraps (whole-
     number text, always one line - see app.py's own comment on
     _bar_number_text_short()). Reverted the font bump back to 11px
     (genuine parity with .hourly-value, not a guess at "bigger"), and
     with the mobile text now genuinely short (a single, usually-
     unbroken number line + one small unit line below it, not the
     3-line worst case the 12px/50%-headroom round was built for),
     nice_axis_max()'s headroom dropped back from 50% to 35% (see that
     function's own updated comment) - restores more of the "bars
     actually fill their space" look from the original ask, still with
     real margin for this shorter 2-line text. */
  .chart-value {
    position: absolute;
    bottom: calc(42px + (var(--bar-fill-pct, 0) * 1.5px));
    left: 0;
    width: 100%;
    z-index: 3;
    font-size: 11px;
    line-height: 1.05;
  }
  .chart-value-number-full { display: none; }
  .chart-value-number-short {
    display: inline;
    white-space: nowrap;
  }
  /* 2026-08-17: matches .hourly-wind-unit-overlay's own treatment
     exactly (font-size 8px, bold, uppercase, letter-spacing 0.3px) -
     text-transform does the uppercase swap without needing a second
     data-mph-text/data-kt-text pair (the underlying " mph"/" kt" text
     driving the mph/kt toggle is untouched, just displayed uppercase).
     display:block drops it onto its own line below the number, the
     same stacked "number, then unit" layout the 48-hour table uses -
     desktop keeps this same rule's OWN separate desktop-scope
     definition (unaffected, still inline, still lowercase, still
     right after the number on the same line). */
  .chart-value-unit {
    display: block;
    font-size: 8px;
    text-transform: uppercase;
    letter-spacing: 0.3px;
  }
  .chart-label { font-size: 8px; }
  .chart-col-footer { overflow: visible; }
  .chart-estimate-label-full { display: none; }
  .chart-estimate-label-short { display: inline; }
  /* 2026-08-17, real bug Caleb caught live on his phone: Current/
     Precipitation/Moon Phase (.currents-row) are 3 equal-width flex
     children (.currents-mini-card, flex:1) - fine on desktop, but at
     phone width the page's own 16px side padding plus this row's 14px
     gaps leaves roughly 100px per box, not enough for Moon Phase's
     icon + its own nowrap phase-name text ("Waxing Crescent"), which
     was overflowing its box's right edge instead of wrapping (the box
     itself shrinks fine via existing min-width:0, but nowrap CONTENT
     inside it doesn't shrink with it - text just pokes out past the
     boundary). This is exactly the squeeze the .weather-mini-heading-
     full/-short 640px breakpoint above was already patching around the
     edges of; below 480px it's tight enough to actually clip content,
     not just crowd a label.
     Stale comment above (2026-08-06, before the "reformat into three
     boxes" split) already says the intended mobile behavior was each
     box getting "the full row width back" past this breakpoint - true
     back when this was one single spanning .currents-card, but the
     split into 3 flex siblings never got its own mobile override to
     match, so that promise silently stopped being true. Restoring it
     directly: stack the row instead of squeezing 3 across. Doesn't
     touch each box's own internal sizing/content/typography at all -
     Current/Precipitation/Moon Phase each still look exactly as
     designed, just one per row instead of three squeezed into one.

     2026-08-18 update: Precipitation has since moved out of
     .currents-row into the top row alongside Wind/Seas (see .data-grid
     above), and Tide joined .currents-row in between - so this row now
     holds Current/Moon Phase/Tide instead, but the underlying "1 per
     row below 480px" fix and reasoning are unchanged and still apply
     to all 3 of today's siblings. */
  .currents-row { flex-direction: column; }
  .currents-mini-card { flex: none; width: 100%; }

  /* 2026-09-29, per Caleb ("the tide chart on the mobile view is still
     not fitting within the box"): full-width here, the Tide card's SVG
     draws ~1.7x wider than its desktop 1-of-3 slot, and its labels are
     sized in viewBox units, so they scale up with it. A high/low label
     sits 12 + ~15 units from its dot, but the curve only leaves PAD_Y=18
     units above/below itself (app.py), so on a phone the labels ran up
     into the TIDE header and down to the card's bottom border. On
     desktop that same overhang lands in the card's own padding, which
     is why it only showed here. Fix at this breakpoint only: slightly
     smaller label text (13 units, ~13-15px real - in line with the other
     cards' secondary text) plus real margin above/below the SVG for the
     labels' overhang to draw into (overflow: visible). */
  .tide-point-label { font-size: 13px; }
  .tide-svg { margin: 12px 0 10px; }

  /* 2026-09-29, per Caleb ("compressed so you don't have to scroll over
     to see the full confidence column"): the Confidence badge's full
     "Moderate Confidence · 72" was the widest thing in the 7-Day
     Outlook, pushing the table past a phone's width into the sideways-
     scroll fallback. Here it shows just the level word (High/Moderate/
     Low) - the full text + score stay in its title - and the table's
     400px floor and cell side-padding come down so all 5 columns fit a
     360px screen (the narrowest common phone width). */
  .outlook-badge-full { display: none; }
  .outlook-badge-short { display: inline; }
  .outlook-table { min-width: 0; }
  .outlook-table th, .outlook-table td { padding-left: 4px; padding-right: 4px; }
  /* 2026-09-29, per Caleb ("keep the bubble size the same regardless of
     the text that is inside... keep it centered"): one fixed width for
     every level instead of hugging its word, so Low/Moderate rows line
     up. 72px = the widest short label ("Moderate", ~54px at 11px bold,
     "Excellent" is ~52px) + 7px side padding + 1.5px borders, with a
     hair of slack. */
  .outlook-table .outlook-badge {
    display: inline-block;
    box-sizing: border-box;
    width: 72px;
    padding: 4px 0;
    text-align: center;
  }

  /* 2026-09-25 (mobile refinement round 2), real bug Caleb caught live
     on his phone: the Leaflet/OpenStreetMap attribution banner (top-
     right corner of the map, see map.html's own setPosition/setPrefix
     comments) still reads at its full desktop font-size here even
     after dropping Leaflet's own optional branding text there - on a
     narrow phone screen that's still wide enough to visibly overlap
     marker pins near the top of the map. Belt-and-suspenders on top of
     the JS-side prefix removal: shrink the remaining required-OSM-
     copyright text itself at this breakpoint too, and tighten its own
     padding, so the banner takes up less of the map's actual width.
     Leaflet applies its own inline background/padding via a stock
     stylesheet loaded from cdnjs (see map.html's own <link>) - these
     overrides win via source order (this file loads after Leaflet's
     own CSS) without needing !important. */
  /* 2026-09-29: the OpenFreeMap/OpenStreetMap credit (coastline vector
     tiles) made this line long enough to wrap and run across the whole
     map width, covering the top of the zoom control (top-left). Capped so
     it wraps short of that corner instead. */
  #ocean-oracle-map .leaflet-control-attribution {
    font-size: 9px;
    padding: 1px 4px;
    line-height: 1.3;
    max-width: calc(100vw - 80px);
    border-radius: 4px;
  }
  /* 2026-09-29: collapsed on phones - hidden until the "i" button below
     is tapped (map.html toggles .attribution-open on the map). */
  #ocean-oracle-map:not(.attribution-open) .leaflet-control-attribution {
    display: none;
  }
  #ocean-oracle-map .attribution-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.85);
    color: #0a2540;
    font: italic 700 14px/1 Georgia, "Times New Roman", serif;
    cursor: pointer;
    box-shadow: 0 1px 3px rgba(10, 37, 64, 0.25);
  }
  #ocean-oracle-map.attribution-open .attribution-toggle {
    background: #0a2540;
    color: #fff;
  }

  /* 2026-09-25 (mobile refinement round 2), real bug Caleb caught live
     on his phone right after the attribution fix above: two SEPARATE
     bottom map controls - leaflet-velocity's own wind speed/direction
     readout (bottomleft) and our .wind-source-badge (bottomright, see
     that rule's own comment) - are deliberately placed in opposite
     corners so they sit side by side on a wide desktop map. Neither one
     has ever had a max-width, so each one's text (the badge's own
     "Ocean Oracle Forecast Engine Sources: GFS + HRRR + ICON + ECMWF"
     is a real mouthful) is free to grow as wide as its content needs -
     fine when there's plenty of spare width on desktop, but on a phone-
     width map each one can end up wider than half the map, so the two
     collide/overlap in the middle instead of staying in their own
     corners.
     Constraining each to roughly half the map's width forces its own
     text to wrap onto 2-3 short lines within its own corner instead of
     spilling across the middle - same "let it wrap, don't let it
     collide" fix already used for currents-row above, applied to two
     map controls instead of two cards. `!important` on the
     .leaflet-control-velocity rule specifically (not needed for
     .wind-source-badge, which is a class this project defines itself
     with no competing rule to beat) - leaflet-velocity.css loads AFTER
     this file (see map.html's own <link> order), so a plain source-
     order override here would lose to the plugin's own later rule for
     any property they both set; !important guarantees this wins
     regardless of load order rather than depending on it. */
  /* 2026-09-29, per Caleb's phone screenshot: the half-width cap above
     left the badge wrapping onto 3 lines in the bottom-right corner. The
     bottom-left readout it was sharing the edge with is gone since mobile
     round 5 (displayValues:false - see .leaflet-control-velocity below),
     so the badge now runs as a full-width strip along the map's bottom
     edge instead, same shape as the credits strip along the top on
     desktop: the bottom-right control corner is stretched to the left
     edge and the badge fills it, centered, usually on one line. */
  #ocean-oracle-map .leaflet-bottom.leaflet-right {
    left: 0;
  }
  #ocean-oracle-map .wind-source-badge {
    float: none;
    width: 100%;
    box-sizing: border-box;
    padding: 2px 8px;
    font-size: 10px;
    line-height: 1.35;
    text-align: center;
    white-space: normal;
  }

  /* 2026-09-25 (mobile round 5): dead as of this round, not deleted -
     map.html's L.velocityLayer() now sets displayValues:false (see its
     own comment), which stops leaflet-velocity from ever creating this
     control element at all, so this rule currently matches nothing.
     Left in place rather than removed in case displayValues ever comes
     back for some other reason - harmless either way since a rule that
     matches no element does nothing. */
  .leaflet-control-velocity {
    max-width: 42vw !important;
    font-size: 10px !important;
    line-height: 1.35 !important;
    white-space: normal !important;
  }

  /* 2026-09-25 (mobile refinement round 2), per Caleb's own live
     phone feedback right after the two fixes above: "compress the
     mobile view so it fits on your phone's screen without having to
     scroll down... maybe just decrease the floor of it to bring up a
     bit more." Real UX problem, not just aesthetics - Leaflet captures
     touch-drag gestures for map panning, so a finger-scroll that
     STARTS on the map pans the map instead of scrolling the page past
     it, meaning a tall map isn't just "more scrolling," it's "the
     scroll gesture itself gets hijacked more often." #ocean-oracle-map's
     base rule (70vh, 420px floor - sized for desktop, where there's no
     touch-drag-capture problem to begin with) shrinks here to roughly
     two-thirds of that: on a typical ~800-900px-tall phone viewport
     this drops the map from ~560-630px down to ~360-405px, a real,
     meaningful amount of extra room for the ribbons/legend below to
     fit without scrolling on most phones, not a cosmetic trim. Floor
     dropped further in proportion (420px -> 260px) so a genuinely
     short/older phone viewport still gets real relief too, not just
     screens tall enough for 45vh to already clear 420px on its own.

     2026-09-25 (mobile round 2, same day): shrunk again, 45vh/260px ->
     40vh/230px. The map+ribbon merge into .map-window just above
     reclaimed the old double border + 14px gap between the two cards,
     which is real but small (roughly 15-20px) - not enough on its own
     to meaningfully change whether the page scrolls.

     2026-09-25 (mobile round 3, same day) - SUPERSEDED, not stacked:
     Caleb's own next ask ("expand the map pop up view to be the full
     width and height of the phone screen... rather than leaving the
     small gaps on the side and top"), directly comparing against
     PredictWind's own map screen, replaced this whole vh-guessing
     approach outright. #ocean-oracle-map's real height (and the
     .page-map/.back-link gap-tightening overrides that used to sit
     here) are now driven by body.map-page-body's flex layout further
     down this file instead - the map genuinely fills 100% of whatever
     vertical space is left after the header, not a hand-picked
     percentage of the viewport that still leaves a guessed-at gap
     below. Left as a comment, not deleted outright, so the "why did we
     try vh percentages first" reasoning above stays discoverable. */
}

/* On-the-water observation log (Phase 8a, 2026-08-06 - see
   templates/observation.html and app.py's log_observation_page()):
   Caleb's own condition reports as calibration data. Deliberately
   plain, phone-first form styling - big touch targets, single column
   except the four numeric fields, which pair up 2x2 so seas low/high
   and wind low/high each read as one row. Inputs reuse the app's
   existing border/radius/color language (var(--border)/var(--navy))
   rather than inventing a new input style. */
.observation-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 480px;
}

.observation-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text);
  margin-top: 10px;
}

.observation-form select,
.observation-form input,
.observation-form textarea {
  font: inherit;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-bg);
  color: var(--text);
}

.observation-field-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.observation-field-grid > div {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.observation-submit {
  margin-top: 16px;
  align-self: flex-start;
  /* Bigger than the shared .estimate-btn base - this is the page's one
     primary action, and it needs to be an easy thumb target on a
     phone on the water. */
  font-size: 15px;
  padding: 12px 22px;
}

.observation-saved {
  background: #e2f0e6;
  border-left: 4px solid #2e7d4f;
  color: #1d5537;
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 14px;
}

.observation-history {
  list-style: none;
  margin: 0;
  padding: 0;
}

.observation-history li {
  padding: 10px 0;
  border-top: 1px solid var(--border);
}

.observation-history li:first-child { border-top: none; padding-top: 0; }

.observation-history-loc {
  font-weight: 700;
  color: var(--navy);
  margin-right: 6px;
}

.observation-history-detail {
  font-size: 13px;
  color: var(--text-muted);
}

.observation-history-notes {
  font-size: 13px;
  color: var(--text-muted);
  margin-top: 3px;
  font-style: italic;
}

/* The quiet "log what you saw" entry point at the bottom of each
   forecast page (see forecast.html's own comment next to it). */
.observation-link-row {
  margin-top: 10px;
  padding: 0 14px;
  font-size: 12.5px;
  color: var(--text-muted);
}

.observation-link-row a {
  color: var(--ocean-blue);
  font-weight: 600;
}

/* Live buoy observed-at bubble (2026-08-06, per Caleb - "auto pop up
   when you scroll to that graph, stay up for 10 seconds, then go
   away... pop back up if you hover"). Body-level position:fixed
   element driven by buoy_tooltip.js - NOT nested in .chart-col, whose
   overflow:hidden would clip it (see the js file's header comment).
   Navy-on-white inverse of the page's cards so it reads as an overlay,
   with a small pointer notch drawn via ::after. Hidden by default;
   .visible fades it in - pointer-events stays none in BOTH states so
   the bubble itself can never trap the hover that .chart-col's own
   mouseenter/mouseleave depend on. */
.buoy-tooltip-bubble {
  position: fixed;
  z-index: 40;
  max-width: 250px;
  padding: 9px 12px;
  background: var(--navy);
  color: #fff;
  font-size: 12px;
  line-height: 1.45;
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(10, 37, 64, 0.35);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s ease;
}

.buoy-tooltip-bubble.visible { opacity: 1; }

.buoy-tooltip-bubble::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -6px;
  border: 6px solid transparent;
  border-top-color: var(--navy);
}

/* Map view (Phase 7f, 2026-09-23) */

/* 2026-09-25 (mobile round 3), per Caleb: "expand the map pop up view
   to be the full width and height of the phone screen page, rather
   than leaving the small gaps on the side and top" - directly
   comparing against PredictWind's own map screen, which fills the
   whole viewport below a thin header with no card, no border, no
   margin around the map itself. body.map-page-body (set only on
   map.html's <body>, so index.html/forecast.html are completely
   unaffected) turns the page into a flex column pinned to the REAL
   viewport height (100vh, not min-height) rather than letting content
   determine page height and scroll past it: the header/wave keep
   their natural size as fixed flex items, .map-window becomes the one
   flex:1 item that stretches to consume every leftover pixel, and the
   legend/refresh-banner strip (.page.page-map, still a normal block
   below the map) takes only the height its own content needs. This is
   what makes the "full height" half of the ask literal rather than a
   guessed vh percentage - the map fills exactly what's left, on any
   phone's actual viewport height, not a hand-picked number that could
   still be wrong on a taller or shorter screen. overflow:hidden here
   means this page intentionally never scrolls as a whole page -
   internal pieces (the day/hour ribbon rows) keep their own horizontal
   scroll independently, unaffected.

   2026-09-25 (mobile round 4, same day) - two real bugs Caleb caught
   live on his actual phone, both root-caused rather than guessed at:

   1. "I see text cut off at the bottom... I can't scroll down." Mobile
      Safari's address/toolbar chrome can show or hide as the person
      scrolls, and the classic 100vh unit is defined against the LARGEST
      possible viewport (chrome fully hidden) - not whatever's actually
      visible right now. With this page's own overflow:hidden (needed
      so the map fills real space instead of the page scrolling past
      it), that mismatch has nowhere to go but clipped content, exactly
      what the screenshot showed. `100dvh` (dynamic viewport height) is
      the real fix - it tracks Safari's actual visible height live, not
      the theoretical max. Kept 100vh as the first declaration for
      browsers without dvh support (the second, later declaration wins
      when both are understood - graceful, not a hack). Paired with
      removing the legend/refresh-banner block below the map this same
      round (see map.html's own comment) - together these close the
      overflow from both directions: less total content AND an
      accurate height to measure it against.
   2. "There is still a gap between the blue banner and where the map
      pops up." header-wave (the small decorative scalloped divider
      between the header and page content on every OTHER page) was
      still rendering here as a real ~16px flex item between the header
      and .map-window - exactly the visible gap Caleb was pointing at.
      Hidden outright on this page only (display:none removes it from
      the flex flow entirely, not just visually) - the wave is a nice
      touch for content pages, but PredictWind's own reference map has
      no divider at all between header and map, flush against each
      other instead. */
body.map-page-body {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  margin: 0;
}

body.map-page-body .site-header {
  flex: 0 0 auto;
}

body.map-page-body .header-wave {
  display: none;
}

/* 2026-09-25 (mobile refinement round 2), per Caleb's own idea: shared
   wrapper around #ocean-oracle-map + .wind-ribbon-table (map.html) so
   the two read as one enclosed "window" instead of two separate cards
   with a gap between them - see map.html's own comment on this wrapper
   for the full context. Owns the border/radius/top-margin/background
   both children used to carry individually (each one's own rule below
   dropped those properties to avoid a doubled border/gap re-appearing
   inside this wrapper). overflow:hidden clips both children to this
   one shared radius, same "clip to the outer shape" approach
   .wind-ribbon-table's own rule already used for its two internal
   rows.

   2026-09-25 (mobile round 3, same day): border/radius/margin/
   background above were right for the earlier "card with a gap around
   it" look - PredictWind's own full-bleed map has none of those, flush
   against every screen edge instead. Reset to 0/none specifically
   inside body.map-page-body (map.html's own body class) so this stays
   scoped to the map page rather than changing what .map-window would
   mean if reused elsewhere later. Also the flex:1 child that actually
   fills the leftover vertical height left by the header (see
   body.map-page-body's own comment above) - min-height:0 is required
   here, not decorative, since a flex child's default min-height:auto
   would otherwise refuse to shrink below its content's natural size
   and silently defeat the whole "fill exactly what's left" goal. */
.map-window {
  margin-top: 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--card-bg);
}

body.map-page-body .map-window {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  width: 100%;
  margin: 0;
  border: none;
  border-radius: 0;
}

#ocean-oracle-map {
  height: 70vh;
  min-height: 420px;
  width: 100%;
}

/* 2026-09-25 follow-up, per Caleb: after switching the basemap to
   Esri's World Ocean tiles (map.html - removes the confusing offshore
   admin-boundary line the old OSM tiles rendered), also "darken the
   true coast outline... so users can easily distinguish the divide
   between land and sea." The Ocean basemap's land/water edge is a
   real hard pixel boundary already (tan land, blue water - not a
   blurred gradient), but the two colors sit fairly close in
   lightness/saturation at a glance, especially on a small phone
   screen. This targets Leaflet's own tile-pane element (the actual
   `<img>` tiles, not .map-popup or any of this app's own overlays -
   the heatmap/particle canvas layers sit in Leaflet's separate
   overlay pane, so they're untouched) with a real CSS filter that
   punches up saturation and contrast, which widens the perceived gap
   between the water and land colors right at the coastline - a
   verifiable, targeted CSS rule, not a guess at "looks a bit better."
*/
#ocean-oracle-map .leaflet-tile-pane {
  filter: saturate(1.45) contrast(1.25);
}

/* Inside the full-bleed flex layout, the map itself is the flex:1
   child of .map-window (the ribbon table below it is fixed-height) -
   same min-height:0 reasoning as .map-window's own comment above. The
   vh/min-height pair above still applies as-is to any OTHER context
   .map-window might render in without the flex body class (there
   isn't one today, but no reason to make the base rule flex-only). */
body.map-page-body #ocean-oracle-map {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
}

/* 2026-09-25 (mobile round 4, same day), per Caleb: "lower the bottom
   of the map/day ribbon so that it is seamless with the bottom of my
   iPhone screen." Two parts, both real: the legend/refresh-banner
   block that used to render after .map-window is gone this same round
   (see map.html's own comment - it was also the thing causing the cut-
   off-text overflow above), which alone makes .wind-ribbon-table the
   LAST element on the page and lets .map-window's flex:1 sizing carry
   it all the way to the real bottom edge with nothing trailing it.
   padding-bottom: env(safe-area-inset-bottom) on top of that accounts
   for the iPhone's own home-indicator bar specifically - without it,
   "seamless with the bottom of the screen" would mean the ribbon's own
   tappable buttons sit UNDER that system gesture area on a notched/
   Dynamic-Island iPhone, not just visually touching the edge. Falls
   back to 0 padding automatically on any device without that env()
   variable (older iPhones, Android, desktop) - not iOS-only code, just
   a no-op everywhere else. */
body.map-page-body .wind-ribbon-table {
  flex: 0 0 auto;
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* Leaflet injects popup content as raw HTML (see map.html's
   marker.bindPopup call) - these rules target that markup directly,
   reusing the app's existing .value/.sev-*/.confidence-badge color
   system rather than inventing a separate popup palette. */
.map-popup {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 13px;
  color: var(--text);
}

.map-popup .popup-name {
  font-family: 'Fraunces', Georgia, 'Times New Roman', serif;
  font-weight: 600;
  font-size: 15px;
  margin-bottom: 6px;
}

.map-popup .popup-line {
  margin: 3px 0;
}

.map-popup .popup-link {
  display: inline-block;
  margin-top: 8px;
  color: var(--ocean-blue);
  text-decoration: none;
  font-size: 13px;
}

.map-popup .popup-link:hover {
  text-decoration: underline;
}

/* 2026-09-25 (mobile round 5), per Caleb: "add a small pop up that
   shows up when you place your finger on the map for a location that
   pulls up what the map is showing, similar to the screenshot" -
   PredictWind's own tap popup (a dark box with speed/direction/coords
   and a close button).

   2026-09-25 follow-up, same day, per Caleb's live look: "make it a
   darker color, and change the opacity a bit so you can still see the
   wind map behind it... add headers for the display information like
   'Speed:, Direction:'... no need to display coordinates right now."
   First pass reused the plain white .map-popup look (fine for marker
   popups sitting over solid land, but this popup sits directly over
   the color-coded wind heatmap, where a solid white box blocks exactly
   the data it's reporting on). Targets Leaflet's own
   .leaflet-popup-content-wrapper/.leaflet-popup-tip elements (via the
   className passed to L.popup() in map.html) rather than .map-popup
   itself, since Leaflet's popup chrome - not the inner content div -
   is what actually paints the background/pointer tip. Color matches
   this app's own --navy (#0a2540) rather than a generic dark gray, at
   0.82 alpha - dark enough to read white text clearly, translucent
   enough that the wind colors/particles underneath still show through,
   same "see the map behind it" ask PredictWind's own reference favors
   too. The coordinates row from the first pass is gone entirely (not
   just hidden) - see map.html's own updated comment. */
.wind-tap-popup-wrapper .leaflet-popup-content-wrapper {
  background: rgba(10, 37, 64, 0.82);
  color: #fff;
  border-radius: 10px;
}

.wind-tap-popup-wrapper .leaflet-popup-tip {
  background: rgba(10, 37, 64, 0.82);
}

.wind-tap-popup-wrapper .leaflet-popup-content {
  margin: 10px 14px;
}

/* Leaflet's default close-button glyph is a dark gray "x" meant for a
   white popup background - invisible against this popup's dark navy
   one without an explicit override. */
.wind-tap-popup-wrapper .leaflet-popup-close-button {
  color: #fff;
}

.wind-tap-popup .wind-tap-label {
  font-weight: 600;
  color: #cfe0f0;
}

/* Wind-field ribbon TABLE (originally two separate pill-button rows,
   2026-09-23; redone the same day per Caleb's follow-up - "a clean
   table that divides the days/hours evenly among the entire bottom of
   the map... think from the most user friendly perspective"). One
   bordered box, exactly as wide as the map above it, day row on top and
   that day's hour row directly beneath it, read as a single connected
   table rather than two floating rows. Starts [hidden] as a whole -
   map.html's JS reveals it only once there's real cached data, same
   "never show an unexplained empty control" pattern .wind-legend below
   already established. */
/* 2026-09-25: margin-top/border/border-radius/background dropped -
   now lives inside .map-window above, which owns all four (a doubled
   border/gap would otherwise reappear INSIDE the shared window this
   rule now sits in). background specifically still matters even
   though the wrapper also sets one - Leaflet's own map tiles/controls
   sit in the sibling #ocean-oracle-map right above this, not behind
   it, so this element's own background is what actually paints behind
   the day/hour ribbon rows, not decorative redundancy. */
.wind-ribbon-table {
  background: var(--card-bg);
}

/* Both rows are CSS Grid, not flex-wrap - map.html's JS sets each row's
   own grid-template-columns to exactly its button count (7 for the day
   row; however many hour_checkpoints the selected day has, which varies
   a lot - "Today" is hourly, every other day is a fixed 4 six-hourly
   checkpoints) so the cells always divide the table's FULL width evenly
   between them, the "clean table" look Caleb asked for, rather than
   left-aligned pills that leave empty space on the right. Each column
   still has a real minimum width (see .wind-day-btn/.wind-hour-btn
   below) - if a day ever has enough hourly checkpoints that evenly
   dividing would make cells illegibly narrow, minmax()'s own floor
   makes the row scroll horizontally instead of cramming unreadable
   text into a sliver. */
.wind-day-ribbon,
.wind-hour-ribbon {
  display: grid;
  overflow-x: auto;
}

.wind-hour-ribbon {
  border-top: 1px solid var(--border);
}

/* Mobile pass (2026-09-24): the day row (7 columns x its own 64px
   minmax() floor = 448px) and a busy "Today" hour row (up to 19
   columns x 46px = 874px) both genuinely overflow a phone-width screen
   and scroll horizontally - already true before this pass, just never
   hinted at. Same right-edge fade affordance already proven for the
   Outlook/Hourly tables on forecast.html (.hourly-table-frame::after/
   .outlook-table-frame::after - see table_scroll.js's own header
   comment for that original bug and fix). One frame per row (not one
   shared frame for both) since each row scrolls independently and
   needs its own fade state - map.html's own updateRibbonScrollFade()
   toggles "at-scroll-end" on whichever frame's row just changed. No
   border/radius of its own here (unlike .hourly-table-frame) - the
   outer .wind-ribbon-table above already draws one shared border/
   radius around both rows together, so these frames only need
   position:relative to anchor their own ::after. */
.wind-day-ribbon-frame,
.wind-hour-ribbon-frame {
  position: relative;
}

.wind-day-ribbon-frame::after,
.wind-hour-ribbon-frame::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 24px;
  background: linear-gradient(to right, transparent, rgba(10, 37, 64, 0.16));
  pointer-events: none;
  z-index: 5;
  opacity: 1;
  transition: opacity 0.15s ease;
}

.wind-day-ribbon-frame.at-scroll-end::after,
.wind-hour-ribbon-frame.at-scroll-end::after {
  opacity: 0;
}

/* Table cells, not pills - shared borders between adjacent cells
   instead of gaps, so the whole row reads as one connected strip. Only
   the ribbon table's own outer corners are rounded (via its
   overflow: hidden above), not each individual button. */
/* 2026-09-25 (mobile refinement round 2) - real tap-target gap found
   auditing against Apple/Google's 44px minimum touch-target guideline:
   the day row's old 10px vertical padding + ~13px text landed around
   33px tall; the hour row's 7px padding + 11px text landed around
   25px - the tightest control on the whole app, and exactly the kind
   of thing someone's tapping one-handed on a moving boat. Day row
   bumped to 14px (only 7 columns, width isn't the constraint there,
   so there's no tradeoff to make - lands right at ~44px). Hour row
   bumped to 10px (up to 19 columns on a busy "Today" - going all the
   way to 14px here would make the row noticeably taller relative to
   how narrow each column already is width-wise, so this is a real,
   meaningful improvement (~25px -> ~31px) without fighting the
   already-established minmax() column-width floor's own density
   tradeoff (see .wind-day-ribbon/.wind-hour-ribbon's own comment). */
.wind-day-btn,
.wind-hour-btn {
  padding: 14px 4px;
  border: none;
  border-right: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text-muted, var(--text));
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-weight: 600;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
}

.wind-day-btn {
  font-size: 13px;
}

.wind-hour-btn {
  padding: 10px 4px;
  font-size: 11px;
  font-weight: 500;
}

.wind-day-btn:last-child,
.wind-hour-btn:last-child {
  border-right: none;
}

.wind-day-btn:hover,
.wind-hour-btn:hover {
  background: var(--bg-alt, #f4f7fa);
  color: var(--ocean-blue);
}

.wind-day-btn.active,
.wind-hour-btn.active {
  background: var(--ocean-blue);
  color: #ffffff;
}

/* Wind source badge (2026-09-23 follow-up, per Caleb: "a small pop up
   that is the same style as the ones labeling the wind speed and
   direction that tells what sources the user is viewing"). Deliberately
   matches leaflet-velocity's OWN control box styling exactly - confirmed
   real values pulled straight from the plugin's own leaflet-velocity.css
   (.leaflet-control-velocity), not guessed/approximated - so this reads
   as the same family of on-map label, not a new visual style. Placed in
   its own Leaflet control at 'bottomright' (map.html's JS) since the
   wind speed/direction box already owns 'bottomleft'. */
.wind-source-badge {
  background-color: rgba(255, 255, 255, 0.7);
  padding: 0 5px;
  margin: 0 !important;
  color: #333;
  font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}

/* Wind-field legend (2026-09-23, per Caleb - "what about the color
   codes that give a visual representation of the wind speed?"). Starts
   [hidden] in the markup and only revealed by map.html's own JS once
   L.velocityLayer() actually initializes, so it never shows up as an
   unexplained empty box before refresh_cache.py's wind_field source has
   been run. The gradient itself is server-rendered inline (app.py's
   WIND_FIELD_LEGEND_GRADIENT_CSS) - same "build it once in Python, not
   in the template" pattern the rest of this app already follows for
   anything derived from severity.py's color logic. */
.wind-legend {
  max-width: 420px;
  margin-top: 14px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  font-size: 12px;
  color: var(--text-muted, var(--text));
}

.wind-legend-title {
  font-weight: 600;
  margin-bottom: 4px;
}

.wind-legend-bar {
  height: 10px;
  border-radius: 5px;
  border: 1px solid var(--border);
}

.wind-legend-ticks {
  display: flex;
  justify-content: space-between;
  margin-top: 3px;
}

/* 2026-09-30: friends-only access page (templates/access.html). Reuses the
   observation form's field/button styles; this just keeps it narrow. */
.access-form { max-width: 360px; }
.access-intro { color: var(--text-muted); margin: 0 0 12px; max-width: 520px; }
