/* ==========================================================================
   Fishlog - "Cosy tackle"
   --------------------------------------------------------------------------
   Kraft khaki ground, deep bark ink, one burnt-amber accent. Rounded
   everything. Documented in docs/19-COSY-TACKLE-BRIEF.md; the colours are
   asserted by tools/palette_check.py and tests/test_palette.py, so a token
   that fails WCAG AA breaks the suite rather than waiting for the nightly
   browser audit to find it.

   Two layers on purpose:
     1. the palette and scales below, which are the design;
     2. the legacy role names (--bg, --primary, ...) mapped onto them, because
        hundreds of declarations already reference those. Re-pointing the
        aliases re-skins the whole app without touching markup, which is what
        keeps the JS bindings and tests/test_template_element_ids.py intact.

   THE ALIAS BLOCK CONTAINS NO LITERAL COLOURS. It used to contain five, which
   palette_check.py could not see and therefore never contrast-tested. Every
   one now points at a real palette token. Do not reintroduce one.
   ========================================================================== */

/* ---- Type. Self-hosted, variable, split by unicode-range so a reader
   downloads only the alphabet they are actually reading: a Polish angler never
   fetches Cyrillic. Nothing is served from a third-party origin -
   tests/test_hardening.py::test_no_third_party_font_origin_is_admitted pins
   the CSP against font CDNs and that stays true.

   Baloo 2 has no Cyrillic. That is handled by the stack rather than by another
   file: the display family lists Nunito immediately after it, so Russian
   headings fall through to Nunito, which does carry Cyrillic. ---- */

@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/static/fonts/nunito-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/static/fonts/nunito-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Nunito'; font-style: normal; font-weight: 400 700; font-display: swap;
  src: url('/static/fonts/nunito-cyrillic.woff2') format('woff2');
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: 'Baloo 2'; font-style: normal; font-weight: 500 700; font-display: swap;
  src: url('/static/fonts/baloo2-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Baloo 2'; font-style: normal; font-weight: 500 700; font-display: swap;
  src: url('/static/fonts/baloo2-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* ---- type families ---- */
  --font-body: 'Nunito', ui-sans-serif, system-ui, -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: 'Baloo 2', 'Nunito', ui-sans-serif, system-ui, -apple-system,
    BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  /* ---- ground. Kraft, warm, matte. Light on purpose: sunlight collapses
     perceived contrast and a light matte ground is what survives it. ---- */
  --canvas: #efe7d6;
  --canvas-deep: #eae1cd;
  --surface: #fbf7ef;
  --surface-sunk: #f4eee0;
  --line: #ded2b8;
  --line-strong: #c9bb9c;

  /* ---- ink. Deep bark, tinted warm from the ground, never flat grey.
     Every value clears 4.5:1 on all five grounds. ---- */
  --ink: #2e2a20;
  --ink-soft: #5a5242;
  --ink-faint: #6b6250;

  /* ---- one accent, locked for the whole app. Burnt amber. ---- */
  --accent: #a04a14;
  --accent-soft: #e9c9a3;
  --accent-wash: #f7eadb;
  --accent-ink: #fff8ec;
  --accent-deep: #7a3a10;

  /* ---- the day bands. Traffic light, green good, red bad (CLAUDE.md
     standing rule 3). The tint carries text; the mark carries the signal.

     THE FOUR MARKS ARE RESERVED. The brief's palette is green, yellow, orange
     and khaki, which is three of the four traffic-light positions - so nothing
     decorative may use a mark hue at mark saturation, or an ornament beside a
     warning stops the warning reading as one. Decoration uses the tints. ---- */
  --band-green: #d6e9d2;
  --band-yellow: #e6ebcb;
  --band-orange: #f7e2c0;
  --band-red: #f3d2cb;
  --mark-green: #2f7d4f;
  --mark-yellow: #6b7f2c;
  --mark-orange: #9e6712;
  --mark-red: #b03d24;

  /* ---- water. A lake outline is a shape, not a verdict, so it takes
     neither the accent (amber reads as land) nor a band hue (green would
     read as a good day). A muted sage sits clear of both. ---- */
  --water: #8fb3a4;
  --water-deep: #5b8574;

  /* ---- semantic inks, read on the washes and tints above ---- */
  --positive-ink: #1f5c3a;
  --warn-ink: #7a2f1a;
  --danger: #9c3a22;
  --danger-deep: #832e1a;

  /* ---- 4px spacing scale. A 4-base keeps the middle steps an 8-only scale
     throws away, and rhythm comes from mixing tight against generous. ---- */
  --s1: 4px;   --s2: 8px;   --s3: 12px;  --s4: 16px;
  --s5: 24px;  --s6: 32px;  --s7: 48px;  --s8: 64px;

  /* ---- type. Tracking tightens as size grows: a heading set at display size
     with body tracking reads loose. ---- */
  --t-xs: 0.75rem;   --t-sm: 0.8125rem; --t-base: 1rem;
  --t-lg: 1.125rem;  --t-xl: 1.375rem;  --t-2xl: 1.75rem;  --t-3xl: 2.25rem;
  --track-tight: -0.022em;
  --track-normal: 0;
  --track-wide: 0.06em;
  --measure: 62ch;

  /* ---- depth. Three steps and no more; if everything is raised, nothing is.
     Tinted to the bark ink - a pure black shadow on a warm ground is dirt. ---- */
  --e1: 0 1px 2px rgba(46, 42, 32, 0.06), 0 0 0 1px rgba(46, 42, 32, 0.05);
  --e2: 0 4px 14px -4px rgba(46, 42, 32, 0.15), 0 0 0 1px rgba(46, 42, 32, 0.05);
  --e3: 0 16px 40px -12px rgba(46, 42, 32, 0.22), 0 0 0 1px rgba(46, 42, 32, 0.06);
  --edge: inset 0 1px 0 rgba(255, 255, 255, 0.7);

  /* ---- motion. Never ease-in on UI: it delays the moment the eye is already
     waiting on. The built-in easings are too weak to read as intentional. ---- */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-water: cubic-bezier(0.34, 1.32, 0.5, 1);
  --dur-fast: 130ms;
  --dur: 200ms;
  --dur-slow: 320ms;

  /* ---- radius. The brief asks for round shapes, so the scale is generous and
     held everywhere; nothing in this app has a sharp corner. ---- */
  --radius: 20px;
  --radius-sm: 13px;
  --radius-pill: 999px;

  /* ======================================================================
     Legacy role names. Aliases only - NEVER a literal colour here. Anything
     written as a hex in this block is invisible to tools/palette_check.py and
     therefore never contrast-tested. Five used to be.
     ====================================================================== */
  --bg: var(--canvas);
  --surface-alt: var(--surface-sunk);
  --border: var(--line);
  --text: var(--ink);
  --muted: var(--ink-faint);
  --muted-strong: var(--ink-soft);
  --primary: var(--accent-soft);
  --primary-strong: var(--accent);
  --primary-text: var(--accent-deep);
  --accent-green: var(--band-green);
  --accent-green-text: var(--positive-ink);
  --accent-coral: var(--band-orange);
  --accent-coral-text: var(--warn-ink);
  --danger-strong: var(--danger-deep);
  --shadow-sm: var(--e1);
  --shadow-md: var(--e2);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
}

/* The face is Nunito, self-hosted and variable; headings take Baloo 2 with
   Nunito behind it so Cyrillic falls through. font-display: swap means text
   paints in the system stack immediately and reflows when the file lands -
   invisible text on a riverbank is a worse failure than a swap. */
body {
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.55;
  letter-spacing: var(--track-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Anything that counts, measures or tabulates gets tabular figures, so a
   column of weights or a ticking clock does not jitter as digits change. */
.counter-number, .dash-value, .catch-meta, .heat-legend, .hour-chip,
.day-chip-date, .weather-table td, .intel-value, .stat-value, time {
  font-variant-numeric: tabular-nums;
}

h1, h2, h3 {
  font-family: var(--font-display);
  margin: 0 0 var(--s2);
  font-weight: 650;
  letter-spacing: var(--track-tight);
  line-height: 1.15;
  color: var(--text);
  text-wrap: balance;
}

h1 { font-size: var(--t-2xl); }
h2 { font-size: var(--t-lg); letter-spacing: -0.014em; }
h3 { font-size: var(--t-base); letter-spacing: -0.01em; }

/* More space above a heading than below it: the gap belongs to the boundary
   between sections, not to the heading-and-body pair. Backwards, and the page
   reads as an undifferentiated list. */
h2:not(:first-child), h3:not(:first-child) { margin-top: var(--s6); }

p { margin: 0 0 var(--s2); color: var(--muted); max-width: var(--measure); text-wrap: pretty; }

a { color: var(--primary-strong); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ...except where the link IS a card or a control. Underlining a whole place
   row on hover drags a line across the name and reads as damage, not as
   affordance - the card already lifts and shadows to say it is clickable. */
.place-link:hover,
.place-link:hover .place-name,
.candidate-card:hover,
.session-fab:hover,
.removed-place a:hover { text-decoration: none; }

/* ---------- layout ---------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  max-width: 960px;
  margin: 0 auto;
}

/* --------------------------------------------------------------------------
   The waterline. The one bespoke element in this design.

   A single hairline under the topbar that behaves like an actual water
   surface, and does three jobs with one element:
     - it is the brand mark, present on every page;
     - it ripples outward from wherever you tap, so a press has a consequence
       even when the page has not navigated yet;
     - it swells while an HTMX request is in flight and settles when it lands,
       which is the loading indicator.

   Cost: one 1px-tall element and two CSS custom properties. No image, no
   library, no layout work - it animates transform and opacity only, on its own
   compositor layer, so it cannot stutter the page it sits on.
   -------------------------------------------------------------------------- */
.waterline {
  position: relative;
  height: 2px;
  max-width: 960px;
  margin: 0 auto var(--s2);
  overflow: hidden;
  background: linear-gradient(90deg, transparent, var(--line-strong) 12%,
              var(--line-strong) 88%, transparent);
  /* The ripple origin, written by app/web/static/waterline.js on pointerdown.
     Defaults to the middle so a keyboard-only press still ripples sensibly. */
  --ripple-x: 50%;
}

.waterline::after {
  content: "";
  position: absolute;
  inset: 0;
  left: var(--ripple-x);
  width: 220px;
  margin-left: -110px;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  opacity: 0;
  transform: scaleX(0.15);
  will-change: transform, opacity;
}

.waterline.is-rippling::after {
  animation: waterline-ripple 620ms var(--ease-out);
}

/* The opacity holds, then falls late. The first cut faded linearly across the
   whole 620ms while the gradient spread 20-fold, and the two together made the
   ripple invisible by 150ms - it read as a flash, not as water. Filmed with
   tools/waterline_filmstrip.py; not visible in the keyframes, which is the
   whole reason that tool exists. */
@keyframes waterline-ripple {
  0%   { opacity: 0.9;  transform: scaleX(0.12); }
  25%  { opacity: 0.85; }
  60%  { opacity: 0.55; }
  100% { opacity: 0;    transform: scaleX(2.4); }
}

/* In flight. A slow swell along the whole line, which reads as "working"
   without the jitter of a spinner. */
.waterline.is-loading {
  background: linear-gradient(90deg, transparent, var(--accent-soft) 12%,
              var(--accent-soft) 88%, transparent);
}
.waterline.is-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: waterline-swell 1150ms var(--ease-out) infinite;
}

@keyframes waterline-swell {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

.brand {
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--primary-text);
  letter-spacing: -0.02em;
}

/* Desktop nav only. The bottom bar takes over below 720px so the two are
   never on screen at the same time. */
.topnav {
  display: none;
  gap: 4px;
}

@media (min-width: 720px) {
  .topnav { display: flex; }
}

.topnav a {
  color: var(--muted);
  font-weight: 500;
  font-size: 0.9rem;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
}

.topnav a.active {
  color: var(--primary-text);
  background: var(--surface-alt);
}

.topnav a:hover { text-decoration: none; background: var(--surface-alt); }

.page {
  max-width: 960px;
  margin: 0 auto;
  padding: 8px 20px 96px;
}

.page.narrow { max-width: 480px; }

@media (min-width: 720px) {
  .page { padding-bottom: 40px; }
}

.bottomnav {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
  box-shadow: 0 -2px 12px rgba(30, 74, 120, 0.05);
}

.bottomnav a {
  flex: 1;
  text-align: center;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--muted);
  padding: 6px 0;
  border-radius: var(--radius-sm);
}

.bottomnav a.active { color: var(--primary-strong); }

/* Must come after the .bottomnav rules above, or the display:flex wins and
   both navs show at once. */
@media (min-width: 720px) {
  .bottomnav { display: none; }
  /* No bottom nav on desktop, so the button sits nearer the bottom edge. */
  .session-fab { bottom: 20px; }
}

/* A water on the places list, plus its two personal controls.

   The card is a link and the controls are forms; they sit side by side rather
   than nested, because a button inside an anchor is invalid HTML and behaves
   differently in every browser. */
/* The row IS the card. The controls sit inside it, beside the score, rather
   than floating outside on the page background - and the link keeps the whole
   of the rest of the row as its tap target. The first attempt put the actions
   in a column outside the card, which detached them from the water they act on
   and squeezed the name onto three lines. */
.place-row {
  display: flex;
  align-items: center;
  gap: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding-right: 10px;
  margin-bottom: 12px;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.place-row:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }

.place-link {
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: inherit;
}

/* Inside a row the card is just layout: the row already carries the surface. */
.place-row .place-card {
  margin-bottom: 0;
  background: none;
  border: none;
  box-shadow: none;
  transform: none;
  padding-right: 4px;
}

.place-row .place-card:hover { box-shadow: none; transform: none; }

.place-actions {
  flex: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
}

.place-actions form { margin: 0; }

.place-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 32px inside the card; the calendar toggle's 34px was too heavy here. */
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--muted-strong);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}

