/* =============================================================================
   313 Tennis — the entire stylesheet.

   Two grounds (near-black, cream) alternating full-bleed by section, one accent
   (the gold off the Warrior Tennis fence banner), one typeface (Archivo,
   self-hosted, two weights).  No gradients, no shadows, no radii.  Every rule
   below belongs to something on the page; nothing here is decoration.
   ========================================================================== */

/* The single @font-face lives in assets/fonts/archivo.css so the base64 payload
   does not sit in the middle of the stylesheet a critic has to read. */

/* --- Tokens ---------------------------------------------------------------
   Colour.  The accent is sampled from the Warrior Tennis banner at #BD8C39.
   That value clears 4.5:1 on the near-black ground, so on ink it ships unchanged
   for text, for fills carrying an ink label, and for focus rings.  On cream it
   manages only 2.6:1, and a fill and a line of text there want different things,
   so cream gets two deepenings of the same hue (H 37-38 throughout):
     #A5762C  fills - 3.5:1 boundary against cream, 4.7:1 for the ink label on it
     #886222  text and focus rings - 4.7:1 against cream
   Three tints, one colour, no tint used where a weaker one would do.  Green
   stays in the photographs and never enters the UI.

   ---------------------------------------------------------------------------
   TYPE.  Six steps, and the point of this revision is that they are a LADDER -
   four visible decisions between the hero and the body rather than two notes
   and a gap.

     display  clamp(2rem, 8vw, 6.25rem)      32 -> 100px  hero headline, once
     close    clamp(2rem, 5.6vw, 4.5rem)     32 ->  72px  closing CTA title, once
     head     clamp(1.5rem, 3.6vw, 2.75rem)  24 ->  44px  every section title
     row      clamp(1.25rem, 2.6vw, 2rem)    20 ->  32px  the things being sold
     body     1.0625rem                      17px         prose
     small    0.875rem                       14px         support and chrome

   What moved and why.  head used to top out at 84px under a 100px display -
   1.19:1 at 1440, 1.07:1 at 375 - so the hero and five section titles were the
   same shout in the same black uppercase and the page had no primary voice.
   Everything downstream followed from that one token: six bands announced at
   hero volume are six interchangeable slabs, whatever the padding between them
   does.  Neither reference does this.  Measured off docs/references/,
   Padel Haus runs 91/37 (2.5:1) and Reserve Padel 156/36 (4.3:1) - Reserve
   holds 40px section heads under a 156px hero, and that is the model.

   The vw terms are set from the RATIO rather than picked: 8vw / 3.6vw is 2.22,
   and 3.6vw / 2.6vw is 1.38, so both intervals hold across the entire fluid
   band instead of only at the width they were eyeballed at.  display:head is
   2.22:1 from 667px (where head leaves its floor) to 1222px (where it reaches
   its cap) and 2.27:1 from there to 1440; head:row is 1.375-1.38 everywhere
   above 769.  A first pass used 3vw and 2.2vw and read correctly at 1440 while
   leaving both tokens pinned to their floors up to 800 and 909 - which put a
   24px section title on a 768px band under a 61px hero, the same flatness one
   viewport over.  PRIVATE / HITTING / GROUP / CEDRIC DRENTH now sit a clear
   step under the titles they stand beneath at every width, not just at one.

   At 375 the ratio is 1.33 rather than 2.3, and that is the viewport being
   honest rather than the ladder failing.  display is already at its own floor
   there - "Real Progress." fills 270px of the 280px measure at 320 - so the
   hero cannot go up to make room, and a section title below 24px would be
   inside arm's reach of 17px body.  All four steps stay distinct; the interval
   between them is what a 375px viewport can pay for.

   `close` is the sixth step and §4 asks for a stated reason.  It is this: the
   page has one job and section 7 is where it asks.  With every title above it
   set quiet, the close is the one place scale is allowed back - 72px at 1440,
   plainly second to the hero, plainly first among everything below it.  Like
   `display`, it is used exactly once.  Below 571px it resolves to the same
   value as `display`, so on a phone the page opens and closes at one volume
   and everything between is quieter: the same composition, printed at the only
   size a phone has room for.

   Why six and not the two per section §4 asks for.  Each section still reads
   at three levels and no more: its title, the objects it lists, and the prose
   or qualifiers hanging off those objects.  `small` is one component size -
   buttons, micro-caps column heads, the risk-reversal line - set identically
   in every section, so it is chrome rather than a level in any section's
   hierarchy.  `display` and `close` appear once each, in the two sections that
   open and close the page.  No section runs more than three steps.

   The step this revision deliberately DELETED is the old `claim` size that sat
   between the row name and body and carried the prices.  It existed because
   "PRIVATE $80 $120 $100 $150" at one size was five equal objects with no
   subject.  That is fixed at the root now: the durations are set once as column
   headings, so a price is identified by the column it stands in, and the row
   name no longer has to outrank it to be understood.  Prices ship at the row
   step, same size and same weight as the format they belong to.  Nothing about
   the money is quiet.

   WEIGHT.  Still two, and spent on rank rather than emphasis:
     700  display, section titles, and the micro-caps (nav, buttons, column
          heads, the wordmark and footer name).  At 14px and below bold is
          legibility, not a claim about rank.
     400  the whole row tier - lesson name, price, coach name, FAQ question,
          address - and all prose.
   assets/fonts holds one variable face at 100-900 in a single 34.9 KB file, so
   the second weight costs zero bytes. */
