/*
  main.css — SwimSheet website styling
  Intent: single stylesheet for the whole site — design tokens, the
    pool-water look, hero layout, cards, feature panels, motion
    (TDD_COMING_SOON_V1), the notify-me signup, and the privacy-policy
    page (TDD_NOTIFY_SIGNUP_V1).
  Constraints: token names and hex values copied exactly from
    SITE_DESIGN_SYSTEM.md §1–4 — change that document and these values
    together (Principle 6). Every color/size/space in every rule
    references a token; no hard-coded values (Principle 5).
  Effects: :root variables are inherited by every element on every page;
    the box-sizing reset and body defaults apply site-wide; the caustic
    water image (body::before) is the backdrop for all sections.
*/

/* Design tokens — defined once, reused everywhere (Principle 5).
   Hex notation is shortened where stylelint's standard ruleset requires
   (#FFF = #FFFFFF); the colors are identical to SITE_DESIGN_SYSTEM.md §1. */
:root {
  /* App-carryover colors (SITE_DESIGN_SYSTEM.md §1) */
  --jet-white: #FFF;
  --deep-basin: #F0F4F8;
  --cobalt-blue: #0047AB;
  --alert-orange: #F60;
  --turquoise: #2AC1CF;
  --pure-black: #000;
  --dark-gray: #444;
  --light-gray: #DDD;

  /* Website-only pool-water colors (SITE_DESIGN_SYSTEM.md §1) */
  --water-surface: #66CCE8;
  --water-mid: #1E8FD5;
  --water-deep: #0047AB;
  --water-floor: #012B66;

  /* Type sizes (SITE_DESIGN_SYSTEM.md §2) */
  --text-hero: clamp(2.5rem, 5.5vw, 3.5rem);
  --text-heading: clamp(1.5rem, 3.5vw, 2rem);
  --text-card-title: 1.125rem;
  --text-body: 1rem;
  --text-small: 0.875rem;

  /* Spacing scale (SITE_DESIGN_SYSTEM.md §4) */
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 1.5rem;
  --space-4: 2.5rem;
  --space-5: 4rem;

  /* Card anatomy (SITE_DESIGN_SYSTEM.md §3) — defined once here so no
     rule hard-codes the shadow color (Principle 5). */
  --card-radius: 12px;
  --card-shadow: 0 2px 8px rgb(0 0 0 / 20%);

  /* Shared section-card column width — the tunable "app column" feel
     (TDD_WATER_NEEDLE_V1 Phase 2; SITE_DESIGN_SYSTEM.md §3). All three
     section cards use this one value so the page reads as one column. */
  --card-column: 56rem;
}

/* Predictable sizing everywhere: padding/borders never grow a box. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/*
  Body: base type + the water fallback (SITE_DESIGN_SYSTEM.md §1).
  The VISIBLE water is the fixed body::before layer just below; body
  carries the same caustic image over a solid --water-mid so that
  (a) text never sits on bare white if the image fails to load, and
  (b) accessibility checkers treat on-water text as text-on-image
  (a manual judgment) instead of measuring it against a flat fallback
  color it never actually sits on — checkers cannot see pseudo-element
  backgrounds (TDD_WATER_NEEDLE_V1 guardrail, Principle 5).
  This body-level copy is fully covered by body::before, so it has no
  visual effect of its own; same URL, so the browser fetches it once.
  overflow-x: hidden is a safety net against any child (e.g. .signup's
  min-width) pushing wider than a narrow phone viewport (owner-reported
  bug, fixed 2026-07-29) — the real fix is that no element should need
  it, but this guarantees the symptom (page-level horizontal scroll)
  can't reach visitors even if a future rule slips past that goal.
*/
body {
  margin: 0;
  min-height: 100vh;
  overflow-x: hidden;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    Helvetica, Arial, sans-serif;
  font-size: var(--text-body);
  line-height: 1.6;
  color: var(--pure-black);
  background: var(--water-mid) url("../images/water-caustics.webp")
    center / cover no-repeat;
}

