/* ============================================
   Setform — Site Styles
   Full-viewport ocean canvas, floating UI islands
   ============================================ */

@import 'tokens.css';
@import 'grid.css';

/* ============================================
   Self-hosted fonts — all served from /fonts on the same origin.
   Rationale: no third-party dependency, strict CSP posture, and
   stable asset URLs the browser caches as immutable.

   font-display: swap — show fallback text immediately, swap in the
     web font once it loads. Prevents invisible-text flashes.

   Material Symbols Rounded is a single variable font file covering
     every icon. It is intentionally untrimmed during development so
     designers can introduce or swap icons freely; before production
     launch the font is subsetted down to only the icons actually
     referenced in HTML/JS (see pre-launch TODO).
   ============================================ */

/* --- Inter (body) — three weights used across the app --- */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/inter-v20-latin-regular.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/inter-v20-latin-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../fonts/inter-v20-latin-600.woff2') format('woff2');
}

/* --- DM Mono (numeric / monospace) — two weights used --- */
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../fonts/dm-mono-v16-latin-regular.woff2') format('woff2');
}
@font-face {
  font-family: 'DM Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../fonts/dm-mono-v16-latin-500.woff2') format('woff2');
}

/* --- Material Symbols Rounded (icons) ---
   Subsetted down to the 14 icons actually referenced in the app
   (air, arrow_drop_down, av_timer, check, check_circle, error,
    keyboard_arrow_down, north, search, star, trending_down,
    trending_up, water, waves). Three of the four variable axes
   (opsz, wght, GRAD) are collapsed to the defaults we use; the
   FILL axis is kept so filled-vs-outlined works (the quality-chip
   star and the wind-chip check_circle / error icons). Net:
   5.0 MB → 368 KB — a 14× reduction in first-load bandwidth.
   `font-display: block` — not swap — intentionally blocks icon
   render until the font loads so we never show raw ligature strings
   like "arrow_drop_down" as visible text. */
@font-face {
  font-family: 'Material Symbols Rounded';
  font-style: normal;
  font-weight: 500;
  font-display: block;
  src: url('../fonts/material-symbols-rounded.woff2') format('woff2');
}

/* Base icon class — sets default axis values. Individual icon
   styles (.quality-chip-icon, .wind-chip-state-icon) override FILL
   to 1 to get filled variants. */
.material-symbols-rounded {
  font-family: 'Material Symbols Rounded';
  font-weight: 500;
  font-style: normal;
  font-size: var(--icon-lg);
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  font-feature-settings: 'liga';
  -webkit-font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
  font-variation-settings: 'FILL' 0;
}

/* Global layout variable — the distance between either card (top bar
   or bottom panel) and the viewport edge it's pinned to. Also used to
   offset the unit toggle and cap the location dropdown. Keeping it on
   :root means one change cascades to all four consumers; they can
   never drift out of alignment at any breakpoint. */
:root {
  --card-inset: var(--space-sm);
}
@media (min-width: 768px)  { :root { --card-inset: var(--space-lg); } }
@media (min-width: 1024px) { :root { --card-inset: var(--space-md); } }

/* Reset */
*, *::before, *::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-family);
  font-size: var(--body-size);
  font-weight: var(--body-weight);
  line-height: var(--body-line-height);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  height: 100vh;
  overflow-y: hidden;
}

/* ---- Ocean Canvas — fixed full viewport ---- */

#particle-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
}

/* ---- Ruler Rail — fixed left edge measurement scale ---- */

.ruler-rail {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: 36px;
  z-index: 5;
  background: var(--card-surface);
  border: none;
  border-radius: 0;
  box-shadow: none;
  pointer-events: none;
  overflow: hidden;
}

.ruler-rail canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* ============================================
   UNIT TOGGLE — MD3 segmented-button pattern, chip-styled
   One interactive control that picks between the "ft + mph" and
   "m + km/h" measurement families. Sits just below the red
   waterline, left-aligned with the top card / bottom panel edge.
   --waterline-y is published by ocean.js on every frame so the
   toggle glides with the horizon instead of jittering.
   ============================================ */

.unit-toggle-wrap {
  position: fixed;
  /* Use the same --card-inset the bottom panel uses (and which the
     top bar's own breakpoint overrides track) so the toggle's left
     edge always lines up with both cards' left edges. Before this
     the toggle was hardcoded to --space-sm, leaving a ~4–10px
     misalignment at 768px+ and 1024px+ breakpoints. */
  left: calc(36px + var(--card-inset));
  top: calc(var(--waterline-y, 50vh) + 16px);
  z-index: 8;
  pointer-events: auto;
}

.unit-toggle {
  display: inline-flex;
  align-items: stretch;
  height: 32px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-surface);
  overflow: hidden;
}

.unit-toggle-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 56px;            /* equal-width segments regardless of label length */
  padding: 0 14px;
  border: 0;
  background: transparent;
  font-family: var(--font-family);
  font-size: var(--chip-size);
  font-weight: var(--chip-weight);
  line-height: var(--chip-line-height);
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Vertical divider between the two segments — 1px hairline at the
   exact border colour so it reads as "same material" rather than a
   heavy separator. */