:root {
  --ink:       #101010;
  --ink-card:  #171614;
  --cream:     #f0ede4;
  --gold:      #bd8c39;
  --gold-mid:  #a5762c;
  --gold-deep: #886222;

  --font: 'Archivo', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;

  --fs-display: clamp(2rem, 8vw, 6.25rem);
  --fs-close:   clamp(2rem, 5.6vw, 4.5rem);
  --fs-head:    clamp(1.5rem, 3.6vw, 2.75rem);
  --fs-row:     clamp(1.25rem, 2.6vw, 2rem);
  --fs-body:    1.0625rem;
  --fs-small:   0.875rem;

  --bar-h:  4rem;
  --pad-y:  clamp(4.5rem, 8.5vw, 8.5rem);
  --gutter: clamp(1.25rem, 4vw, 2.5rem);
  --wrap:   75rem;
}

/* Ground-scoped tokens.  A section declares which ground it sits on; every
   component inside reads its colours from these and needs no variants. */
.band--ink,
.bar {
  --bg:        var(--ink);
  --fg:        var(--cream);
  --fg-dim:    #b9b4a8;
  --rule:      #33312b;
  --accent:    var(--gold);
  --plate-bg:  var(--cream);
  --plate-fg:  var(--ink);
  --btn-bg:    var(--gold);
  --btn-fg:    var(--ink);
  --btn-bg-on: var(--cream);
  --btn-fg-on: var(--ink);
  background: var(--bg);
  color: var(--fg-dim);
}

.band--cream {
  --bg:        var(--cream);
  --fg:        var(--ink);
  --fg-dim:    #3b3934;
  --rule:      #cfc8b6;
  --accent:    var(--gold-deep);
  --plate-bg:  var(--ink);
  --plate-fg:  var(--cream);
  --btn-bg:    var(--gold-mid);
  --btn-fg:    var(--ink);
  --btn-bg-on: var(--ink);
  --btn-fg-on: var(--cream);
  background: var(--bg);
  color: var(--fg-dim);
}

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

/* Deliberately no `scroll-behavior: smooth`.  It overrides programmatic
   scrolling too, which starves `loading="lazy"` images on any automated pass
   over the page.  In-page jumps are instant. */
html { -webkit-text-size-adjust: 100%; }

body, h1, h2, h3, p, dl, dd, figure, table {
  margin: 0;
  padding: 0;
}

body {
  background: var(--ink);
  color: var(--cream);
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  font-synthesis: none;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

section { scroll-margin-top: var(--bar-h); }

/* --- Focus --------------------------------------------------------------
   Two signals, both required.  A ring outside the box for the pointer-free
   user, and a state change inside the box (fill or colour) so focus survives
   being clipped, printed or screenshotted. */
:is(a, button):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* --- Buttons -------------------------------------------------------------
   Every Book control on the page is now wired.  The three in the lesson rows
   and the two on the coach cards are pickers (see below) and open a panel; the
   nav, hero and closing controls are anchors into #lessons, because they
   navigate rather than book. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 1.15rem 1.9rem;
  border: 1px solid var(--btn-bg);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-family: inherit;
  font-size: var(--fs-small);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  transition: background-color 0.16s ease, color 0.16s ease, border-color 0.16s ease;
}

/* :focus-visible, not :focus — matching the ring rule above.  Plain :focus kept
   the inversion after a MOUSE click: a lesson Book button, which by design has
   no destination, stayed filled ink-on-cream with the pointer long gone and read
   as "selected".  :focus-visible still matches a keyboard Tab, so the two focus
   signals the rule above describes are both intact for the user who needs them. */
.btn:hover,
.btn:focus-visible {
  background: var(--btn-bg-on);
  border-color: var(--btn-bg-on);
  color: var(--btn-fg-on);
}

/* The terminal control.  Sized to be the biggest target on the page and capped
   at 27rem so it stays a button on a 1200px band instead of a stripe; on a
   phone the cap never binds and it takes the full column, like the coach
   Books directly above it. */
.btn--lg {
  width: 100%;
  max-width: 27rem;
  padding: 1.75rem 3rem;
}

/* --- The booking picker ---------------------------------------------------
   Booking needs a format and a coach.  A lesson row supplies the format, a
   coach card supplies the coach, and each opens a panel asking for the other
   half — so both routes reach the same six Cal.com links, and adding a third
   coach is a block of markup rather than a new arrangement.

   The panel is absolutely positioned, which is the whole reason this is a
   panel and not an inline expansion: the lesson rows are a fixed grid where
   the control column governs the width of the two columns beside it, and a
   control that grows on click would reflow the price table next to it.  Out of
   flow, opening costs nothing.  It also keeps the site's one animation rule
   intact — opacity and transform only, no layout touched.

   No shadow and no radius, like everything else here.  What separates the
   panel from the page is a 1px line in the foreground colour, which is the
   heaviest border on the site and the correct weight for the only element that
   sits on top of another. */
.pick { position: relative; }

.pick__trigger { width: 100%; }

/* Static, in flow, and open, until JS builds a trigger to hang it from — with
   the script gone these links are the booking control, so they must be exactly
   as usable as they are with it.  The sibling combinator IS the enhancement
   flag; there is no .js class on <html> to go stale. */
.pick__menu {
  min-width: 100%;
  border: 1px solid var(--fg);
  background: var(--bg);
}

.pick__trigger ~ .pick__menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 30;
  transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s;
}

/* visibility, not display: it takes the links out of the tab order while
   leaving the box measurable, which is what the overhang check on open reads
   before deciding whether to nudge the page. */
.pick__trigger ~ .pick__menu.is-shut {
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
}

.pick.no-anim .pick__menu { transition: none; }