/*
  The water: the app's caustic pool texture (SITE_DESIGN_SYSTEM.md §1),
  shown BRIGHT and edge-to-edge — legibility comes from white cards and
  the hero's local depth treatment, never from dimming the water (owner
  decision 2026-07-08, TDD_WATER_NEEDLE_V1). Fixed and oversized
  (scale 1.08) so the slow drift never exposes an edge; z-index: -1
  keeps it beneath all content; decorative only, so it never captures
  clicks. The --water-mid underlay doubles the body fallback while the
  image loads. Drift is disabled in the reduced-motion block (§5).
*/
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--water-mid) url("../images/water-caustics.webp")
    center / cover no-repeat;
  transform: scale(1.08);
  animation: drift 45s ease-in-out infinite alternate;
}

/*
  Hero: a COMPACT centered column on open bright water — deliberately
  not full-screen (owner decision 2026-07-21): the hero takes only the
  height its content needs so the notify-me signup card lands above the
  fold on a typical laptop/phone screen. This reverses the original
  full-viewport hero (TDD_WATER_NEEDLE_V1); no min-height may be
  reintroduced without pushing the signup back below the fold.
  No local depth treatment behind the text (owner decision 2026-07-21,
  reverses TDD_WATER_NEEDLE_V1 Phase 1): all hero text is now black
  (see h1/.tagline/.hero-sub), so the dark pool that used to help WHITE
  text stand out was removed rather than left to fight the new color.
*/
.hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: var(--space-3) var(--space-2) 0;
}

/* Sized for the compact above-the-fold hero (owner decision
   2026-07-21): big enough for the needle animation to read clearly,
   small enough to leave room for the signup card on the first screen.
   display: block removes the inline baseline gap so the wrap's box —
   and therefore the needle overlay — matches the icon exactly. */
.hero-logo {
  display: block;
  width: clamp(140px, 22vw, 220px);
  height: auto;
}

/* Anchors the absolutely-positioned needle overlay; z-index: 0 creates
   a stacking context so the needle stacks over the logo but never over
   page content (TDD_WATER_NEEDLE_V1 Phase 3). */
.hero-logo-wrap {
  position: relative;
  z-index: 0;
}

/*
  Ticking stopwatch needle (TDD_WATER_NEEDLE_V1 Phase 3): a decorative
  orange hand pinned to the icon's dial, ticking like a running
  stopwatch — motion as a deliberate focal point, owner decision
  2026-07-08 (SITE_DESIGN_SYSTEM.md §5).
  Constraints: the needle is pinned to the icon, never the reverse —
    the pivot (50.5% 54%) is the dial-pin center measured from
    images/swimsheet-icon.webp (1143x1238); refine centering by nudging
    these two percentages. Only transform animates; frozen upright in
    the reduced-motion block below.
  Effects: fills the wrap (so it scales with the responsive logo) and
    sits above the logo image; pointer-events: none means it can never
    trap a click.
*/
.needle {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--alert-orange);
  fill: currentcolor;
  pointer-events: none;
  z-index: 1;
  transform-origin: 50.5% 54%;
  animation:
    tick 12s steps(60) forwards,
    needle-fade 0.4s ease-in 12s forwards;
}

/* "Coming Soon" pill — the accent spark (SITE_DESIGN_SYSTEM.md §1).
   The oversized radius is not a size, just "round the ends fully".
   White-on-orange is 2.94:1 contrast — below WCAG AA — kept as an
   approved owner exception (2026-07-08); see architecture.md Quality
   Gates. Do not "fix" without a new owner decision. */
.badge {
  display: inline-block;
  background: var(--alert-orange);
  color: var(--jet-white);
  font-weight: 700;
  padding: var(--space-1) var(--space-3);
  border-radius: 999px;
  margin: var(--space-2) 0 0;
}