.unit-toggle-option + .unit-toggle-option {
  border-left: 1px solid var(--border);
}

.unit-toggle-option:hover {
  color: var(--text);
}

.unit-toggle-option:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  position: relative;
  z-index: 1;
}

.unit-toggle-option[aria-pressed="true"] {
  color: var(--text);
}

/* Check glyph — collapses when the segment isn't pressed (same
   mechanism as .iconic-spot-chip's leading check in the location
   dropdown, so the "selected" visual is consistent across the app). */
.unit-toggle-check {
  font-size: var(--icon-md);
  line-height: 1;
  font-variation-settings: 'FILL' 0, 'wght' 500, 'GRAD' 0, 'opsz' 20;
  color: inherit;
  display: none;
}

.unit-toggle-option[aria-pressed="true"] .unit-toggle-check {
  display: inline-block;
}

.unit-toggle-label {
  /* Keep the label itself tabular-friendly — lowercase, no padding
     tricks. Segment min-width already guarantees equal columns. */
  line-height: 1;
}

/* ---- UI Layer ---- */

.ui-layer {
  position: relative;
  z-index: 1;
  height: 100vh;
  display: flex;
  flex-direction: column;
  pointer-events: none;
}

.ui-layer > * {
  pointer-events: auto;
}

.viewport-spacer {
  flex: 1;
  pointer-events: none;
}

/* ============================================
   TOP BAR — Wordmark + Location Selector
   ============================================ */

/* Responsive priority:
     1. Everything on one row when the viewport allows.
     2. Chips fall to a second row (still aligned with the wordmark's
        left edge) when brand + chips can't coexist on row 1.
     3. Location name truncates with an ellipsis as the final fallback
        so the title never wraps to multiple lines or pushes chips into
        overlap territory.
   Achieved entirely by flex-wrap: the natural widths of brand-row and
   top-chips drive the line break; justify-content: space-between lays
   brand-left / chips-right when they share row 1; on row 2 only chips
   remain so justify-content has nothing to distribute and they align
   at start (matching the wordmark's left edge on row 1). */
.top-bar {
  position: fixed;
  /* Uses --card-inset (breakpoint overrides on :root) so top, left,
     right insets automatically match the bottom panel and unit toggle
     on every breakpoint. One source of truth for card edge spacing. */
  top: var(--card-inset);
  left: calc(36px + var(--card-inset));
  right: var(--card-inset);
  /* z-index 11 (vs bottom-panel's 10) so the whole top card and its
     descendants (most importantly the search suggestions dropdown)
     always render above the bottom panel when they overlap. Before
     this both cards tied at 10 and the bottom panel won the tie by
     coming later in source order, so the dropdown was hidden behind
     the bottom card even though its own z-index was 200. */
  z-index: 11;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  row-gap: 8px;
  padding: 12px var(--space-sm);
  background: var(--card-surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.03);
  pointer-events: auto;
  flex-shrink: 0;
}

/* .top-bar inset breakpoint overrides now live on :root as
   --card-inset (top of file) — shared across top bar, bottom panel,
   unit toggle, and the location dropdown's dynamic max-height cap. */

.wordmark {
  font-size: var(--heading-size);
  font-weight: var(--heading-weight);
  letter-spacing: var(--heading-tracking);
  line-height: var(--heading-line-height);
  color: var(--text);
  flex-shrink: 0;
}

/* Clock */

/* Hide the clock while it's empty (pre-load / location-TZ not yet
   known). Prevents a flash of UTC-0 time in the timeline track. */
.timeline-clock:empty {
  display: none;
}

.timeline-clock {
  position: absolute;
  bottom: 100%;
  font-family: var(--font-mono);
  font-size: var(--timeline-clock-size);
  font-weight: var(--timeline-clock-weight);
  color: var(--muted);
  letter-spacing: var(--timeline-clock-tracking);
  white-space: nowrap;
  pointer-events: none;
  z-index: 3;
  /* Swap animation — when the value changes "meaningfully" (location
     switch, scrub across an hour boundary), the JS toggles .is-swapping
     briefly while the text is rewritten, producing a soft crossfade
     instead of an abrupt snap. Opacity only — no position shift — so
     the label stays optically anchored while it changes. Live-tick
     seconds updates bypass this (see _setClockText). */
  transition: opacity 160ms cubic-bezier(0.4, 0, 0.2, 1);
}

.timeline-clock.is-swapping {
  opacity: 0;
}

/* ============================================
   LOCATION SELECTOR
   ============================================ */

/* Brand + location row: the wordmark "Setform." and the location
   trigger ("Pipeline ▾") sit directly adjacent with no gap so they
   visually read as a single string — "Setform.Pipeline" — while
   remaining two independent elements semantically and behaviourally. */
.brand-row {
  display: flex;
  align-items: center;
  gap: 0;
  min-width: 0;
}

.location-selector {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;   /* allow the location name inside to ellipsis-truncate */
  /* No left margin here — the brand-row wrapper above controls the
     horizontal spacing between the wordmark and the selector.
     Not position: relative — we want the dropdown menu's containing
     block to be the .top-bar (already position: fixed) so the menu
     can span the full card width with left:0 / right:0. */
}