.pick__lead {
  padding: 0.8rem 1.15rem;
  color: var(--fg-dim);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

/* Set as buttons, not as links — same micro-caps as .btn, because that is what
   they are.  A row rather than a filled button apiece: three gold slabs stacked
   in a panel under a fourth gold slab is the loudest object on a page whose
   whole design is one accent used sparingly. */
.pick__opt {
  display: block;
  padding: 0.95rem 1.15rem;
  border-top: 1px solid var(--rule);
  color: var(--fg);
  font-size: var(--fs-small);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 0.16s ease, color 0.16s ease;
}

.pick__opt:hover,
.pick__opt:focus-visible {
  background: var(--btn-bg);
  color: var(--btn-fg);
}

/* Inside the panel the ring would be clipped by the border it sits against, so
   the fill above is this control's focus signal and the offset ring is pulled
   inward to sit on the row rather than around it. */
.pick__opt:focus-visible { outline-offset: -3px; }

/* --- Inline links ---------------------------------------------------------
   The mailto is a second path off this page, kept for anyone whose time is not
   in either calendar, so it is styled as the strongest inline element here. */
.coach__link a,
.mailline a,
.foot__mail a {
  position: relative;
  padding: 0.1em 0.25em;
  margin: 0 -0.25em;
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.22em;
  transition: background-color 0.16s ease, color 0.16s ease;
}

/* These three links are the only working actions on the page, and 0.1em of
   padding on 17px type left them 18-22px tall — under half a finger.  The hit
   area is grown with a pseudo-element rather than real padding, because vertical
   padding on an inline box paints its hover fill across the lines above and
   below.  This renders nothing: same visual height, same page height, 44px of
   target. */
.coach__link a::after,
.mailline a::after,
.foot__mail a::after {
  content: '';
  position: absolute;
  top: 50%;
  right: 0;
  left: 0;
  height: 44px;
  transform: translateY(-50%);
}

.coach__link a:hover, .coach__link a:focus,
.mailline a:hover,   .mailline a:focus,
.foot__mail a:hover, .foot__mail a:focus {
  background: var(--accent);
  color: var(--bg);
  text-decoration-color: transparent;
}

/* --- Sticky bar ----------------------------------------------------------
   The bar is chrome, not a section: its type is fixed at 12px micro-caps and
   does not read off the page's scale.  Four section links now fit beside the
   wordmark and the Book control from 34rem up, which is where they stop
   colliding. */
.bar {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--rule);
}

.bar__in {
  display: flex;
  align-items: center;
  gap: clamp(0.85rem, 3vw, 2.5rem);
  height: var(--bar-h);
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.wordmark {
  flex: none;
  color: var(--fg);
  font-size: var(--fs-body);
  font-weight: 700;
  letter-spacing: -0.015em;
  text-decoration: none;
  white-space: nowrap;
}

.wordmark:hover,
.wordmark:focus { color: var(--accent); }

.bar__nav {
  display: none;
  flex: none;
  gap: clamp(0.9rem, 2.2vw, 2rem);
  margin-inline: auto;
}

@media (min-width: 34rem) { .bar__nav { display: flex; } }

.bar__nav a {
  padding-block: 0.45rem;
  border-bottom: 1px solid transparent;
  color: var(--fg-dim);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.16s ease, border-color 0.16s ease;
}

.bar__nav a:hover,
.bar__nav a:focus {
  border-bottom-color: var(--accent);
  color: var(--fg);
}

/* The nav Book control duplicates the hero CTA, so it stays out of the way
   until the hero is gone.  visibility keeps its box, so nothing reflows. */
.btn--bar {
  flex: none;
  padding: 0.8rem 1.2rem;
  font-size: 0.75rem;
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.2s ease, background-color 0.16s ease, color 0.16s ease;
}

.btn--bar.is-in {
  visibility: visible;
  opacity: 1;
}

/* --- 1 · Hero ------------------------------------------------------------
   Huddle desktop, ActionShot mobile.  The desktop crop hands the headline a
   flat pale wall across its top 44%, which is the whole reason that photograph
   replaced the empty-court hero — so the scrim is set at the weight legibility
   needs and no heavier.  0.44 on ink puts cream at 5.6:1 over the wall.  The
   revision-1 hero ran 0.72 over a photograph that had nothing to lose; carried
   across unchanged it would have thrown this one away.

   The mobile frame is lit ceiling at the top rather than flat wall, and it has
   a moving subject under the type, so it takes 0.62.  Two photographs, two
   legibility problems, one rule each.

   The desktop figure is 0.58 and it was measured, not guessed: the brightest
   thing under the offer block is a white shirt at L~230, and 0.54 is the point
   where 17px cream over it clears 4.5:1, sampled off the render rather than
   assumed.  Revision 1 ran 0.72 over a photograph
   that had nothing to lose. */
.hero {
  position: relative;
  isolation: isolate;
  display: flex;
  overflow: hidden;
  min-height: 34rem;
  min-height: min(88svh, 46rem);
}

.hero__img {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  will-change: transform;
}

.hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(10, 10, 10, 0.62);
}

@media (min-width: 45.0625em) {
  .hero::after { background: rgba(10, 10, 10, 0.58); }
}

/* Headline hard against the top of the frame, on the wall; the offer hard
   against the bottom.  The photograph is what sits between them, so the hero
   reads as one composition rather than a caption laid over a picture. */
.hero__in {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(2rem, 5vw, 3.5rem);
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding: clamp(2.25rem, 4.5vw, 4rem) var(--gutter) clamp(2.5rem, 4.5vw, 3.75rem);
}

.hero__title {
  color: var(--cream);
  font-size: var(--fs-display);
  font-weight: 700;
  line-height: 0.92;
  letter-spacing: -0.04em;
  text-transform: uppercase;
}