/* All hero text is black on the bright water (owner decision
   2026-07-21, replacing the original white-on-water rule in
   SITE_DESIGN_SYSTEM.md §2). Do not "fix" back to white without a new
   owner decision. Margins are deliberately tight (--space-1) — part of
   the compact above-the-fold hero. */
h1 {
  font-size: var(--text-hero);
  color: var(--pure-black);
  margin: 0;
  line-height: 1.1;
}

/* Centering and edge spacing were previously inherited from .hero's
   flex column (text-align + align-items + .hero's own side padding).
   Both now live in <main> instead — below the signup card, not inside
   .hero — so each needs its own: text-align for centering, the side
   padding so text doesn't touch the screen edge on phones (.hero-sub's
   38rem max-width is far wider than any phone, so it fills 100% of
   <main> there with no inset unless given its own; owner-reported
   regression, fixed 2026-07-29), and (on .hero-sub) auto side margins
   to center the box itself on wider screens where the max-width caps
   it narrower than <main> (owner decision 2026-07-29, moved together
   with the signup-card reposition above). */
.tagline {
  color: var(--pure-black);
  font-weight: 700;
  text-align: center;
  padding: 0 var(--space-2);
  margin: var(--space-1) 0 0;
}

.hero-sub {
  color: var(--pure-black);
  max-width: 38rem;
  text-align: center;
  padding: 0 var(--space-2);
  margin: var(--space-1) auto 0;
}

/* Section headings live INSIDE white cards → dark, centered
   (SITE_DESIGN_SYSTEM.md §2/§3; TDD_WATER_NEEDLE_V1 Phase 2). The
   privacy page's .policy h2 rule below overrides for that card. */
h2 {
  font-size: var(--text-heading);
  color: var(--pure-black);
  text-align: center;
  margin: 0 0 var(--space-4);
}

/*
  Section cards (TDD_WATER_NEEDLE_V1 Phase 2): the signup, benefits,
  and features sections each float as ONE white card on the water —
  the app's card anatomy at section scale (SITE_DESIGN_SYSTEM.md §3).
  Intent: legibility on the bright water comes from cards, never from
    dimming the water (owner decision 2026-07-08); section headings
    moved off the water into the cards (Principles 1, 4, 5).
  Constraints: tokens only; all three share --card-column; the width
    calc keeps water visible on both sides at every viewport width.
  Effects: replaces the old on-water section rhythm and the individual
    sections' own column sizing; sibling margins collapse, so cards sit
    one --space-5 apart.
*/
.section-card {
  background: var(--jet-white);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
  width: calc(100% - 2 * var(--space-2));
  max-width: var(--card-column);
  margin: var(--space-5) auto;
  padding: var(--space-4);
}

/*
  Benefit items: a responsive grid INSIDE the benefits card — plain
  items, not nested cards (TDD_WATER_NEEDLE_V1 Phase 2). auto-fit keeps
  the grid responsive with no media queries; the gap separates items
  cleanly on the shared white surface.
*/
.benefit-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-3) var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* High-Glare Legibility on the card: black titles, dark-gray body
   (SITE_DESIGN_SYSTEM.md §2). */
.benefit h3 {
  font-size: var(--text-card-title);
  color: var(--pure-black);
  margin: 0 0 var(--space-1);
}

.benefit p {
  color: var(--dark-gray);
  margin: 0;
}

/* Feature-catalog intro: dark on the white features card (§2). */
.features > p {
  color: var(--dark-gray);
  text-align: center;
  margin: 0 0 var(--space-3);
}

/*
  Downloads card: tighter top than the shared .section-card padding, and
  its heading sits closer to the list below (owner decision 2026-07-29
  — "less space between the title and top of the card" / "...and the
  documents"). Overrides only padding-top and h2's margin-bottom;
  everything else still comes from .section-card and the shared h2 rule.
*/
.downloads {
  padding-top: var(--space-2);
}

