/* ==========================================================================
   Lodestar Fitness — the shipping stylesheet
   --------------------------------------------------------------------------
   As of the direction-C rollout (Task 2), this file owns EVERYTHING: the
   spacing scale, the container, the breakpoints, the layout primitives, the
   shared components, and the direction-C theme itself (palette, three font
   stacks, every tone). It used to own structure only, with a separate
   theme-*.css supplying colour and typeface — that split ended when
   theme-ribs.css and comp.css were lifted in here; both files still exist on
   disk but Task 10 deletes them.

   It exists because the previous stylesheet had no system underneath it:
   37 separate grid/flex declarations, 10 breakpoints with 900px defined as
   BOTH a max- and a min-width (so rules collided at exactly 900), and 99
   inline style= attributes in the markup — 39 of them the same
   `padding-top: 0`, which is what "no section rhythm" looks like when you
   patch it one element at a time.

   Three rules keep that from coming back:

   1. EVERY breakpoint is min-width. Mobile-first, additive cascade. Two rules
      can no longer disagree at the same pixel, because a min-width rule never
      un-applies a narrower one — it extends it.
   2. Spacing comes from the scale or it does not exist. No bare px margins.
   3. Section rhythm is automatic (see "Section rhythm" below). Nothing in the
      markup should ever need to zero its own padding.
   ========================================================================== */

/* ---------- Breakpoints -------------------------------------------------
   Five, all min-width, named for what changes rather than for a device:

     (base)   < 600   one column
     sm       600     two-up starts, side-by-side meta
     md       900     nav goes horizontal, split layouts engage
     lg       1280    three-up grids, full composition
     xl       1760    container and gutters open up

   CSS custom properties cannot be used in a media query, so the numbers are
   written literally below. These four comments are the only place they are
   defined in prose; keep them in sync by grepping "min-width".
   ------------------------------------------------------------------------ */

/* ---------- Scale -------------------------------------------------------- */
:root {
  /* Spacing. One scale. Roughly 1.5x, snapped to whole pixels so nothing
     lands on a half-pixel and blurs a border. */
  --space-1: 0.25rem;   /*  4px */
  --space-2: 0.5rem;    /*  8px */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-5: 1.5rem;    /* 24px */
  --space-6: 2rem;      /* 32px */
  --space-7: 3rem;      /* 48px */
  --space-8: 4rem;      /* 64px */
  --space-9: 6rem;      /* 96px */
  --space-10: 8rem;     /* 128px */
  --space-11: 10rem;    /* 160px */

  /* Type scale. Fluid between the sm and lg breakpoints, then it stops —
     an unbounded clamp is why a headline that reads well at 1440 becomes a
     billboard at 2560. */
  --text-xs:   0.8125rem;                              /* 13px  labels, legal */
  --text-sm:   0.875rem;                               /* 14px  meta, captions */
  --text-base: 1rem;                                   /* 16px  body */
  --text-md:   clamp(1.0625rem, 0.98rem + 0.35vw, 1.1875rem);
  --text-lg:   clamp(1.25rem,  1.13rem + 0.5vw,  1.5rem);
  --text-xl:   clamp(1.5rem,   1.3rem  + 0.9vw,  2rem);
  --text-2xl:  clamp(1.875rem, 1.5rem  + 1.6vw,  2.75rem);
  --text-3xl:  clamp(2.25rem,  1.6rem  + 2.8vw,  4rem);
  --text-4xl:  clamp(2.75rem,  1.7rem  + 4.5vw,  5.5rem);

  /* Measure. Body copy never exceeds this, whatever container it sits in.
     Eight of the deleted inline styles were `max-width: 68ch`. */
  --measure: 68ch;
  --measure-tight: 46ch;

  /* Container. One width, one gutter, computed once. */
  --container: 1240px;
  --container-wide: 1600px;
  --gutter: var(--space-5);

  /* Section rhythm — the vertical unit between major page bands. Task 3:
     tightened to --space-8 below 600px (the comp measured 9,787px total
     page height at 390 against an 8,000px ceiling) — restored to
     --space-9 at 600 and up below. */
  --section-y: var(--space-7);

  /* Vestigial as of the direction-C rollout (Task 2, Step 1b): card grids
     are now declared by a fixed count modifier (.grid--1-2-3 etc., below),
     not by a minimum card width — `auto-fit` is banned (DESIGN.md §11).
     Nothing references this token any more; kept rather than removed so a
     revert of the `.grid` change below isn't also a token hunt. */
  --card-min: 17rem;

  --radius: 0px;        /* themes override */
  --radius-lg: 0px;
  --border-width: 1px;

  --transition: 180ms cubic-bezier(0.2, 0, 0, 1);
}