.location-trigger {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0;
  background: none;
  border: none;
  cursor: pointer;
  font-family: inherit;
  min-width: 0;   /* allow the trigger-name child to ellipsis-truncate */
}

.location-trigger:hover .location-trigger-name {
  opacity: 0.7;
}

.location-trigger[aria-expanded="true"] .location-trigger-name {
  opacity: 0.7;
}

.location-trigger-name {
  font-size: var(--heading-size);
  font-weight: var(--heading-weight);
  letter-spacing: var(--heading-tracking);
  line-height: var(--heading-line-height);
  /* Primary interactive colour — signals "this piece of the header
     is tappable" without fragmenting the "Setform.Pipeline" string
     (wordmark keeps its neutral --text colour). */
  color: var(--interactive);
  /* Keep the location name on a single line. When the card is too
     narrow to fit "Setform.<long name>" even after chips have wrapped
     to row 2, the name truncates with an ellipsis rather than wrapping
     vertically — preferred per project design guidance. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.location-trigger-icon {
  /* State indicator, not a primary affordance — muted gray matches
     how iOS / GitHub / Notion / Linear / MD3 outlined selects treat
     dropdown chevrons. Keeps the location name as the visual hero. */
  color: var(--muted);
  flex-shrink: 0;
  font-size: var(--icon-xl);
  transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.location-trigger[aria-expanded="true"] .location-trigger-icon {
  transform: rotate(180deg);
}

/* Location Dropdown Menu */

/* Dropdown spans the full width of the .top-bar card at every
   breakpoint — its containing block is the top-bar (position: fixed),
   so left:0 / right:0 mirror the card's padding edges exactly. The
   menu joins the card seamlessly rather than floating offset: its
   top border overlaps the card's bottom border so they read as one
   continuous surface, with rounding only on the bottom two corners.
   Paired with flattened card bottom corners while open (see
   .top-bar:has rule below). */
.location-menu {
  position: absolute;
  /* -1px horizontal offsets cancel the .top-bar's 1px border so the
     menu's outer edges align with the card's outer edges (absolute
     positioning resolves against the padding box, not the border
     box). top: 100% lands the menu's 1px top border exactly on top
     of the card's 1px bottom border — the two lines coincide on a
     single pixel row, giving a clean seam. */
  top: 100%;
  left: -1px;
  right: -1px;
  /* Max-height: min of
       1. the dynamic cap that keeps the dropdown from ever extending
          past the bottom panel's bottom edge (viewport − top-bar top
          inset − top-bar own height − bottom panel's bottom inset);
       2. 480px absolute cap on larger screens — fits ~5½ result rows
          after the iconic-spot chips + search input above them, so
          users see the bottom of a row peeking beneath the last full
          row as a "more below, scroll me" affordance.
     On small viewports #1 wins and the dropdown shrinks to fit the
     available space between top and bottom cards. */
  max-height: min(
    calc(100vh - var(--space-sm) - var(--top-card-height) - var(--card-inset)),
    480px
  );
  overflow-y: auto;
  background: #f7f9fc;
  border: 1px solid var(--border);
  border-radius: 0 0 12px 12px;
  z-index: 200;
  box-shadow:
    0 1px 2px rgba(0, 0, 0, 0.08),
    0 2px 6px rgba(0, 0, 0, 0.06),
    0 8px 24px rgba(0, 0, 0, 0.06);
  padding: 0;
}

/* While the mega-menu is open, flatten the card's bottom corners so
   the card + menu form one continuous rounded surface (rounded only
   on the outer corners). Keyed on aria-expanded of the trigger for
   single-source-of-truth state. */
.top-bar:has(.location-trigger[aria-expanded="true"]) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}

/* Match the bottom panel's scrollbar styling — transparent track so
   the menu's rounded bottom-right corner shows through instead of
   being covered by the default opaque scrollbar track. */
.location-menu {
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.18) transparent;
}
.location-menu::-webkit-scrollbar {
  width: 8px;
}
.location-menu::-webkit-scrollbar-track {
  background: transparent;
}
.location-menu::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.18);
  border-radius: 4px;
}
.location-menu::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 0, 0, 0.28);
}

/* MD3 filter chips — iconic surf spots */

.iconic-spots-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px;
}

/* --- MD3 assist chips (top card, right side) --- */
/* https://m3.material.io/components/chips/specs — assist chip spec:
   • 32dp height
   • 8dp corner radius
   • 1dp outline
   • 18dp leading icon, 8dp gap
   • 16dp horizontal padding (8dp when leading icon present)
   Chips are informational (role="status"). Wind chip tints to match
   the offshore/onshore/cross-shore semantics; period chip stays
   neutral because period is a raw measurement, not a graded condition. */

.top-chips {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  /* No margin-left:auto — the parent .top-bar uses
     justify-content: space-between to position chips on row 1 and
     left-align them on row 2 when they wrap. The chip group itself
     wraps internally so that on very narrow cards, chips that can't
     fit side-by-side stack instead of overflowing the card's edge. */
}