.downloads h2 {
  margin-bottom: var(--space-1);
}

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

/*
  Disclosure panels INSIDE the features card (SITE_DESIGN_SYSTEM.md §6):
  hairline-bordered rows, not nested floating cards — the section card
  provides the white surface (§3). The native summary marker is kept —
  it is the accessible affordance for open/close.
*/
.feature-panel {
  border: 1px solid var(--light-gray);
  border-radius: var(--card-radius);
  padding: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-2);
}

.feature-panel summary {
  font-weight: 700;
  color: var(--cobalt-blue);
  cursor: pointer;
}

.feature-panel ul {
  color: var(--dark-gray);
  margin: var(--space-2) 0 var(--space-1);
  padding-left: var(--space-3);
}

/* Footer: fine print resting on the pool floor. Black text (owner
   decision 2026-07-21) — resolves the long-open "footer links unreadable
   on open water" item from TDD_WATER_NEEDLE_V1's final checklist. */
footer {
  text-align: center;
  color: var(--pure-black);
  font-size: var(--text-small);
  padding: var(--space-5) var(--space-2);
}

/*
  Notify-me signup: a white card holding the embedded EmailOctopus form
  (TDD_NOTIFY_SIGNUP_V1 + TDD_WATER_NEEDLE_V1 Phase 2; owner decision
  2026-07-21: no heading/subtext, card holds only the widget and the
  privacy line).
  Constraints: the form's OWN look (fields, colours, text) is configured
    inside EmailOctopus, NOT here — do not try to restyle its internals;
    the flex column here only positions the widget and privacy line,
    it does not touch what's inside the script's injected markup.
    margin-top overrides the shared --space-5 card rhythm to tuck the
    card close under the compact hero — required for the above-the-fold
    goal (owner decision 2026-07-21).
    Width was originally "width: fit-content" (size the card to match
    the widget). That was the cause of an owner-reported phone
    horizontal-scroll bug: fit-content sizes a box by its content's
    natural width BEFORE any stretching is applied, so the moment the
    widget's own content wanted to be wider than a phone screen (its
    email field and button side by side), the card grew to match it —
    on any width smaller than that, no matter what. Switched to the
    same width formula .section-card already uses (card width comes
    from the SCREEN, never from what's inside it) — this is what
    actually keeps the card, and everything inside it, from ever
    exceeding the phone's width (diagnosed via live DOM inspection,
    fixed 2026-07-29). align-items: stretch (not center) is required
    alongside this so the widget's injected div is forced to fill that
    screen-based width rather than reverting to its own preferred size.
    margin-bottom overrides the shared --space-5 card rhythm (same
    reasoning as margin-top) now that the tagline/hero-sub text sits
    directly below this card instead of above it — without this, the
    text would sit a full --space-5 (4rem) below the card instead of
    reading as part of the same intro block (owner decision 2026-07-29).
  Effects: on-card text is dark per SITE_DESIGN_SYSTEM.md §2.
*/
.signup {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  text-align: center;
  width: calc(100% - 2 * var(--space-2));
  max-width: 32rem;
  margin-top: var(--space-3);
  margin-bottom: var(--space-3);
  padding: var(--space-3) var(--space-3) var(--space-1);
}

/* Footer nav: the shared footer's links (duplicated on every page —
   Principle 1). White on the pool floor; visible focus ring for
   keyboard users. No transitions, so nothing to reduce for motion.
   These element-level link rules sit BEFORE the class-scoped link rules
   below (stylelint requires ascending specificity per element). */
footer nav {
  display: flex;
  justify-content: center;
  gap: var(--space-3);
  margin-bottom: var(--space-1);
}

footer nav a {
  color: var(--pure-black);
  font-size: var(--text-small);
}

/* Links are the only focusable things in the footer nav. */
footer nav :focus-visible {
  outline: 2px solid var(--pure-black);
  outline-offset: 2px;
}