@media (min-width: 600px) {
  :root { --gutter: var(--space-6); --section-y: var(--space-9); }
}
@media (min-width: 900px) {
  :root { --gutter: var(--space-7); --section-y: var(--space-10); }
}
@media (min-width: 1760px) {
  :root { --gutter: var(--space-8); --container: 1360px; --container-wide: 1760px; }
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  font-size: var(--text-base);
  line-height: 1.6;
  /* The previous stylesheet let long unbroken strings (a URL, an email) blow
     out a card's width on phones. */
  overflow-wrap: break-word;
  /* Round-1 I-3 / Round-2 N-3: clearance for .sticky-cta (defined later, in
     "New components"), which is fixed and permanently covers the bottom of
     every mobile page below md — measured on the ribs comp at 390:
     scrollHeight 9739, .footer-fine spans 9625-9691, the bar covered
     9666-9739, so 25px of "Built and maintained by Keelson Systems LLC" was
     unreachable at maximum scroll. The first fix used a fixed 73px
     (12+12+48+1: padding-block twice, .btn's own 3rem min-height, the 1px
     top border) and missed that the bar's own bottom padding is
     `max(--space-3, env(safe-area-inset-bottom))`, not a flat --space-3 — on
     an iPhone with a 34px home-indicator inset the real bar is 95px
     (12+34+48+1), not 73, and the last 22px of .footer-fine stayed
     unreachable. This expression mirrors the bar's own three padding/border
     terms exactly, term for term — accurate as long as the bar's own
     button stays one line. Task 3 correction: below 382px the sticky-cta's
     primary-button label can wrap to two lines, and the button's real
     height then exceeds the 3rem this expression hardcodes, so the two
     expressions are NOT guaranteed to stay in sync at every width — a
     known, narrow-viewport exception (this task did not re-verify or fix
     it; the original claim that they "can never drift apart" was false).
     Reset to 0 from md below, where the bar itself is display: none. */
  padding-block-end: calc(
    var(--space-3) + max(var(--space-3), env(safe-area-inset-bottom)) + 3rem + var(--border-width)
  );
}
@media (min-width: 900px) {
  body { padding-block-end: 0; }
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd, ul, ol {
  margin: 0;
}
ul[class], ol[class] { list-style: none; padding: 0; }

img, picture, video, canvas, svg { display: block; max-width: 100%; }
img { height: auto; }

input, button, textarea, select { font: inherit; color: inherit; }

a { color: inherit; }

/* One focus treatment for the whole site. :focus-visible only, so a mouse
   click never draws a ring but every keyboard path does. */
:focus-visible {
  outline: 3px solid var(--focus, currentColor);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  /* Round-1 I-11: this had no colour plan of its own — the comp only ever
     survived because it also carries .btn.btn--primary, which supplies
     both. A bare .skip-link from Task 3's generator (no .btn class) would
     focus into near-black --ink text with no background at all, over
     whatever photograph sits under it. Explicit paper ground + ink text,
     independent of whatever else the element is classed as. */
  background: var(--paper, #FFFFFF);
  color: var(--ink, #0E2030);
  /* This is the site's FIRST keyboard stop and it overlays the navy-900
     header when focused, but sits outside every navy scope — the root
     --focus (--signal-deep) resolves to 2.04:1 on navy-900 and fails the
     3:1 floor. Override locally rather than relying on an ancestor scope. */
  --focus: var(--signal-bright);
  transform: translateY(-200%);
  transition: transform var(--transition);
}
.skip-link:focus-visible { transform: translateY(0); }

/* ==========================================================================
   Layout primitives
   Seven of them (container, stack, cluster, grid, split, media, bleed).
   Every layout on the site composes siblings with one of these, or two
   nested. A component may still use flex/grid for its own internal anatomy
   (DESIGN.md §5's amendment, 2026-08-24) — never to lay out page content.
   Every selector permitted to do that is enumerated in
   docs/.audit/layout-allowlist.txt and enforced by check-layout.mjs's
   layout-outside-allowlist rule.
   ========================================================================== */

/* --- container: the one horizontal measure ------------------------------- */
.container {
  width: min(100% - (var(--gutter) * 2), var(--container));
  margin-inline: auto;
}
.container--wide { --container: var(--container-wide); }
.container--text { --container: 46rem; }

/* --- stack: vertical flow. The only thing that puts space between siblings.
   Owl selector so the first child never gets a leading margin — the single
   most common cause of a "why is there a gap at the top" inline override. */
/* The [hidden] attribute must always win: primitives like .stack and
   .day-tabs declare their own display, and an author display beats the UA
   stylesheet's [hidden] { display: none } — which silently un-hides the
   schedule's collapsed day panels. One global guard, by design. */
[hidden] { display: none !important; }

.stack { display: flow-root; }
.stack > * + * { margin-block-start: var(--stack, var(--space-4)); }
.stack--tight  { --stack: var(--space-2); }
.stack--snug   { --stack: var(--space-3); }
.stack--loose  { --stack: var(--space-6); }
.stack--xloose { --stack: var(--space-8); }

/* --- cluster: a wrapping horizontal group. Buttons, tags, meta rows.
   Replaces every ad-hoc flex row in the old file — the two-button pair in
   the hero (ribs.html:66), the single primary CTA in the NSI band
   (ribs.html:106), the button pair beside the visit copy (ribs.html:402) —
   with one gap-based wrapping rule instead of hand-tuned margins per
   instance. Round-2 N-7: this used to cite "the header's own action row",
   which does not use .cluster — the header is `.site-header__inner >
   (.brand, nav.site-nav, a.btn.nav-cta)`, and those two are on the layout
   allowlist as chrome precisely *because* they are not clusters. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, var(--space-4));
}
.cluster--tight { --cluster-gap: var(--space-2); }
.cluster--wide  { --cluster-gap: var(--space-6); }
.cluster--between { justify-content: space-between; }

/* --- grid: the base is one column. `auto-fit` is banned (DESIGN.md §11) —
   it orphaned a seventh card on its own row at some in-between width.
   Every real usage carries a fixed-count modifier (below); the generator's
   `grid(modifier, items)` helper (Task 3) throws on a missing modifier, so
   this primitive is never used bare. */
.grid {
  display: grid;
  gap: var(--grid-gap, var(--space-5));
  grid-template-columns: 1fr;
}
.grid--tight { --grid-gap: var(--space-3); }
.grid--loose { --grid-gap: var(--space-7); }

/* Fixed-count grids.
   `auto-fit` is wrong for a set whose length you know, because the column
   count lands wherever the width happens to put it — a 6-item set went to
   four columns at 1425px and left two stranded on the second row, which is
   the same "orphaned card" defect as the old 3-across grid with 7 programs,
   just at a different width. Round-2 N-9: `.grid`'s one-column base is not
   *why* this primitive is never used bare — that guarantee is the comment
   above it (the generator's `grid(modifier, items)` helper throwing on a
   missing modifier). One column is the safe fallback *if it ever were*
   used bare regardless — the opposite causal direction from what this
   comment used to claim.

   For a known set, pick counts that DIVIDE it. Every step is min-width, so
   they stack additively and cannot collide.

     --1-2-3  six items  ->  6 / 3 / 2 rows
     --1-3    three items ->  stacked, then one row
     --2-5    ten items  ->  5 / 2 rows   (3 is deliberately skipped: it would
                              leave 3+3+3+1 and strand the tenth) */
.grid--1-2-3 { grid-template-columns: 1fr; }
.grid--1-3   { grid-template-columns: 1fr; }
.grid--1-2   { grid-template-columns: 1fr; }

/* Mobile length, not mobile stacking (rollout plan Task 3 Step 6: home <= 8,000
   at 390). One column means four full 3:2 programme photographs cost ~1,000px
   on their own. The two second-tier cards letterbox; the two primaries keep
   their full frame, because those are the two we want chosen. Mobile-first --
   DESIGN.md §5 bans max-width queries. */
.card--tier2 .media { --ratio: 21 / 9; }
#visit .media--photo { --ratio: 21 / 9; }
.grid--2-4   { grid-template-columns: repeat(2, 1fr); }
.grid--2-5   { grid-template-columns: repeat(2, 1fr); }

/* Sean, 2026-09-03: the seven non-featured coaches on /coaches wrap
   CENTERED instead of stranding a left-aligned orphan row (eleven does not
   divide by five). Sizing mirrors .grid--2-5's columns at its breakpoint;
   the gap is the same token the grid uses. */
.roster--flow {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--grid-gap, var(--space-5));
}
.roster--flow > .roster__item {
  flex: 0 0 calc((100% - var(--grid-gap, var(--space-5))) / 2);
}

@media (min-width: 600px) {
  .grid--1-2-3 { grid-template-columns: repeat(2, 1fr); }
  .grid--1-2   { grid-template-columns: repeat(2, 1fr); }
  .card--tier2 .media { --ratio: 3 / 2; }
  #visit .media--photo { --ratio: 3 / 2; }
}
@media (min-width: 900px) {
  .grid--1-3 { grid-template-columns: repeat(3, 1fr); }
  .grid--2-4 { grid-template-columns: repeat(4, 1fr); }
  .grid--2-5 { grid-template-columns: repeat(5, 1fr); }
  .roster--flow > .roster__item {
    flex: 0 0 calc((100% - 4 * var(--grid-gap, var(--space-5))) / 5);
  }
}
@media (min-width: 1280px) {
  .grid--1-2-3 { grid-template-columns: repeat(3, 1fr); }
}

/* --- split: two columns that share one grid, so the text and the image are
   actually aligned to each other. The old hero had the copy in one block and
   the photo in another with no shared row, which is why they floated at
   unrelated vertical positions. */
.split {
  display: grid;
  gap: var(--space-6);
  align-items: var(--split-align, start);
}
@media (min-width: 900px) {
  .split {
    grid-template-columns: var(--split, 1fr 1fr);
    gap: var(--space-8);
  }
  .split--media-first > :first-child { order: 2; }
  .split--wide-start { --split: 1.25fr 1fr; }
  .split--wide-end   { --split: 1fr 1.25fr; }
  .split--sidebar    { --split: 1fr 20rem; }
  .split--center     { --split-align: center; }
}

/* --- media: an aspect-locked image frame.
   Replaces five different inline `aspect-ratio + object-fit + border-radius`
   declarations. The background tone is the photo's own dominant colour
   (from build-images.mjs), so a slow load shows the image's tone rather than
   a grey hole punched in the layout. */
.media {
  position: relative;
  overflow: hidden;
  /* Guard. An aspect-ratio box whose height is constrained by its parent
     derives its WIDTH from that height and can exceed the container. This
     caps the class so the failure can never reach a horizontal scrollbar. */
  max-inline-size: 100%;
  border-radius: var(--radius-lg);
  background: var(--navy-800);
  aspect-ratio: var(--ratio, 16 / 9);
}
/* `display: contents` on <picture> so the <img> participates in .media's own
   box. Without it `.media > img` matches nothing — <picture> is the real
   child — and every responsive image renders at intrinsic height inside an
   aspect-locked frame with dead space beneath it. */
.media picture { display: contents; }
.media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--media-pos, center);
}
.media--square    { --ratio: 1; }
.media--portrait  { --ratio: 3 / 4; }
.media--photo     { --ratio: 3 / 2; }
/* A 3:1 banner is 40px tall on a phone and shows nothing, so 16:10 — a ratio
   a person can actually see something in — is the mobile-first BASE for
   both wide bands. The real wide ratios only apply from sm up, once there
   is room for them. Mobile-first, min-width only (DESIGN.md §5): the old
   file expressed this the other way around, as a narrower-viewport-only
   override — this was the file's one remaining banned query shape
   (DESIGN.md §11). */
.media--band      { --ratio: 16 / 10; }
.media--banner    { --ratio: 16 / 10; }
@media (min-width: 600px) {
  .media--band   { --ratio: 2.4 / 1; }
  .media--banner { --ratio: 3 / 1; }
}

/* --- bleed: a full-viewport-width band from inside a contained flow.
   Uses the scrollbar-safe 100vw fallback so it never introduces a horizontal
   scrollbar, which is what `width: 100vw` alone does on Windows. */
.bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}
@supports (width: 100dvw) {
  .bleed { width: 100dvw; margin-inline: calc(50% - 50dvw); }
}

/* ==========================================================================
   Section rhythm
   The fix for 39 inline `padding-top: 0` overrides. A section owns its
   vertical padding; two adjacent sections of the SAME tone collapse the
   boundary between them automatically, so stacking never doubles the gap and
   the markup never has to say so.
   ========================================================================== */
.section {
  padding-block: var(--section-top, var(--section-y)) var(--section-bottom, var(--section-y));
}
/* Modifiers change the UNIT; adjacency changes the EDGE. Because they write to
   different custom properties, neither can out-specify the other — the class
   that used to lose this fight silently is what produced 39 hand-written
   `padding-top: 0` overrides in the markup. */
.section--tight { --section-y: var(--space-7); }
.section--flush { --section-y: 0; }
.section[data-tone="page"] + .section[data-tone="page"] { --section-top: 0; }

/* A band is a section that carries its own background across the full
   viewport width. Tone values are defined by the theme, not here. */
.section[data-tone="ink"],
.section[data-tone="accent"],
.section[data-tone="raised"] {
  /* themes set background/color; this is only the shared geometry */
  position: relative;
  isolation: isolate;
}

/* ==========================================================================
   Shared components
   Structure only — a theme decides what they look like.
   ========================================================================== */

/* --- prose: the one place body copy gets its measure and rhythm ---------- */
.prose { max-width: var(--measure); }
.prose > * + * { margin-block-start: var(--space-4); }
.prose h2, .prose h3 { margin-block-start: var(--space-7); }
.prose > :first-child { margin-block-start: 0; }

/* --- eyebrow: the small tracked label above a heading -------------------- */
.eyebrow {
  font-size: var(--text-xs);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 600;
}

/* --- button ------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  /* 48px minimum target on every button, everywhere. */
  min-height: 3rem;
  padding: var(--space-3) var(--space-6);
  border: var(--border-width) solid transparent;
  border-radius: var(--radius);
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  transition: background-color var(--transition), color var(--transition),
              border-color var(--transition), transform var(--transition);
}
.btn:active { transform: translateY(1px); }
.btn--lg { min-height: 3.5rem; padding-inline: var(--space-6); font-size: var(--text-md); }
.btn--block { width: 100%; }

/* --- card --------------------------------------------------------------- */
/* Cards are flex columns so that every card in a row is the same height and
   the action pins to the bottom edge. The old grid let each card size to its
   own copy, so the "Learn more" buttons sat at seven different heights. */
.card {
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-lg);
  overflow: hidden;
  transition: transform var(--transition), border-color var(--transition);
}
.card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  flex: 1;
}
.card__body > .btn,
.card__foot { margin-block-start: auto; }
.card__title { font-size: var(--text-lg); line-height: 1.2; }

