/* ============================================================================
   fb-polish.css — Fast Basketball
   Load LAST, after tokens.css, fonts.css, base.css, site.css, features.css.

   Two jobs, no copy or markup changes:
     PART 1  Rebuilds the intro lockup choreography (.alogo.play): FAST slams
             in, the stage and glow recoil off the hit, and the crescent sweeps
             out of the strike point along its own line. Transform + opacity
             only: the old clip-path reveal on a 23,770-char path is gone.
     PART 2  iPhone-Safari and large-screen corrections.

   Every rule is scoped to an existing class. Nothing here needs a rebuild of
   the templates. Pair with the two-line change in js/main.js (see patch notes)
   so the JS dismissal timer reads --t-intro instead of a hard-coded 3100ms.
   ========================================================================= */

/* ---------------------------------------------------------------- tokens --- */
:root{
  --ease-strike:cubic-bezier(.62,0,.36,1);    /* accelerate in, hard stop     */
  --ease-whip:cubic-bezier(.14,.92,.22,1);    /* fast out, long settle        */
  --ease-recoil:cubic-bezier(.34,1.42,.44,1); /* overshoot, for impacts       */
  --t-intro:2.5s;                             /* MINIMUM intro display time */
}

/* ============================================================================
   PART 1 — THE SWOOSH BUILD
   Geometry (viewBox 0 0 1537.26 649.16, measured off logo-anim.svg):
     FAST row      y  24-294   speed lines x 24-331, F 155-686, A 434-841, ST 784-1515
     BASKETBALL    y 344-386   x 358-1204, rules at x 168-319 and 1246-1388
     crescent      y 411-625   x 454-1080  — left tip low, right tip high
   FAST is thrown upward into place, the stage and glow recoil off that hit,
   and the crescent sweeps out of the strike point along its own line — a
   whip, not a curtain. Nothing is revealed by a wipe. After the build the
   stage holds on a slow breathe until the page's critical assets are in, so
   the intro doubles as the loading cover.
   ========================================================================= */

.alogo{position:relative;}
.alogo.play .al-cres{will-change:transform,opacity;}

/* --- the crescent sweeps out of the strike point, left tip first --------- */
.alogo .al-cres{transform-origin:0% 50%;}
.alogo.play .al-cres{animation:alSwoosh .64s var(--ease-whip) .30s both;}
@keyframes alSwoosh{
  0%  {opacity:0;transform:translateX(-2%) scaleX(.03) scaleY(.6) rotate(-5deg);}
  14% {opacity:1;}
  74% {transform:scaleX(1.05) scaleY(1.02) rotate(1.1deg);animation-timing-function:var(--ease-recoil);}
  100%{opacity:1;transform:none;}
}

/* --- FAST is knocked up and out of the impact, not slid in from the left -- */
.alogo.play .al-f {--dx:-15%;animation:alLetterHit .54s var(--ease-recoil) .10s both;}
.alogo.play .al-a {--dx:0%;  animation:alLetterHit .54s var(--ease-recoil) .145s both;}
.alogo.play .al-st{--dx:13%; animation:alLetterHit .54s var(--ease-recoil) .19s both;}
@keyframes alLetterHit{
  0%  {opacity:0;transform:translate(var(--dx,0),48%) skewX(-11deg) scale(.9);}
  58% {opacity:1;transform:translate(0,-5%) skewX(2deg) scale(1.02);}
  100%{opacity:1;transform:none;}
}

/* --- speed lines are the exhaust of the hit, and the one live beat in the
       hang: two gusts, the second wider, so 1.4s-2.3s is never a dead frame - */
.alogo.play .al-ln{
  animation:
    alLineOut .46s var(--ease-whip) both,
    alGustA .40s ease-in-out,
    alGustB .52s ease-in-out;
  animation-delay:
    calc(.16s + var(--i,0)*.034s),
    calc(1.02s + var(--i,0)*.028s),
    calc(1.68s + var(--i,0)*.038s);
}
@keyframes alLineOut{
  0%  {opacity:0;transform:translateX(-46%) scaleX(3.2);}
  55% {opacity:1;}
  100%{opacity:1;transform:none;}
}
@keyframes alGustA{0%,100%{transform:none;}45%{transform:translateX(-6%) scaleX(1.18);}}
@keyframes alGustB{0%,100%{transform:none;}45%{transform:translateX(-11%) scaleX(1.34);opacity:.72;}}

/* --- the A-notch keeps its cross-axis strike, retimed to read after impact  */
.alogo.play .al-notch{animation:alNotchHit .38s var(--ease-recoil) .26s both;}
@keyframes alNotchHit{
  0%  {opacity:0;transform:translate(88%,-96%) rotate(14deg);}
  64% {opacity:1;transform:translate(-5%,4%) rotate(-2deg);}
  100%{opacity:1;transform:none;}
}

/* --- rules wipe outward, BASKETBALL rises between them ------------------- */
.alogo.play .al-rule{animation:alRuleOut .44s var(--ease-whip) .56s both;}
@keyframes alRuleOut{0%{opacity:0;transform:scaleX(0);}100%{opacity:1;transform:none;}}
.alogo.play .al-wl{animation:alWordUp .40s var(--ease-recoil) both;animation-delay:calc(.60s + var(--i,0)*.026s);}
@keyframes alWordUp{
  0%  {opacity:0;transform:translateY(74%) scaleY(.86);}
  62% {opacity:1;transform:translateY(-9%) scaleY(1.03);}
  100%{opacity:1;transform:none;}
}

/* --- stage: pushed in, snapped by the impact, then held on a slow breathe
       for as long as loading needs — the exit is a separate animation fired
       by .done, so the overlay genuinely covers the page load. No blur: the
       old scale(8)+blur(14px) was a full-screen filter on exit. ------------ */
.intro-mark{
  animation:introBuild 1.1s var(--ease-strike) both,introBreathe 3.6s ease-in-out 1.1s infinite;
  will-change:transform,opacity;
}
@keyframes introBuild{
  0%  {transform:scale(1.075);animation-timing-function:var(--ease-strike);}
  22% {transform:scale(1.052);}
  32% {transform:scale(1);animation-timing-function:var(--ease-recoil);}   /* impact */
  41% {transform:scale(.988);}
  55% {transform:scale(1.006);}
  100%{transform:scale(1);}
}
@keyframes introBreathe{0%,100%{transform:scale(1);}50%{transform:scale(1.014);}}
#intro.done .intro-mark{animation:introOut .55s var(--ease-launch) both;}
@keyframes introOut{0%{transform:scale(1);opacity:1;}100%{transform:scale(2.15);opacity:0;}}

/* --- the glow is the impact flash, then a low pulse for the hold. -------- */
.intro-glow{
  width:92vmax;height:92vmax;
  animation:introGlowIn 1.3s var(--ease-rush) both,introGlowPulse 3.6s ease-in-out 1.3s infinite;
}
@keyframes introGlowIn{
  0%  {transform:scale(2.6);opacity:0;}
  18% {opacity:.24;}
  27% {transform:scale(.88);opacity:.95;}
  60% {transform:scale(1.06);opacity:.4;}
  100%{transform:scale(1.1);opacity:.34;}
}
@keyframes introGlowPulse{0%,100%{opacity:.34;transform:scale(1.1);}50%{opacity:.26;transform:scale(1.16);}}

/* --- tagline gets a drawn rule so it lands rather than just fading ------- */
.intro-mark .im-sub{position:relative;animation:imSubV2 .7s var(--ease-rush) .92s both;}
@keyframes imSubV2{0%{opacity:0;letter-spacing:.34em;}100%{opacity:1;letter-spacing:.5em;}}
.intro-mark .im-sub::after{
  content:"";position:absolute;left:50%;bottom:-14px;width:64px;height:2px;margin-left:-32px;
  background:var(--fast-red);transform:scaleX(0);
  animation:imRule .5s var(--ease-whip) 1.12s both;
}
@keyframes imRule{to{transform:none;}}

/* --- the mark needs presence on a phone: 283px was under the brand floor -- */
@media(max-width:860px){.intro-mark .im-logo{width:min(86vw,430px);}}

/* ============================================================================
   PART 2 — iPHONE 16 SAFARI + DESKTOP CORRECTIONS
   ========================================================================= */

/* 1. iOS dynamic toolbar. 100vh is the LARGE viewport on iOS Safari, so the
      intro overlay and the hero were both taller than the visible area — the
      Skip button and the hero CTA sat under the toolbar on first paint. */