/* Point-of-collection notice: now inside the white signup card → dark
   text, cobalt privacy link like every on-card link
   (SITE_DESIGN_SYSTEM.md §2; TDD_WATER_NEEDLE_V1 Phase 2).
   margin-top matches the card's --space-1 bottom padding so the line
   sits visually centered in the card's bottom band (owner decision
   2026-07-21) — change both together or the balance breaks. */
.fineprint {
  color: var(--dark-gray);
  font-size: var(--text-small);
  margin: var(--space-1) 0 0;
}

.fineprint a {
  color: var(--cobalt-blue);
  text-decoration: underline;
}

/*
  Export-examples download links (SITE_DESIGN_SYSTEM.md §10).
  Intent: direct PDF downloads styled like the other on-card links
    (cobalt on white, §2), full-width so each list row (see
    .download-list above) is a comfortable tap target. padding is
    --space-1 (not the --space-2 used elsewhere) and there are no row
    dividers, deliberately tighter than a typical list (owner decision
    2026-07-29 — "taking up entirely too much vertical room").
  Constraints: tokens only (Principle 5), EXCEPT font-size and
    font-family, both deliberate one-off owner values (2026-07-29,
    "shrink the font by 1 point and use a subtly serif font") scoped to
    this one component — not a change to the site's type scale or
    system-font stack (SITE_DESIGN_SYSTEM.md §2 records the exception).
    calc(var(--text-body) - 1pt) is literally "1 point smaller than
    body text," not a new size token. Georgia is a widely-preinstalled
    serif (no web-font request needed — see architecture.md "No
    surprises") subtle enough to read as a quiet typographic accent
    rather than a mismatched font. Placed here, after `footer nav a`, to
    keep stylelint's no-descending-specificity happy — matching where
    `.fineprint a` and `.policy a` already sit.
*/
.download-list a {
  display: block;
  padding: var(--space-1) 0;
  color: var(--cobalt-blue);
  font-size: calc(var(--text-body) - 1pt);
  font-family: Georgia, "Times New Roman", serif;
  text-decoration: underline;
}

/*
  Privacy policy page: one readable white card floating on the water
  (SITE_DESIGN_SYSTEM.md §3; tokens only, Principle 5). The water
  background comes from the shared body rule — nothing re-declared here.
*/

/* Small white "back home" link sitting on the water above the card. */
.back {
  max-width: 46rem;
  margin: 0 auto;
  padding: var(--space-3) var(--space-2) 0;
  font-size: var(--text-small);
}

.back a {
  color: var(--jet-white);
}

/* The policy card. width keeps a water margin on narrow screens. */
.policy {
  background: var(--jet-white);
  border-radius: var(--card-radius);
  box-shadow: var(--card-shadow);
  width: calc(100% - 2 * var(--space-2));
  max-width: 46rem;
  margin: var(--space-3) auto var(--space-5);
  padding: var(--space-4);
  color: var(--dark-gray);
}

/* Page title on the card: black, card-scale — not the on-water hero. */
.policy h1 {
  font-size: var(--text-heading);
  color: var(--pure-black);
  margin: 0 0 var(--space-1);
}

/* Section headings INSIDE the card override the on-water h2 default
   (white, centered) — cobalt emphasis per SITE_DESIGN_SYSTEM.md §1. */
.policy h2 {
  font-size: var(--text-card-title);
  color: var(--cobalt-blue);
  text-align: left;
  margin: var(--space-4) 0 var(--space-1);
}

.policy p,
.policy ul {
  margin: 0 0 var(--space-2);
}

.policy a {
  color: var(--cobalt-blue);
}