/* --- site header -------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
}
/* Below 900px this row holds brand + nav-cta + nav-toggle at --space-3
   (12px), not --space-5 (24px). The I-4 wordmark-restore fix below put a
   fourth, wider brand back into this row starting at 430px, and
   --space-5 no longer fit once a real vertical scrollbar is counted:
   this page is several thousand px tall at every mandated width, so a
   non-overlay-scrollbar browser (Windows, most of Linux) always reserves
   ~15-17px of the nominal viewport width for it, and .site-header__inner
   never actually gets the full 430/600/etc. Measured live with the
   scrollbar counted (scripts/check-header.mjs — the gate this exact
   interaction was missing): 21px short at 430 with --space-5; --space-3
   clears it with room to spare. 390 and 600 keep the same margin they
   already had — this only tightens the range where the deficit was. */
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: 4.5rem;
}
.site-nav { display: none; }
@media (min-width: 900px) {
  /* Round-2 N-5: at exactly 900px five links + the nav-cta's own short
     label leave "Our Story" (the longest label) about 10px short of its
     single-line width, and a flex item with no explicit basis shrinks by
     wrapping its text rather than by overflowing — one link goes to two
     lines while scrollWidth stays clean, which is why this was easy to
     miss by checking only for a horizontal scrollbar. --space-5 recovered
     32px across the row's four gaps — enough on a viewport with no
     reserved scrollbar, which is what N-5's own measurement used.

     Correction, found the same way N-5 itself was: with a real scrollbar
     counted (see the comment above this block), --space-5 is short
     again — "Our Story" still wraps at BOTH 900 and 1100, not just the
     900 N-5 already knew about. --space-4 (the same token the outer gap
     below already uses, not a new one) recovers the extra 8px x 5 gaps =
     40px this needs. Verified with a real scrollbar present at every
     mandated width (390/430/600/900/1100/1440/2560) via
     scripts/check-header.mjs, which asserts both scrollWidth===clientWidth
     AND every .site-nav__link's rendered height, so a wrap that leaves
     scrollWidth clean (this exact failure) cannot pass silently again.

     I-3 fix: .site-header__inner's own outer gap (brand / site-nav /
     nav-cta) tightens to --space-4 starting here too. This is the row's
     OUTER gap, not .site-nav's internal one above — it never touched the
     "Our Story" wrap fix's own gap, so it carries none of that risk.

     I-3 also removed .site-nav's own 1100px gap bump to --space-6 (was a
     sibling @media (min-width: 1100px) block right here): at exactly
     1100px that extra --space-6 (32px vs 24px, across four gaps = +32px)
     was exactly what pushed .site-header__inner into a 17px scrollWidth
     overflow the moment .nav-cta's full label swaps in on the same
     breakpoint. The now-shared --space-4 is smaller still than the old
     --space-5 baseline, so this does not reintroduce that bug. */
  .site-nav { display: flex; align-items: center; gap: var(--space-4); }
  .site-header__inner { gap: var(--space-4); }
}
.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
  /* 44px hit target (DESIGN.md §3), same floor every other control on the
     site is held to. */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  /* .nav-toggle is now a <summary> (I-1, below) — suppress the native
     disclosure triangle. inline-flex above already overrides the UA
     stylesheet's `display: list-item`, which is what ::marker requires, so
     this mainly guards older/other engines' ::-webkit-details-marker. */
  list-style: none;
}
.nav-toggle::-webkit-details-marker { display: none; }
.nav-toggle__bar { display: block; width: 1.5rem; height: 2px; background: currentColor; }

/* --- mobile menu ---------------------------------------------------------
   I-1: the whole disclosure is now one native <details class="mobile-menu">
   with <summary class="nav-toggle"> as its toggle — DESIGN.md §3 permits
   <details> for chrome, and this is chrome. Previously .nav-toggle was a
   <button> wired to site.js's [data-nav-toggle]/[data-mobile-menu] click
   handler, which toggled a hand-managed `aria-expanded` and a plain
   <div class="mobile-menu">'s `.is-open` class — and the div was
   display:none by default, so with JS off there was no route to
   /pricing, /our-story, /faq or /coaches below 900px at all. The comment
   this replaced claimed the Programs sub-<details> "kept every link
   reachable with no JS" — false: that sub-disclosure lived INSIDE the
   outer display:none container, so it was exactly as unreachable as
   everything else in it. The browser now owns open/close, keyboard
   activation and the accessible expanded/collapsed state entirely; no
   [data-nav-toggle] markup, no site.js involvement for this control.

   .mobile-menu (the <details>) is a flex item of .site-header__inner,
   sized to its <summary> — same footprint .nav-toggle always had there.
   Its disclosure content (.mobile-menu__panel) is taken out of flow
   (position:absolute against .site-header, the nearest positioned
   ancestor — sticky counts) so the open panel renders full-width under
   the WHOLE header bar, not just under wherever the toggle sits in the
   row. Neither of these declares display:grid/flex/inline-flex, so
   neither needs a docs/.audit/layout-allowlist.txt entry; .nav-toggle's
   existing inline-flex (line above) already has one. */
@media (min-width: 900px) {
  .mobile-menu { display: none; }
}
.mobile-menu__panel {
  position: absolute;
  inset-inline: 0;
  top: 100%;
  background: var(--navy-900);
  color: #EAF2F6;
  border-block-start: var(--border-width) solid rgba(185, 206, 212, 0.24);
}
.mobile-menu nav > a:not(.btn),
.mobile-menu__group summary,
.mobile-menu__submenu a {
  display: block;
  min-height: 2.75rem;
  padding-block: var(--space-3);
  color: inherit;
  text-decoration: none;
  font-weight: 600;
}
.mobile-menu__group summary { cursor: pointer; list-style: none; }
.mobile-menu__group summary::-webkit-details-marker { display: none; }
.mobile-menu__submenu { padding-inline-start: var(--space-4); }
.mobile-menu .btn { margin-block: var(--space-3); }

/* --- site footer -------------------------------------------------------- */
/* Round-1 (Task 2 review) I-4: `.footer-grid` used to live here as its own
   grid declaration. Deleted — it computed byte-identically to
   `.grid.grid--1-3.grid--loose`, which makes it a re-implementation of a §5
   primitive laying out siblings (brand block, Visit, Call, Hours), not a
   chrome component's own anatomy. Round-2 N-1: deleting the CSS without
   also fixing the markup left `.footer-grid` an inert class stacking all
   four children as plain blocks — worse than the auto-fit orphan it
   replaced. `public/comps/ribs.html` now emits the brand block as a
   sibling above `<div class="grid grid--1-3 grid--loose">` instead — three
   children meet three columns, the orphan is structurally impossible
   rather than papered over, and the comp is the working reference Task 3's
   `footer()` copies. */
.site-footer { padding-block: var(--space-7) var(--space-7); }
@media (min-width: 600px) {
  .site-footer { padding-block: var(--space-9) var(--space-7); }
}

/* ==========================================================================
   Utilities — a deliberately short list. If a page needs something that is
   not here, the answer is a component, not a new utility.
   ========================================================================== */
.measure       { max-width: var(--measure); }
.measure-tight { max-width: var(--measure-tight); }
.text-center   { text-align: center; }
.flow-end      { margin-block-start: auto; }

/* ==========================================================================
   Components
   --------------------------------------------------------------------------
   Lifted from the design-comp stylesheet (public/assets/comp.css) now that
   Direction C ("Ribs") is the shipping direction — brand lockup, nav link,
   section-head, lede, feature-card, steps, roster, media--pending,
   quote-plate, footer bits, hero-ribs, fact band. Structurally unchanged
   from the comp except: the trust strip (the audit's I3,
   docs/DESIGN-AUDIT-2026-08-24.md:113 — the old site.css's `.trust-rail`, a
   sage-green random tonal break) was never lifted, and the two directions
   that lost the pick (A "Bearing", B "The Board") stay behind in comp.css,
   which Task 10 deletes along with the rest of the comp set.

   Round-2 N-6: this used to also list the comp-only review chrome
   (`.comp-flag`) as never lifted. It was — separately, and later, under
   "Comps-only review chrome" near the bottom of this file — once it turned
   out `public/comps/*.html` no longer links comp.css at all and needed
   `.comp-flag` styled from here or not at all. See that section for why.
   ========================================================================== */

/* --- brand lockup ------------------------------------------------------- */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-lg);
  letter-spacing: 0.02em;
  line-height: 1;
}
/* The real brand-guide star mark, an <img> at its native 101:150 aspect —
   height set, width auto. It was an inline currentColor <svg> square; no
   other rule targets .brand__mark path or relies on currentColor. */
.brand__mark { height: 2.1em; width: auto; flex: none; } /* 1.5em → 2.1em, Sean 2026-09-03: bigger mark */
/* Header-specific step up to 2.6em — Sean's second pass the same day: 2.1em
   still read too small in the bar. Footer keeps 2.1em. */
.site-header .brand__mark { height: 2.6em; }
.brand__word { text-transform: uppercase; }
.brand--footer { font-size: var(--text-md); }

/* Task 3: below 900px .site-header__inner now carries brand + nav-cta +
   a real 44px-wide .nav-toggle (previously ~0 width) — all three plus two
   row gaps no longer fit the container at 390 even with nav-cta's own
   tight treatment (measured live: 151 + 143 + 44 + 48 = 386 vs 342
   available). The wordmark is the one element with real slack: the icon
   mark alone still identifies the brand and links home, so it carries the
   row below 900, where nav-toggle vacates and the wordmark returns. The
   footer's brand (.brand--footer, no .site-header ancestor) is untouched.

   I-4: the restore only needs to wait for 390's own deficit to clear.
   Reviewer measured live overflow of 0 at 430/480/600/768 with
   .brand__word forced back to display:inline — 900 was never the real
   constraint, only the breakpoint this rule happened to reuse. Below 430
   the wordmark is unreachable above the fold at all (next appearance:
   the footer, ~y9,400) with only aria-label carrying the accessible
   name; 430 restores it everywhere it was never actually short on room.

   Correction: that 430/480/600/768 sweep didn't account for a real
   vertical scrollbar (see .site-header__inner's own comment above,
   further up this file) — with one counted, 430 itself regressed to a
   21px overflow the moment the wordmark returned. Fixed there, at
   .site-header__inner's gap, not by moving this breakpoint — the whole
   point of restoring it at 430 was the name being visible on every
   phone, and pushing the breakpoint out would have undone that. */
.site-header .brand__word { display: none; }
@media (min-width: 430px) {
  .site-header .brand__word { display: inline; }
}