.period-chip,
.wind-chip,
.location-chip,
.tide-chip,
.quality-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 16px 0 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: transparent;
  font-family: var(--font-family);
  font-size: var(--chip-size);
  font-weight: var(--chip-weight);
  line-height: var(--chip-line-height);
  white-space: nowrap;
  color: var(--text);
  transition:
    color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
    border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  user-select: none;
  -webkit-user-select: none;
}

/* Interactive chips — location (opens Google Maps) and period /
   tide / wind (open the bottom panel + scroll to the matching
   section). They share a single affordance set so the row reads
   as a uniform group of tap targets.
   Quality chip is informative-only (no cursor:pointer, no hover,
   no focus ring) — usability testing showed users don't expect to
   click it, so we removed the affordance rather than mislead them. */
.location-chip,
.period-chip,
.tide-chip,
.wind-chip {
  text-decoration: none;
  cursor: pointer;
}
.location-chip:hover,
.period-chip:hover,
.tide-chip:hover,
.wind-chip:hover {
  border-color: color-mix(in srgb, var(--text) 35%, var(--border));
}
/* Keyboard focus ring — matches the project convention used by
   .iconic-spot-chip and .attribution-text so all interactive chips
   share the same focus affordance. */
.location-chip:focus-visible,
.period-chip:focus-visible,
.tide-chip:focus-visible,
.wind-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: color-mix(in srgb, var(--text) 35%, var(--border));
}

.location-chip-flag {
  font-size: var(--icon-sm);
  line-height: 1;
  flex-shrink: 0;
}

.location-chip-label {
  line-height: 1;
}

.period-chip-icon,
.wind-chip-icon,
.tide-chip-icon,
.quality-chip-icon {
  font-size: var(--icon-md);
  line-height: 1;
  /* Outlined variant (unfilled) — matches the icon family reference. */
  font-variation-settings: 'FILL' 0;
  flex-shrink: 0;
}

/* Tide-chip icon hides when trend is slack (empty textContent). Same
   collapse pattern used by the wind-chip-state-icon. Keeps the chip
   layout tight when there's no glyph to show. */
.tide-chip-icon:empty {
  display: none;
}

/* Trailing state icon on the wind chip (check_circle / error) —
   mirrors the Type-row icon in the expansion section so the same
   signal appears on both surfaces. Inherits colour from the chip's
   is-offshore / is-onshore / is-cross class. Collapsed via :empty
   when the wind is cross-shore (no icon — text + tint carry the
   signal there, no Material glyph reads cleanly as "sideways"). */
.wind-chip-state-icon {
  font-size: var(--icon-sm);
  line-height: 1;
  font-variation-settings: 'FILL' 1;
  flex-shrink: 0;
}
.wind-chip-state-icon:empty {
  display: none;
}

.period-chip-label,
.wind-chip-label,
.tide-chip-label,
.quality-chip-label {
  line-height: 1;
}

/* Period chip stays neutral — period is a raw measurement, not a
   graded condition. The icon takes the muted colour so it reads as
   a secondary/contextual signal next to the tinted wind chip. */
.period-chip-icon {
  color: var(--muted);
}

/* At narrow widths the chips wrap to their own row inside the top card
   (see @media rule on .top-bar). That gives them enough horizontal
   space to keep their full labels — so no icon-only collapse needed. */

/* Wind-type tinting — matches the colours already used in the
   expansion card's "Type" value for visual consistency. */
.wind-chip.is-offshore {
  color: var(--wind-offshore);
  border-color: color-mix(in srgb, var(--wind-offshore) 35%, var(--border));
}
.wind-chip.is-onshore {
  color: var(--wind-onshore);
  border-color: color-mix(in srgb, var(--wind-onshore) 35%, var(--border));
}
.wind-chip.is-cross {
  color: var(--wind-cross);
  border-color: var(--border);
}

/* Tide-chip tinting — rising/falling use the same palette as the
   expansion-card trend chip so chip and trend chip read as one
   consistent signal. Slack is neutral/muted. */
.tide-chip.is-rising {
  color: var(--wind-offshore);
  border-color: color-mix(in srgb, var(--wind-offshore) 35%, var(--border));
}
.tide-chip.is-falling {
  color: var(--muted);
}
.tide-chip.is-slack {
  color: var(--muted);
}

/* Quality-chip tinting — star icon takes the tier colour so a
   surfer can read "how good is it?" without reading the phrase.
   Same palette as the (removed) expansion quality section: green
   for great/epic, blue for good, muted for fair/poor.
   Text stays --text for legibility; only the star is tinted so
   the chip still reads cleanly at all tiers. */
.quality-chip .quality-chip-icon {
  /* Use filled star when rendered (JS sets textContent 'star'). */
  font-variation-settings: 'FILL' 1;
}
.quality-chip.is-epic  .quality-chip-icon { color: var(--wind-offshore); }
.quality-chip.is-great .quality-chip-icon { color: var(--wind-offshore); }
.quality-chip.is-good  .quality-chip-icon { color: var(--wave-solid); }
.quality-chip.is-fair  .quality-chip-icon { color: var(--muted); }
.quality-chip.is-poor  .quality-chip-icon { color: var(--muted); }