/*
  Screenshot carousel (.screenshots / .carousel / .slide).
  Intent: show one tall phone screenshot at a time inside a normal white
    section card. Previous/Next buttons look like one static pair that
    never moves and greys out at each end (owner decision 2026-07-29).
  Constraints: every value is a token from :root (Principle 5). NO
    JavaScript (architecture.md 5a) — every .slide is display: none
    except the one matching :target; the :has() rule below shows slide
    1 by default before any button has been clicked. This is why
    navigation is click-only, not swipe: :target only updates when a
    link is clicked, so it's the only zero-JS way to always know which
    slide is current and grey the right button. Both button positions
    are ALWAYS rendered in the HTML (a real <a> or a disabled <span> —
    see index.html) specifically so nothing shifts between slides;
    "static-looking" buttons are really just a side effect of only one
    slide ever being on screen at once. margin-top and padding-top on
    .screenshots are literal cm/mm measurements, explicit owner values
    (2026-07-29) rather than --space tokens — deliberate one-offs, not
    missed tokens. margin-bottom is calc(var(--space-5) - 1.5cm) so it
    stays anchored to the shared card rhythm rather than becoming its
    own disconnected magic number. width overrides the shared
    .section-card formula so THIS card alone is 1cm narrower than the
    screen on each side, leaving visible water framing (owner decision
    2026-07-29 — "left/right margins" meant the card's own width, not
    the gap between the card and the image; padding-left/right stay 0
    now that the card itself is narrower, so the image still runs flush
    to this card's own edges).
  Effects: because slides are shown/hidden rather than scrolled, there
    is no horizontal scroll position and nothing to "jitter" there. A
    button click is still a same-page anchor jump, so the browser may
    scroll the page vertically if the card isn't fully in view already;
    html's scroll-behavior: smooth (below) makes that animate. This was
    OFF for a while (owner decision 2026-07-29): with it on, switching
    :target between two differently-SIZED slides visibly "jerked" — the
    browser appeared to scroll toward the old slide's position before
    correcting to the new one, since the layout changed (old slide
    hides, new one shows, at a different height) at the same moment the
    scroll animation started targeting it. Re-enabled (owner decision
    2026-07-29) once figcaption's height was locked to a fixed two
    lines (below), which removes the height change between slides
    entirely — verified via sampled scroll position during the
    animation: strictly monotonic toward the target, no reversal, at
    several different starting scroll positions and slide pairs.
    .slide's scroll-margin-top still keeps that scroll from pinning the card's
    top edge flush to the viewport — some water stays visible above the
    card (owner decision 2026-07-29).
    This section reuses .section-card for column width and the
    water showing on both sides; only padding-top is overridden here
    (SITE_DESIGN_SYSTEM.md §3).
*/
.screenshots {
  width: calc(100% - 2cm);
  margin-top: 0.25cm;
  margin-bottom: calc(var(--space-5) - 1.5cm);
  padding-top: 1mm;
  padding-left: 0;
  padding-right: 0;
  text-align: center;
}

/* On a desktop monitor, the shared .section-card width (up to 56rem)
   badly outgrows a single portrait phone screenshot — the card was
   measured at 896px wide while the image inside rendered at only
   250px, leaving roughly 320px of blank white space on each side
   (owner decision 2026-07-29 — "the white card is way too wide").
   Capped here instead, using the same vh-based math as the image's own
   max-height: 60vh and the caption's max-width above, so the card's
   width tracks the image at any window height; + 4rem is a little
   framing around it. Gated to widths comfortably above phone size so
   mobile (which already fits the image via the width rule above,
   tuned separately) is untouched — this formula would actually be
   narrower than that on a phone, which nobody asked for. */
@media (width >= 600px) {
  .screenshots {
    max-width: calc(60vh * 415 / 900 + 4rem);
  }
}

html {
  scroll-behavior: smooth;
}

.carousel {
  display: flex;
  justify-content: center;
}