.site-nav__link {
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.01em;
  /* Round-1 I-10: --space-2 (8px) measured 40px live on all five links —
     under DESIGN.md §3's 44px hit-target floor. --space-3 (12px) clears it
     comfortably (each link is a .site-nav flex item, so padding-block
     already governs its own box height — no display change needed here). */
  padding-block: var(--space-3);
  border-block-end: 2px solid transparent;
}
.site-nav__link:hover { border-block-end-color: var(--accent); }
/* The persistent state for the current page — not just :hover — per
   DESIGN.md §3's aria-current requirement. */
.site-nav__link[aria-current="page"] { border-block-end-color: var(--signal); }

/* The header action carries the site's whole conversion path, so it stays
   visible on a phone rather than hiding behind a menu. Short label by
   default, full label once there is genuinely room for it.

   Round-1 I-1 removed `white-space: nowrap` to fix a real horizontal-
   scrollbar bug at exactly md (900px) — .site-nav's five links and the full-
   label button both went flex/inline at the same 900px step and overflowed
   by 76px together. Round-2 N-4 found that fix regressed the PHONE case:
   below md, .nav-cta__full is display:none and only the SHORT label ("No
   Sweat Intro") renders — with nowrap gone, that short label itself now
   wraps to two lines at 360/375/390 (the three most common phone widths),
   which is worse than the 900px bug it replaced. N-5 additionally found the
   original "defer the swap to 1024" idea insufficient: .site-nav__link
   stays ragged (48/48/48/71/48px) all the way through 900-1024 inclusive,
   settling to a clean 48px everywhere only at 1100.

   Fix: restore `nowrap` (the short label never needs to wrap) and move the
   full-label swap itself out to 1100 — past the whole 900-1024 band where
   .site-nav's five links, the nav-cta and .nav-toggle's disappearance all
   land on the same 900px step with nothing to spare. Between 900 and 1099
   the header shows the short label, single line, comfortably. */
.nav-cta {
  /* Round-3 R3-C: nowrap (restored above, N-4) removed the only escape
     valve a wrapping label had left — below 390 the brand lockup, the
     header's own gap and the short label's own min-content width (nowrap
     forbids shrinking below it) sum past the container's own width, and a
     flex row with nothing left to shrink overflows instead of wrapping.
     Measured live: scrollWidth 388 at 360px, +28px past the viewport.
     Tightened padding + a smaller label size buy back exactly the room a
     360-414px phone is short, without touching text-wrap.
     Task 3 correction: the widen-at-390 swap below was tuned for a row
     with only brand + nav-cta in it. Task 3 gives .nav-toggle a real,
     44px hit-target size (DESIGN.md §3) — a THIRD sibling in the same
     flex row, below 900px, where it used to contribute ~0. With it
     present, brand + nav-cta(wide) + nav-toggle(44px) + two gaps overflows
     the container at 390 (measured live: site-header__inner scrollWidth
     412 vs clientWidth 342). The tight treatment now holds for the WHOLE
     range nav-toggle shares the row — below 900, where .nav-toggle goes
     display:none and nav-cta gets the row to itself again. */
  padding-inline: var(--space-2);
  font-size: var(--text-xs);
  white-space: nowrap;
}
.nav-cta__full { display: none; }
@media (min-width: 900px) {
  /* I-3: same padding-inline and font-size as the below-900 tight
     treatment — neither widens until 1100, where .nav-cta's full label
     needs the room anyway. Widening padding-inline alone at 900 (tried
     first) got site-header__inner's own overflow to zero, but only by
     starving .site-nav of the ~24px it needs for "Our Story" to stay on
     one line — .site-nav has no explicit basis, so flex shrinks it by
     wrapping text rather than by overflowing (this is the exact Round-2
     N-5 failure mode: scrollWidth reads clean while a link silently goes
     to two lines). Leaving .nav-cta's padding AND font-size at their
     below-900 values frees the room .site-nav actually needs. */
  .nav-cta { padding-inline: var(--space-2); }
}
@media (min-width: 1100px) {
  /* font-size now steps up here instead of at 900 (see the 900 block's
     comment) — .nav-cta__full's longer label needs the room anyway. */
  .nav-cta { padding-inline: var(--space-6); font-size: inherit; }
  .nav-cta__full { display: inline; }
  .nav-cta__short { display: none; }
}

/* --- section head: eyebrow + heading + lede, always the same rhythm ------ */
.section-head > * + * { margin-block-start: var(--space-3); }
.section-head h2 { font-size: var(--text-3xl); }
.section-head .lede { margin-block-start: var(--space-5); }

.lede {
  font-size: var(--text-md);
  line-height: 1.55;
  color: var(--ink-dim, inherit);
}
.section[data-tone="ink"] .lede { color: inherit; opacity: 0.86; }

/* --- feature card: the one program that carries a photograph ------------- */
.feature-card {
  display: grid;
  border: 1px solid var(--line);
  background: var(--surface);
  overflow: hidden;
}
.feature-card__media { --ratio: 16 / 10; border-radius: 0; }
.feature-card__body {
  padding: var(--space-6);
  align-self: center;
}
.feature-card__title { font-size: var(--text-2xl); }
@media (min-width: 900px) {
  .feature-card { grid-template-columns: 1.1fr 1fr; align-items: stretch; }
  .feature-card__media { --ratio: auto; height: 100%; }
  .feature-card__body { padding: var(--space-8); }
}

/* Two feature cards sharing a row are ~690px each at 1440, and the side-by-side
   split above then leaves the body column near 300px: the copy runs as a narrow
   ribbon and the button label breaks over three lines. A card in a 2-up row
   stacks instead -- media over body -- at every width. */
.feature-card--stacked { grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .feature-card--stacked { grid-template-columns: 1fr; }
  .feature-card--stacked .feature-card__media { --ratio: 16 / 10; height: auto; }
  .feature-card--stacked .feature-card__body { padding: var(--space-6); }
}

/* --- steps -------------------------------------------------------------- */
.steps { counter-reset: step; }
.step { position: relative; }
.step__n {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 800;
  line-height: 1;
  /* --signal-deep, not --accent/--signal: the rails sit on light bands and
     the brand orange is 2.22:1 on paper-2 — under even the 3:1 large-text
     floor. Same class of fix as .fact__n (2026-09-03). */
  color: var(--signal-deep);
}
.step__title { font-size: var(--text-lg); }

/* --- roster ------------------------------------------------------------- */
.roster__name { font-weight: 700; line-height: 1.2; }
.roster__role {
  font-family: var(--font-data);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* --- pending photo -----------------------------------------------------
   An honest gap. It must never be mistaken for a real photograph: no
   silhouette, no stock face, no blur. A monogram on the theme's own surface
   plus a plain label, so the absence reads as a decision. */
.media--pending {
  display: grid;
  place-items: center;
  gap: var(--space-1);
  background: var(--surface);
  border: 1px dashed var(--line-strong, var(--line));
  color: var(--ink-dim);
}
.media__pending-mark {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: 800;
  line-height: 1;
  opacity: 0.55;
}
.media__pending-note {
  font-family: var(--font-data);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.7;
}

/* Coach portraits use the existing brand mark while photographs are pending. */
.media--coach-pending {
  flex-direction: column;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  background: var(--navy-900);
  color: var(--paper);
}
.media--coach-pending .media__pending-logo {
  width: 44%;
  height: auto;
  object-fit: contain;
}
.media__pending-wordmark {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.media__pending-banner {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  padding: var(--space-2);
  background: var(--signal);
  color: var(--navy-900);
  font-family: var(--font-data);
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  text-transform: uppercase;
}

/* --- pull quote --------------------------------------------------------- */
.quote-plate {
  padding: var(--space-6);
  border: 1px solid var(--line);
  background: color-mix(in srgb, currentColor 6%, transparent);
}
.quote-plate blockquote {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  line-height: 1.3;
  text-wrap: pretty;
}
.quote-plate figcaption { opacity: 0.8; }

/* --- footer ------------------------------------------------------------- */
.footer-tag, .footer-dim { color: var(--ink-dim); font-size: var(--text-sm); }
.section[data-tone] .footer-dim,
.site-footer .footer-dim,
.site-footer .footer-tag { opacity: 0.75; color: inherit; }
.footer-fine { font-size: var(--text-xs); opacity: 0.6; line-height: 1.7; }

/* --- C. Ribs -----------------------------------------------------------
   A full-bleed steel band. The photograph fills it; a ribbed scrim sits over
   the photograph; the plate carrying the headline sits over the scrim on the
   container's grid, so the type is still on the same measure as every other
   section rather than floating free over the image. */
.hero-ribs { padding: 0; }
.hero-ribs__frame {
  position: relative;
  isolation: isolate;
  display: grid;
}
.hero-ribs__frame > * { grid-area: 1 / 1; }
/* Mobile first: the photograph sets the band's height from its own ratio.
   Giving it BOTH an aspect-ratio and height:100% makes the browser derive the
   WIDTH from the frame's height — which blew the page out to 828px inside a
   375px viewport. Ratio or stretch, never both.

   4/3 -> 3/2 (Sean, 2026-09-02: "hero is a bit large"). The value is changed
   HERE, where --ratio is owned, and not in an appended block at the end of
   the file: an appended `.hero-ribs__media { --ratio: 3/2 }` has the same
   specificity as the @media rule below and wins on source order, which gave
   the media a ratio AND height:100% at every desktop width — the pairing
   this comment forbids — and made the home hero 950px tall in a 900px
   viewport, 18.5% TALLER than before the fix and the opposite of the ask.
   Any future --ratio on this class has to be neutralised in that block too;
   the block lists every selector that sets one, for exactly that reason. */
.hero-ribs__media { --ratio: 3 / 2; height: auto; border-radius: 0; }
.hero-ribs__scrim {
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--navy-900) 40%, transparent) 0%,
    color-mix(in srgb, var(--navy-900) 92%, transparent) 62%
  );
}
.hero-ribs__inner {
  position: relative;
  z-index: 1;
  align-self: end;
  padding-block: var(--space-6) var(--space-7);
}
.hero-ribs__plate { max-width: 46rem; }

.hero-ribs__plate .lede { color: inherit; opacity: 0.9; }