.place-action:hover { background: var(--surface-alt); color: var(--primary-text); }

/* A pinned water. Coral rather than red: this is affection, not an alarm. */
.place-action.is-on {
  color: var(--accent-coral-text);
  border-color: rgba(199, 87, 62, 0.35);
  background: #fbe9e5;
}

.place-action-remove:hover {
  color: var(--accent-coral-text);
  border-color: rgba(199, 87, 62, 0.35);
}

/* Put away, not deleted - so there is a way back. Folded by default, because
   these are no longer part of the list. */
.removed-places {
  margin-top: 16px;
  font-size: 0.86rem;
  color: var(--muted-strong);
}

.removed-places summary {
  cursor: pointer;
  padding: 6px 2px;
}

.removed-place {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 2px;
  border-top: 1px solid var(--border);
}

.removed-place form { margin: 0; }

/* The running-session button.

   A 44px circle in the same idiom as the lake page's calendar toggle, not a
   banner. The first version was a full-width bar pinned to the foot of the
   page and it sat on top of whatever control was down there - "+ New place",
   "End session" - which is exactly what a floating element must never do.

   Bottom-RIGHT, clear of the primary action column, and the page gets a little
   foot-room so the last control is never trapped under it. */
.session-fab {
  position: fixed;
  right: 16px;
  bottom: calc(64px + env(safe-area-inset-bottom));
  z-index: 40;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 48px, not 44. 44 is Apple's floor and 48 is Google's; this is tapped
     one-handed with cold wet hands, so it takes the larger. */
  width: 48px;
  height: 48px;
  border-radius: 999px;
  text-decoration: none;
  color: var(--accent-green-text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  box-shadow: var(--e2);
  transition: color 0.15s ease, background 0.15s ease, transform 0.15s ease;
}

.session-fab:hover {
  background: var(--accent-wash);
  transform: translateY(-1px);
}

.session-fab:active { transform: translateY(0); }

/* The float rides, then goes under: a bite.

   Most of the cycle is the small restless movement a float makes sitting on
   the water. Then it dips - twice, because a real bite is rarely one clean
   pull - and rides back up with a little overshoot. The ring ripples outward
   on the dip, not on a timer of its own, so the two read as one event rather
   than as two things blinking near each other.

   Slow on purpose. This sits on every page while somebody is fishing; it has
   to be noticeable out of the corner of an eye and ignorable the rest of the
   time. */
.session-fab-float {
  animation: session-float-bite 4.2s ease-in-out infinite;
  transform-origin: 50% 45%;
}

@keyframes session-float-bite {
  /* Riding: small and slow, the way a float sits. */
  0%   { transform: translateY(0)     rotate(0deg)    scaleY(1); }
  12%  { transform: translateY(-1px)  rotate(-4deg)   scaleY(1); }
  26%  { transform: translateY(0.8px) rotate(3deg)    scaleY(1); }
  40%  { transform: translateY(-1px)  rotate(-2deg)   scaleY(1); }
  50%  { transform: translateY(0)     rotate(0deg)    scaleY(1); }
  /* The knock: a short sharp pull that does not take it under. */
  55%  { transform: translateY(4px)   rotate(2deg)    scaleY(0.96); }
  60%  { transform: translateY(0.5px) rotate(-2deg)   scaleY(1); }
  /* Away: it buries, and squashes as it goes down. Amplitude matters here -
     at 22px an elegant 3px dip is invisible, which is what the first attempt
     was. This is roughly half the icon's height. */
  70%  { transform: translateY(11px)  rotate(0deg)    scaleY(0.88); }
  78%  { transform: translateY(9px)   rotate(-3deg)   scaleY(0.9); }
  /* And back up, overshooting the way a released float does. */
  88%  { transform: translateY(-2.5px) rotate(4deg)   scaleY(1.04); }
  95%  { transform: translateY(0.8px) rotate(-2deg)   scaleY(1); }
  100% { transform: translateY(0)     rotate(0deg)    scaleY(1); }
}

.session-fab-ring {
  position: absolute;
  inset: -1px;
  border-radius: 999px;
  border: 1px solid var(--accent-green-text);
  opacity: 0;
  /* Same 4.2s cycle as the float, so the ripple belongs to the dip. */
  animation: session-ripple 4.2s ease-out infinite;
  pointer-events: none;
}

@keyframes session-ripple {
  0%, 56% { transform: scale(1);    opacity: 0; }
  60%     { transform: scale(1);    opacity: 0.6; }
  74%     { transform: scale(1.28); opacity: 0.45; }
  92%     { transform: scale(1.6);  opacity: 0; }
  100%    { transform: scale(1.6);  opacity: 0; }
}

/* Anyone who has asked not to see motion gets a plain ring instead. */
/* Anyone who asked not to see motion gets a still float and a plain ring. */
@media (prefers-reduced-motion: reduce) {
  .session-fab-ring { animation: none; opacity: 0.4; }
  .session-fab-float { animation: none; }
}

/* Enough foot-room that the last control on a page clears the button. */
body:has(.session-fab) .page { padding-bottom: 72px; }

/* ---------- cards & panels ---------- */

/* Borderless. A hairline plus a shadow plus a fill is three ways of saying the
   same thing; the edge light and the tinted shadow carry the surface on their
   own and the page stops looking like a stack of boxes. */
.card {
  background: var(--surface);
  border: none;
  border-radius: var(--radius);
  box-shadow: var(--e1), var(--edge);
  padding: var(--s5);
  margin-bottom: var(--s4);
}

.card-flat {
  background: var(--surface-sunk);
  border-radius: var(--radius);
  padding: var(--s5);
  margin-bottom: var(--s4);
  box-shadow: inset 0 0 0 1px var(--line);
}

.place-card {
  display: flex;
  align-items: center;
  gap: 14px;
  background: var(--surface);
  border: none;
  border-radius: var(--radius);
  box-shadow: var(--e1), var(--edge);
  padding: var(--s4);
  margin-bottom: 10px;
  cursor: pointer;
  transition: box-shadow var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
  .place-card:hover { box-shadow: var(--e2), var(--edge); transform: translateY(-2px); }
}
.place-card:active { transform: translateY(1px); box-shadow: var(--e1); }

.place-thumb {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-sm);
  background: linear-gradient(155deg, var(--water), var(--water-deep) 130%);
  flex-shrink: 0;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
}