/* Hidden by default; :target shows the current slide, and the :has()
   fallback shows slide 1 specifically when nothing is targeted yet
   (fresh page load, before any button click). display:none, not
   opacity/visibility, is deliberate: it's the only mechanism that stops
   the browser from loading every hidden slide's image. Two animated
   transitions were tried and reverted (owner decision 2026-07-29): a
   true cross-fade (all 13 slides stacked via CSS grid, faded with
   opacity) forced every image to load and paint at once regardless of
   loading="lazy", since they were all technically on-screen (just
   transparent), sinking Lighthouse Performance from 95 to 74 (LCP
   5.3s) — not viable with 13 real photos without JavaScript. A
   fade-in-only compromise (animating just the incoming slide's opacity)
   was performance-safe but read as worse than the plain instant
   snap — the owner's call, not a technical constraint this time. Back
   to the plain instant display:none/flex swap. */
.slide {
  display: none;
  flex-direction: column;
  align-items: center;
  margin: 0;
  width: 100%;
  scroll-margin-top: var(--space-4);
}

.slide:target,
.carousel:not(:has(.slide:target)) .slide:first-of-type {
  display: flex;
}

/* width is a definite calc(), not auto — deliberate (owner decision
   2026-07-29, fixing a real bug: "the whole thing disappears for a
   millisecond"). The old width:auto/max-height:60vh pair needed the
   image's OWN decoded intrinsic size to resolve a used width, so for
   one frame right as display:none -> flex flips — even on an already
   -cached/prefetched image, decode still isn't instant — naturalWidth
   was 0 and the box collapsed to ~4px before recovering. Deriving
   width from the same 60vh-based formula used elsewhere on this card
   (figcaption's max-width, the desktop card max-width) needs nothing
   from the image itself, so it's correct on the very first rendered
   frame regardless of decode state; height then follows from
   aspect-ratio. min(..., 100%) is the same width-vs-height-limited
   fallback the old max-width:100% provided. Verified via per-frame
   sampling around a click: no collapse, on both a prefetched slide and
   a real cold network fetch. */
.slide img {
  width: min(calc(60vh * 415 / 900), 100%);
  height: auto;
  aspect-ratio: 415 / 900;
  border: 1px solid var(--light-gray);
  border-radius: var(--card-radius);
}

/* max-width tracks the image's own rendered width rather than a fixed
   guess (owner decision 2026-07-29 — captions were stretching to the
   full card width, well past the image on each side). The image's
   width is derived from its max-height: 60vh capped by its 415:900
   intrinsic ratio, so the same vh-based math reproduces that width
   here; + 2rem is "a little past" on each side. min(..., 100%) is the
   fallback for the rarer case where the image is instead width-limited
   (short viewport height, tall/narrow window) rather than height-limited
   — then the card's own width wins, same as before this change. */

/* Fixed to exactly two lines (owner decision 2026-07-29 — captions of
   different lengths were making the whole card grow and shrink between
   slides). height, not min-height/max-height, is deliberate: it's what
   keeps every slide's card the same size regardless of caption length
   — a one-line caption leaves blank space below it rather than
   shrinking the box, and a caption longer than two lines truncates
   with an ellipsis (-webkit-line-clamp) rather than wrapping further
   and growing it. Current captions all fit within two lines; if a
   future caption doesn't, it will truncate rather than resize the
   card — shorten it, or revisit this cap intentionally. */
.slide figcaption {
  max-width: min(calc(60vh * 415 / 900 + 2rem), 100%);
  height: calc(1.6em * 2);
  margin-top: var(--space-2);
  overflow: hidden;
  color: var(--dark-gray);
  font-size: var(--text-small);
  line-height: 1.6;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* Button pair, close above the image — always both, one per side, at a
   fixed height so nothing shifts whichever slide is showing. */
.slide-nav {
  display: flex;
  gap: var(--space-3);
  justify-content: center;
  margin-bottom: var(--space-1);
}

/* 44px circles are the accessible min touch target — shrinking below
   that is a regression. Shared by the real <a> button and the disabled
   <span> placeholder so both look identical apart from colour. */
.btn {
  display: inline-flex;
  width: 2.75rem;
  height: 2.75rem;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--light-gray);
  border-radius: 50%;
  background: var(--jet-white);
  box-shadow: var(--card-shadow);
  text-decoration: none;
}