@media (min-width: 900px) {
  /* The band's height is its PLATE's height, floored by a design minimum.
     The photograph does not get a vote.

     Two rounds were spent learning that. First the media carried a ratio and
     height:100% at once and derived the band's height from its own width —
     950px at 1440, 1697px at 2560, 170% of the viewport, with every
     min-height inert. Then a max-block-size was added to bound it, and on
     /personal-training, whose plate needs 608px, the 34rem cap bounded only
     the container's border box: the grid row still sized to the plate, the
     photograph and scrim painted 64px past the frame, and they buried the
     `hr.beam` that closes the band. A cap that can sit below its own content
     is not a bound, it is a clipping hazard, and /olympic-weightlifting was
     37px of lede from the same break.

     So the media leaves the flow entirely. Absolutely positioned to the
     frame's four edges, it fills whatever height the plate asks for and
     contributes nothing to the row's intrinsic size. There is now no value
     to tune, no cap to keep above a content height nobody measured, and no
     arrangement of ratio and stretch that can make the photograph grow the
     band — which is the whole class of defect, closed structurally rather
     than by picking a better number. `.hero-ribs__frame` is already
     `position: relative`.

     What bounds the band now is the plate, which is bounded by its own copy.
     Measured across all nine pages carrying this frame, at seven widths:
     nothing overhangs, nothing clips. scripts/check-header.mjs asserts both
     on every one of them, which is the gate whose absence let the 170% hero
     and the 64px overhang each ship green. */
  .hero-ribs__frame { min-height: min(66svh, 38rem); }
  .hero-ribs__media {
    position: absolute;
    inset: 0;
    --ratio: auto;
    height: 100%;
  }
  /* --space-9 -> --space-8: 64px off the band, and 64px the headline was not
     using. This padding is now half of what sets the band's height, so it is
     load-bearing — see the note above. */
  .hero-ribs__inner { align-self: center; padding-block: var(--space-8); }
  .hero-ribs__scrim {
    background: linear-gradient(
      100deg,
      color-mix(in srgb, var(--navy-900) 94%, transparent) 0%,
      color-mix(in srgb, var(--navy-900) 88%, transparent) 38%,
      color-mix(in srgb, var(--navy-900) 55%, transparent) 68%,
      color-mix(in srgb, var(--navy-900) 22%, transparent) 100%
    );
  }
}

/* --- fact band (direction C) -------------------------------------------
   Four flat facts under the hero, in the F45 register. The unit rides inside
   the number at a smaller size so "20min" reads as one quantity rather than
   as a number with a word stuck to it. */
.fact { display: flex; flex-direction: column; gap: var(--space-2); }
.fact__unit {
  font-size: 0.4em;
  letter-spacing: 0;
  margin-inline-start: 0.1em;
  /* Sits on the number's baseline rather than floating at cap height, so
     "20min" reads as one quantity. */
  vertical-align: baseline;
}
.facts__claims { color: var(--ink-dim); }

/* ==========================================================================
   New components — direction C rollout, Task 2
   Not yet wired into any shipping page (public/*.html still link the old
   site.css until Task 3+ migrates them); defined here so the generator has
   real classes to reach for. Each one's own display: grid/flex is internal
   anatomy (DESIGN.md §5's amendment), never page composition — see
   docs/.audit/layout-allowlist.txt for the justification of each.
   ========================================================================== */

/* --- hero--type: a photo-less hero for a page with no imagery yet --------
   Reuses .hero-ribs__plate as-is (eyebrow + h1 + lede + actions), staged on
   a plain navy .ribbed--dark ground instead of a photograph + scrim. An
   honest "no photo yet" band — never a placeholder that reads as a real one
   (DESIGN.md §8). Vertically centers its own single plate child, the same
   division of labour .hero-ribs__frame already has for the photo variant. */
.hero--type {
  display: flex;
  align-items: center;
  min-height: 52vh;
  /* The plate must never touch the band's edges when its content is taller
     than the 52vh floor (a 390px viewport with a long lede). */
  padding-block: var(--space-8);
}

/* --- sticky-cta: a fixed mobile-only booking bar --------------------------
   Below md the header's own nav-cta scrolls out of reach; this keeps the
   one primary action always in hand. Removed entirely from md up, where the
   header CTA already does the job.

   Round-1 (Task 2 review) C-B/I-2/I-9: this is a THIRD navy ground —
   alongside .section[data-tone="ink"] and .site-footer — and it shipped
   with no text-colour plan of its own, so anything inside it that relies on
   inherited colour or --ink-dim fell through to body's near-black --ink,
   invisible on navy-900 (.btn--ghost measured 1.02:1). color/--ink-dim/
   --line/--surface here mirror .site-footer's own set exactly, so anything
   placed inside either surface is correct via ordinary inheritance without
   needing its own hand-written override. */
.sticky-cta {
  position: fixed;
  inset-block-end: 0;
  inset-inline: 0;
  z-index: 60;
  display: flex;
  gap: var(--space-3);
  padding-inline: var(--gutter);
  padding-block: var(--space-3);
  padding-bottom: max(var(--space-3), env(safe-area-inset-bottom));
  background: var(--navy-900);
  color: #EAF2F6;
  --ink-dim: var(--steel-200);
  --line: rgba(185, 206, 212, 0.28);
  --surface: var(--navy-800);
  --focus: var(--signal-bright);   /* 11.93:1 on navy-900 */
  --btn-primary-border: var(--signal);   /* keep the on-navy .btn--primary
     border orange, not the light-ground --navy-700 default. */
  border-block-start: var(--border-width) solid rgba(185, 206, 212, 0.24);
}
.sticky-cta .btn--primary { flex: 1; }
@media (min-width: 900px) {
  .sticky-cta { display: none; }
}

/* Round-2 N-10: body's own clearance for this bar (the 900px reset pairs
   correctly with the display:none above) lives with body's other rules near
   the top of the file, not as a third mid-file body {} block — see the
   comment there for the derivation. */

/* --- day-tabs: a segmented day-of-week control for the schedule page -----
   Without JS every day still renders in the markup below it (DESIGN.md
   §7) — this only ever changes which day looks current, it never gates
   content. The tab strip is the control's own anatomy. */
.day-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}
.day-tabs__tab {
  display: inline-block;
  min-width: 3.25rem;
  height: 2.75rem;   /* 44px — DESIGN.md §3's hit-target floor */
  line-height: 2.75rem;
  padding-inline: var(--space-3);
  border: var(--border-width) solid var(--line);
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-data);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.day-tabs__tab[aria-selected="true"] {
  background: var(--navy-700);
  border-color: var(--navy-700);
  color: #fff;
}

/* --- timetable: schedule rows, time · class · coach -----------------------
   Stacked (one column) below sm; a fixed three-column grid from sm up is
   the row's own anatomy, not page composition — the list of rows is laid
   out by .stack in the markup. Azeret Mono with tabular numerals for the
   time column. */
.timetable {
  border-block-start: var(--border-width) solid var(--line);
}
.timetable__row {
  display: grid;
  gap: var(--space-1);
  padding-block: var(--space-3);
  border-block-end: var(--border-width) solid var(--line);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}
@media (min-width: 600px) {
  .timetable__row {
    grid-template-columns: 7.5rem 1fr 8rem;
    align-items: center;
    gap: var(--space-4);
  }
}

/* A muted timetable row: the schedule's No Sweat Intro slots are
   one-on-one sit-downs, not classes — dimmed so the class rows lead. */
.timetable__row--dim { color: var(--ink-dim); }

/* Compact variant for the schedule's 43 slot rows: below sm the stacked
   three-line row costs ~100px a slot and blew the page past its mobile
   budget — two columns (time spanning, kind over coach) halve it. From sm
   the standard three-column row returns. No display declaration here; the
   grid itself comes from .timetable__row. */
.timetable--compact .timetable__row {
  grid-template-columns: auto 1fr;
  column-gap: var(--space-4);
}
.timetable--compact .timetable__row > :first-child { grid-row: span 2; }
@media (min-width: 600px) {
  .timetable--compact .timetable__row { grid-template-columns: 8.5rem 1fr 8rem; }
  .timetable--compact .timetable__row > :first-child { grid-row: auto; }
}

/* --- qa: a two-column Q&A list, question beside its answer ----------------
   Sean's FAQ feedback: "Theres a big gap between the questions and the
   answers."

   The diagnosis was that an h3 is a block, so under the old fixed
   `1fr 1.4fr` split every question box stretched to fill its ~500px track
   however short the question ran, and the visible gap was that dead strip
   plus the 32px column gap on top of it. That much is right, and it rules
   out both a fixed fraction and a track with a finite growth limit
   (`minmax(auto, 55rem)` measured 880px on EVERY row, short questions
   included — a track with a growth limit takes free space whenever the row
   has any to give).

   What content-sized tracks alone cannot fix is that .qa__item was a
   SEPARATE grid per pair. Sizing each one to its own question made every
   answer start somewhere different: measured across the fourteen pairs at
   1440, the answer's left edge ranged from 443px to 997px and its width
   from 890px down to 336px. Fourteen ragged rows is not less distracting
   than one wide channel, it is more.

   So the LIST is the grid, and .qa__item hands its two children up to it
   with `display: contents`. One pair of tracks, shared by every row: the
   question column sized once, every answer starting at the same x, every
   answer the same width. The answer track is capped at --measure rather
   than 1fr so a 1240px container does not set 60-word answers at 110
   characters a line — the leftover sits as margin at the right edge, which
   reads as air, not as a channel between two things that belong together.

   Below 900px there is one column and .qa__item is its own small grid
   again, as it always was. */
.qa__item {
  display: grid;
  gap: var(--space-3);
}
@media (min-width: 900px) {
  .qa {
    display: grid;
    grid-template-columns: minmax(0, 24rem) minmax(0, var(--measure));
    /* Row gap is deliberately larger than the column gap: the column gap
       separates a question from ITS answer and must read as the smaller
       distance of the two, or the pairing inverts and each answer looks
       attached to the question below it. */
    column-gap: var(--space-5);
    row-gap: var(--space-7);
    align-items: start;
  }
  .qa__item { display: contents; }
}