.place-info { flex: 1; min-width: 0; }
.place-name { font-weight: 600; font-size: 1.02rem; }
.place-meta { color: var(--muted); font-size: 0.85rem; margin-top: 2px; }

.badge {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 4px 10px;
  border-radius: 999px;
  letter-spacing: 0.02em;
}

/* Every pair below is measured, not eyeballed - tools/palette_check.py.
   `.badge-neutral` was one of the two the nightly audit failed on: the old
   --muted (#6c8299) on --surface-alt was 3.5:1. It now inherits --ink-faint,
   and the ground is the accent wash rather than the blue-grey. */
.badge-go { background: var(--band-green); color: var(--positive-ink); }
.badge-no-go { background: var(--band-red); color: var(--warn-ink); }
.badge-neutral { background: var(--surface-sunk); color: var(--ink-soft); box-shadow: inset 0 0 0 1px var(--line); }

/* The day-quality badges are the bands, so they use the band tints - not
   --accent-wash, which is amber in this palette and made "Great" read as a
   warning. Green good, red bad (standing rule 3) has to survive a re-skin. */
.badge-red    { background: var(--band-red); color: var(--warn-ink); }
.badge-orange { background: var(--band-orange); color: var(--warn-ink); }
.badge-yellow { background: var(--band-yellow); color: var(--ink); }
.badge-green  { background: var(--band-green); color: var(--positive-ink); }

/* ---------- score / dashboard ---------- */

.verdict-block { text-align: center; margin: 4px 0 20px; }

.verdict-pill {
  display: inline-block;
  padding: 14px 32px;
  border-radius: 999px;
  font-size: 1.3rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

.verdict-pill.red    { background: var(--band-red); color: var(--warn-ink); }
.verdict-pill.orange { background: var(--band-orange); color: var(--warn-ink); }
.verdict-pill.yellow { background: var(--band-yellow); color: var(--ink); }
.verdict-pill.green  { background: var(--accent-wash); color: var(--accent-green-text); }

.best-hours { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.hour-chip {
  background: var(--surface-alt);
  color: var(--primary-text);
  font-weight: 600;
  font-size: 0.85rem;
  padding: 6px 12px;
  border-radius: 999px;
}

.reasons { list-style: none; padding: 0; margin: 8px 0 0; color: var(--muted); font-size: 0.9rem; }
.reasons li { padding: 3px 0; }

.dash-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}

@media (min-width: 560px) {
  .dash-grid { grid-template-columns: repeat(4, 1fr); }
}

.dash-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 14px;
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}

.dash-tile:hover { box-shadow: var(--shadow-md); }
.dash-tile.active { border-color: var(--primary); background: var(--surface-alt); }

.dash-label { font-size: 0.72rem; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.dash-value { font-size: 1.4rem; font-weight: 700; color: var(--text); margin-top: 4px; }
.dash-sub { font-size: 0.78rem; color: var(--muted); margin-top: 2px; }

.outlook-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}
.outlook-row:last-child { border-bottom: none; }

/* ---------- map ---------- */

/* `position: relative` is load-bearing, not cosmetic. The splash elements are
   absolutely positioned children of this box and are placed in ITS coordinate
   space. Without it they resolve against the initial containing block, so every
   ripple was drawn at page coordinates - hundreds of pixels away from the map,
   somewhere off the top of the document. That, plus the z-index below, is why
   the splash appeared not to exist. */
.map-wrap {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--border);
  margin-bottom: 16px;
}

#map { height: 360px; width: 100%; }

@media (min-width: 720px) {
  #map { height: 480px; }
}

.zone-panel {
  display: none;
}
.zone-panel.visible { display: block; }

.zone-panel .badge { margin-bottom: 6px; }

.map-legend-note {
  font-size: 0.78rem;
  color: var(--muted);
  margin-top: 6px;
}

/* ---------- buttons ---------- */

.btn {
  display: inline-block;
  position: relative;
  width: 100%;
  padding: 15px;
  border-radius: var(--radius-sm);
  border: none;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.006em;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  min-height: 48px;
  font-family: inherit;
  /* transform and opacity only. Never `transition: all` - it animates
     properties you did not choose, including ones that force layout. */
  transition: background var(--dur-fast) var(--ease-out),
              box-shadow var(--dur) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}

.btn:hover { text-decoration: none; }

/* The float dip. A button pressed sinks the way a float does when a fish takes
   it: down fast, then back up. Every pressable thing in the app does this, so
   the gesture reads as one idea rather than a decoration on one screen. */
.btn:active { transform: translateY(2px) scale(0.995); }