a.btn:hover {
  background: var(--deep-basin);
}

a.btn:focus-visible {
  outline: 2px solid var(--cobalt-blue);
  outline-offset: 2px;
}

/* Chevron glyph drawn from a rotated corner, not a font character (owner
   decision 2026-07-29 — text glyphs read as too small/thin at this
   button size). */
.chevron {
  width: 0.6rem;
  height: 0.6rem;
  border-right: 3px solid var(--cobalt-blue);
  border-bottom: 3px solid var(--cobalt-blue);
}

.chevron-left {
  margin-left: 0.2rem;
  transform: rotate(135deg);
}

.chevron-right {
  margin-right: 0.2rem;
  transform: rotate(-45deg);
}

/* Disabled placeholder for the no-loop ends: greyed and inert, not
   removed, so the button position never shifts (owner decision
   2026-07-29 — "both buttons on screen at all times"). */
.btn-disabled {
  border-color: var(--deep-basin);
  box-shadow: none;
}

.btn-disabled .chevron {
  border-color: var(--light-gray);
}

/*
  ============================== Motion =================================
  Intent: gentle, continuous water motion — never flashy
    (SITE_DESIGN_SYSTEM.md §5).
  Constraints: ONLY transform and opacity are animated (smooth on every
    device; protects the Lighthouse performance gate). Every animation
    below is switched off in the prefers-reduced-motion block at the end
    of this section — mandatory, not optional.
  Effects: purely decorative; the page is complete and fully usable with
    all of it disabled.
*/

/* Hero entrance: the text rises softly into place, once, on load.
   Scoped to .hero h1 — the policy page's h1 must not animate. */
.badge,
.hero h1,
.tagline,
.hero-sub {
  animation: fade-rise 0.8s ease-out;
}

/* Needle tick: one full revolution in 60 discrete steps — stepped, not
   swept, so it reads as a running stopwatch. Runs once (no infinite
   loop): it ticks all the way back up to 12 o'clock and stops there,
   at which point needle-fade (below) takes over. The 12s duration and
   steps(60) granularity are the tunable tick speed
   (TDD_WATER_NEEDLE_V1 refine point). */
@keyframes tick {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

/* Needle fade: once the tick animation above completes its single
   revolution at 12 o'clock, this fades the needle out — reading as the
   stopwatch winding down and stopping. Delay (12s) matches the tick
   duration so the fade starts exactly when the needle arrives back at
   the top; forwards keeps it invisible afterward instead of popping
   back to opacity: 1. */
@keyframes needle-fade {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

/* Water drift: the caustic layer slides gently within the 8% overscan
   that scale(1.08) provides, so no edge ever shows. scale() must repeat
   in every frame or it would be dropped mid-animation. The 45s duration
   is the tunable drift speed (TDD_WATER_NEEDLE_V1 refine point). */
@keyframes drift {
  from {
    transform: scale(1.08) translate(-0.8%, -0.5%);
  }

  to {
    transform: scale(1.08) translate(0.8%, 0.5%);
  }
}

/* One-time entrance: fade in while rising 16px. */
@keyframes fade-rise {
  from {
    transform: translateY(16px);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

/* Reduced motion: visitors who ask for less motion get a perfectly
   still, complete page (SITE_DESIGN_SYSTEM.md §5 — mandatory). The
   needle freezes pointing up (its unrotated position); the water
   holds still at its base scale. */
@media (prefers-reduced-motion: reduce) {
  .needle,
  .badge,
  .hero h1,
  .tagline,
  .hero-sub,
  body::before {
    animation: none;
  }

  html {
    scroll-behavior: auto;
  }
}