/* --- timeline: the Our Story spine -----------------------------------
   Lifted structure from the old site.css (a border spine + item markers),
   re-expressed on the space scale in place of hardcoded px, and squared
   off per DESIGN.md's radius-0 rule (the old marker was a circle). No
   grid/flex here — item spacing comes from .stack in the markup; this class
   owns only the spine and each item's marker. */
.timeline {
  border-inline-start: calc(var(--border-width) * 2) solid var(--line);
  max-width: var(--measure);
}
.timeline__item {
  position: relative;
  padding-inline-start: var(--space-5);
}
.timeline__item::before {
  content: "";
  position: absolute;
  inset-inline-start: calc(var(--space-2) / -2 - var(--border-width));
  top: 0.3em;
  width: var(--space-2);
  height: var(--space-2);
  background: var(--signal);
}
.timeline__year {
  display: block;
  font-family: var(--font-display);
  font-variation-settings: "wdth" 100, "wght" 700;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: var(--text-sm);
  color: var(--signal-deep);
  margin-block-end: 0.3em;
}

/* ==========================================================================
   Theme — direction C rollout, Task 2
   All of theme-ribs.css, lifted as the shipping theme now that direction C
   is chosen. Two accessibility corrections applied per DESIGN.md §2/§3
   (C-7) — see the inline comments at --signal-bright and .btn--primary.
   ========================================================================== */

/* ==========================================================================
   Direction C — "Ribs", revised 2026-08-21
   --------------------------------------------------------------------------
   Revision brief: C was closest but not there. Pull harder on the client's own
   current site, on F45, and on HWPO. All three were measured in the browser
   rather than recalled:

     the client's current site  navy #173F5B + white, black-scrimmed full-bleed photo
                           heroes, Outfit 900 UPPERCASE at line-height 1.0 with
                           -0.02em tracking. Inter for body.
     f45training.com       committed deep ground (#211551), one hot accent
                           (#D6001C), Gotham Condensed 700 uppercase at 80px /
                           line-height 1.0, short stacked two-line headlines.
     hwpotraining.com      square corners (radius 0), warm accent (#935D38),
                           near-black #1C1C1C scrims at 0.75 over athletic
                           photography, uppercase small headings with WIDE
                           POSITIVE tracking.

   What changed from the first pass, and why:

   1. THE GROUND IS THE CLIENT'S NAVY, NOT NEUTRAL STEEL. Every reference
      commits its ground to a saturated colour — navy, indigo, sand. The
      galvanised grey was the weakest thing in the comp: cold, and not theirs.
      #173F5B is the hex their site actually runs on today.
   2. THE ACCENT IS HOTTER. The muted amber was invented ("work light"). The
      brand audit's real finding was that their live site has NO warm accent at
      all — so the accent should supply actual heat, at F45's intensity rather
      than HWPO's muted bronze.
   3. THE DISPLAY TYPE IS CONDENSED, HEAVY AND UPPERCASE AT LINE-HEIGHT 0.92.
      This is the one move all three references share and the first pass missed
      entirely by setting the headline in sentence case at normal width.
   4. BODY IS INTER — the client's own body face, kept for continuity.
   5. The rib texture survives but is subordinate. It was the concept's hook,
      not the client's; it now reads as material under the navy rather than as
      the thing carrying the design.
   ========================================================================== */

:root {
  /* The client's live palette, used as theirs. */
  --navy-900: #0A1E2E;   /* deepest — header, footer */
  --navy-800: #102B40;
  --navy-700: #173F5B;   /* THE brand navy, straight off their site */
  --navy-600: #1F5177;
  --steel-300: #8EADB4;  /* their declared --secondary, unchanged */
  --steel-200: #B9CED4;

  --paper: #FFFFFF;
  --paper-2: #F1F4F6;    /* cool near-white — deliberately not a cream */
  --ink: #0E2030;        /* navy-tinted near-black, never pure #000 */
  --ink-dim: #55707F;

  /* The client's own brand guide (received 2026-09-03): navy #173F5B and
     slate #8EADB4 were already --navy-700 and --steel-300 — the accents
     below complete it. The star mark's own gradient runs
     #FFD369 → #F79658 → #91281E, so the accent system is literally the
     star's colors, not an invented "warm accent". The 2026-08 interim
     signal orange (F45-derived heat, chosen before the brand guide
     existed) and its two derived shades are retired below — their old
     values are recorded in DESIGN.md's dated note, not repeated here. */
  --signal: #F79658;          /* Pantone 157C. Graphic accent — but it also
     passes as TEXT on every navy: 7.64:1 on navy-900, 6.56 on navy-800,
     4.98 on navy-700. On white it is 2.22:1, so it is never text-bearing
     on light grounds, and (new) no longer the focus ring there. */
  --signal-deep: #91281E;     /* Pantone 7623C. Text-bearing accent on
     LIGHT grounds — 8.31:1 on white, 7.53 on paper-2. No longer the
     primary-button fill; never on navy (1.33-2.04:1 across the navys). */
  --signal-bright: #FFD369;   /* Pantone 134C. On-navy text accent —
     11.93:1 on navy-900, 10.25 on navy-800, 7.78 on navy-700. */

  --bg: var(--paper);
  --surface: var(--paper);
  --line: #D3DCE1;
  --line-strong: var(--navy-700);
  --accent: var(--signal);
  --focus: var(--signal-deep);   /* 8.31:1 on white ≥ 3:1. Navy-ground
     scopes override to --signal-bright (≥7.78:1) — see
     .section[data-tone="ink"], .site-header, .site-footer, .sticky-cta. */

  /* Square. HWPO ships radius 0 and it is the more disciplined choice than
     F45's pills. */
  --radius: 0px;
  --radius-lg: 0px;

  /* The bay pitch for the rib texture. */
  --rib: 14px;

  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-data: "Azeret Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
}

/* ---------- Display type ------------------------------------------------
   Archivo is variable on both width and weight. wdth 84 + wght 800 lands very
   close to the Gotham Condensed 700 that F45 sets its 80px headlines in, and
   to the Outfit 900 the client sets theirs in — without loading a third
   family. Uppercase, line-height 0.92, tracking pulled tight: at this weight
   and size the counters close up and the line needs the negative tracking to
   stop looking loose. */
h1, h2, h3 {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 84, "wght" 800;
  font-weight: 800;
  text-transform: uppercase;
  line-height: 0.92;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
h1 { font-size: clamp(2.75rem, 1.9rem + 3.8vw, 5rem); }
h2 { font-size: clamp(2.25rem, 1.6rem + 2.8vw, 4rem); }
h3 { font-size: var(--text-xl); }

.card__title {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 90, "wght" 700;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: -0.005em;
  font-size: var(--text-lg);
  line-height: 1;
}

/* HWPO's small-heading move: uppercase display with WIDE POSITIVE tracking.
   The first pass used a mono eyebrow, which read technical rather than
   athletic. Mono is kept below for things that are genuinely numeric. */
.eyebrow {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 100, "wght" 700;
  font-weight: 700;
  font-size: var(--text-xs);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--signal-deep);
}
/* Round-1 C-B: all three navy grounds, not just .section[data-tone="ink"] —
   .site-footer's VISIT/CALL/HOURS eyebrows measured 3.27:1 on --signal-deep
   before this (need 4.5 at 13px); --signal-bright measures 8.16:1 on
   navy-900. Every other fixed (non-inherited) ink-ground colour override
   below is hoisted onto the same three-surface list, for the same reason. */
.section[data-tone="ink"] .eyebrow,
.site-footer .eyebrow,
.sticky-cta .eyebrow { color: var(--signal-bright); }

/* Round-1 I-9: .timeline__year (DESIGN.md §7 puts it on Our Story) had NO
   ink-ground variant at all — --signal-deep measures 2.13:1 on navy-700.
   Nothing renders it yet, which is exactly why it was a trap for whichever
   task wires it in; closed here rather than left live. Same three-surface
   list as every other fixed-colour ink override on this page. */
.section[data-tone="ink"] .timeline__year,
.site-footer .timeline__year,
.sticky-cta .timeline__year { color: var(--signal-bright); }

/* ---------- Materials ---------------------------------------------------- */

/* Corrugated rib, drawn not photographed. Subordinate now — it sits under the
   navy as texture rather than carrying the design. */
.ribbed { position: relative; isolation: isolate; }
.ribbed::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: repeating-linear-gradient(
    90deg,
    rgba(10, 30, 46, 0.045) 0 1px,
    transparent 1px calc(var(--rib) - 1px),
    rgba(255, 255, 255, 0.5) calc(var(--rib) - 1px) var(--rib)
  );
}
.section[data-tone="ink"] .ribbed::before,
.ribbed--dark::before {
  background-image: repeating-linear-gradient(
    90deg,
    rgba(0, 0, 0, 0.28) 0 1px,
    transparent 1px calc(var(--rib) - 1px),
    rgba(255, 255, 255, 0.05) calc(var(--rib) - 1px) var(--rib)
  );
}

/* A hard structural edge. Signal orange rather than the old red-oxide — this
   is the line that caps a band, and it should carry the brand's heat. */
.beam { height: var(--space-1); background: var(--signal); border: 0; }
.rule { height: 1px; background: var(--line); border: 0; }

/* ---------- Tones -------------------------------------------------------- */
.section[data-tone="page"] { background: var(--paper); }
.section[data-tone="raised"] { background: var(--paper-2); }
.section[data-tone="ink"] {
  background: var(--navy-700);
  color: #EAF2F6;
  --ink-dim: var(--steel-200);
  --line: rgba(185, 206, 212, 0.28);
  --surface: var(--navy-800);
  --focus: var(--signal-bright);   /* root --focus (--signal-deep) is
     1.33-2.04:1 across the navys and fails; --signal-bright is 7.78:1 on
     navy-700. */
  --btn-primary-border: var(--signal);   /* keep the on-navy .btn--primary
     border orange, not the light-ground --navy-700 default. */
}
.section[data-tone="ink"] .rule { background: rgba(185, 206, 212, 0.28); }
.section[data-tone="ink"] .card { background: var(--navy-800); border-color: rgba(185, 206, 212, 0.24); }