/* The clip mask the load animation runs inside.  overflow:hidden is permanent
   and harmless: with no JavaScript the line already sits at y:0 and the mask
   is exactly its own height.  The line is real text inside the <h1>, so a
   screen reader reads one headline and not three fragments. */
.mask { display: block; overflow: hidden; }
.mask__line { display: block; }

.hero__foot {
  display: grid;
  gap: clamp(1.5rem, 3vw, 3rem);
  padding-top: clamp(1.5rem, 2.6vw, 2.25rem);
  border-top: 1px solid rgba(240, 237, 228, 0.28);
}

/* Both of these sit on a photograph rather than flat ink, so neither reads off
   --fg-dim: the subline takes the full cream and the risk line the one step
   down that still clears 4.5:1 against the brightest pixel under it. */
.hero__sub {
  max-width: 46ch;
  color: var(--cream);
  font-size: var(--fs-body);
  line-height: 1.5;
}

.hero__act {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}

.risk {
  max-width: 42ch;
  color: var(--fg-dim);
  font-size: var(--fs-small);
  line-height: 1.5;
}

.hero .risk { color: #ded9cd; }

@media (min-width: 60rem) {
  .hero__foot {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
  }
  .hero__act {
    align-items: flex-end;
    text-align: right;
  }
}

/* --- Sections ------------------------------------------------------------- */
.sect { padding-block: var(--pad-y); }

/* 46px under the title at 1440, against 136px of --pad-y between one band and
   the next.  It was 64px, set when the title was 84px and needed the room; at
   44px that same gap left every title floating unattached above its own
   content and reading as a label on the band rather than the opening of it.
   Three to one is the ratio that makes a section a section: the title binds
   downwards to the thing it introduces, and the air belongs to the join
   between bands, which is the only place §4's identical section padding can
   put it. */
.sect__title {
  margin-bottom: clamp(1.75rem, 3.2vw, 3rem);
  color: var(--fg);
  font-size: var(--fs-head);
  font-weight: 700;
  line-height: 0.88;
  letter-spacing: -0.042em;
  text-transform: uppercase;
}

/* Only one section takes a paragraph before its content — Coaches, where the
   fact that price does not vary by coach is what unblocks the choice. */
.sect__title--lede { margin-bottom: clamp(0.85rem, 1.5vw, 1.5rem); }

.sect__lede {
  max-width: 46ch;
  margin-bottom: clamp(1.75rem, 3.2vw, 3rem);
  color: var(--fg-dim);
  font-size: var(--fs-body);
}

/* --- 2 · For who ----------------------------------------------------------
   Three headings and three paragraphs.  No price, no photograph, no icon, no
   button: it routes a visitor to the section below it, and anything that could
   be mistaken for an offer would compete with the thing it is routing to.

   Hairline over each item, no box.  It is the same rule the offer rows and the
   questions are built from, so the page has one way of listing things. */
.who { display: grid; gap: clamp(2rem, 3.5vw, 2.75rem); }

.who__item {
  padding-top: clamp(0.9rem, 1.6vw, 1.35rem);
  border-top: 1px solid var(--rule);
}

.who__name {
  color: var(--fg);
  font-size: var(--fs-row);
  font-weight: 400;
  line-height: 0.95;
  letter-spacing: -0.022em;
  text-transform: uppercase;
}

.who__copy {
  max-width: 42ch;
  margin-top: clamp(0.9rem, 1.6vw, 1.35rem);
  color: var(--fg-dim);
  font-size: var(--fs-body);
  line-height: 1.6;
}

/* Three-up, and the names share a row so the paragraphs start on one line.
   "Competitive Juniors" wraps in a third of the measure and the other two do
   not; without the shared row the three paragraphs step down a staircase and
   the section stops reading as three peers. */
@media (min-width: 52rem) {
  .who {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    grid-template-rows: auto auto;
    column-gap: clamp(1.5rem, 3vw, 3rem);
    row-gap: 0;
  }
  .who__item {
    grid-row: 1 / -1;
    display: grid;
    grid-template-rows: subgrid;
    align-content: start;
  }
  .who__item:nth-child(1) { grid-column: 1; }
  .who__item:nth-child(2) { grid-column: 2; }
  .who__item:nth-child(3) { grid-column: 3; }
}

/* --- 3 · Lessons ----------------------------------------------------------
   Reserve Padel's programme row is still the model — format left, money right,
   one block per item — but the section now opens with a two-up lede and each
   price carries its own duration.

   Why the shared duration headings went. .prog__head and .prog__row were two
   separate grids that had to agree column for column at every width, and they
   only agreed while the row stayed one line tall.  A cell that states its own
   duration is atomic: it can wrap, stack or move column and it is still
   readable on its own, which is what a price on a page nobody checks out on
   has to be.  The repetition that argument was originally made against is
   answered by size instead — the duration is a 10px cap line sitting on the
   price, a full three steps below it, so it reads as a label and not as eight
   more lines of copy. */

/* --- 3a · Lede ---------------------------------------------------------- */
/* The payment rule used to sit under the stack as one dim line and was the
   last thing read.  It is the single most common question this section gets,
   so it moves up beside the heading and is given a rule of its own accent —
   the only gold rule in the section, spent on the only sentence that changes
   whether someone books. */
.prog__lede {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2rem);
  align-items: end;
  padding-bottom: clamp(1.25rem, 2.4vw, 1.75rem);
  border-bottom: 1px solid var(--rule);
}

.sect__title--flush { margin-bottom: 0; }

.prog__intro {
  max-width: 44ch;
  margin-top: clamp(0.6rem, 1.2vw, 0.9rem);
  color: var(--fg-dim);
  font-size: var(--fs-body);
  line-height: 1.5;
  text-wrap: pretty;
}