/* --- MD3 plain tooltip (hover-triggered, desktop only) ---
   Spec: https://m3.material.io/components/tooltips/specs
     • Container: inverse-surface (dark in light theme)
     • Corner: 4dp
     • Supporting text: label-small (12sp), inverse-on-surface
     • Padding: 4dp vertical, 8dp horizontal
     • Show delay: ~500ms
     • 4dp gap from trigger element
   Implemented via the ::after pseudo-element with data-tooltip. No
   tooltip on touch devices (no hover) — aria-label + icon context
   covers that case. */

.period-chip,
.wind-chip,
.location-chip,
.tide-chip,
.quality-chip {
  position: relative;   /* anchor for the ::after tooltip */
}

.period-chip::after,
.wind-chip::after,
.location-chip::after,
.tide-chip::after,
.quality-chip::after {
  content: attr(data-tooltip);
  position: absolute;
  top: calc(100% + 4px);               /* 4dp gap below the chip */
  /* Right-align the tooltip to the chip so it can never spill past
     the card / viewport edge. The chip group is already pushed to
     the right side of the top card, so the tooltip grows leftward
     toward the available empty space. */
  right: 0;
  left: auto;
  transform: translateY(-2px);
  padding: 4px 8px;
  background: #322F35;                 /* MD3 inverse-surface-container */
  color: #F5EFF7;                      /* MD3 inverse-on-surface */
  font-family: var(--font-family);
  font-size: var(--meta-size);
  font-weight: var(--ui-weight);
  line-height: var(--meta-line-height);
  letter-spacing: 0.02em;
  border-radius: 4px;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  z-index: 30;
  transition:
    opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1),
    transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Show on hover with a small delay so it doesn't flash on casual
   mouse traversal (matches MD3 show-delay guidance). */
.period-chip:hover::after,
.period-chip:focus-visible::after,
.wind-chip:hover::after,
.wind-chip:focus-visible::after,
.location-chip:hover::after,
.location-chip:focus-visible::after,
.tide-chip:hover::after,
.tide-chip:focus-visible::after,
.quality-chip:hover::after,
.quality-chip:focus-visible::after {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 0.5s;
}

/* Hide the plain tooltip on touch devices — there's no "hover" state
   and a tap-and-hold would conflict with native long-press behaviour. */
@media (hover: none) {
  .period-chip::after,
  .wind-chip::after,
  .location-chip::after,
  .tide-chip::after,
  .quality-chip::after {
    display: none;
  }
}

.iconic-spot-chip {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 16px;
  font-family: var(--font-family);
  font-size: var(--chip-size);
  font-weight: var(--chip-weight);
  color: var(--text);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
  user-select: none;
  transition:
    background 0.15s cubic-bezier(0.4, 0, 0.2, 1),
    border-color 0.15s cubic-bezier(0.4, 0, 0.2, 1),
    padding 0.15s cubic-bezier(0.4, 0, 0.2, 1);
  line-height: 1;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}

.iconic-spot-chip:hover {
  background: rgba(0, 0, 0, 0.04);
}

.iconic-spot-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.iconic-spot-chip.selected {
  background: rgba(0, 0, 0, 0.08);
  border-color: transparent;
  padding-left: 8px;
}

.iconic-spot-chip.selected:hover {
  background: rgba(0, 0, 0, 0.12);
}

.iconic-spot-chip-icon {
  display: none;
  font-size: var(--icon-md);
  margin-right: 8px;
  line-height: 1;
}

.iconic-spot-chip.selected .iconic-spot-chip-icon {
  display: inline;
}

/* MD3 outlined text field — search input */

.location-search-wrap {
  position: relative;
  padding: 4px 12px 12px;
}

.location-search-icon {
  position: absolute;
  left: 24px;
  top: 50%;
  transform: translateY(calc(-50% - 4px));
  font-size: var(--icon-md);
  color: var(--muted);
  pointer-events: none;
}

.location-search-input {
  width: 100%;
  padding: 10px 12px 10px 36px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-family: var(--font-family);
  font-size: var(--ui-size);
  font-weight: var(--body-weight);
  line-height: var(--ui-line-height);
  color: var(--text);
  background: #fff;
  outline: none;
}

.location-search-input:focus {
  border-color: var(--accent);
}

.location-search-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent);
}

.location-search-input::placeholder {
  color: var(--muted);
}

/* Search results */

/* No own scroll container — the outer .location-menu owns the single
   scrollbar for the whole dropdown. Previously results had their own
   max-height + overflow-y, which produced two nested scrollbars on
   narrow viewports (outer menu + inner results). One surface, one
   scrollbar. */
.search-results {}

.search-result-item {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding: 10px 12px;
  background: none;
  border: none;
  border-bottom: 1px solid rgba(0, 0, 0, 0.04);
  cursor: pointer;
  text-align: left;
  font-family: var(--font-family);
  transition: background 0.15s;
}

.search-result-item:hover,
.search-result-item:focus {
  background: rgba(0, 0, 0, 0.04);
  outline: none;
}

.search-result-name {
  font-size: var(--ui-size);
  font-weight: var(--ui-weight);
  color: var(--text);
}