/* ---------- Controls ----------------------------------------------------- */
.btn {
  font-family: var(--font-display);
  font-variation-settings: "wdth" 100, "wght" 700;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.btn--primary {
  /* Fill is --signal, ink is navy-900 (DESIGN.md §3, brand guide 2026-09-03):
     white on --signal (#F79658) is 2.22:1 and fails; navy-900 on --signal is
     7.64:1 — the brand sign's own orange-on-navy, inverted. --signal-deep is
     no longer the primary-button fill; it is the text-bearing accent for
     LIGHT grounds instead (see the --signal-deep token comment above). */
  background: var(--signal);
  color: var(--navy-900);
  /* Border was --signal on --signal — 2.22:1 on white, ~2.0 on paper-2,
     under the 3:1 UI-component floor on light grounds. --navy-700 is
     8.59:1 on white and wins by default; every navy-ground scope
     (.section[data-tone="ink"], .site-header, .site-footer, .sticky-cta)
     sets --btn-primary-border back to --signal so on-navy buttons keep
     the clean orange block. */
  border-color: var(--btn-primary-border, var(--navy-700));
}
.btn--primary:hover {
  /* Same structural darken-on-hover as before; color-mix keeps it derived
     from --signal rather than a new hand-picked hex. Ink stays navy-900
     (inherited from the base rule — the mix never gets dark enough to need
     a lighter label). */
  background: color-mix(in srgb, var(--signal) 85%, var(--navy-900));
  border-color: color-mix(in srgb, var(--signal) 85%, var(--navy-900));
}
.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--navy-700);
  border-width: 2px;
}
.btn--ghost:hover { background: var(--navy-700); color: #fff; }
/* Round-1 I-2: .sticky-cta .btn--ghost measured 1.02:1 (label) and 1.53:1
   (border, UI floor 3:1) before this — invisible. Same three-surface hoist
   as C-B above. */
.section[data-tone="ink"] .btn--ghost,
.site-footer .btn--ghost,
.sticky-cta .btn--ghost { color: #EAF2F6; border-color: rgba(234, 242, 246, 0.5); }
.section[data-tone="ink"] .btn--ghost:hover,
.site-footer .btn--ghost:hover,
.sticky-cta .btn--ghost:hover { background: #EAF2F6; color: var(--navy-700); border-color: #EAF2F6; }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-block-start: var(--space-1) solid var(--navy-700);
}
.card:hover { border-block-start-color: var(--signal); }

/* ---------- Chrome ------------------------------------------------------- */
.site-header {
  background: var(--navy-900);
  color: #EAF2F6;
  border-block-end: 3px solid var(--signal);
  /* --signal-bright is 11.93:1 on navy-900; root --focus (--signal-deep) is
     2.04:1 here and fails. Inherited by .mobile-menu__panel, .site-nav__menu
     and every other descendant scope — no separate override needed there. */
  --focus: var(--signal-bright);
  --btn-primary-border: var(--signal);   /* keep the on-navy .btn--primary
     border orange, not the light-ground --navy-700 default. */
}
/* --line/--surface added (round-1 I-9/C-B): the footer already set
   color/--ink-dim but not these two — anything using var(--line) or
   var(--surface) inside the footer (e.g. .rule, .card) was falling through
   to the light-ground defaults. Matches .sticky-cta's own full set. */
.site-footer {
  background: var(--navy-900);
  color: #EAF2F6;
  --ink-dim: var(--steel-200);
  --line: rgba(185, 206, 212, 0.28);
  --surface: var(--navy-800);
  --focus: var(--signal-bright);   /* 11.93:1 on navy-900 */
  --btn-primary-border: var(--signal);   /* keep the on-navy .btn--primary
     border orange, not the light-ground --navy-700 default. */
}

a.link { color: var(--signal-deep); text-underline-offset: 3px; font-weight: 600; }
.section[data-tone="ink"] a.link,
.site-footer a.link,
.sticky-cta a.link { color: var(--signal-bright); }

/* ---------- The fact band ------------------------------------------------
   F45's "WHY F45 WORKS" move: a small set of large, flat facts stated in the
   display face. Every number here is true and checkable — 2018, the included
   monthly 1:1, the twenty-minute No Sweat Intro, the seven programs. No
   invented statistics, which is the failure mode this pattern invites. */
.facts { border-block: 1px solid var(--line); }
.fact__n {
  display: block;
  font-family: var(--font-display);
  font-variation-settings: "wdth" 84, "wght" 800;
  font-size: clamp(2.75rem, 1.8rem + 3.4vw, 4.5rem);
  line-height: 0.9;
  letter-spacing: -0.02em;
  /* Both .facts bands (home, pricing) render on data-tone="page" — a light
     ground. --signal is 2.22:1 on white and is not text-bearing there;
     --signal-deep is 8.31:1. */
  color: var(--signal-deep);
}
.fact__label {
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--ink-dim);
}
.section[data-tone="ink"] .fact__label,
.site-footer .fact__label,
.sticky-cta .fact__label { color: var(--steel-200); }

/* ==========================================================================
   Comps-only review chrome — NOT a shipping component
   --------------------------------------------------------------------------
   Round-1 (Task 2 review, ALSO FIX): .comp-flag used to be styled by
   comp.css, which public/comps/*.html no longer links now that lodestar.css
   is the single stylesheet — it was rendering as raw, unstyled text at the
   top of every design-comp page. Restored verbatim from comp.css so the
   comps stay readable for review. Task 10 deletes this rule along with the
   comps directory and every page that references the class. */
.comp-flag {
  position: relative;
  z-index: 200;
  padding: 0.4rem var(--space-4);
  background: #101418;
  color: #9FB3C0;
  font: 600 0.75rem/1.4 ui-monospace, Menlo, monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: center;
  pointer-events: none;
}

/* ==========================================================================
   Program-page archetype — direction C rollout, Task 4+
   ========================================================================== */

/* A program hero is a doorway, not the front door: shorter than the home
   hero at every width so the "What to expect" answer is one swipe away.
   It used to carry `.hero-ribs--program .hero-ribs__media { --ratio: 3/2 }`
   here. That declaration is GONE, twice over. It is redundant — the base
   .hero-ribs__media now runs 3/2 too, and a program hero carries both
   classes — and it was actively harmful: at 0,2,0 it outranked the
   `--ratio: auto` in the @media block above on specificity, so from 900px up
   every program hero had a ratio AND height:100% and derived its height from
   its width (950px at 1440, 1697px at 2560, 170% of the viewport), with the
   cap below inert. Naming this selector inside that @media block does not
   fix it either — equal specificity there, and this block is further down
   the file, so source order would hand it back. The only stable arrangement
   is ONE selector setting --ratio, above ONE block clearing it. */
@media (min-width: 900px) {
  /* A lower FLOOR than home's, and no cap — see the .hero-ribs__frame note
     above for why a cap was removed rather than raised. A program hero is
     shorter than the front door because its plate is shorter, which is the
     honest reason for it to be shorter; the floor only stops a very short
     plate from producing a stub of a band. */
  .hero-ribs--program .hero-ribs__frame { min-height: min(46svh, 30rem); }
}

/* B-variant split hero ground: vertical padding only — the two columns are
   laid out by .split in the markup, never by this class. */
.hero-split { padding-block: var(--space-8) var(--space-9); }

/* Roster-card bio text (Task 6). The fold is the one sanctioned <details>
   (DESIGN.md §6): the preview line is the content and the full bio is
   optional behind it — used only when a bio runs past 45 words. */
.roster__line { font-size: var(--text-sm); }
.roster__more summary {
  cursor: pointer;
  color: var(--signal-deep);
  font-weight: 600;
  font-size: var(--text-sm);
}
.section[data-tone="ink"] .roster__more summary { color: var(--signal-bright); }
.roster__more[open] summary { margin-block-end: var(--space-2); }
.roster__note { color: var(--ink-dim); font-style: italic; font-size: var(--text-sm); }

/* --- forms: the contact and wellness-interest forms --------------------
   The .trial-form / .trial-form-error / .contact-form-status class names
   are a kept contract (TBS integration + contact-form.js/wellness-form.js
   + server.js's no-JS banner) — style them, never rename them. Square
   corners, token colors, no layout primitives. */
.form-row { margin-block-end: var(--space-4); }
.form-row label {
  display: block;
  font-weight: 600;
  font-size: var(--text-sm);
  margin-block-end: var(--space-1);
}
.form-row input,
.form-row textarea {
  width: 100%;
  padding: var(--space-3);
  border: var(--border-width) solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
}
.trial-form-error {
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--signal-deep);
  color: var(--signal-deep);
  font-size: var(--text-sm);
  margin-block-end: var(--space-4);
}
.contact-form-status { font-weight: 600; }
.contact-form-status[data-state="error"] { color: var(--signal-deep); }
.form-banner { padding-block-start: var(--space-5); }

/* An unfilled [David: …] mark, visible on purpose (DESIGN.md §10): dashed
   signal border so it cannot be mistaken for finished copy. Tokens only. */
.draft-todo {
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) dashed var(--signal-deep);
  background: color-mix(in srgb, var(--signal) 8%, transparent);
  font-family: var(--font-data);
  font-size: var(--text-sm);
}

/* ==========================================================================
   Client review, 2026-09-02
   Sean's pass over the built site before the next deploy to David. These
   rules come last on purpose: several of them are deliberate overrides of a
   value set earlier in this file, and appending is the honest way to say
   "this replaces that", with the reason written next to it, rather than
   editing the original value and losing why it used to be what it was.
   ========================================================================== */

/* --- the header leaves on the way down and comes back on the way up ------
   Feedback: "is it possible to have the header dissapear when scrolling down
   and reapear when scrolling back up? Keep the lodestar logo that links to
   home and the NSI CTA like we have on mobile view."

   The whole bar moves, so the wordmark and the No Sweat Intro button ride
   back in with it at every width — that is the "like we have on mobile"
   part: below 900px those two ARE the header, and above it they are still
   the first and last things in the row.

   site.js adds and removes .is-hidden; this file only says what the class
   means. With JavaScript off the class is never applied and the header is
   the plain sticky bar it has always been. */