.prog__note {
  padding-left: clamp(0.85rem, 1.6vw, 1rem);
  border-left: 2px solid var(--accent);
}

.prog__note-h {
  color: var(--fg);
  font-size: clamp(1.1rem, 1.8vw, 1.35rem);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.01em;
}

.prog__note-p {
  max-width: 42ch;
  margin-top: 0.35rem;
  color: var(--fg-dim);
  font-size: var(--fs-small);
  line-height: 1.5;
  text-wrap: pretty;
}

@media (min-width: 62rem) {
  .prog__lede {
    grid-template-columns: minmax(0, 1fr) minmax(18rem, 24rem);
    gap: clamp(2rem, 4vw, 3rem);
  }
}

/* --- 3b · Rows ---------------------------------------------------------- */
.prog__row {
  display: grid;
  gap: clamp(1.25rem, 2.4vw, 1.75rem);
  padding-block: clamp(1.6rem, 2.8vw, 2rem);
  border-top: 1px solid var(--rule);
}

.prog__row:first-child { border-top: 0; }
.prog__row:last-child { border-bottom: 1px solid var(--rule); }

.prog__lead { max-width: 46ch; }

.prog__title {
  display: flex;
  align-items: center;
  gap: 0.65rem;
}

.prog__name {
  color: var(--fg);
  font-size: var(--fs-row);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.022em;
  text-transform: uppercase;
}

/* The blurb is always visible.  Nothing that decides between two formats is
   allowed to live behind the `?` — the panel adds detail, it does not hold the
   answer hostage. */
.prog__blurb {
  margin-top: clamp(0.5rem, 1vw, 0.7rem);
  color: var(--fg-dim);
  font-size: var(--fs-small);
  line-height: 1.55;
  text-wrap: pretty;
}

/* --- 3c · The `?` ------------------------------------------------------- */
/* A 22px ring, optically centred on the format name's cap height rather than
   its box, and the only circle in the layout — which is precisely why it reads
   as a control in a section built entirely from rules and rectangles.  Open,
   it fills with the accent: the state is carried by the button itself, so the
   panel never has to announce which question it is answering. */
.prog__info {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--fg) 32%, transparent);
  border-radius: 50%;
  background: transparent;
  color: color-mix(in srgb, var(--fg) 62%, transparent);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.prog__info:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.prog__info[aria-expanded='true'] {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg);
}

/* Same 0fr → 1fr disclosure the FAQ uses, and allowed for the same reason
   (motion contract §3): the panel is a disclosure, and its height is the thing
   being disclosed. */
.prog__detail {
  display: grid;
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows 0.32s ease, visibility 0.32s ease;
}

.prog__detail.is-shut {
  grid-template-rows: 0fr;
  visibility: hidden;
}

.prog.no-anim .prog__detail { transition: none; }

.prog__detail-in {
  min-height: 0;
  overflow: hidden;
}

/* One rule for the whole panel, not one per paragraph: the accent line is what
   says "this is the answer to the question you just asked", and two of them
   with a gap between reads as two unrelated notes. It lives on a child of the
   clipped box so that at 0fr there is no 2px sliver of gold left behind. */
.prog__detail-body {
  max-width: 48ch;
  margin-top: clamp(0.9rem, 1.6vw, 1.1rem);
  padding-left: clamp(0.75rem, 1.4vw, 0.9rem);
  border-left: 2px solid var(--accent);
}

.prog__detail p {
  color: var(--fg-dim);
  font-size: var(--fs-small);
  line-height: 1.6;
  text-wrap: pretty;
}

.prog__best {
  margin-top: 0.55rem;
  color: color-mix(in srgb, var(--fg-dim) 78%, transparent);
  font-weight: 500;
  font-size: 0.8125rem;
}

/* --- 3d · Rates --------------------------------------------------------- */
.prog__rates {
  display: grid;
  gap: clamp(0.75rem, 1.4vw, 1rem);
}

/* Who the rate is for, hard against the money it qualifies — a qualifier a
   whole column away from its number is a qualifier the eye pairs with the
   wrong thing.  Full contrast, not --fg-dim: every row now carries exactly one
   rate, so this label is the only thing saying whether a price buys a court for
   one player or for each of six.

   Private's label survives the two-player rate it used to distinguish, because
   it is also what holds the price columns.  Drop it and Private's $80 slides
   into the column where Hitting and Group print their labels, and the three
   rows stop lining up. */
/* Narrow: the label takes its own line and the two durations split the width,
   because a phone cannot hold "1-on-1 with a coach" and two prices on one line
   without either scrolling sideways or breaking the label mid-phrase.  From
   30rem the label sits inline again, hard against its money. */
.prog__rate {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.5rem clamp(0.75rem, 2vw, 1.25rem);
  align-items: end;
  padding-top: clamp(0.6rem, 1.2vw, 0.85rem);
  border-top: 1px solid var(--rule);
}

.prog__who {
  grid-column: 1 / -1;
  align-self: end;
  color: var(--fg);
  font-size: var(--fs-small);
  line-height: 1.35;
}

@media (min-width: 30rem) {
  .prog__rate { grid-template-columns: minmax(0, 1fr) repeat(2, minmax(3.5rem, 7rem)); }
  .prog__who  { grid-column: 1; }
}

/* Its own line rather than a trailing clause: "per player" is the qualifier
   that decides whether $35 is the price of the lesson or the price of one
   racket in it, and at 62rem the label column is too narrow to hold both on
   one line without breaking mid-phrase. */
.prog__per {
  display: block;
  color: var(--fg-dim);
}

/* Duration above money, both set right, so the units line up down the stack
   and the label can never be read as belonging to the cell beside it. */