.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
  box-shadow: var(--e1), var(--edge);
}
.btn-primary:hover { background: var(--accent-deep); box-shadow: var(--e2); }
.btn-secondary {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--line);
  box-shadow: var(--e1);
}
.btn-secondary:hover { border-color: var(--line-strong); background: var(--surface-sunk); }
.btn-ghost { background: transparent; color: var(--muted); border: 1px dashed var(--line-strong); }
.btn-ghost:hover { color: var(--ink); border-color: var(--accent); }
.btn-block-gap { margin-bottom: 10px; }

/* Focus must be visible, themed, and offset. A control with only a resting
   state is half-built, and a keyboard angler cannot use it at all. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Hover motion is gated to real pointers: a touch device fires a false hover
   on tap, which leaves a card stuck in its lifted state after the finger goes. */
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { transform: translateY(-1px); }
}

/* ---------- method/rods picker ---------- */

.method-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 16px 0;
}

.method-card {
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 8px;
  text-align: center;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
}

.method-card input { display: none; }
.method-card.selected,
.method-card:has(input:checked) {
  border-color: var(--primary-strong);
  background: var(--surface-alt);
  color: var(--primary-text);
}

.stepper {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 20px;
  margin: 12px 0 20px;
}

.stepper button {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 1.3rem;
  font-weight: 600;
  color: var(--primary-strong);
  cursor: pointer;
}

.stepper .count { font-size: 1.8rem; font-weight: 700; min-width: 40px; text-align: center; }

/* ---------- session logging ---------- */

.chip-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 16px 0;
}

.chip {
  padding: 16px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  text-align: center;
  font-weight: 600;
  min-height: 48px;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.95rem;
  color: var(--text);
}

.chip.secondary { color: var(--muted); font-size: 0.85rem; }
.chip:hover { border-color: var(--primary); }

.counter { text-align: center; margin: 16px 0; }
.counter-number { font-size: 2.6rem; font-weight: 700; color: var(--primary-text); }
.counter-label { color: var(--muted); font-size: 0.85rem; }

.catch-list { list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto; }
.catch-list li {
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  color: var(--muted);
  font-size: 0.9rem;
}

label { display: block; margin: 14px 0 6px; color: var(--muted); font-size: 0.85rem; font-weight: 500; }

input[type=text], input[type=number], textarea {
  width: 100%;
  padding: 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 1rem;
  font-family: inherit;
}

input:focus, textarea:focus { outline: 2px solid var(--primary); outline-offset: 1px; }

textarea { min-height: 76px; resize: vertical; }

.blank-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px;
  background: var(--surface-alt);
  border-radius: var(--radius-sm);
  margin: 14px 0;
  font-size: 0.92rem;
}

.blank-toggle input { width: 20px; height: 20px; }

/* ---------- history/stats ---------- */

table.history { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
table.history th, table.history td { text-align: left; padding: 9px 6px; border-bottom: 1px solid var(--border); }
table.history th { color: var(--muted); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.03em; }

.stat-row { display: flex; gap: 12px; margin-bottom: 18px; }
.stat { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; text-align: center; box-shadow: var(--shadow-sm); }
.stat-number { font-size: 1.5rem; font-weight: 700; color: var(--primary-text); }
.stat-label { color: var(--muted); font-size: 0.76rem; margin-top: 2px; }

.timer { text-align: center; color: var(--muted); font-size: 0.9rem; margin-bottom: 4px; }

.bottom-fixed {
  position: fixed;
  left: 0; right: 0; bottom: 56px;
  padding: 10px 16px;
  background: linear-gradient(180deg, transparent, var(--bg) 35%);
}

@media (min-width: 720px) {
  .bottom-fixed { position: static; background: none; padding: 12px 0 0; }
}

.section-label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 4px 0 10px;
}

/* ---------- weather table ---------- */

table.weather {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

table.weather th {
  text-align: left;
  padding: 8px 6px;
  color: var(--muted);
  font-weight: 600;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--border);
}

table.weather td {
  padding: 11px 6px;
  border-bottom: 1px solid var(--border);
}

table.weather tbody tr {
  cursor: pointer;
  transition: background 0.12s ease;
}

table.weather tbody tr:hover { background: var(--surface-alt); }
table.weather tbody tr.selected { background: var(--surface-alt); box-shadow: inset 3px 0 0 var(--primary-strong); }
table.weather tbody tr:last-child td { border-bottom: none; }

.wind-arrow {
  display: inline-block;
  font-size: 1.05rem;
  color: var(--primary-strong);
  margin-right: 4px;
}

/* ---------- heat legend ---------- */

.heat-legend {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 0.72rem;
  color: var(--muted);
  margin-top: 10px;
}

.heat-bar {
  flex: 1;
  height: 10px;
  border-radius: 999px;
  background: linear-gradient(90deg, #c73e2d, #de6a3a, #ed9844, #f6c359,
                                     #f3e378, #cee07a, #9cd07a, #6abc7c, #3aa06a);
}

.lang-switch {
  display: flex;
  gap: 2px;
  background: var(--surface-alt);
  border-radius: 999px;
  padding: 3px;
}

.lang-switch a {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--muted-strong);
  padding: 6px 9px;
  border-radius: 999px;
  text-decoration: none;
  min-width: 32px;
  text-align: center;
}

.lang-switch a:hover { text-decoration: none; color: var(--primary-strong); }
.lang-switch a.active { background: var(--surface); color: var(--primary-text); box-shadow: var(--shadow-sm); }

.season-line {
  font-size: 0.88rem;
  color: var(--text);
  margin-bottom: 12px;
}

.season-caveat { color: var(--muted); font-size: 0.8rem; }

.session-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--accent-wash);
  color: var(--accent-green-text);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-bottom: 14px;
  font-size: 0.86rem;
  text-decoration: none;
}

.session-banner:hover { text-decoration: none; background: #cfe9dd; }

.inline-link-row {
  display: flex;
  gap: 10px;
  margin-bottom: 14px;
}

.inline-link-row a {
  flex: 1;
  text-align: center;
  padding: 11px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--primary-strong);
  text-decoration: none;
}

.inline-link-row a:hover { text-decoration: none; border-color: var(--primary); }

.provenance-note {
  font-size: 0.75rem;
  color: var(--muted);
  background: #fbf5e4;
  border: 1px solid #e8d9b4;
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-top: 10px;
}

/* ---------- fish pin + splash ---------- */

.pin-dock {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface-alt);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 12px;
}

.fish-pin {
  width: 54px;
  height: 40px;
  cursor: grab;
  flex-shrink: 0;
  touch-action: none;
  animation: fish-bob 2.4s ease-in-out infinite;
}

.fish-pin:active { cursor: grabbing; }

@keyframes fish-bob {
  0%, 100% { transform: translateY(0) rotate(-3deg) scaleY(1); }
  35%      { transform: translateY(-4px) rotate(1deg) scaleY(0.97); }
  70%      { transform: translateY(-1px) rotate(3deg) scaleY(1.02); }
}

.pin-hint { font-size: 0.82rem; color: var(--muted); }

/* ---------- splash ----------

   The old splash was three concentric white circles growing from 4px to 120px
   by animating `width`/`height`. Two things were wrong with it. It never
   appeared at all - see the z-index note below and `position` on `.map-wrap` -
   and even where it did, perfect circles growing on a map read as a radar
   ping, not as water. Water seen at an angle spreads as an ELLIPSE, the rings
   travel at different speeds, and the impact throws droplets.

   Everything here animates `transform` and `opacity` only, so the whole splash
   runs on the compositor and stays smooth on a phone. Animating width/height
   forced layout on every frame of every ring, which is most of why it stuttered.

   z-index 500 clears Leaflet's panes, which sit at 400. Below that the rings
   are painted UNDER the map tiles and are invisible even when positioned
   correctly - the second half of the "there are no splashes" bug. */

.splash-layer {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 500;
}

.splash-ring {
  position: absolute;
  width: 130px;
  height: 58px;              /* flattened: surface ripples seen at an angle */
  margin-left: -65px;
  margin-top: -29px;
  border: 2px solid rgba(255, 255, 255, 0.92);
  border-radius: 50%;
  will-change: transform, opacity;
  animation: ripple 1150ms cubic-bezier(0.16, 0.85, 0.3, 1) forwards;
}