body.intro-locked{height:100dvh;}
.hero{min-height:100svh;}

/* 2. The sticky mobile CTA bar pads itself for the home indicator; body did
      not, so the last ~34px of the footer sat under the bar on an iPhone 16. */
@media(max-width:860px){
  body{padding-bottom:calc(70px + env(safe-area-inset-bottom));}
}

/* 3. Landscape notch: the shell was a flat 24px, which puts copy under the
      rounded corner when an iPhone is turned sideways. */
.shell{
  padding-left:max(24px,env(safe-area-inset-left));
  padding-right:max(24px,env(safe-area-inset-right));
}

/* 4. THE BIG ONE. Inputs were .97rem = 15.5px. Under 16px, iOS Safari zooms
      the whole page when a field takes focus — every form on the site did it:
      tap the email box and the layout jumps. 16px stops it. */
@media(pointer:coarse){
  .fld input,.fld select,.fld textarea,
  .adm-f input,.adm-f textarea{font-size:16px;}
}

/* 5. iOS paints a translucent grey rectangle over any tapped link or button.
      On near-black bands that reads as a rendering fault. Kill it and let the
      real press state do the work — which the DS specifies as a compression
      the ghost and locker buttons never got. */
a,button,[role=button],summary{-webkit-tap-highlight-color:transparent;}
/* Two-word button labels ("Slow Motion", "Check Availability") wrapped inside the
   pill because .btn never set white-space. A pill is a single-line component. */
.btn{white-space:nowrap;}
.btn-primary:active{transform:translateY(-1px) scale(.97);}
.btn-ghost:active{border-color:var(--fast-red);transform:translateY(-1px) scale(.97);}
.res-get:active:not(:disabled){transform:translateY(-1px) scale(.97);}
.area:active{transform:translateY(-1px) scale(.98);}

/* 6. The mobile nav drawer had no height limit. Seven links plus padding
      overflow a landscape iPhone with no way to reach the last two. */
@media(max-width:860px){
  .nav-links{
    max-height:calc(100dvh - var(--nav-h));
    overflow-y:auto;overscroll-behavior:contain;
    -webkit-backdrop-filter:blur(16px);
    padding-bottom:calc(26px + env(safe-area-inset-bottom));
  }
}

/* 7. Micro-label floor. A cluster of uppercase tracked labels shipped between
      9.9px and 10.6px — legible on a 3x phone, not on a 27" monitor at 100%.
      Nothing under 11px now, and the tightest tracking is loosened. */
.res-lock,.res-open{font-size:.7rem;}
.res-meta{font-size:.69rem;}
.cc-stat span{font-size:.69rem;letter-spacing:.11em;}
.cc-label{font-size:.7rem;}
.cc-cap em{font-size:.7rem;}
.prog-tag{font-size:.7rem;}
.rcp-yr{font-size:.72rem;}
/* #82828F on court-black measured 4.76:1 at 11.8px uppercase — technically AA,
   practically hard work. #9A9AA8 takes it to 6.6:1. */