.prog__cell {
  display: grid;
  gap: 0.4rem;
  justify-items: end;
  text-align: right;
}

.prog__dur {
  color: var(--fg-dim);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.prog__amt {
  color: var(--fg);
  font-size: var(--fs-row);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.prog__foot {
  margin-top: clamp(1.25rem, 2.2vw, 1.75rem);
  color: var(--fg-dim);
  font-size: var(--fs-small);
  line-height: 1.5;
}

/* Wide: format and its description left, the rate table right, Book at the end
   of the row.  The rate column is capped rather than fluid so the money never
   drifts away from the name it belongs to on a 1440 display. */
@media (min-width: 62rem) {
  .prog__row {
    grid-template-columns: minmax(0, 1fr) minmax(19rem, 26rem) minmax(9rem, auto);
    gap: clamp(1.5rem, 3vw, 2.5rem);
    align-items: start;
  }

  /* Top-aligned with the first price rather than centred on the row: an open
     `?` panel can double the height of the left column, and a centred button
     slides down the page every time someone opens a description. */
  .pick--row {
    align-self: start;
    justify-self: end;
    margin-top: clamp(0.6rem, 1.2vw, 0.85rem);
    width: auto;
    min-width: 9rem;
  }
}

/* --- 4 · Coaches ----------------------------------------------------------
   The funnel, so it is the heaviest block on the page: a full-column 4:5 plate
   per coach and the largest two-up in the layout.  §5 lists four defects and
   all four are answered here.

   1 · The border box is gone.  It was decoration — the ground alternation and
       the plate already say where one card ends and the next begins.

   2 · The portrait placeholder is a designed plate, not a failed image: a true
       4:5 panel in the page's other ground, full column width, carrying the
       coach's initials and the words "portrait to follow".  Nobody reads that
       as a broken <img>.

   3 · The two cards align line for line.  .coaches owns seven explicit rows —
       plate, name, three facts, profile link, button — and each card spans all
       seven through `grid-template-rows: subgrid`, with the <dl> spanning its
       three.  Cedric's honours wrapping to two lines now grows the row on both
       cards, so Benny's Tour line stays level with Cedric's instead of every
       line below sliding up.  No min-height, no reserved blank line, and the
       fix cannot drift when the copy changes. */
.coaches { display: grid; gap: clamp(2.5rem, 5vw, 3.5rem); }

.coach { display: grid; align-content: start; }

.coach__plate {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: clamp(0.6rem, 1.2vw, 1rem);
  aspect-ratio: 4 / 5;
  width: 100%;
  padding: clamp(1rem, 2vw, 1.6rem);
  overflow: hidden;
  background: var(--plate-bg);
  color: var(--plate-fg);
}

/* The portrait covers the plate rather than replacing it, which is why the
   monogram below stays in the markup instead of being deleted along with the
   "Portrait to follow" label it used to sit beside.  If the WebP 404s or the
   decode fails, the img collapses to nothing and the plate underneath is still a
   monogram on the plate colour - the same panel the page shipped before these
   photos existed, rather than the empty rectangle that an image-shaped hole
   would leave.  `cover` has nothing to crop in practice: both files are authored
   at 4:5 to match the plate, so this only absorbs sub-pixel rounding. */
.coach__portrait {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The monogram is a mark inside the plate, not a step in the page's type
   scale — it has no reading order and is aria-hidden, because the name is set
   in full directly below it. It is sized to the plate rather than to the text
   ramp, which is why this is the one font-size on the page not taken from a
   token: at the display step it left a 4:5 panel looking mostly empty, and an
   empty panel is exactly the "image failed" read the plate exists to kill. */
.coach__mono {
  display: flex;
  flex: 1;
  align-items: center;
  font-size: clamp(5rem, 20vw, 18rem);
  font-weight: 700;
  line-height: 0.74;
  letter-spacing: -0.055em;
}

.coach__name {
  margin-top: clamp(1.25rem, 2.2vw, 1.9rem);
  color: var(--fg);
  font-size: var(--fs-row);
  font-weight: 400;
  line-height: 1;
  letter-spacing: -0.022em;
  text-transform: uppercase;
}

/* The label column is sized to the longest label on the card and nothing else.
   As a 0.4fr share it took 146px of a 536px card at 1440, and with the 24px gap
   that is 170px of gutter in front of every fact — enough to push "5-time
   All-American, NCAA National Runner-Up" onto a second line.  That honour is the
   highest-value fact on the page and it should not break.  `auto` resolves to
   the max-content width of COACHING, 94px, so the gutter is ~7rem and the value
   column keeps the rest.  The 4.5rem floor only ever matters if the labels are
   rewritten shorter than TOUR.

   The track list lives HERE, on the <dl>, and each row borrows it through
   `grid-template-columns: subgrid`.  Sized per row it would resolve three times
   — 94px under COACHING, 87 under HONOURS, 48 under TOUR — and the three values
   would start at three different left edges, which is the alignment defect this
   section was rebuilt to kill, reintroduced one level down.  Both cards carry
   the same three labels, so both resolve to the same 94px and the two spec
   blocks stay level with each other as well. */
.coach__facts {
  display: grid;
  grid-template-columns: minmax(4.5rem, auto) minmax(0, 1fr);
  column-gap: clamp(0.75rem, 2vw, 1.5rem);
  row-gap: 0;
  margin-top: clamp(1rem, 1.8vw, 1.5rem);
}

.fact {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: baseline;
  padding-block: clamp(0.55rem, 1vw, 0.75rem);
  border-top: 1px solid var(--rule);
}

.fact dt {
  color: var(--fg-dim);
  font-size: var(--fs-small);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* 38ch, not 34.  The longest fact on the page sets in 37ch, so 34 was a cap that
   guaranteed the wrap the narrower label column was widened to prevent — the two
   have to move together or neither does anything.  Still 43 characters, well
   inside the 65-character measure the design system sets. */
.fact dd {
  max-width: 38ch;
  color: var(--fg);
  font-size: var(--fs-body);
  line-height: 1.4;
}

.coach__link {
  margin-top: clamp(1.1rem, 2vw, 1.6rem);
  font-size: var(--fs-body);
}

/* The picker, not the button inside it: the .pick is the card's seventh grid
   row, and putting the gap on its child would leave the row itself flush
   against the profile link and break the two cards' shared subgrid. */
.coach .pick { margin-top: clamp(0.9rem, 1.6vw, 1.35rem); }

.mailline {
  margin-top: clamp(2rem, 3.5vw, 3.25rem);
  color: var(--fg-dim);
  font-size: var(--fs-body);
}

/* Two-up, and the row grid the alignment fix depends on.  Below this width the
   cards are stacked, nothing is being compared side by side, and each card
   simply closes up.

   42rem, and it is the one multi-column rule on this page that does NOT break
   where the rest of the layout does.  Everything else here goes wide at 52 or
   62rem because that is where its text stops crowding.  The coach card is not
   governed by its text: it is governed by a 4:5 plate at the full width of its
   column, and that plate grows as fast as the column does.  Held at 62rem it
   measured 912x1140 at 991px — a screen and a quarter of cream before the first
   fact about the coach — and the page ran 8139px at 991 against 5051px at 1024,
   a 61% swing across 33 pixels of viewport.  Two-up as early as a column can
   still carry a name and three fact rows, which is 42rem. */
@media (min-width: 42rem) {
  .coaches {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: auto auto auto auto auto auto auto;
    column-gap: clamp(1.5rem, 3.5vw, 3rem);
    row-gap: 0;
  }

  /* The monogram is sized to its plate, and the plate is now half a column
     rather than a whole one, so the viewport factor has to change with it.
     23.7vw holds the mark at ~0.54 of the plate width from 42rem all the way to
     the 18rem cap — the same proportion it has always had at 1440, where the
     wrap is capped and the plate is a fixed 536px.  Left at 20vw the mark would
     shrink to a fifth of its plate the moment the cards went two-up, and a
     small mark in a big empty panel is the "image failed" read this plate
     exists to kill. */
  .coach__mono { font-size: clamp(5rem, 23.7vw, 18rem); }

  .coach {
    grid-row: 1 / -1;
    grid-template-rows: subgrid;
  }

  .coach:nth-child(1) { grid-column: 1; }
  .coach:nth-child(2) { grid-column: 2; }

  /* Rows come from the card, columns stay the ones declared on the <dl> above.
     Both cards therefore agree on where a value starts AND on where each fact
     row sits, which is what makes Cedric's honours and Benny's honours the same
     line rather than two lines that happen to be near each other. */
  .coach__facts {
    grid-row: span 3;
    grid-template-rows: subgrid;
  }
}

/* --- 5 · Location ---------------------------------------------------------
   The one place the page breaks its container.  Everywhere else the cream/ink
   alternation carries the design alone; here the photography sets the tempo,
   which it cannot do boxed inside the same text measure as the prose.  The
   strip is full bleed, edge to edge, and each frame is held at 2:1 — a near
   square of an empty athletic complex reads as *nobody is here*, which is the
   one thing the crop must not say. */
.strip {
  position: relative;
  display: grid;
}

/* height:auto is load-bearing.  The width/height attributes map to presentational
   hints, so without it the box is 700px tall, max-height clamps it and the frame
   silently lands under the ratio this whole band exists to hold. */
.strip__img {
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 1;
  max-height: 31rem;
  object-fit: cover;
}

.strip__img:first-of-type { object-position: 50% 52%; }
.strip__img:last-of-type  { object-position: 50% 56%; }

/* LOCATION sits inside the band, lower left, on the photograph — the one
   section title on this page that legitimately does, because it is what turns
   cream → photo → cream into a change of gear instead of another stripe. */
.strip__title {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1;
  display: flex;
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.strip__title span {
  margin-left: -0.36em;
  padding: 0.26em 0.36em 0.3em;
  background: var(--ink);
  color: var(--cream);
}

.place {
  display: grid;
  gap: 1.15rem;
  margin-top: clamp(2.25rem, 4vw, 3.5rem);
}

/* Row level, like the lesson names and the coach names — but set sentence case,
   the way the FAQ questions are.  Uppercasing it would render an approved string
   differently from the way it was approved. */
.place__addr {
  color: var(--fg);
  font-size: var(--fs-row);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.place__addr span {
  display: block;
  max-width: 46ch;
  margin-top: 0.85rem;
  color: var(--fg-dim);
  font-size: var(--fs-body);
  line-height: 1.5;
  letter-spacing: 0;
}

.place__note {
  color: var(--fg-dim);
  font-size: var(--fs-body);
}

@media (min-width: 62rem) {
  .strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- 6 · FAQ --------------------------------------------------------------
   Same hairline row stack as the offers and the For who items: questions run
   the full width of the page's one container, on the same left edge, under
   their own title. */
.faq__item { border-top: 1px solid var(--rule); }
.faq__item:last-child { border-bottom: 1px solid var(--rule); }

.faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  width: 100%;
  padding: clamp(1.1rem, 1.8vw, 1.45rem) 0;
  border: 0;
  background: none;
  color: var(--fg);
  font-family: inherit;
  font-size: var(--fs-row);
  font-weight: 400;
  line-height: 1.1;
  letter-spacing: -0.022em;
  text-align: left;
  cursor: pointer;
  transition: color 0.16s ease;
}

/* :focus-visible for the same reason as .btn, and here it is load-bearing rather
   than cosmetic.  Now that a question toggles only its own panel, several can be
   open at once — so a gold colour left behind by the last mouse click marks one
   open question and not the others, and the colour starts reporting "last
   clicked" while looking like "open".  The +/- marker is the open/closed
   affordance and it is driven by aria-expanded, so it stays truthful. */
.faq__q:hover,
.faq__q:focus-visible { color: var(--accent); }

.faq__mark {
  position: relative;
  flex: none;
  width: 0.875rem;
  height: 0.875rem;
}

.faq__mark::before,
.faq__mark::after {
  content: '';
  position: absolute;
  top: 50%;
  right: 0;
  left: 0;
  height: 2px;
  margin-top: -1px;
  background: currentColor;
}

.faq__mark::after {
  transform: rotate(90deg);
  transition: transform 0.25s ease, opacity 0.25s ease;
}

.faq__q[aria-expanded='true'] .faq__mark::after {
  opacity: 0;
  transform: rotate(0deg);
}

/* grid-template-rows 0fr → 1fr, per the motion contract §3.  It is the one
   place on this site allowed to animate a layout property, because it is
   user-initiated and cannot move text the visitor is already reading.  It
   replaces a max-height transition that had to guess a ceiling.

   visibility is what actually removes the answer from the accessibility tree —
   a collapsed grid row still leaves the text live otherwise, and three buttons
   would be reporting aria-expanded="false" over prose a screen reader is
   happily reading out.  It is transition-safe: an interpolation with a visible
   endpoint stays visible for the whole duration. */
/* THE OPEN STATE IS THE DEFAULT, and that is not a style preference — it is
   SKILL.md §2, the rule the whole motion contract rests on: CSS defines the
   final state, JavaScript sets the initial one.  This block previously shipped
   `grid-template-rows: 0fr; visibility: hidden` and relied on a JS-added
   `.is-open` to reveal, which is the exact inversion §2 calls a gate failure.
   The cost was not theoretical: with JS off, blocked or slow, three of the four
   answers were in the DOM and unreachable — the group-payment rule that
   handoff §6 says appears NOWHERE ELSE in this build, the 12-hour cancellation
   expectation, and the "same price" indoor guarantee.

   So: open by default, and `.is-shut` — added by JS at runtime, never present
   in the markup — is the closed state.  No JavaScript now means four readable
   answers rather than one. */
.faq__a {
  display: grid;
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows 0.32s ease, visibility 0.32s ease;
}

.faq__a.is-shut {
  grid-template-rows: 0fr;
  visibility: hidden;
}

/* Applied by JS for exactly one frame, around the initial close. Panels default
   to open, so without this the three that start shut animate their collapse on
   every load. */
.faq.no-anim .faq__a { transition: none; }

/* 48ch, down from 58.  §4 caps the measure at 65 CHARACTERS and `ch` is not a
   character: it is the advance of "0", which in Archivo is wider than the
   average lowercase letter.  58ch was setting lines of 74-75 real characters
   from 768px up - measured on the rendered text, not estimated - so the longest
   prose on the page was the one block over the limit.  48ch measures 62. */
.faq__a p {
  min-height: 0;
  max-width: 48ch;
  overflow: hidden;
  color: var(--fg-dim);
  font-size: var(--fs-body);
  line-height: 1.6;
}

.faq__a.is-open p { padding-bottom: clamp(1.1rem, 1.8vw, 1.45rem); }

/* --- 7 · Closing CTA ------------------------------------------------------
   The whole page exists to arrive here, and this is the section that used to
   read as the weakest thing on it.  Three faults, all composition, all fixed
   below without adding an element, a photograph or a word of copy.

   1 · It centred itself.  It was the only block on the page that left the
       x=160 left rail, with nothing else centred to justify the switch, so the
       close read as an afterthought pinned to the middle of an empty band
       rather than as the end of an argument.  It is back on the rail every
       other section starts on.

   2 · Its title was 84px - the same shout as the five titles above it and as
       the hero, which is exactly the flatness this revision's type scale went
       after.  It is the one title on the page set from `close`: 72px at 1440,
       audibly under the 100px hero and half again over the 44px heads.  Scale
       returns exactly once, here, at the moment the page asks.

   3 · The button was SMALLER than the controls it was meant to close over -
       332x64 = 21,241px² against 536x53 = 28,300 for a coach Book.  The last
       control on the page is now the largest on it: 432x72 = 31,104 at 1440,
       and the full column at 375.  It grows by area rather than by type size,
       so the section still runs the two steps §4 asks for - `close` and the
       14px micro-caps every other button and support line on the page uses. */
.sect--cta .wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.sect--cta .sect__title {
  margin-bottom: clamp(1.5rem, 2.6vw, 2.25rem);
  font-size: var(--fs-close);
}

.sect--cta .risk { margin-top: 1.25rem; }

/* --- Footer --------------------------------------------------------------- */
.foot {
  padding-block: clamp(2.5rem, 4vw, 3.5rem);
  border-top: 1px solid var(--rule);
}

.foot__in {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem 2rem;
}

.foot__name {
  color: var(--fg);
  font-weight: 700;
  letter-spacing: -0.015em;
}

/* --- Reduced motion -------------------------------------------------------
   Every entrance and scroll reveal is already off: the script returns before
   it writes a single initial state, so nothing was ever moved and there is
   nothing to turn off here.  What remains are the CSS transitions — hover
   fills, the accordion, the nav Book fade — and §5 of the contract says those
   become instant colour changes rather than shortened movements. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}