.site-header {
  transition: transform var(--transition);
  /* The header is the only thing on the page that animates on scroll, so it
     is worth promoting; without this the translate repaints the full-bleed
     hero behind it on a phone. */
  will-change: transform;
}
.site-header.is-hidden { transform: translateY(-100%); }
/* Every in-page anchor has to clear the sticky header. There was no
   scroll-padding anywhere in this file, so /coaches#coach-katie-freeman —
   one of the deep links the home roster tiles now emit, and the ONLY path
   for a browser without <dialog> — put the coach's photograph exactly under
   the 4.5rem bar the moment the reader scrolled up and it came back. Set on
   the root so the skip link and every future anchor get it too, rather than
   a scroll-margin on each target as it is discovered. */
html { scroll-padding-block-start: 5.5rem; }
@media (prefers-reduced-motion: reduce) {
  /* Still hides and shows — that is the requested behaviour, not decoration —
     but arrives instantly rather than sliding. */
  .site-header { transition: none; }
}

/* --- Programs: the dropdown, restored ------------------------------------
   Feedback: "why did the programs button in the header loose the drop down
   of the different programs? Now users have to scroll throughout pages to be
   bounced to the program they want."

   It was not lost to a bug: rollout Task 3 rebuilt the header and shipped
   the desktop nav flat, with the seven children wired only into the mobile
   panel, and content/site.mjs recorded that as "no dropdown CSS exists yet".
   This is that CSS. The panel is absolutely positioned out of flow, so it
   adds nothing to .site-header__inner's width — the row's horizontal budget
   at 900 and 1100 is genuinely tight (see the long note at .site-nav above)
   and this must not spend any of it. The only in-flow addition is the caret,
   which is sized in em and rides inside the existing link box.

   Whole thing is inside .site-nav, which is display:none below 900px, so
   the mobile disclosure panel is untouched and there is no second
   breakpoint to keep in sync. */
.site-nav__item {
  position: relative;
  flex: none;
  /* Stretched to the full header row so the panel's `top: 100%` is the
     header's own bottom edge. Without it the span is only as tall as its
     text and the panel opens below a gap the pointer has to cross, which
     closes it on the way. Header anatomy — allowlisted for that reason. */
  display: flex;
  align-items: center;
  align-self: stretch;
}
/* The caret sits INSIDE the link, so it is the one part of this feature
   that spends the header row's horizontal budget — a budget the long note
   at .site-nav above records as already tight at 900 and 1100. It is
   therefore em-sized (it scales with the label rather than adding a fixed
   cost), and the link is told not to wrap: without `white-space: nowrap`
   the flex row shrinks the item and breaks the caret onto a second line
   under the word, which is what it did on the first build. `flex: none`
   stops the row shrinking the item to make that happen at all. */
.site-nav__caret {
  display: inline-block;
  inline-size: 0.45em;
  block-size: 0.45em;
  margin-inline-start: 0.35em;
  vertical-align: 0.08em;
  opacity: 0.75;
}
.site-nav__link--menu { white-space: nowrap; }
.site-nav__menu {
  position: absolute;
  top: 100%;
  inset-inline-start: calc(var(--space-4) * -1);
  display: none;
  min-inline-size: 15rem;
  padding-block: var(--space-2);
  background: var(--navy-900);
  border: var(--border-width) solid rgba(185, 206, 212, 0.28);
  border-block-start: 3px solid var(--signal);
}
/* One selector, one state. Hover used to open the panel with a second rule
   here (`.site-nav__item:hover > .site-nav__menu`), which made it visible in
   a way the JS could not observe: aria-expanded stayed "false", and the
   auto-hide handler — which reads .is-open — believed nothing was open and
   translated the header, panel and all, off the top of the viewport on the
   next wheel notch. Hover now lives in site.js's initNavMenus() and sets
   this same class, so the CSS, the ARIA and the header agree by construction
   rather than by two authors remembering to. */
.site-nav__item.is-open > .site-nav__menu { display: block; }
.site-nav__menu-link {
  display: block;
  padding: var(--space-2) var(--space-5);
  color: inherit;
  text-decoration: none;
  font-size: var(--text-sm);
}
.site-nav__menu-link:hover,
.site-nav__menu-link:focus-visible {
  background: var(--navy-800);
  color: var(--signal-bright);
}

/* --- the review beside the No Sweat Intro CTA ----------------------------
   Feedback: "Start with a no sweat intro section's review from Terry should
   have a different background of the review box, maybe white or bolded so it
   stands out more."

   The base .quote-plate is `currentColor 6%` — on the ink band that is a
   barely-there wash of white over navy, which is right where a quote is
   support and wrong where it is the proof next to the ask. This variant
   inverts it: real paper, real ink, and the signal cap the cards use, so it
   reads as a card sitting on the band rather than a tint of it. */
.quote-plate--solid {
  background: var(--paper);
  color: var(--ink);
  border-color: transparent;
  border-block-start: var(--space-1) solid var(--signal);
}
.quote-plate--solid blockquote { color: inherit; }
/* The base sets opacity 0.8 for a caption on a tinted plate; at full
   contrast on white that is a legibility loss, so the attribution takes the
   dim ink token instead of a transparency. */
.quote-plate--solid figcaption { opacity: 1; color: var(--ink-dim); }

/* --- the mission verse plate --------------------------------------------
   Feedback: "Lets put a bible or the actual luke 10:27 quote up in The
   Mission section." The verse is quoted from the King James Version, which
   is public domain — a commercial site quoting a modern translation needs
   the publisher's permission line, and this page should not carry a
   permissions footnote to say one sentence. */
.quote-plate--verse { border-inline-start: var(--space-1) solid var(--signal); }

/* --- Our Story: one road, not two lists ----------------------------------
   Feedback: "The following section with before the gym to lexington would be
   cool to have like a timeline along the left hand side of it all to show
   the congruency and connection of that story. could be a road curving or
   something."

   Not a curve: a curve drawn beside prose has to be redrawn at every width
   and every content length, and the first time a paragraph grows by a line
   it stops meeting its own markers. What it needed instead was to be ONE
   line — the page previously ran a compact five-point timeline and then, in
   a differently-toned section below it, three long-form chapters covering
   the same three events. The spine stopped and restarted, which is exactly
   the break in "congruency" being described. content/pages/our-story.mjs
   merges them into one list; this makes that list read as a road: a heavier
   rail in the signal colour, and markers big enough to be stops on it. */
.timeline--road {
  /* The base .timeline caps at --measure, which is right for one-line
     entries and wrong once each stop carries three paragraphs of its own
     .measure-capped prose — the cap then applies twice and the column
     narrows to nothing on a wide screen. */
  max-width: none;
  border-inline-start-color: var(--signal);
  border-inline-start-width: calc(var(--border-width) * 3);
}
.timeline--road .timeline__item { padding-inline-start: var(--space-6); }
.timeline--road .timeline__item::before {
  inline-size: var(--space-4);
  block-size: var(--space-4);
  inset-inline-start: calc(var(--space-4) / -2 - var(--border-width) * 1.5);
  top: 0.1em;
  /* A ring in the section's own ground so the marker sits ON the rail
     instead of being a square the rail runs through. */
  outline: var(--space-1) solid var(--paper-2);
}

/* --- the home roster: a tile is now a link that opens a bio --------------
   Feedback: "THe people in the room section, can we have it pop up the coach
   and their bio when you click on their card?"

   The tile ships as a link to that coach's entry on /coaches — that is what
   it does with JavaScript off, what a middle-click does, and what a crawler
   follows. site.js upgrades the click to open the matching <dialog>. */
.roster__link {
  display: block;
  color: inherit;
  text-decoration: none;
}
.roster__link .roster__name,
.roster__link .roster__role,
.roster__cue { display: block; }
.roster__cue {
  font-family: var(--font-data);
  font-size: var(--text-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--signal-deep);
}
/* The affordance has to be on the photograph, which is the part of the tile
   a pointer is actually over. */
.roster__link:hover .media,
.roster__link:focus-visible .media {
  outline: var(--space-1) solid var(--signal);
  outline-offset: calc(var(--space-1) * -1);
}
.roster__link:hover .roster__cue { text-decoration: underline; }

/* --- the coach dialog ----------------------------------------------------
   Native <dialog>, so the focus trap, the Escape key and the backdrop are
   the browser's rather than ours. The element itself is stripped back to
   nothing and .coach-dialog__body carries the card, which is what lets a
   click on the <dialog> box mean "the backdrop" in site.js. */
.coach-dialog {
  padding: 0;
  border: 0;
  background: transparent;
  inline-size: min(38rem, calc(100vw - var(--space-6)));
  max-block-size: min(80svh, 44rem);
  overflow: auto;
  color: var(--ink);
}
/* A literal, not var(--navy-900): ::backdrop lives in the top layer and did
   not inherit custom properties from its originating element until very
   recently, so the token resolved to nothing in older engines and the whole
   declaration was dropped — leaving the UA's near-transparent default, which
   is what the first build rendered. This value IS --navy-900 at 78%. */
.coach-dialog::backdrop { background: rgba(10, 30, 46, 0.78); }
.coach-dialog__body {
  background: var(--paper);
  border: var(--border-width) solid var(--line);
  border-block-start: var(--space-1) solid var(--signal);
  padding: var(--space-6);
}

/* --- heroes: shorter, and no longer opening on the roof ------------------
   Feedback: "hero is a bit large, and the sizing feels off compared to the
   message. Crop the top which is all roof so it all shifts up a bit … over a
   third of the users visable page is roof before you see people or
   lexington,sc. This applies across all heros on all pages."

   The roof half of that is fixed in the source, not here — see `preCrop` in
   scripts/build-images.mjs, which trims the ceiling off each interior frame
   before any display crop runs. Doing it in CSS instead, by moving
   object-position down, would have cost the bottom of every frame: on the
   home hero that is Pepper.

   The size half lives at .hero-ribs__media and its @media block, NOT here.
   It was appended here first and that was the bug: an appended declaration
   silently outranks an earlier @media rule that re-specifies the same
   property at the same specificity, so the media ended up with a ratio and
   height:100% at once and every hero grew 18.5%. The rule this file adopted
   — "appending is the honest way to say 'this replaces that'" — is only safe
   for a property no earlier media query touches. --ratio is not one. */