@keyframes ripple {
  0%   { transform: scale(0.06); opacity: 0; }
  9%   { opacity: 0.95; }
  55%  { opacity: 0.5; }
  100% { transform: scale(1); opacity: 0; }
}

/* The contact ring: small, fast, and bright, so the moment of impact has a
   hard edge before the slower rings take over. */
.splash-ring.contact {
  width: 44px; height: 20px; margin-left: -22px; margin-top: -10px;
  border-width: 3px;
  animation-duration: 420ms;
  animation-timing-function: cubic-bezier(0.1, 0.8, 0.2, 1);
}

/* Water displaced upward, then falling back. Stretches vertically on the way
   up and squashes as it collapses. */
.splash-crown {
  position: absolute;
  width: 26px; height: 26px;
  margin-left: -13px; margin-top: -13px;
  border-radius: 50% 50% 46% 46%;
  background: radial-gradient(ellipse at 50% 70%,
              rgba(255, 255, 255, 0.95), rgba(214, 236, 250, 0.75) 60%,
              rgba(180, 216, 240, 0) 100%);
  will-change: transform, opacity;
  animation: crown 520ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}

@keyframes crown {
  0%   { transform: translateY(2px) scale(0.3, 0.25); opacity: 0; }
  18%  { transform: translateY(-9px) scale(0.8, 1.45); opacity: 1; }
  55%  { transform: translateY(-4px) scale(1.05, 0.85); opacity: 0.7; }
  100% { transform: translateY(3px) scale(1.3, 0.28); opacity: 0; }
}

/* Thrown droplets. Each gets its own --dx / --peak / --fall inline, so the
   spray is asymmetric - a symmetric one reads as a graphic, not a splash.
   The arc comes from the 42% keyframe sitting above the endpoint. */
.splash-drop {
  position: absolute;
  width: 6px; height: 6px;
  margin-left: -3px; margin-top: -3px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 0 3px rgba(255, 255, 255, 0.8);
  will-change: transform, opacity;
  animation: droplet 620ms cubic-bezier(0.3, 0.5, 0.6, 1) forwards;
}

@keyframes droplet {
  0%   { transform: translate(0, 0) scale(0.4); opacity: 0; }
  14%  { opacity: 1; }
  42%  { transform: translate(calc(var(--dx) * 0.6), var(--peak)) scale(1); opacity: 1; }
  100% { transform: translate(var(--dx), var(--fall)) scale(0.35); opacity: 0; }
}

/* A brief dark dimple where the fish went through the surface. */
.splash-hole {
  position: absolute;
  width: 30px; height: 14px;
  margin-left: -15px; margin-top: -7px;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 50%,
              rgba(22, 60, 92, 0.5), rgba(22, 60, 92, 0) 72%);
  will-change: transform, opacity;
  animation: hole 700ms ease-out forwards;
}

@keyframes hole {
  0%   { transform: scale(0.2); opacity: 0; }
  16%  { transform: scale(1); opacity: 1; }
  100% { transform: scale(1.7); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .splash-layer { display: none; }
}

/* The pin follows the finger while held, instead of the browser's drag ghost
   (which is invisible on touch). */
.pin-ghost {
  position: fixed;
  z-index: 3000;
  width: 60px;
  height: 44px;
  margin-left: -30px;
  margin-top: -22px;
  pointer-events: none;
  filter: drop-shadow(0 6px 10px rgba(30, 74, 120, 0.35));
  transition: transform 80ms ease-out;
}

.pin-ghost.lifted { transform: scale(1.15) rotate(-8deg); }

/* Release: gather, arc over the top, then drop nose-first through the surface.

   Rotation is NEGATIVE throughout the descent. The silhouette's head is at the
   LEFT, and positive (clockwise) rotation swings the left side UP - which dives
   the fish tail first. This shipped tail-first three times. Check it with
   `tools/animation_filmstrip.py` and look at the frames; you cannot see it from
   the keyframes.

   Why it read as stiff before: one linear-ish ease carried the whole descent,
   so the fish fell at a constant rate. Real falling accelerates. Each phase now
   sets its own `animation-timing-function` - slow out of the anticipation, then
   a steep gravity curve from the apex down. The rotation also leads the path
   slightly, the way a real fish pitches before it drops rather than after. */
.pin-ghost.diving {
  animation: dive 860ms forwards;
  transform-origin: 50% 50%;
  transition: none;
  will-change: transform, opacity;
}

@keyframes dive {
  /* Gather: a small dip and a nose-up pitch. Anticipation in the opposite
     direction to the move is what stops it looking like a dropped object. */
  0%   { transform: translate(0, 0) rotate(6deg) scale(1.15);
         animation-timing-function: cubic-bezier(0.34, 0.02, 0.62, 0.5); }
  10%  { transform: translate(-2px, 5px) rotate(14deg) scale(1.13);
         animation-timing-function: cubic-bezier(0.16, 0.7, 0.4, 1); }
  /* Up and over the apex. Decelerating into the top of the arc. */
  30%  { transform: translate(6px, -26px) rotate(20deg) scale(1.1);
         animation-timing-function: cubic-bezier(0.4, 0, 0.7, 0.42); }
  /* Pitching down through horizontal while barely losing height - the pivot
     that makes it read as a dive rather than a fall. */
  46%  { transform: translate(11px, -20px) rotate(-30deg) scale(1.06);
         animation-timing-function: cubic-bezier(0.5, 0, 0.85, 0.5); }
  /* Committed, accelerating, stretched along its own axis to sell speed. */
  66%  { transform: translate(14px, 12px) rotate(-72deg) scale(1.02, 1.12);
         animation-timing-function: cubic-bezier(0.55, 0, 0.9, 0.6); }
  /* Contact. Squash back to neutral as the surface takes it. The splash is
     fired from JS at this instant. */
  82%  { transform: translate(15px, 46px) rotate(-88deg) scale(0.9, 0.94);
         opacity: 1; animation-timing-function: cubic-bezier(0.4, 0, 0.9, 0.7); }
  /* Under the surface. It only fades once it is below, so it reads as entering
     water rather than dissolving in mid-air. */
  100% { transform: translate(15px, 86px) rotate(-95deg) scale(0.42); opacity: 0; }
}



.fish-pin.is-held { opacity: 0.25; }

.leaflet-container .fish-marker { animation: fish-bob 2.4s ease-in-out infinite; }

/* ---------- catch cards + species search ---------- */

.catch-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.catch-card:last-child { border-bottom: none; }

.catch-photo {
  width: 46px; height: 46px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}

.catch-photo-empty {
  width: 46px; height: 46px;
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  flex-shrink: 0;
}

.catch-main { flex: 1; min-width: 0; }
.catch-name { font-weight: 600; font-size: 0.95rem; }
.catch-meta { color: var(--muted); font-size: 0.8rem; }

.catch-actions { display: flex; gap: 6px; }

.icon-btn {
  border: 1px solid var(--border);
  background: var(--surface);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  cursor: pointer;
  font-size: 0.8rem;
  color: var(--muted);
  font-family: inherit;
}

.icon-btn:hover { border-color: var(--primary); color: var(--primary-strong); }
.icon-btn.danger:hover { border-color: var(--danger); color: var(--accent-coral-text); }

/* ---------- one-tap fish buttons ---------- */

.fish-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin-bottom: 18px;
}

.fish-btn {
  width: 100%;
  min-height: 80px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 12px 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  font-family: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1.15;
  transition: transform 0.08s ease, box-shadow 0.15s ease;
}

.fish-btn:hover { box-shadow: var(--shadow-md); }
.fish-btn:active { transform: scale(0.96); }

/* Icons inherit `color`, so each species can tint its own silhouette while
   the shapes stay shared. */
.fish-icon { display: block; }
.catch-photo-empty .fish-icon { opacity: 0.55; }

/* Idle: a slow tail-side swim. Deliberately small - this sits behind a
   number the angler is reading, so it must never pull focus. */
.fish-btn .fish-icon { animation: swim 3.2s ease-in-out infinite; transform-origin: 70% 50%; }
.fish-btn:nth-child(2n) .fish-icon { animation-delay: -0.8s; }
.fish-btn:nth-child(3n) .fish-icon { animation-delay: -1.6s; }

@keyframes swim {
  0%, 100% { transform: translateX(0) rotate(0deg); }
  25%      { transform: translateX(1.5px) rotate(-3deg); }
  75%      { transform: translateX(-1.5px) rotate(3deg); }
}