.search-result-detail {
  font-size: var(--meta-size);
  font-weight: var(--meta-weight);
  line-height: var(--meta-line-height);
  color: var(--muted);
  margin-top: 2px;
}

.search-result-empty {
  padding: 16px 12px;
  font-size: var(--ui-size);
  color: var(--muted);
  text-align: center;
}

.location-menu-helper {
  padding: 8px 12px 8px;
  border-top: 1px solid var(--border);
  font-size: var(--meta-size);
  font-weight: var(--meta-weight);
  line-height: var(--meta-line-height);
  color: var(--muted);
}

/* ============================================
   BOTTOM PANEL — Material expansion panel
   ============================================ */

.bottom-panel {
  /* --card-inset is defined globally on :root (see top of file) so
     both the bottom panel and the location dropdown read the same
     breakpoint-aware value. */
  position: fixed;
  bottom: var(--card-inset);
  left: calc(36px + var(--card-inset));
  right: var(--card-inset);
  z-index: 10;
  /* Expanded-panel cap: the panel can grow upward until there's a
     --waterline-safe-padding gap below the top card's bottom edge,
     then the detail area scrolls internally. The red waterline is NOT
     a reference point anymore (ocean.js centers the waterline between
     the cards' collapsed edges, decoupled from the panel's height).

     Formula: viewport − top-card-top-inset − top-card-height − gap −
     bottom-card-bottom-inset. Both --card-inset values are the same
     number at any given breakpoint (top inset from viewport top
     mirrors the bottom inset from viewport bottom). */
  max-height: calc(
    100vh
      - (var(--card-inset) * 2)
      - var(--top-card-height)
      - var(--waterline-safe-padding)
  );
  background: var(--card-surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.03);
  pointer-events: auto;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* --card-inset breakpoint overrides are on :root (top of file) so
   every consumer (bottom panel, location dropdown) shares them. */

/* ---- Collapsed row ---- */

.bottom-panel-row {
  position: relative;
  height: 72px;
  /* The row carries the only-ever-visible UI (timeline + expand
     button). Never let flex distribute pressure here — if the panel's
     max-height is tight (short viewport + expanded detail), we want
     the .bottom-panel-detail to absorb the shrink and scroll its
     content internally, not the row. Without this, the row collapses
     below 72px and the clock (which sits above the track via
     bottom: 100%) gets clipped by the panel's top edge. */
  flex-shrink: 0;
}

.bottom-panel-title {
  display: none;
}

/* Default layout — puts the strip inside the panel's content area
   before JS runs, so the skeleton is visible on first paint even if
   _positionTimeline hasn't executed yet. Once the first forecast
   arrives, _positionTimeline sets inline `left` + `width` styles
   that override these (width takes precedence over right for absolute
   positioning, so the right default is harmlessly ignored). */
.bottom-panel-timeline {
  position: absolute;
  top: calc(50% - 12px);
  left: 16px;
  right: 72px; /* 16px inset + 40px expand button + 16px inset */
}

/* Expand button */

.bottom-panel-expand {
  position: absolute;
  right: var(--space-sm);
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  background: none;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  color: var(--muted);
  transition:
    background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
    color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  -webkit-tap-highlight-color: transparent;
}

.bottom-panel-expand:hover {
  background: rgba(0, 0, 0, 0.04);
  color: var(--text);
}

.bottom-panel-expand:active {
  background: rgba(0, 0, 0, 0.08);
}

.bottom-panel-expand:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.bottom-panel-chevron {
  font-size: var(--icon-lg);
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.bottom-panel-expand[aria-expanded="true"] .bottom-panel-chevron {
  transform: rotate(180deg);
}

/* ---- Timeline track ---- */

.timeline-track {
  position: relative;
  height: 20px;
  width: 100%;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  border-radius: 0;
  /* Skeleton backdrop — visible through the transparent canvas until
     _renderHeatmap paints opaque pixels over it on first forecast. */
  background: var(--border);
}

.timeline-heatmap {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
}

/* Needle */

.timeline-needle {
  position: absolute;
  top: -14px;
  left: 0;
  width: 2px;
  height: calc(100% + 27px);
  background: #e02020;
  pointer-events: none;
  transition: left 0.22s cubic-bezier(0.4, 0, 0.2, 1);
  z-index: 2;
  box-shadow:
    0 0 0 0.5px rgba(255, 255, 255, 0.6),
    0 1px 3px rgba(0, 0, 0, 0.3);
}

/* Hour labels */

.timeline-hours {
  position: relative;
  height: 1rem;
  margin-top: 0;
}

.timeline-hour-label {
  position: absolute;
  top: 0;
  font-family: var(--font-mono);
  font-size: var(--timeline-hour-size);
  font-weight: var(--timeline-hour-weight);
  color: var(--muted);
  letter-spacing: var(--timeline-hour-tracking);
  white-space: nowrap;
}

.timeline-hour-label::before {
  content: '';
  position: absolute;
  bottom: 100%;
  left: 0;
  width: 2px;
  height: 8px;
  background: var(--accent);
  margin-bottom: 0;
}

/* Skeleton hour-labels wrapper — pre-rendered in HTML so the loading
   state appears on first paint, before JS has a chance to interfere.
   The whole wrapper is wiped by _renderHourLabels' replaceChildren
   call once the forecast lands. We don't reuse .timeline-hour-label
   on the skeleton spans to keep them fully decoupled from any
   cached older stylesheet that only knew the real-label rules. */
.timeline-hours-skeleton {
  display: flex;
  justify-content: space-between;
  padding-inline: 2px;
  width: 100%;
}
.timeline-hours-skeleton > span {
  font-family: var(--font-mono);
  font-size: var(--timeline-hour-size);
  font-weight: var(--timeline-hour-weight);
  letter-spacing: var(--timeline-hour-tracking);
  color: var(--border);
  white-space: nowrap;
}

.timeline-hour-tick {
  position: absolute;
  top: 0;
  width: 1px;
  height: 0;
}

.timeline-hour-tick::before {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  width: 1px;
  height: 5px;
  background: var(--accent);
}

/* ---- Expanded detail ---- */

/* Detail panel spans the full card width so the scrollbar lands in a
   dedicated lane at the card's inside right edge — not 16px inset next
   to the content. Inner blocks (.surf-breakdown / .readings-footer)
   provide their own horizontal padding for breathing room, mirroring
   the mega-menu's scroll pattern. scrollbar-gutter reserves the lane
   so content doesn't shift when the scrollbar appears on overflow. */
.bottom-panel-detail {
  max-height: 0;
  overflow: hidden;
  padding: 0;
  border-top: 1px solid transparent;
  transition:
    max-height 0.195s cubic-bezier(0.4, 0, 1, 1),
    padding 0.195s cubic-bezier(0.4, 0, 1, 1),
    border-color 0.195s cubic-bezier(0.4, 0, 1, 1);
}

.bottom-panel-detail.expanded {
  /* Fill whatever remains of the panel after the timeline row.
     The panel itself has a hard max-height that respects the safe
     zone, so content inside scrolls when it would exceed the
     available space (especially on mobile where sections stack). */
  max-height: 100vh;                /* effectively unlimited within the parent */
  padding: var(--space-sm) 0;
  border-top-color: var(--border);
  overflow-y: auto;
  scrollbar-gutter: stable;
  -webkit-overflow-scrolling: touch;
  transition:
    max-height 0.225s cubic-bezier(0.4, 0, 0.2, 1),
    padding 0.225s cubic-bezier(0.4, 0, 0.2, 1),
    border-color 0.225s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Custom scrollbar — reserves a consistent lane on every platform.
   Without explicit styling, macOS uses overlay scrollbars that reserve
   no space, so the scrollbar-gutter above has no effect and content
   runs to the card's inside edge. Forcing a width makes the gutter
   reservation take effect, so content is predictably inset from a
   visible scrollbar lane regardless of OS or scrollbar preference. */
.bottom-panel-detail {
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.18) transparent;
}
.bottom-panel-detail::-webkit-scrollbar {
  width: 8px;
}
.bottom-panel-detail::-webkit-scrollbar-track {
  background: transparent;
}
.bottom-panel-detail::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.18);
  border-radius: 4px;
}
.bottom-panel-detail::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 0, 0, 0.28);
}