.trust-i span{color:#9A9AA8;}

/* 8. .hollow is a 2px text-stroke. At the desktop h1 (90px) that is 2% of cap
      height; at the mobile floor (41.6px) it is 4.8% and the letterforms clog.
      Scale the stroke with the type. */
@media(max-width:640px){.hollow{-webkit-text-stroke-width:1.6px;}}
@media(max-width:400px){.hollow{-webkit-text-stroke-width:1.35px;}}

/* 9. At one column every method step kept its right-hand hairline, drawing a
      stray rule down the right edge of the stack. */
@media(max-width:520px){
  .mth-s{border-right:0;}
  .mth-s:last-child{border-bottom:0;}
}

/* 10. Four 4:5 résumé cards stacked one-up ran the section past 1700px on a
       phone. A 16:11 crop keeps every caption and cuts it roughly in half. */
@media(max-width:520px){
  .rcp-shot{aspect-ratio:16/11;}
  .rcp-shot img{object-position:50% 32%;}
}

/* 11. Laptop fold. On a 1440x900 screen the hero ran ~1100px tall, so the h1,
       the CTA and the championship photo could never be seen together. Keyed
       to viewport HEIGHT, so nothing changes on a tall monitor. */
@media (min-width:1041px) and (max-height:900px){
  .hero{padding-top:calc(var(--nav-h) + 22px);padding-bottom:34px;}
  .hero-cta{margin:24px 0 22px;}
  .hero-trust{padding-top:20px;}
  .cc-shot{aspect-ratio:4/3;}
  .cc-shot img{height:100%;object-fit:cover;object-position:50% 30%;}
}

/* 12. Very wide screens: 1240px of content in a 2560px black field left the
       résumé row reading as four narrow slivers. The shell stays 1240 (it is a
       brand decision) but the gallery gets air back. */
@media(min-width:1600px){
  .rcp{gap:20px;}
  .head{max-width:820px;}
}

/* 13. The mobile Swish flash was centred with margin-left:-120px against a
       ~150px string, so it sat 30px right of centre. */
@media(max-width:960px){
  .swish-flash{left:50%;margin-left:0;transform:translateX(-50%);}
  .swish-flash.on{animation:nSwishInM .9s var(--ease-reveal);}
}
@keyframes nSwishInM{
  0%  {opacity:0;transform:translateX(-50%) scale(.6);}
  20% {opacity:1;transform:translateX(-50%) scale(1.06);}
  70% {opacity:1;transform:translateX(-50%) scale(1);}
  100%{opacity:0;transform:translateX(-50%) scale(1.15);}
}

/* 14. Long-press callout on the interactive night-court stage felt broken. */
.nite-stage,.nball,.hoop{-webkit-touch-callout:none;user-select:none;}

/* 15. THE NIGHT COURT WAS AMPUTATED ON MOBILE.
       features.css hides .hoop, .nite-guide and .nball below 960px, so a phone
       got the copy, a "Take the Shot" button, and a Swish that flashes with no
       ball, no rim and no court. That is a broken feature, not a responsive
       adaptation — and phones are most of this audience.

       The desktop scene cannot reflow: .nball's offset-path is authored in
       absolute px against a 1240x560 box. So the court gets its own portrait
       layout at phone size, with its own arc.

       MARKUP: wrap the court layers (.nite-city, .nite-floor, .nite-guide,
       .hoop, .nball, .swish-flash) in <div class="nite-court"> inside
       .nite-stage, leaving .nite-copy as a sibling. Nothing else changes. */
.nite-court{position:absolute;inset:0;}
@media(max-width:960px){
  .nite-stage{height:auto;padding:0;cursor:auto;display:flex;flex-direction:column;}
  .nite-copy{position:static;left:auto;top:auto;margin:0 24px;max-width:60ch;order:1;}
  .nite-court{
    position:relative;inset:auto;order:2;
    width:320px;height:400px;margin:36px auto 0;cursor:pointer;
    transform:scale(var(--court-k,1));transform-origin:top center;
    margin-bottom:calc(400px * (var(--court-k,1) - 1));
  }
  .hoop,.nball{display:block;}
  .nite-guide{display:none;}          /* authored for the 1240px stage */
  .nite-city{left:0;right:0;bottom:58px;height:118px;gap:4px;}
  .nite-floor{height:58px;}
  .nite-horizon{bottom:0;height:200px;}
  .hoop{left:auto;right:2px;top:18px;width:104px;transform:scale(.82);transform-origin:top right;}
  .nball{offset-path:path("M 45 301 Q 150 6 282 86 L 284 140");width:34px;height:34px;}
  .nball:not(.idle){left:0;top:0;}
  .nball.idle{offset-path:none;left:28px;top:284px;}
  .swish-flash{left:50%;top:150px;bottom:auto;margin-left:0;transform:translateX(-50%);font-size:2.4rem;}
}
@media(min-width:561px) and (max-width:960px){.nite-court{--court-k:1.3;}}

/* 17. NIGHT COURT FEEL. (a) The make felt thin: the whole net skewed once. It
       now snaps down under the ball and ripples through three decaying swings,
       and the rim pops with a hot flash. (b) The ball was a flat gradient
       circle with two straight seams; the markup now carries a real seamed
       ball SVG inside .nball i — these rules retire the old pseudo-element
       seams. Red ramp tokens only. */
.net.sway{animation:nNetSwish .9s var(--ease-whip);}
@keyframes nNetSwish{
  0%  {transform:none;}
  16% {transform:scaleY(1.24) scaleX(.86) translateY(3px);}
  36% {transform:scaleY(1.05) skewX(7deg);}
  56% {transform:scaleY(1.1) skewX(-5deg);}
  76% {transform:scaleY(1.02) skewX(2deg);}
  100%{transform:none;}
}
.rim.hit{animation:nRimPop .55s var(--ease-whip);}
@keyframes nRimPop{
  30% {transform:translateY(3px);box-shadow:0 0 30px rgba(255,58,65,.95);}
}
.nball i{background:none;}
.nball i::before,.nball i::after{display:none;}
.nball svg{display:block;width:100%;height:100%;}

/* 18. SLINGSHOT. The shot is aimed now: grab the ball, pull back, release.
       All geometry is rect-derived in js/night-court.js; these are just the
       affordances. Red ramp + chalk white only. */
.nball{pointer-events:auto;touch-action:none;cursor:grab;}
.nball:active{cursor:grabbing;}
.nball::after{content:"";position:absolute;inset:-22px;border-radius:50%;}
.nite-sling{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:3;}
.nite-sling line{stroke:rgba(255,58,65,.9);stroke-width:3;stroke-linecap:round;stroke-dasharray:5 7;}
.nite-sling .dot{fill:rgba(255,255,255,.55);}

/* 19. LIGHT MODE — the default. Dark stays one tap away (theme button by the
       hamburger, stored in fb_theme). The Night Court and the intro overlay
       stay night on purpose. Palette values only; teal is darkened along its
       own hue for light surfaces (logged per the DS iconography rule). */
.only-light{display:none;}
html.fb-light .only-light{display:block;}
html.fb-light .only-dark{display:none;}
/* The toggle must HUG the right-side cluster, not take its own distributed
   slot in a space-between bar: switch the bar to flex-start and push with
   auto margins (links on desktop; the toggle itself once links are a drawer). */
.nav-in{justify-content:flex-start;}
.nav-links{margin-left:auto;margin-right:14px;}
@media(max-width:1040px){.theme-btn{margin-left:auto;}}
.theme-btn{width:44px;height:44px;display:grid;place-items:center;color:var(--bone);border-radius:50%;transition:color .2s,transform .25s var(--ease-rush);}
.theme-btn:hover{color:var(--red-hot);transform:rotate(18deg);}
.theme-btn .ic-moon{display:none;}
html.fb-light .theme-btn{color:var(--court-black);}
html.fb-light .theme-btn .ic-moon{display:block;}
html.fb-light .theme-btn .ic-sun{display:none;}

html.fb-light body{background:var(--bone);color:var(--court-black);}
html.fb-light .band-dark:not(.nite){background:var(--bone);color:var(--court-black);}
html.fb-light .band-ink{background:var(--chalk);color:var(--court-black);}
html.fb-light .band-dark.nite{color:var(--bone);}
html.fb-light .nite .lede{color:#C9C9D2;}
html.fb-light .nite .trust-line{color:#8F8F9C;}
html.fb-light .hollow{filter:none;}
html.fb-light .lede{color:var(--text-body-light);}
html.fb-light .band-red .lede{color:var(--bone);}
html.fb-light .nav.stuck{background:rgba(245,243,239,.9);border-bottom-color:var(--line-light);}
html.fb-light .nav-links a{color:var(--text-soft-light);}
html.fb-light .nav-links a:hover{color:var(--court-black);}
html.fb-light .brand-name{color:var(--court-black);}
html.fb-light .nav-toggle i,html.fb-light .nav-toggle i::before,html.fb-light .nav-toggle i::after{background:var(--court-black);}
html.fb-light .intro-skip{color:#9C9CA9;}
@media(max-width:1040px){
  html.fb-light .nav-links{background:rgba(245,243,239,.97);border-bottom-color:var(--line-light);}
  html.fb-light .nav-links a{border-bottom-color:var(--line-light);}
}
html.fb-light .hero-court{background-image:linear-gradient(rgba(10,10,12,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(10,10,12,.05) 1px,transparent 1px);}
html.fb-light .hero-wash{display:none;}   /* red blush reads as a stain on white */
html.fb-light .hero-trust{border-top-color:var(--line-light);}
html.fb-light .trust-i span{color:var(--text-soft-light);}
html.fb-light .trust-line{color:var(--text-soft-light);}
html.fb-light .court-card{background:var(--bone);border-color:var(--chalk-2);box-shadow:var(--shadow-light);}
html.fb-light .cc-label{color:var(--text-soft-light);border-bottom-color:var(--line-light);}
html.fb-light .cc-foot{border-top-color:var(--line-light);}
html.fb-light .cc-stat{border-right-color:var(--line-light);}
html.fb-light .cc-stat b{color:var(--court-black);}
html.fb-light .cc-stat span{color:var(--text-soft-light);}
html.fb-light .rcp-note{color:var(--text-soft-light);border-top-color:var(--line-light);}
html.fb-light .rcp-note a{color:var(--red-deep);}
html.fb-light .rcp-next{background:var(--bone);border-color:var(--chalk-2);}
html.fb-light .rcp-next p{color:var(--text-body-light);}
html.fb-light .rcp-next p b{color:var(--court-black);}
html.fb-light .aud-c{background:var(--bone);border-color:var(--chalk-2);}
html.fb-light .aud-c:hover{background:var(--bone);border-color:rgba(230,12,32,.45);}
html.fb-light .aud-c p{color:var(--text-soft-light);}
html.fb-light .mth{border-top-color:var(--line-light);}
html.fb-light .mth-s{border-right-color:var(--line-light);border-bottom-color:var(--line-light);}
html.fb-light .mth-s:hover{background:var(--bone);}
html.fb-light .mth-s p{color:var(--text-soft-light);}
html.fb-light .coach-img{border-color:var(--chalk-2);background:var(--chalk);}
/* Same trap as .cc-cap: the badge chip is court-black in BOTH themes, but its
   name inherited the theme text colour, so light mode painted "Blake Kingsley"
   #0A0A0C on #0A0A0C — the name plate rendered blank. */
.coach-badge b{color:var(--bone);}
html.fb-light .coach-body p{color:var(--text-body-light);}
html.fb-light .coach-body p:first-of-type{color:var(--court-black);}
html.fb-light .cred{border-color:var(--line-light);}
html.fb-light .cred span{color:var(--slate);}
html.fb-light .pb-form{background:var(--bone);border-color:var(--chalk-2);box-shadow:var(--shadow-light);}
html.fb-light .fld label{color:var(--text-soft-light);}
html.fb-light .fld input,html.fb-light .fld select,html.fb-light .fld textarea{background:var(--bone);border-color:var(--chalk-2);color:var(--court-black);}
html.fb-light .fld input::placeholder,html.fb-light .fld textarea::placeholder{color:var(--slate);}
html.fb-light .pb-form .fld label{color:var(--text-body-light) !important;}
html.fb-light .pb-note{color:var(--text-soft-light);}
html.fb-light .pb-list a{color:var(--red-deep);}
html.fb-light .pb-out p{color:var(--text-body-light);}
html.fb-light .res-c{background:var(--bone);border-color:var(--chalk-2);}
html.fb-light .res-c p{color:var(--text-soft-light);}
html.fb-light .res-lock{color:var(--slate);}
html.fb-light .res-get{border-color:var(--chalk-2);}
html.fb-light .res-open,html.fb-light .openres .res-get{color:#0C7C74;}
html.fb-light .gbp p{color:var(--text-soft-light);}
html.fb-light .ct-r{border-bottom-color:var(--line-light);}
html.fb-light .ct-r b{color:var(--slate);}
html.fb-light .btn-ghost{border-color:var(--line-light);color:var(--court-black);}
html.fb-light .nite .btn-ghost{border-color:var(--line-dark);color:var(--bone);}
html.fb-light .ft{background:var(--chalk);border-top-color:var(--line-light);}
html.fb-light .ft-col h3{color:var(--slate);}
html.fb-light .ft-col a{color:var(--text-soft-light);}
html.fb-light .ft-col a:hover{color:var(--red-deep);}
/* Small red labels on light surfaces. Red-hot is a dark-theme colour: 3.5:1 on the white
   court card, the Locker meta and its button, 3.2:1 on the tinted review box (4.5 needed).
   The deep red this theme already gives links clears 8:1; the Locker's unlocked teal-dark
   rule above is more specific and still wins. The flag card turns white in this theme but
   kept its dark-card caption grey (3.4:1); slate is what its sibling cards use. */
html.fb-light .cc-label b,html.fb-light .res-meta,html.fb-light .gbp b,html.fb-light .res-get,html.fb-light .rv-more,html.fb-light .enr-bring b{color:var(--red-deep);}
html.fb-light .prog-c.flag .prog-price small{color:#8A8A96;}
/* The training-page hero caption used to carry this grey inline, so it stayed a dark-band
   grey after the light theme turned the band white (3.4:1). Dark keeps it; light gets the
   caption colour the homepage cards use. */
.suburb-hero .prog-price{color:var(--bone);}
html.fb-light .suburb-hero .prog-price{color:var(--court-black);}
.suburb-hero .prog-price small{color:#8A8A96;}
html.fb-light .suburb-hero .prog-price small{color:var(--slate);}
html.fb-light .ft-bot{color:var(--slate);border-top-color:var(--line-light);}
html.fb-light .ft p{color:var(--text-soft-light) !important;}
html.fb-light .mob-bar{background:rgba(245,243,239,.97);border-top-color:var(--line-light);}

/* 20. HERO MARK. The lockup sits right of the headline column on desktop
       (above the championship card) and above the headline on a phone.
       250px floor per the logo rules; theme-swapped red/white masters. */
.hero-grid{grid-template-areas:"copy mark" "copy card";grid-template-rows:auto 1fr;}
.hero-copy{grid-area:copy;}
.hero-grid .hero-mark{grid-area:mark;width:min(250px,80%);margin:0 auto 18px;align-self:center;opacity:0;animation:fadeSeq .7s .15s var(--ease-rush) forwards;}
.hero-grid .court-card{grid-area:card;}
.hero-mark img{width:100%;height:auto;display:block;}
@media(max-width:1040px){
  .hero-grid{grid-template-areas:"mark" "copy" "card";grid-template-rows:auto;}
  /* 58vw put the lockup at 218px on a 375px phone, under the 250px floor the logo
     rules set. The shell is 327px wide there, so the floor fits with room to spare. */
  .hero-grid .hero-mark{width:min(100%,250px);margin:0 0 6px;align-self:start;}
}

/* 16. Between 861px and 1040px — small laptops, split-screen windows, iPad in
       portrait — the seven nav links wrapped onto two lines and the "Free First
       Look" pill overflowed the shell, because base.css only switches to the
       hamburger at 860px. Move the switch to 1040px, where the hero, coach,
       playbook and contact grids already collapse, and bring the sticky CTA bar
       with it so the primary action is never the thing that disappears. */
.nav-links a{white-space:nowrap;}
@media(max-width:1040px){
  .nav-links{
    position:fixed;top:var(--nav-h);left:0;right:0;
    background:rgba(10,10,12,.97);
    backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
    flex-direction:column;gap:0;
    padding:14px 24px calc(26px + env(safe-area-inset-bottom));
    border-bottom:1px solid var(--line-dark);
    transform:translateY(-120%);visibility:hidden;
    transition:transform .45s var(--ease-rush),visibility .45s;
    max-height:calc(100dvh - var(--nav-h));
    overflow-y:auto;overscroll-behavior:contain;
  }
  .nav-links.open{transform:none;visibility:visible;}
  .nav-links a{width:100%;padding:14px 0;border-bottom:1px solid var(--line-dark);}
  .nav-toggle{display:grid;}
  .nav .btn-primary{display:none;}
  .mob-bar{display:flex;}
  body{padding-bottom:calc(70px + env(safe-area-inset-bottom));}
  .adm-fab{bottom:calc(70px + env(safe-area-inset-bottom) + 12px);}
}


/* 20. THE HERO LOCKUP DOUBLED IN DARK MODE. Section 15 added
       `.hero-mark img{display:block}` (0-1-1), which outranks the theme rule
       `.only-light{display:none}` (0-1-0) — so the red lockup painted UNDER the
       white one and the dark hero opened on two stacked logos. Scoping the
       theme rule to the theme root puts it back in front. With the duplicate
       gone the single mark sat alone at the top of a 229px row, so it now
       drops down to cap the card it belongs to and the hero sheds ~135px of
       empty column. */
html:not(.fb-light) .only-light{display:none;}
@media(min-width:1041px){
  .hero-grid{row-gap:28px;}
  .hero-grid .hero-mark{margin:clamp(40px,5.2vw,74px) auto 20px;align-self:end;}
}

/* 21. THE NIGHT COURT BALL WAS BEHIND THE COPY. `.nball.idle` sits at
       left:118px — inside the 440px copy column, which carries z-index:2. The
       one object the section asks you to grab was painted behind a paragraph.
       Move it out to mid-court, still on the floor line, and re-aim the dashed
       guide (markup, same 1240x520 space) along the exact parabola
       night-court.js solves — T = 0.85s, g = 2.7H — so the hint matches the
       shot it is hinting at. */
@media(min-width:961px){
  .nball.idle{left:540px;}
}

/* 22. THE SWISH. A make skewed the whole net once, as one card-like plane. Cord
       does not do that. The net is now four elements: the cords snap taut and
       flash white at the strike, then each ring bulges and drops in sequence so
       a wave runs top to bottom and the bottom ring flares last, as the ball
       leaves. The rim slams down into an expanding shock ring, and the word
       lands a beat later, once the ball is clear of the net. Transform, stroke
       and opacity only — nothing here triggers layout. */
.net{overflow:visible;}
.net .net-cord{transform-box:fill-box;transform-origin:50% 0;}
.net .net-ring{transform-box:fill-box;transform-origin:50% 50%;}
.net.sway{animation:nNetSwish .9s var(--ease-whip);}
@keyframes nNetSwish{
  0%  {transform:none;}
  9%  {transform:translateY(2px) scaleY(1.22) scaleX(.9);}
  28% {transform:scaleY(1.07) skewX(6deg);}
  50% {transform:scaleY(1.03) skewX(-4deg);}
  70% {transform:scaleY(1.01) skewX(2.2deg);}
  86% {transform:skewX(-1deg);}
  100%{transform:none;}
}
.net.sway .net-cord{animation:nCordSnap .86s var(--ease-whip);}
@keyframes nCordSnap{
  0%  {transform:none;stroke-width:1.5;stroke:rgba(255,255,255,.5);}
  12% {transform:scaleY(1.14);stroke-width:2.1;stroke:rgba(255,255,255,.95);}
  38% {transform:scaleY(.97);stroke-width:1.6;}
  62% {transform:scaleY(1.02);}
  100%{transform:none;stroke-width:1.5;stroke:rgba(255,255,255,.5);}
}
.net.sway .net-ring{animation:nRingWave .6s var(--ease-whip) both;}
.net.sway .r2{animation-delay:.06s;}
.net.sway .r3{animation-delay:.12s;}
@keyframes nRingWave{
  0%  {transform:none;stroke-width:1.5;stroke:rgba(255,255,255,.5);}
  20% {transform:translateY(4px) scaleX(1.18) scaleY(1.12);stroke-width:2.2;stroke:rgba(255,255,255,1);}
  46% {transform:translateY(1px) scaleX(.93);stroke-width:1.7;}
  72% {transform:translateY(1px) scaleX(1.04);}
  100%{transform:none;stroke-width:1.5;stroke:rgba(255,255,255,.5);}
}
.rim.hit{animation:nRimSlam .6s var(--ease-recoil);}
@keyframes nRimSlam{
  0%  {transform:none;box-shadow:0 0 18px rgba(230,12,32,.8);}
  20% {transform:translateY(4px) scaleX(1.05);box-shadow:0 0 38px rgba(255,58,65,1);}
  52% {transform:translateY(-1px);box-shadow:0 0 26px rgba(255,58,65,.9);}
  100%{transform:none;box-shadow:0 0 18px rgba(230,12,32,.8);}
}
.rim::after{content:"";position:absolute;left:50%;top:50%;width:36px;height:36px;margin:-18px 0 0 -18px;border-radius:50%;border:2px solid var(--red-hot);opacity:0;pointer-events:none;}
.rim.hit::after{animation:nShockRing .62s var(--ease-rush);}
@keyframes nShockRing{
  0%  {opacity:.9;transform:scale(.3);}
  100%{opacity:0;transform:scale(2.8);}
}
.swish-flash.on{animation:nSwishIn .8s .13s var(--ease-reveal) both;}
@media(prefers-reduced-motion:reduce){
  .net.sway,.net.sway .net-cord,.net.sway .net-ring,.rim.hit::after{animation:none;}
}
/* html[data-motion="off"] / html[data-night="off"] twin of the block above. */
html[data-motion="off"] .net.sway,html[data-motion="off"] .net.sway .net-cord,html[data-motion="off"] .net.sway .net-ring,html[data-motion="off"] .rim.hit::after,
html[data-night="off"] .net.sway,html[data-night="off"] .net.sway .net-cord,html[data-night="off"] .net.sway .net-ring,html[data-night="off"] .rim.hit::after{animation:none;}

/* 23. WCAG on the court-card caption: the "Horizon League Champions" label ran
       --red-hot on the near-black pill — 3.6:1 at 11px bold, under the 4.5:1
       floor for text that small. White on the same pill clears it at 19.6:1. */
.cc-cap em{color:var(--bone);}

/* 24. THE INTRO RAN 3.05s (2.5s hold + .55s exit). Retimed to land at ~1.43s
       end to end: the build is scaled to ~0.62x so FAST, the notch, the sweep
       and BASKETBALL all resolve by 0.78s, the hold drops to 0.95s, and the
       second "hang" gust is cut — with no hang left to fill, it was only ever
       there to keep 1.4-2.3s alive. Retimed in CSS, not with a playbackRate
       hack, so the shipped site's --t-intro dismissal matches exactly. */
/* Speed-routed: --t-intro is read live by main.js (getComputedStyle) so the JS
   dismissal timer follows automatically. The letter/stage sub-animations below
   (572-584) are scaled the same way so the build/exit choreography never runs
   longer than the dismissal timer that cuts it off at high speed multipliers. */
:root{--t-intro:calc(.95s / var(--motion-speed,1));}
.alogo.play .al-cres{animation:alSwoosh calc(.40s / var(--motion-speed,1)) var(--ease-whip) calc(.18s / var(--motion-speed,1)) both;}
.alogo.play .al-f {animation:alLetterHit calc(.34s / var(--motion-speed,1)) var(--ease-recoil) calc(.06s / var(--motion-speed,1)) both;}
.alogo.play .al-a {animation:alLetterHit calc(.34s / var(--motion-speed,1)) var(--ease-recoil) calc(.09s / var(--motion-speed,1)) both;}
.alogo.play .al-st{animation:alLetterHit calc(.34s / var(--motion-speed,1)) var(--ease-recoil) calc(.12s / var(--motion-speed,1)) both;}
.alogo.play .al-ln{
  animation:alLineOut calc(.29s / var(--motion-speed,1)) var(--ease-whip) both,alGustA calc(.30s / var(--motion-speed,1)) ease-in-out;
  animation-delay:calc((.10s + var(--i,0)*.021s) / var(--motion-speed,1)),calc((.52s + var(--i,0)*.018s) / var(--motion-speed,1));
}
.alogo.play .al-notch{animation:alNotchHit calc(.24s / var(--motion-speed,1)) var(--ease-recoil) calc(.16s / var(--motion-speed,1)) both;}
.alogo.play .al-rule{animation:alRuleOut calc(.27s / var(--motion-speed,1)) var(--ease-whip) calc(.35s / var(--motion-speed,1)) both;}
.alogo.play .al-wl{animation:alWordUp calc(.25s / var(--motion-speed,1)) var(--ease-recoil) both;animation-delay:calc((.37s + var(--i,0)*.016s) / var(--motion-speed,1));}
.intro-mark{animation:introBuild calc(.68s / var(--motion-speed,1)) var(--ease-strike) both,introBreathe calc(3.6s / var(--motion-speed,1)) ease-in-out calc(.68s / var(--motion-speed,1)) infinite;}
#intro.done .intro-mark{animation:introOut calc(.48s / var(--motion-speed,1)) var(--ease-launch) both;}

/* 25. THE COURT-CARD CAPTION FAILED ON BOTH COUNTS. The label is an
       inline-block pill inside a 1.05 line-height Anton block, so the headline
       ran straight through it — two strings on one line, neither readable. And
       the scrim under them faded out at 46%, leaving white display type sitting
       on the lit net and the ladder. Label to its own line; scrim carries a
       hard floor under the whole caption zone (>= .88 alpha where the text
       sits, so white clears 4.5:1 against any pixel of the photo). */
.cc-shot::after{background:linear-gradient(to top,rgba(10,10,12,.96) 0%,rgba(10,10,12,.88) 22%,rgba(10,10,12,.46) 46%,transparent 66%);}
.cc-cap em{display:block;width:fit-content;margin-bottom:8px;}
/* Light mode repainted the caption near-black — .cc-cap inherits the card's
   text colour, and the card turns dark-on-white. Text sitting on a photo scrim
   is white in BOTH themes; it is never theme text. */
.cc-cap{color:var(--bone);}

/* 26. The right column still read as a hole: a 250px lockup floating in a
       523px column with 65px of air above it. Fill the column instead of
       padding around it — the mark scales to 390px (well over the 250px
       floor) and the air above drops to 18px, so the hero loses ~25px and the
       lockup reads as the cap of the card rather than a stray object. */
@media(min-width:1041px){
  .hero-grid .hero-mark{width:min(390px,100%);margin:18px auto 16px;}
}

/* 27. FACES FIRST. Measured face boxes, as % of each source image (x0-x1 /
       y0-y1), read off a 5% grid laid over the originals:
         hero-nets       42-56 / 46-60   Blake under the net
         coach-portrait  41-58 /  7-24   Blake courtside
         resume-trophy   44-60 /  3-17   Blake + the Horizon League trophy
         resume-team     46-73 / 22-36   two faces: the player, then Blake
         resume-juco     30-73 / 12-26   two faces: the player, then Blake
         resume-work     58-70 / 35-45   Blake in the RMU weight room
       At 4:5 the résumé and coach photos are their NATIVE ratio (640x800,
       880x1100), so the desktop grid crops nothing. Every failure was in the
       narrower crops, and all of them are vertical: these photos are all
       taller than their crop boxes, so cover scales to width and only the Y
       term of object-position does anything.

   (a) Résumé cards on a phone (16:11) keep 55% of the source height, and one
       blanket 50% 32% put that window at 14-69% for all four. Checked crop by
       crop against the boxes above: it holds resume-team (faces 22-36) and
       resume-work (35-45) with room to spare, and frames both tighter than any
       nudge would — those two keep the blanket value on purpose. The other two
       sit high in their frames and were losing skulls. */
@media(max-width:520px){
  .rcp-shot{aspect-ratio:16/11;}
  .rcp-shot img{height:100%;object-fit:cover;}
  .rcp-shot img[src*="resume-trophy"]{object-position:50% 2%;}  /* cap at 3%  -> window 1-56 */
  .rcp-shot img[src*="resume-juco"]  {object-position:50% 8%;}  /* hair at 12% -> window 4-59 */
}

/* (b) The hero court card crops to 4:3 on a phone and on laptops under 900px
       tall. At the default 50% the face sat 43-68% down the frame — inside the
       caption scrim, which is already .38 alpha at 66% and climbing. 72% lifts
       him to 25-50%, clear of the scrim and of the two-line caption, and still
       holds the net, the scissors and the jersey.
       height + object-fit are repeated here ON PURPOSE. object-position is
       inert on an `height:auto` image, so if the earlier rule that sets cover
       ever loses (stale cached stylesheet, reordered link, a rebuild that drops
       item 11) this block silently does nothing and the card top-clips to the
       rim — which is exactly what shipped. It now stands on its own. */
@media(max-width:520px){
  .cc-shot img{height:100%;object-fit:cover;object-position:50% 72%;}
}
@media(min-width:1041px) and (max-height:900px){
  .cc-shot{aspect-ratio:4/3;}
  .cc-shot img{height:100%;object-fit:cover;object-position:50% 72%;}
}

/* (c) Hover zoom pushed through the faces. .rcp-shot img scales to 1.09 from
       the centre, which eats the outer 4% of the frame — exactly where the cap
       and forehead sit on the trophy card. Zoom from the face instead, so the
       photo grows around it and the face stays put. */
.rcp-shot img[src*="resume-trophy"]{transform-origin:52% 10%;}
.rcp-shot img[src*="resume-juco"]  {transform-origin:51% 19%;}
.rcp-shot img[src*="resume-team"]  {transform-origin:59% 29%;}
.rcp-shot img[src*="resume-work"]  {transform-origin:64% 40%;}
.cc-shot img{transform-origin:49% 53%;}
.coach-img img{transform-origin:50% 16%;}

/* ---------- Pricing grid, fine print, enrollment steps (programs.html / enroll.html) ---------- */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
.price-grid{width:100%;border-collapse:collapse;margin:0 0 22px;font-family:var(--font-body);}
/* Clearance around a price (Blake, 14 September 2026): the figure sat 10px under the row
   line, 3px over its wording and 8px from the next column. Now 14px above and below, 20px
   between columns, 6px to the wording, and the figure keeps a little air in its own line. */
.price-grid th,.price-grid td{padding:14px 20px 14px 0;text-align:left;border-bottom:1px solid rgba(255,255,255,.09);vertical-align:top;}
.price-grid th:last-child,.price-grid td:last-child{padding-right:0;}
.price-grid thead th{font-family:var(--font-eyebrow);font-weight:700;font-size:.66rem;letter-spacing:.18em;text-transform:uppercase;color:#8A8A96;}
.price-grid tbody th{font-family:var(--font-eyebrow);font-weight:700;font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;color:var(--bone);}
.price-grid td b{display:block;font-family:var(--font-num);font-size:1.55rem;line-height:1.1;color:var(--bone);letter-spacing:.02em;}
.price-grid td span{display:block;font-size:.76rem;line-height:1.45;color:#AFAFBB;margin-top:6px;max-width:30ch;}
/* At 375px the Unlimited column is 57px wide and "$1,000" at 1.55rem is 57px: no air at all
   (measured). A smaller figure and tighter row labels give it room without a second layout. */
@media(max-width:480px){
  .price-grid th,.price-grid td{padding:12px 12px 12px 0;}
  .price-grid td b{font-size:1.3rem;}
  .price-grid tbody th{font-size:.72rem;letter-spacing:.06em;}
}
/* The grid above is drawn for the dark membership card. The 1-on-1 card is white in BOTH
   themes (.band-light), so its copy of the grid takes dark ink unconditionally, and the figure
   takes court black, like every price on the site. */
.prog-private .price-grid th,.prog-private .price-grid td{border-bottom-color:var(--chalk-2);}
.prog-private .price-grid thead th{color:var(--slate);}
.prog-private .price-grid tbody th{color:var(--court-black);}
.prog-private .price-grid td b{color:var(--court-black);}
.prog-private .price-grid td span{color:var(--text-soft-light);}
/* The figure sat flush under the corner tag, and the 16ch title cap broke "1-on-1" at a
   hyphen once a price shared the row. The title takes its own width; on a phone the price
   wraps under it instead of squeezing it. */
.prog-private .prog-top{flex-wrap:wrap;}
.prog-private h3{max-width:none;}
.prog-private .prog-price{margin-top:14px;}
.fine-print{margin-top:48px;padding:30px 32px;border:1px solid var(--line-light);border-radius:var(--r-card-light);background:var(--bone);box-shadow:var(--shadow-light);}
/* The box is --bone in both themes, so the eyebrow must not inherit the dark section's bone
   text: in dark mode it measured white on white. */
.fine-print .eyebrow{margin-bottom:16px;color:var(--court-black);}
.fp-list{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px 28px;margin:0 0 18px;padding:0;}
.fp-list li{font-size:.92rem;color:var(--text-body-light);line-height:1.5;}
.fp-list b{color:var(--court-black);font-family:var(--font-eyebrow);font-weight:700;letter-spacing:.06em;text-transform:uppercase;font-size:.84rem;}
.fp-note{font-size:.9rem;color:var(--text-soft-light);max-width:70ch;margin:0;}
.fp-note a{color:var(--red-deep);text-decoration:underline;text-underline-offset:3px;display:inline-block;padding:10px 0;}
.mth.four{grid-template-columns:repeat(4,1fr);}
.enr-foot{display:flex;justify-content:space-between;align-items:center;gap:32px;flex-wrap:wrap;margin-top:36px;padding-top:28px;border-top:1px solid var(--line-dark);}
.enr-bring{max-width:58ch;}
.enr-bring b{display:block;font-family:var(--font-eyebrow);font-weight:700;font-size:.8rem;letter-spacing:.16em;text-transform:uppercase;color:var(--red-hot);margin-bottom:8px;}
.enr-bring p{font-size:.95rem;color:var(--text-body-dark);margin:0;}
.enr-cta{display:flex;gap:14px;flex-wrap:wrap;}
@media(max-width:1040px){.mth.four{grid-template-columns:repeat(2,1fr);}}
@media(max-width:640px){.mth.four{grid-template-columns:1fr;}.fine-print{padding:24px 20px;}}
html.fb-light .enr-foot{border-top-color:var(--line-light);}
html.fb-light .enr-bring p{color:var(--text-soft-light);}

/* ---------- The Evidence (hero): the word evaporates, the record appears ---------- */
.evd{position:relative;display:block;}
.evd-trigger{display:inline-flex;flex-direction:column;align-items:flex-start;gap:8px;background:none;border:0;padding:0;cursor:pointer;color:inherit;text-align:left;font:inherit;}
.evd-trigger:focus-visible{outline:3px solid var(--red-hot);outline-offset:6px;border-radius:4px;}
.evd-word{display:inline-block;white-space:nowrap;font-family:var(--font-display);font-size:clamp(1.9rem,4vw,3rem);line-height:1;text-transform:uppercase;letter-spacing:.02em;color:transparent;-webkit-text-stroke:2px var(--fast-red);filter:drop-shadow(0 0 14px var(--glow-red));transition:color var(--t-hover);}
.evd-trigger:hover .evd-word,.evd-trigger:focus-visible .evd-word{color:var(--fast-red);}
.evd-ch{display:inline-block;will-change:transform,opacity;}
.evd-ch.go{animation:evdGo .95s var(--ease-launch) forwards;animation-delay:var(--d,0ms);}
@keyframes evdGo{to{transform:translate(var(--dx,0),var(--dy,-60px)) scale(.55);opacity:0;filter:blur(7px);}}
.evd-p{position:absolute;width:4px;height:4px;border-radius:50%;background:var(--red-hot);box-shadow:0 0 8px var(--glow-red);pointer-events:none;opacity:0;animation:evdP 1.4s ease-out forwards;animation-delay:var(--d,0ms);}
@keyframes evdP{0%{opacity:1;transform:none}100%{opacity:0;transform:translate(var(--dx,0),var(--dy,-80px)) scale(.2)}}
.evd-hint{font-family:var(--font-eyebrow);font-weight:600;font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;color:var(--text-faint-dark);}
/* Author display on the trigger outranks the UA [hidden] rule, the same trap as .modal-ovl:
   without this the "Hover or tap" hint and the evaporated word kept their space above the record. */
.evd-trigger[hidden]{display:none;}
.evd-panel[hidden]{display:none;}
.evd-panel.in{animation:zrIn .7s var(--ease-reveal);}
/* The X borrows .modal-x for its strokes and 44px box; the panel is positioned so the
   box hangs off its own top-right corner, centred on the first line of the record, and
   the right padding keeps the 46ch line from running under it on a phone. */
.evd-panel{position:relative;padding-right:48px;}
.evd-close{top:-10px;right:0;background:none;border:0;padding:0;cursor:pointer;border-radius:4px;}
.evd-close:focus-visible{outline:3px solid var(--red-hot);outline-offset:2px;}
.evd-line{font-size:1rem;line-height:1.55;color:var(--text-lede-dark);margin:0 0 16px;max-width:46ch;}
.evd-stats{display:flex;gap:26px;flex-wrap:wrap;}
html.fb-light .evd-hint{color:var(--text-soft-light);}
html.fb-light .evd-line{color:var(--text-body-light);}
html.fb-light .evd-word{filter:none;}
@media(prefers-reduced-motion:reduce){.evd-ch.go,.evd-p,.evd-panel.in{animation:none;}}

/* ---------- Ticker: idle technique cues by default, the record while The Evidence is open ----------
   Both tracks share one grid cell. The idle track stacks its cues in one cell too (grid-area 1/1), so
   the band is as tall as the longest cue and never moves on rotation; in record mode the idle track
   is hidden by visibility, not display, so the height holds across the mode switch as well.
   minmax(0,1fr) keeps the column at the band width: the record track is max-content wide and would
   otherwise stretch the column to the marquee's full length. The base marquee (base.css .ticker-track
   animation) still drives the record track, which is what the noscript block relies on. */
.ticker{display:grid;grid-template-columns:minmax(0,1fr);}
.ticker .ticker-track{grid-area:1/1;align-self:center;}
.ticker .tk-idle{display:grid;width:100%;animation:none;}
.ticker .tk-idle .ticker-i{grid-area:1/1;justify-self:center;text-align:center;white-space:normal;gap:0;opacity:0;transition:opacity .45s var(--ease-rush);}
.ticker .tk-idle .ticker-i::after{display:none;}
.ticker .tk-idle .ticker-i.show{opacity:1;}
.ticker[data-mode="idle"] .tk-record{display:none;}
.ticker[data-mode="record"] .tk-idle{visibility:hidden;}
/* The entrance rides the items, not the track: the track's transform belongs to the marquee. */
.ticker[data-mode="record"] .tk-record .ticker-i{animation:tickerIn .6s var(--ease-rush);}
@keyframes tickerIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:none}}
/* Phones: one size for BOTH tracks, so the longest idle cue fits on one line and the band is the
   same height in idle and record mode (a bigger record line would grow it 3px on the switch). */
@media(max-width:480px){.ticker .ticker-i{font-size:1rem;}}

/* Scoreboard tiles inside The Résumé (folded in from their own red band). */
.rcp-score+.score-src{margin-bottom:44px;color:var(--text-dim-dark);}
html.fb-light .score-c{background:#fff;border-color:var(--line-light);}
html.fb-light .rcp-score+.score-src{color:var(--text-soft-light);}
/* .band-light is light in BOTH themes (base.css), so --text-dim-dark inside one measured
   2.45:1 on the coach page. The homepage keeps the same selector inside a dark band, which is
   why this scopes by band rather than by theme. */
.band-light .rcp-score+.score-src{color:var(--text-soft-light);}

/* Per-session line under the membership price. */
.prog-per{font-size:.88rem;margin-top:-4px;}

/* Programs is three cards since the September 2026 sheet retired Private Small Group.
   .prog is a two-column grid, so the third card would sit alone in a half-width column
   with dead space beside it. Span it instead: the one on one term is the step up, and a
   full-width card reads as one. The single-column media query below 960px is unaffected. */
@media(min-width:961px){
  .prog-c:nth-child(3):last-child{grid-column:1/-1;}
  .prog-c:nth-child(3):last-child .prog-list{columns:2;column-gap:34px;}
  /* The spanning card is ~1130px wide; its paragraph ran 146 characters a line. */
  .prog-c:nth-child(3):last-child > p{max-width:64ch;}
}

/* ---------- September 2026 copy: multi-paragraph section intros, and the 1-on-1 card subtitle ---------- */
.head .lede + .lede{margin-top:14px;}
.prog-sub{font-family:var(--font-eyebrow);font-weight:600;font-size:.8rem;letter-spacing:.14em;text-transform:uppercase;color:var(--red-deep);margin:-4px 0 14px;}

/* 28. THE STACKED BAND SHOWED THE PHOTOS UNCROPPED AND FULL-WIDTH. The 4:3 crop in
       (b) above is scoped to phones, but the hero and coach grids collapse to one
       column at 1040px (base.css). Between 521 and 1040 — an iPad either way up, a
       split-screen laptop — the court card ran the whole shell: 705×1080 at 768 wide,
       961×1425 at 1024×768, close to two screens of one photo before a parent reaches
       the copy, and the coach portrait 961×1201. Crop the card the way the phone does
       and cap both boxes, so a 4:5 portrait stays a portrait rather than a wall. They
       sit left, on the same edge as the lockup and the copy above them. */
@media(max-width:1040px){
  .cc-shot{aspect-ratio:4/3;}
  .cc-shot img{height:100%;object-fit:cover;object-position:50% 72%;}
  .hero-grid .court-card{max-width:640px;}
  .coach .coach-img{max-width:520px;}
}

/* 29. THE FOOTER TAGLINE SAT 6px UNDER THE LOCKUP. The logo rules keep 9% of the
       mark's width clear on every side, 22.5px at the 250px footer lockup; the design
       system's own footer carries 14px on each side of that gap. render.mjs dropped both
       margins and left only .brand's 6px padding. 18px restores the kit's 24px. */
.ft-brand{margin-bottom:18px;}

/* ---------- Folded sections ----------
   Five homepage bodies ship inside a native <details class="fold"> under the section head. The
   summary is a row: a .btn-ghost pill (the brand icon at the leading end, the label, and the FAQ's
   red plus folding to a minus, drawn as an inline SVG) and one plain fact beside it, so the closed state still says the essential thing and the
   pill never has to carry a sentence on a phone. Native so it works with JS off. Links and hashes
   leave a fold closed (owner's call); main.js only arms the reveals inside a fold when it opens.
   The open/close animates only where interpolate-size exists (Chromium 131+) AND motion is
   allowed: base.css's reduced-motion reset is *,*::before,*::after and cannot reach
   ::details-content, so the gate lives here, with data-motion="off" in it too. Everywhere else
   the toggle is the instant native one. overflow-clip-margin gives the card hover lifts
   (-6 to -8px) room while open; the same 12px peeks below the shrinking edge during a close. */
.band:has(> .shell > .fold) .head{margin-bottom:28px;}
.fold>summary{display:flex;flex-wrap:wrap;align-items:center;gap:12px 18px;cursor:pointer;list-style:none;}
.fold>summary::-webkit-details-marker{display:none;}
/* The gap under the pill is padding on the body's first block, so it grows with the body instead
   of popping in as a margin (and nothing animates a margin). */
.fold>summary+*{padding-top:44px;}
.fold>summary:hover .fold-pill{border-color:var(--fast-red);transform:translateY(-3px);}
.fold>summary:active .fold-pill{transform:translateY(-1px) scale(.97);}
.fold>summary:focus-visible{outline:none;}
.fold>summary:focus-visible .fold-pill{outline:3px solid var(--red-hot);outline-offset:3px;}
/* The brand icon (kit: icon below the 250px lockup floor, min 16px, red only, no shadow, no
   stretch). 24px is the pill's own line height, so the mark fills the line without growing the
   pill; the tighter leading padding lets it nest into the rounded end. */
.fold-mark{width:24px;height:24px;flex-shrink:0;}
.fold-pill{padding-left:16px;}
.fold-ic{flex-shrink:0;stroke:var(--fast-red);stroke-width:2;stroke-linecap:round;fill:none;}
.fold-ic .fold-v{transform-box:fill-box;transform-origin:center;transition:transform calc(.35s / var(--motion-speed,1)) var(--ease-rush);}
.fold[open] .fold-ic .fold-v{transform:scaleY(0);}
.fold-fact{font-size:.86rem;color:var(--text-dim-dark);}
.band-light .fold-fact,html.fb-light .fold-fact{color:var(--text-soft-light);}
.fold[open]>summary .fold-fact,.fold[open]>summary .fold-show,.fold:not([open])>summary .fold-hide{display:none;}
@supports (interpolate-size:allow-keywords){
  @media(prefers-reduced-motion:no-preference){
    html:not([data-motion="off"]) .fold{interpolate-size:allow-keywords;}
    html:not([data-motion="off"]) .fold::details-content{block-size:0;overflow:clip;overflow-clip-margin:12px;transition:block-size calc(.45s / var(--motion-speed,1)) var(--ease-rush),content-visibility calc(.45s / var(--motion-speed,1)) allow-discrete;}
    html:not([data-motion="off"]) .fold[open]::details-content{block-size:auto;}
  }
}

/* ---------- Head art (September 2026) ----------
   .head is capped at 760px, so on a desktop the right third of the shell above every fold sat
   empty. A photo fills it: a plain <img> with no data-img, so it is decoration rather than an
   owner-editable slot (the same call as the fold pills; give it data-img plus a content key when
   Blake wants to swap them). Four are the media library's mono court set; The Résumé takes the
   hero's net-cutting shot, and the grayscale filter keeps any photo in the same mono treatment.
   Desktop only: below 1041px the head stacks over the fold and the figure is display:none, and
   since it is loading="lazy" a hidden image is never fetched, so a phone pays nothing for it.
   Absolute rather than a grid so the eyebrow, h2 and lede keep their own rhythm. The figure is
   as tall as the head or 250px, whichever is more: a short head (Pricing and options is one line)
   must not stretch to fit the photo, which opened a gap between the lede and the pill, so the
   photo hangs down beside the pill row instead, and pointer-events:none keeps that overhang
   from swallowing clicks on the summary. Ceiling: a head under ~170px followed by a full-width
   block would be overlapped; none is (the Locker page head runs three lines). --art-y on the img
   picks which band of a portrait shot survives the crop. */
.head-art{display:none;}
@media(min-width:1041px){
  .head:has(> .head-art){position:relative;max-width:none;padding-right:40%;}
  .head-art{display:block;position:absolute;top:0;right:0;width:36%;height:max(100%,250px);margin:0;border-radius:14px;overflow:hidden;border:1px solid var(--line-dark);background:var(--ink-lift);pointer-events:none;}
  .head-art img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% var(--art-y,50%);filter:grayscale(1);}
  .band-light .head-art,html.fb-light .head-art{border-color:var(--line-light);background:var(--chalk-2);}
}
@media print{.head-art{display:none;}}


/* Actionable toast (September 2026). The plain toast is a pill that times out; this one
   carries links, so it has to stay put, wrap on a phone, and give the actions a hit area
   that clears the 44px target the rest of the site holds to. */
.toast-rich{
  display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;
  max-width:min(560px, calc(100vw - 32px));border-radius:16px;padding:12px 14px 12px 20px;
  text-transform:none;letter-spacing:normal;font-family:var(--font-body);font-size:.92rem;
  text-align:left;
}
.toast-rich .toast-msg{flex:1 1 12rem;}
.toast-acts{display:flex;flex-wrap:wrap;align-items:center;gap:8px;}
.toast-act{
  display:inline-flex;align-items:center;min-height:38px;padding:0 14px;
  border:1px solid rgba(255,255,255,.55);border-radius:100px;
  color:var(--bone);text-decoration:none;white-space:nowrap;font-size:.86rem;
}
.toast-act:hover,.toast-act:focus-visible{background:rgba(255,255,255,.16);}
.toast-x{
  display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;
  border:0;border-radius:100px;background:none;color:var(--bone);
  font-size:1.3rem;line-height:1;cursor:pointer;
}
.toast-x:hover,.toast-x:focus-visible{background:rgba(255,255,255,.16);}

/* The contact form runs as three steps when JavaScript is on (contact-form.js adds
   .stepped); with it off every fieldset shows in order and the plain POST still works.
   The slide is a keyframe, so base.css's reduced-motion and data-motion="off" resets
   already kill it. */
/* Both set their own display, so the UA [hidden] rule loses to them (see the project notes). */
.ct-steps[hidden],.ct-direct[hidden]{display:none;}
.ct-steps{display:flex;align-items:center;gap:10px;margin:0 0 18px;font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;color:#8E8E9B;}
.ct-steps b{flex:0 0 auto;font-weight:600;}
.ct-steps span{flex:1;height:3px;border-radius:3px;background:var(--line-dark);}
.ct-steps span.on{background:var(--fast-red);}
.step{border:0;padding:0;margin:0;min-width:0;}
.step legend{font-family:var(--font-display);font-size:1.15rem;text-transform:uppercase;margin-bottom:14px;color:var(--bone);}
.stepped .step[hidden]{display:none;}
.stepped .step{animation:stepIn .32s ease-out both;}
@keyframes stepIn{from{opacity:0;transform:translateX(24px);}to{opacity:1;transform:none;}}
.stepped .step.back{animation-name:stepBack;}
@keyframes stepBack{from{opacity:0;transform:translateX(-24px);}to{opacity:1;transform:none;}}
.step-nav{display:flex;gap:10px;margin-top:4px;}
.step-nav .btn{flex:1;}
.step-nav .btn-ghost{flex:0 0 auto;}
/* Direct contact details, written in by contact-form.js after a send. */
.ct-direct{display:flex;flex-wrap:wrap;gap:8px 18px;margin:0 0 14px;font-size:.95rem;}
.ct-direct a{color:var(--bone);text-decoration:underline;text-underline-offset:3px;}
/* The kit has no light theme, so these were measured on the site: bone on the bone form
   surface is invisible, and the dark track vanishes too. */
html.fb-light .step legend,html.fb-light .ct-direct a{color:var(--court-black);}
html.fb-light .ct-steps{color:var(--text-soft-light);}
html.fb-light .ct-steps span{background:var(--chalk-2);}
html.fb-light .ct-steps span.on{background:var(--fast-red);}

/* Google review cards (14 September 2026). Every quote is a real review copied verbatim from
   the profile linked under them; the rules for adding one are at the top of families.html.
   Gold is not in the brand palette, which governs the logo marks, not a star rating: gold is
   what a five star row reads as everywhere, and red stars beside a red brand would not. */
.rv-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-bottom:26px;}
.rv-c{
  background:var(--ink-lift);border:1px solid var(--line-dark);border-radius:14px;
  padding:26px 24px 22px;margin:0;display:flex;flex-direction:column;gap:14px;
  transition:transform .4s var(--ease-rush),border-color .3s;
}
.rv-c:hover{transform:translateY(-6px);border-color:rgba(230,12,32,.45);}
.rv-top{display:flex;align-items:center;gap:13px;}
.rv-av{flex:0 0 44px;width:44px;height:44px;display:block;}
.rv-av text{font-family:var(--font-eyebrow);font-weight:700;}
.rv-id{min-width:0;}
.rv-name{
  display:block;font-family:var(--font-eyebrow);font-size:.82rem;letter-spacing:.1em;
  text-transform:uppercase;color:var(--bone);
}
.rv-stars{display:block;width:86px;height:15px;margin-top:6px;fill:#F7B500;}
.rv-c blockquote{margin:0;font-size:.97rem;line-height:1.6;color:#C4C4CE;}
.rv-src{
  margin-top:auto;font-family:var(--font-eyebrow);font-size:.68rem;letter-spacing:.16em;
  text-transform:uppercase;color:#9C9CA8;
}
html.fb-light .rv-c{background:var(--bone);border-color:var(--chalk-2);box-shadow:var(--shadow-light);}
html.fb-light .rv-name{color:var(--court-black);}
html.fb-light .rv-c blockquote{color:var(--text-body-light);}
html.fb-light .rv-src{color:var(--text-soft-light);}
/* #F7B500 measures about 1.9:1 on the bone surface, which is fine for a solid shape and thin
   for one this small, so the light theme takes a deeper gold. Checked on the site, not the kit. */
html.fb-light .rv-stars{fill:#C8860D;}
@media(max-width:900px){.rv-grid{grid-template-columns:1fr;}}

/* Google's own "More" affordance on a truncated review. It is a SIBLING of the quote span,
   never a child of it: applyTextEdits splices from a hooked tag's ">" to the next "</", so an
   anchor nested inside a data-edit hook would be destroyed at build time. */
.rv-more{
  color:var(--red-hot);font-family:var(--font-eyebrow);font-size:.78rem;letter-spacing:.1em;
  text-transform:uppercase;text-decoration:underline;text-underline-offset:3px;white-space:nowrap;
  /* 28x14 is a mean target on a phone. Padding grows the hit area to about 40x34 without
     changing the line box; the negative side margin keeps the inline flow where it was. */
  padding:10px 6px;margin:0 -6px;
}

/* Slogan ribbon between the pricing fold and the service areas (14 September 2026). It borrows
   the hero ticker's whole marquee, so only the colour is new here. Unlike the hero ticker it
   flips with the theme, which is the point: red is the brand accent against a dark page, and on
   the light page a black bar is crisper and stops the page carrying two identical red bars.
   Measured, not assumed: white on #0A0A0C is 19:1, white on --fast-red is 4.6:1, and the ticker
   type is 1.12rem uppercase display, which clears AA large text at 3:1. */
.ribbon{background:var(--court-black);border-top-color:rgba(255,255,255,.14);border-bottom-color:rgba(255,255,255,.14);}
html:not(.fb-light) .ribbon{background:var(--fast-red);border-top-color:rgba(0,0,0,.2);border-bottom-color:rgba(0,0,0,.2);}