/* Squash on press, overshoot on release: the standard tactile-feedback pair.
   Kept to 0.94/1.04 so it reads as responsive rather than bouncy. */
.fish-btn:active { transform: scale(0.94); transition: transform 70ms ease-out; }
.fish-btn { transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.15s ease; }

.fish-icon.pop { animation: icon-pop 420ms cubic-bezier(0.34, 1.56, 0.64, 1); }

@keyframes icon-pop {
  0%   { transform: scale(0.6) rotate(-12deg); opacity: 0.4; }
  60%  { transform: scale(1.08) rotate(3deg); opacity: 1; }
  100% { transform: scale(1) rotate(0deg); }
}

.species-row-inline { display: flex; align-items: center; gap: 12px; }
.species-row-inline .select-input { flex: 1; }

@media (prefers-reduced-motion: reduce) {
  .fish-btn .fish-icon, .fish-icon.pop, .fish-pin { animation: none !important; }
}

.catch-photo-empty {
  display: flex;
  align-items: center;
  justify-content: center;
}

details.card summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.92rem;
  color: var(--primary-strong);
  list-style: none;
}

details.card summary::-webkit-details-marker { display: none; }
details.card summary::before { content: "＋ "; font-weight: 700; }
details.card[open] summary::before { content: "－ "; }

.select-input {
  width: 100%;
  padding: 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 1rem;
  font-family: inherit;
}

/* ---------- size sliders ---------- */

.slider-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin: 16px 0 2px;
}

.slider-value {
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--primary-text);
}

.slider {
  width: 100%;
  height: 40px;
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  margin: 0;
}

.slider::-webkit-slider-runnable-track {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
}

.slider::-moz-range-track {
  height: 8px;
  border-radius: 999px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
}

/* Deliberately oversized: this is dragged with cold, wet hands. */
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 32px;
  height: 32px;
  margin-top: -13px;
  border-radius: 50%;
  background: var(--primary-strong);
  border: 3px solid #fff;
  box-shadow: var(--shadow-sm);
  cursor: grab;
}

.slider::-moz-range-thumb {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--primary-strong);
  border: 3px solid #fff;
  box-shadow: var(--shadow-sm);
  cursor: grab;
}

.search-row { display: flex; gap: 8px; margin-bottom: 12px; }
.search-row input { flex: 1; }

.species-results { max-height: 260px; overflow-y: auto; }

.species-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.species-latin { font-size: 0.75rem; color: var(--muted); font-style: italic; }

.btn-danger-solid {
  background: var(--danger);
  color: #fff;
  border: none;
}

.btn-danger-solid:hover { background: var(--danger-strong); }

/* ---------- current conditions ---------- */

.now-card {
  display: flex;
  align-items: center;
  gap: 18px;
  background: linear-gradient(135deg, var(--surface), var(--surface-alt));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  padding: 16px 18px;
  margin-bottom: 16px;
}

/* The ring. Direction B's circle in direction A's colours (owner's pick,
   2026-08-28).

   It is a FULL ring, deliberately, not an arc that fills further on a better
   day. A partial arc is a progress bar, and a progress bar is a score out of
   ten wearing a different hat - CLAUDE.md standing rules 2 and 16 forbid
   exactly that. The hue is the whole message; the amount of ring is not a
   quantity. */
.now-ring {
  position: relative;
  flex: 0 0 auto;
  width: 92px;
  height: 92px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  /* The inside of the ring is always the design's own recessed ground, never
     the band tint. Owner's call, 2026-08-28, and it makes the rule cleaner
     than it was: the day's colour lives in the STROKE and nowhere else, so the
     card reads as this app rather than as a green card on a kraft page. */
  background: var(--canvas-deep);
}

.now-ring::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 999px;
  border: 5px solid var(--line-strong);
}

.now-card.ring-green  .now-ring::after { border-color: var(--mark-green); }
.now-card.ring-yellow .now-ring::after { border-color: var(--mark-yellow); }
.now-card.ring-orange .now-ring::after { border-color: var(--mark-orange); }
.now-card.ring-red    .now-ring::after { border-color: var(--mark-red); }

/* Showing another day's forecast: the ring must stop asserting anything about
   today, so it goes neutral and dashed rather than keeping a stale colour. */
.now-card.is-forecast .now-ring::after {
  border-style: dashed;
  border-color: var(--line-strong);
}

.now-temp {
  font-size: 2.6rem;
  font-weight: 700;
  color: var(--primary-text);
  line-height: 1;
}

.now-temp span { font-size: 1.1rem; font-weight: 600; color: var(--muted); }
.now-meta { font-size: 0.86rem; color: var(--text); }
.now-label {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: 3px;
}

.partial-flag {
  display: inline-block;
  font-size: 0.62rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--surface-alt);
  border-radius: 999px;
  padding: 2px 7px;
  margin-left: 5px;
  vertical-align: middle;
}

/* Segmented switch. One track, one thumb, exactly one option on - the same
   control as the language picker in the topbar, scaled up to a full-width row.
   Used for the water-type filter on the places list: a PZW water and a
   commercial fishery are alternatives, and a switch says that where three
   separate pills only said "here are three buttons".

   The thumb is positioned from --switch-index (0-based) over --switch-count
   equal columns, so the same CSS serves any number of options and the slide is
   a single transition rather than a class swap per segment. */
.switch {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--switch-count, 3), 1fr);
  align-items: stretch;
  gap: 0;
  margin: 0 0 14px;
  padding: var(--switch-pad, 3px);
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 999px;
  /* Nothing inside is selectable text - it is a control, and a long-press that
     highlights "Commercial fishery" is never what was meant. */
  user-select: none;
  -webkit-user-select: none;
}

.switch-thumb {
  position: absolute;
  top: var(--switch-pad, 3px);
  bottom: var(--switch-pad, 3px);
  left: var(--switch-pad, 3px);
  width: calc((100% - 2 * var(--switch-pad, 3px)) / var(--switch-count, 3));
  transform: translateX(calc(var(--switch-index, 0) * 100%));
  background: var(--surface);
  border-radius: 999px;
  box-shadow: var(--shadow-sm);
  transition: transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
  pointer-events: none;
}