/* ============================================
   SURF BREAKDOWN — expansion panel content
   Adaptive grid: 1 column on mobile → 3 on desktop.
   Quality lives in the top-card chip row now, so the expansion
   holds Swell + Tide + Wind (3 columns). Each section's internal
   layout is flex-based so content wraps gracefully at any column
   width.
   ============================================ */

.surf-breakdown {
  display: grid;
  grid-template-columns: 1fr;                   /* mobile: single column */
  gap: var(--space-md) var(--space-lg);
  padding-inline: var(--space-sm);
}

@media (min-width: 900px) {
  .surf-breakdown {
    /* Desktop: three equal columns — Swell | Tide | Wind.
       Takes the full card width so each section has proportional
       room instead of being crammed into a 4-col slot. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.surf-section {
  min-width: 0;  /* allow flex children to shrink inside the grid cell */
}

/* Unified empty state — centered copy that replaces the 3-section
   grid when surf data is unavailable. Uses the UI type scale so it
   feels of-a-piece with the rest of the card. */
.surf-empty-state {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--space-lg) var(--space-sm);
  min-height: 120px;
  font-family: var(--font-family);
  font-size: var(--ui-size);
  font-weight: var(--ui-weight);
  color: var(--muted);
}

/* Section title — plain icon + title. Any secondary state info
   (trend, shore, source caveat) moves into the first rows of the
   data list below, not into the title. */
.surf-section-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-family);
  font-size: var(--subtitle-size);
  font-weight: var(--subtitle-weight);
  letter-spacing: var(--subtitle-tracking);
  line-height: var(--subtitle-line-height);
  color: var(--text);
  margin: 0 0 12px;
}

.surf-section-title-icon {
  font-size: var(--icon-lg);
  line-height: 1;
  /* Outlined to match the calm, editorial feel of the section title. */
  font-variation-settings: 'FILL' 0;
  flex-shrink: 0;
}