.switch-option {
  position: relative;   /* above the thumb */
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 32px;
  padding: 6px 8px;
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1.15;
  /* A long translation must never break out of the track - it gets clipped
     instead, and the aria-label still names the control. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
  text-decoration: none;
  color: var(--muted-strong);
  border-radius: 999px;
  transition: color 0.18s ease;
  -webkit-tap-highlight-color: transparent;
}

.switch-option:hover { text-decoration: none; color: var(--primary-strong); }
.switch-option.is-active { color: var(--primary-text); }

/* A tap that lands on the option the thumb is already under still has to feel
   like a press. */
.switch-option:active { transform: scale(0.97); }

@media (prefers-reduced-motion: reduce) {
  .switch-thumb { transition: none; }
  .switch-option:active { transform: none; }
}

.empty-state {
  color: var(--muted);
  font-size: 0.9rem;
  padding: 18px 2px;
}

/* ---------------------------------------------------------------------------
   Accounts: the topbar control, and the two forms.

   Same pastel instrument language as the rest of the app - a sign-in page that
   looks like a bank's would be the loudest screen in a quiet app. The only
   colour that is not ours is Google's mark, which their brand terms require to
   be drawn as issued.
   --------------------------------------------------------------------------- */
.account-form {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
}

.account-name {
  font-size: 0.74rem;
  font-weight: 700;
  color: var(--primary-text);
  max-width: 84px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-signout,
.account-signin {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--muted-strong);
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 10px;
  text-decoration: none;
  font-family: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.account-signout:hover,
.account-signin:hover { color: var(--primary-text); text-decoration: none; }

.auth-lede {
  color: var(--muted);
  font-size: 0.9rem;
  margin: -6px 0 18px;
}

.auth-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 18px; }

.field { display: flex; flex-direction: column; gap: 6px; }

.field-label {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.field input[type=email],
.field input[type=password],
.field input[type=text] {
  width: 100%;
  padding: 13px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 1rem;
  font-family: inherit;
}

.field-hint { font-size: 0.78rem; color: var(--muted); }

/* Empty until something is wrong, so the form does not reserve a red line
   under every field before anyone has typed. */
.field-error { font-size: 0.78rem; color: var(--accent-coral-text); }
.field-error:empty { display: none; }

.field.has-error input { border-color: var(--danger); }

.form-error-banner {
  background: #fae4df;
  color: var(--accent-coral-text);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-bottom: 16px;
  font-size: 0.86rem;
}

/* A rule with the word "or" sitting in the gap. */
.auth-or {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 6px 0 14px;
  color: var(--muted);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.auth-or::before,
.auth-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.btn-google {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
}
.btn-google:hover { background: var(--surface-alt); }
.btn-google svg { flex: none; }

.auth-switch { font-size: 0.86rem; color: var(--muted); margin-top: 4px; }
.auth-switch a { font-weight: 600; }

/* ---------------------------------------------------------------------------
   The day strip: a calendar icon on the lake header, and eight day chips that
   roll down under it.

   The chips carry a colour and a time, nothing else. There is no number on
   them on purpose - the day score is never shown raw, and a percentage would
   claim a calibration this project has not earned yet.
   --------------------------------------------------------------------------- */
.lake-heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.lake-heading .section-label { margin-bottom: 0; }

.calendar-toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  color: var(--muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}

.calendar-toggle:hover { color: var(--primary-text); background: var(--surface-alt); }
.calendar-toggle.is-open {
  color: #ffffff;
  background: var(--primary-strong);
  border-color: var(--primary-strong);
}

/* Rolls open to a measured height rather than `auto`, which cannot be
   transitioned. The height is set from scrollHeight in the template's script. */
.day-strip {
  overflow: hidden;
  margin: 10px 0 14px;
}

.day-strip.is-rolling {
  max-height: 0;
  transition: max-height 0.26s cubic-bezier(0.32, 0.72, 0, 1);
}

.day-strip.is-rolling.is-open { max-height: var(--roll-height, 200px); }

@media (prefers-reduced-motion: reduce) {
  .day-strip.is-rolling { transition: none; }
}

/* Horizontal rail, not a month grid: eight days is what the predictions cover,
   and a grid of mostly empty squares would be four taps of scrolling to say so. */
.day-strip-rail {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 2px 2px 6px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.day-strip-rail::-webkit-scrollbar { display: none; }

.day-chip {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 54px;
  padding: 9px 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.day-chip-weekday {
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}

.day-chip-date { font-size: 1rem; font-weight: 700; color: var(--primary-text); }

/* The band. A bar, not a dot: at this size a dot reads as a bullet point and a
   bar reads as a measurement. */
.day-chip-band {
  width: 28px;
  height: 6px;
  border-radius: 999px;
  margin: 3px 0 0;
}

/* The four reserved marks. Nothing decorative may use these at this
   saturation - see the palette block. */
.band-red    { background: var(--mark-red); }
.band-orange { background: var(--mark-orange); }
.band-yellow { background: var(--mark-yellow); }
.band-green  { background: var(--mark-green); }
/* No prediction row for that day. Hollow, so it cannot be mistaken for a band
   at a glance, and never filled in from the day beside it (law 4). */
.band-none {
  background: repeating-linear-gradient(
    45deg, var(--border), var(--border) 2px, transparent 2px, transparent 4px
  );
}

.day-chip.is-today { border-color: var(--primary); }

.day-chip.is-selected {
  border-color: var(--primary-strong);
  box-shadow: 0 0 0 2px rgba(111, 177, 232, 0.35);
}

/* Far out, and saying so. Faded rather than hidden: pretending the model sees
   only five days would be its own inaccuracy. */
.day-chip.is-far { opacity: 0.55; }
.day-chip.is-empty { opacity: 0.7; }

.day-strip-note {
  font-size: 0.72rem;
  color: var(--muted);
  line-height: 1.35;
  margin: 2px 2px 0;
}

/* Which day the overlay is drawn for, over the map, because the conditions
   card deliberately keeps showing now. */
.map-day-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 500;
  padding: 5px 10px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--primary-text);
  pointer-events: none;
}

/* The conditions card while a forecast day is selected. Same card, quietly
   marked, so a glance tells you it is not a reading. */
.now-card.is-forecast {
  border: 1px dashed var(--border);
  background: linear-gradient(135deg, var(--surface), var(--surface-alt));
}

/* A quiet notice block. Used by the static build's "this site only reads"
   banner - which has been rendering unstyled since that build was written -
   and by the stage note on a water that is still being prepared. */
.callout {
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-bottom: 14px;
  font-size: 0.86rem;
  line-height: 1.45;
  color: var(--text);
}

/* A search result you can add. It is a submit button styled as a place card,
   so the whole row is the tap target on a phone rather than a small link. */
.candidate-form { margin: 0 0 8px; }

.candidate-card {
  width: 100%;
  border: none;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.candidate-card.is-not-water { opacity: 0.7; }

/* A water the PZW list does not name asks which kind it is before it is added.
   Two submit buttons rather than a radio and a confirm: it is still one tap,
   and water_type is the segmentation key for every CPUE aggregate (law 3), so
   it must not be possible to add a water without answering. */
.water-type-choice {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}

.btn-badge {
  border: 1px solid transparent;
  font-family: inherit;
  font-size: inherit;
  cursor: pointer;
  /* Comfortably tappable with wet hands in bright sun, which is the whole
     brief for every control in this app (docs/07-UI-SPEC.md). */
  min-height: 34px;
  padding-left: 12px;
  padding-right: 12px;
}

.btn-badge:hover { border-color: currentColor; }

.candidate-hint {
  margin: -2px 0 10px;
}

/* The registered-catch line. Quiet: it is context, not a headline, and it sits
   directly under the conditions card it qualifies. */
.registered-catch {
  margin: -4px 0 16px;
  font-size: 0.84rem;
  line-height: 1.45;
  color: var(--muted-strong);
}

/* What stage a newly added water is at. Quiet by default - it is progress, not
   a problem - and only coloured when something actually failed. */
.build-note {
  font-size: 0.84rem;
  line-height: 1.45;
}

.build-note-failed {
  background: #fae4df;
  color: var(--accent-coral-text);
}

/* ---------- collected local knowledge ---------- */
/* Deliberately quieter than the weather card above it. Nothing in here has
   been verified by anybody, and it must not read with the same authority as a
   measurement. Hence muted text, no colour band, and the source link carrying
   the only emphasis in the row - the source is the part worth tapping. */

.intel-note {
  font-size: 0.78rem;
  color: var(--muted);
  margin: -4px 0 10px;
}

.intel-card { padding: 4px 14px 10px; }

.intel-topic + .intel-topic {
  border-top: 1px solid var(--border);
}

.intel-topic-label {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 12px 0 4px;
}

.intel-list { list-style: none; margin: 0; padding: 0 0 8px; }

.intel-fact {
  font-size: 0.88rem;
  line-height: 1.45;
  padding: 5px 0;
}

.intel-key {
  font-weight: 600;
  color: var(--text);
}

.intel-key::after { content: " — "; font-weight: 400; color: var(--muted); }

.intel-value { color: var(--text); }

.intel-source {
  display: inline-block;
  margin-left: 6px;
  font-size: 0.74rem;
  color: var(--primary-strong);
  text-decoration: none;
  border-bottom: 1px solid var(--primary);
  max-width: 14em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

.intel-source:hover { border-bottom-color: var(--primary-strong); }

/* A source that answered 404. The claim is kept and shown - the page may have
   moved - but it must not look like one that checked out. */
.intel-source-dead {
  color: var(--muted);
  border-bottom-style: dotted;
}

.intel-dead {
  font-size: 0.7rem;
  color: var(--danger-strong);
  margin-left: 4px;
}

/* ---------- 404 / 500 ---------- */
/* Reached by someone who did not mean to be here, quite possibly one-handed
   on a riverbank. Big number, one sentence, one button back. */

.error-page { text-align: center; padding: 32px 0; }

.error-code {
  font-size: 3.2rem;
  font-weight: 700;
  color: var(--primary);
  line-height: 1;
  margin-bottom: 8px;
}

.error-page h1 { margin-bottom: 6px; }
.error-page .auth-lede { margin-bottom: 20px; }

/* ==========================================================================
   Waterline motion layer
   --------------------------------------------------------------------------
   Three behaviours, all built from transform and opacity so none of them can
   force layout or paint on the main thread. Nothing here downloads anything.

   The constraint that shaped all of it: this app is read on a riverbank on
   whatever signal is going, on whatever phone the angler owns. Motion that
   needs a fast device is motion that makes the app feel broken on a slow one.
   ========================================================================== */

/* ---- 1. The splash. A ring that expands from the point of contact. ----
   Anchored to the pointer, not to the element, so it reads as the water
   answering the touch rather than as a button lighting up. One node, reused
   and repositioned; it never accumulates in the DOM. */
.splash {
  position: fixed;
  z-index: 60;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}

.splash.is-live { animation: splash-ring 520ms var(--ease-out) forwards; }

@keyframes splash-ring {
  0%   { opacity: 0.5;  transform: scale(0.4); }
  70%  { opacity: 0.16; }
  100% { opacity: 0;    transform: scale(3.6); }
}

/* ---- 2. Surfacing. Content rises into place as it enters the viewport. ----
   Two pixels of travel and a fade, staggered. Deliberately small: a card that
   flies 40px reads as a website, and this is an instrument.

   `.js-reveal` is added by waterline.js, never written into the templates, so
   with JS off or broken every element keeps its resting style and the page is
   fully readable. That is the whole reason the hidden state lives behind the
   class rather than in the base rule. */
.js-reveal { opacity: 0; transform: translateY(6px); }

.js-reveal.is-surfaced {
  opacity: 1;
  transform: none;
  transition: opacity var(--dur-slow) var(--ease-out),
              transform var(--dur-slow) var(--ease-out);
}

/* ---- 3. The bite. For the one control that starts a session. ----
   A float does not sink smoothly; it dips, checks, and goes under. The
   overshoot easing is what makes this read as a take rather than a wobble. */
.btn-primary.is-biting { animation: float-bite 480ms var(--ease-water); }

@keyframes float-bite {
  0%   { transform: translateY(0); }
  22%  { transform: translateY(3px); }
  40%  { transform: translateY(-1px); }
  70%  { transform: translateY(4px); }
  100% { transform: translateY(0); }
}

/* --------------------------------------------------------------------------
   Reduced motion means fewer and gentler, not zero. The opacity that carries
   comprehension stays; every position change goes. A page that removes all
   feedback for a vestibular user has removed information, not decoration.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .js-reveal { transform: none; }
  .js-reveal.is-surfaced { transition: opacity var(--dur) linear; }
  .splash, .waterline::after, .waterline.is-loading::before { animation: none; opacity: 0; }
  .btn-primary.is-biting { animation: none; }
  .btn:active, .place-card:active { transform: none; }
  @media (hover: hover) and (pointer: fine) {
    .btn-primary:hover, .place-card:hover { transform: none; }
  }
}

/* ==========================================================================
   The generated water assets. Both are decorative backdrops: they carry no
   information, so they are marked aria-hidden in the markup and the page reads
   identically with images off.
   ========================================================================== */

/* The home hero. The image is the ground, the type sits on the empty sky the
   prompt deliberately left at the top of the frame - so there is no scrim
   here, and none is needed. */
.water-hero {
  position: relative;
  margin: 0 0 var(--s5);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--canvas-deep);
  box-shadow: var(--edge);
}

.water-hero img {
  display: block;
  /* Both dimensions overridden, never one. Overriding only width leaves height
     resolving to the attribute's raw pixel value, which renders a 1600px-wide
     image 889px tall inside a narrow column and pushes the page off the fold. */
  width: 100%;
  height: 150px;
  object-fit: cover;
  /* The shipped file is already cropped to the half of the frame that carries
     the picture (tools/design_assets_pack.py), so this only has to centre it.
     The first attempt cropped in CSS instead and rendered a featureless cream
     rectangle - caught by looking at tools/design_shots, not by any test. */
  object-position: 50% 50%;
}

.water-hero-copy {
  position: absolute;
  left: var(--s5);
  bottom: var(--s4);
  right: var(--s5);
}

.water-hero-copy h1 { margin: 0; color: var(--ink); }
.water-hero-copy p { margin: 2px 0 0; color: var(--ink-soft); font-size: var(--t-sm); }

@media (min-width: 720px) {
  .water-hero img { height: 200px; }
}

/* The empty state, when there is nothing logged yet. A float on still water is
   the right picture for "no sessions": it is waiting, not broken. */
.empty-state-art {
  display: block;
  width: 100%;
  max-width: 320px;
  height: auto;
  margin: 0 auto var(--s3);
  border-radius: var(--radius);
  opacity: 0.92;
}


/* ==========================================================================
   Ambient fish and tackle
   --------------------------------------------------------------------------
   The owner asked for small fish of different species to jump and dive across
   the page from time to time, and for tackle used as decoration.

   Everything here is inline SVG and CSS keyframes: zero bytes on the wire, no
   sprite sheet, no library, no requestAnimationFrame loop. Only `transform`
   and `opacity` animate, so none of it can force layout.

   Three things keep it honest on a phone at a riverbank:
     - the layer is `aria-hidden` and `pointer-events: none`; it states nothing
       and it can never eat a tap meant for a control underneath;
     - it PAUSES when the tab is hidden (`html.is-hidden`, set from
       visibilitychange in waterline.js) - an animation running in a pocket is
       pure battery;
     - `prefers-reduced-motion` removes the crossings entirely.

   The species are drawn per species, per CLAUDE.md: a roach has an even fork,
   a bream a long unequal one, a crucian a rounded convex fan. Recolouring one
   silhouette four times would be worse than showing none.
   ========================================================================== */

.ambient {
  position: fixed;
  inset: 0;
  /* ABOVE the page, not behind it. The first cut sat at z-index 0 under the
     content, which meant the fish were invisible on every page with a hero or
     a full-width card - which is most of them. Caught by filming it; the
     stylesheet read perfectly well. Safe to sit on top because the layer is
     aria-hidden and pointer-events:none, so it can never take a tap. */
  z-index: 5;
  pointer-events: none;
  overflow: hidden;
}

/* The running-session button stays above the fish: it is a control, and a fish
   crossing in front of it would read as a rendering fault. */
.session-fab { z-index: 40; }

.ambient-fish {
  position: absolute;
  left: -80px;
  opacity: 0;
  will-change: transform, opacity;
  animation: fish-cross var(--cross, 26s) var(--ease-out) infinite;
  animation-delay: var(--delay, 0s);
}

.ambient-fish svg { display: block; }

/* The arc is split across two elements so the horizontal travel stays linear
   while the vertical rise and the pitch are eased - one element cannot do both
   without the fish appearing to slow down mid-air. */
.ambient-fish-arc {
  animation: fish-arc var(--cross, 26s) var(--ease-out) infinite;
  animation-delay: var(--delay, 0s);
}

@keyframes fish-cross {
  0%, 4%    { transform: translateX(0); opacity: 0; }
  8%        { opacity: 0.5; }
  16%       { opacity: 0.5; }
  22%       { opacity: 0; }
  100%      { transform: translateX(115vw); opacity: 0; }
}

/* Out of the water, over the top, and back down - nose up on the way up and
   nose down on the way in, which is the whole reason the pin bug is famous
   here. Verified with tools/animation_filmstrip.py, not by reading this. */
@keyframes fish-arc {
  0%   { transform: translateY(40px)  rotate(-26deg); }
  8%   { transform: translateY(4px)   rotate(-18deg); }
  13%  { transform: translateY(-14px) rotate(0deg); }
  18%  { transform: translateY(4px)   rotate(18deg); }
  24%  { transform: translateY(46px)  rotate(30deg); }
  100% { transform: translateY(46px)  rotate(30deg); }
}

/* Hidden tab: stop. This is the difference between a charming detail and a
   battery complaint. */
html.is-hidden .ambient-fish,
html.is-hidden .ambient-fish-arc {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  /* Fewer and gentler, not zero - but a fish flying across the page is
     position, and position is the part that goes. */
  .ambient { display: none; }
}

/* ---- tackle used as decoration, never as a control ---- */
.tackle-mark {
  position: absolute;
  pointer-events: none;
  opacity: 0.34;
  color: var(--line-strong);
}

.tackle-mark--float { top: -18px; right: -22px; transform: rotate(22deg); }
.tackle-mark--reel  { bottom: 8%; left: -26px; transform: rotate(-12deg); }