/* ============================================
   MD3 NON-INTERACTIVE DATA LIST
   Shared structure for Swell, Tide, and Wind expansion-panel data.
   Every row is `label | value(+ meta)` — same anatomy, same rhythm,
   same heights across all three sections.
   ============================================ */

.data-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.data-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  min-height: 44px;            /* WCAG AA touch-comfort — also gives consistent row height */
  background: rgba(0, 0, 0, 0.025);
  border-radius: 8px;
}

.data-row-label {
  font-family: var(--font-family);
  font-size: var(--row-label-size);
  font-weight: var(--row-label-weight);
  line-height: var(--row-label-line-height);
  color: var(--muted);
  flex-shrink: 0;
}

/* Value cluster — primary text + optional meta + optional trailing
   icon (direction arrow or trend glyph). Flows on one row normally,
   wraps below for very narrow columns. */
.data-row-value {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  text-align: right;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.data-row-primary {
  font-family: var(--font-family);
  font-size: var(--row-primary-size);
  font-weight: var(--row-primary-weight);
  line-height: var(--row-primary-line-height);
  color: var(--text);
  white-space: nowrap;
}

/* Semantic text tints carried over from the old pill palette. Weight
   alone distinguishes primary from label; tone distinguishes good
   vs warning state information. Both wind-offshore (#1f7a45, 5.24:1
   on card) and wind-onshore (#96610c, 5.13:1) pass WCAG AA on the
   card surface — measured after the pill deletion.

   IMPORTANT: these data-row-primary tints apply ONLY inside the
   expansion card. The top-card chip tints (.wind-chip.is-*) are a
   separate rule set and are not touched by these selectors. */
.data-row-primary--good { color: var(--wind-offshore); }
.data-row-primary--warn { color: var(--wind-onshore); }

.data-row-meta {
  font-family: var(--font-family);
  font-size: var(--row-meta-size);
  font-weight: var(--row-meta-weight);
  line-height: var(--row-meta-line-height);
  color: var(--muted);
  white-space: nowrap;
}

.data-row-caveat {
  font-family: var(--font-family);
  font-size: var(--row-caveat-size);
  font-weight: var(--row-caveat-weight);
  line-height: var(--row-caveat-line-height);
  color: var(--muted);
}

/* Trailing arrow — used by swell rows (rotated `north` for direction)
   and tide trend rows (trending_up / trending_down — no rotation). */
.data-row-arrow {
  color: var(--muted);
  font-size: var(--icon-md);
  line-height: 1;
  transform-origin: 50% 50%;
  transition: transform 0.32s cubic-bezier(0.4, 0, 0.2, 1);
  font-variation-settings: 'FILL' 0;
}

/* Tone-inheriting trailing arrow: picks up the primary's tint so a
   "Rising · green" value has a green trending_up icon beside it. */
.data-row-arrow--tone { color: inherit; }

/* Wind section — list only (compass visualization retired in favor
   of the Direction row's inline arrow, which carries the same
   bearing cue without the extra visual moment). */
.wind-section { min-width: 0; }

/* ---- Readings footer ---- */

.readings-footer {
  margin-top: var(--space-sm);
  padding-inline: var(--space-sm);
  font-family: var(--font-family);
  font-size: var(--meta-size);
  font-weight: var(--meta-weight);
  line-height: var(--meta-line-height);
  color: var(--muted);
}

/* Plain inline text flow — .attribution-text links carry their own
   white-space: nowrap so source names never break mid-word, but the
   surrounding prose (separators + trailing privacy sentence) wraps
   at ordinary word boundaries as the viewport narrows. */
.attribution-line {
  display: block;
}

.attribution-sep {
  color: var(--muted);
}

.attribution-text {
  white-space: nowrap;
  color: var(--muted);
  text-decoration: none;
  transition: color 0.15s;
}

.attribution-text:hover,
.attribution-text:focus-visible {
  color: var(--text);
}

.attribution-text:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

/* ---- Utility ---- */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

[hidden] {
  display: none !important;
}

.is-loading .reading-value {
  color: var(--muted);
}

/* ============================================
   DATA DEGRADATION
   ============================================ */

.timeline-hour-label--outage {
  text-transform: none;
  letter-spacing: 0;
  font-family: var(--font-mono);
  color: var(--muted);
}
.timeline-hour-label--outage::before {
  display: none;
}

/* ============================================
   STATUS OVERLAY
   (centered floating card — unified surface for data-health messages:
    outage, stale + retrying, and active retry countdowns. Driven by
    ui.js::_syncStatusOverlay. The legacy `#retry-overlay` id is kept
    to avoid churning the HTML.)
   ============================================ */

.retry-overlay {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 20;
  pointer-events: none;
  font-family: var(--font-family);
  font-size: var(--ui-size);
  font-weight: var(--ui-weight);
  line-height: 1;
  color: var(--text);
  padding: 10px 16px;
  background: var(--card-surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow:
    0 1px 3px rgba(0, 0, 0, 0.04),
    0 4px 12px rgba(0, 0, 0, 0.03);
  white-space: nowrap;
  user-select: none;
}

/* ============================================
   REDUCED MOTION
   ============================================ */

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