/* ═══════════════════════════════════════════════════════════════════════
   site.css — MyGymDay site · V2 EDITORIAL COMPONENT LAYER
   Loaded third, after tokens.css (values) and base.css (reset, type ramp,
   layout, .prose, the reveal contract). Nothing here re-declares a token
   and nothing here re-states a base rule; this file is components only.

   ── THE FIVE RULES THIS FILE IS HELD TO ─────────────────────────────────
   1 · DEPTH COMES FROM THREE THINGS AND NOTHING ELSE: a hairline
       (1px, --mgd-color-border or --mgd-color-divider), the edge of an
       image, and a step in the surface ramp (bg → bg-alt → surface →
       surface-sunken → surface-strong). If a block needs to separate from
       its neighbour, it steps or it rules. It never floats.
   2 · FORBIDDEN OUTRIGHT, no exceptions, no @supports escape hatch:
       box-shadow, text-shadow, radial-gradient, backdrop-filter, opacity
       used as a surface tint, and any animation that repeats.
       Exactly one gradient may appear in this file — var(--mgd-site-scrim),
       and only to keep text legible over a photograph. It is used once, on
       .band--media::before. The literal linear-gradient() lives in
       tokens.css, so a grep of THIS file for "gradient" should return this
       comment, the comment above that one rule, and the one declaration.
       This is the whole difference between V2 and the version the client
       called a game website.
       NO CARVE-OUTS REMAIN. There was one — filter: invert(1) on
       .lockup__img, to make near-black brand art read on a near-black page.
       The brand is now exported light-on-dark by tools/brand-logotype.html,
       so the correction is gone and a grep of this file for "filter" should
       find nothing but this paragraph.
   3 · LIME IS FLAT AND LIME IS RARE. It appears as a solid fill under dark
       ink, as a 1px rule, or as a 6px dot. Never as a glow, a wash behind
       type, or a gradient. At most two lime elements per viewport; one
       lime word per headline; one .band--lime per page.
   4 · MOTION IS THREE PRIMITIVES: reveal (base.css owns it), settle (the
       bottom of this file, CSS only), shrink (.site-header.is-stuck).
       Every duration is a token, so tokens.css's prefers-reduced-motion
       block stills nearly all of it from one place. NEARLY: the settle
       block at the bottom of this file carries its own reduced-motion
       query, because a 1ms transition from scale(1.06) still snaps — the
       scale itself has to be dropped, not shortened. That query and the
       two in base.css are the complete list of hand-maintained exceptions.
   5 · NEVER WRITTEN HERE: .stat, .quote, .marquee. MyGymDay has no
       download count, no rating, no review and no named customer, so the
       components that would display one do not exist. A component that
       cannot be filled honestly is not a styling gap, it is a lie waiting
       for a copywriter.

   ── UNDEFINED, AND CORRECTLY SO ─────────────────────────────────────────
   V1's .backdrop, .backdrop__bloom, .card--glass, .art, .instr, .signet,
   .panel, .feature, .figure and .hero appear in no rule below. When this
   note was first written they were still in the markup of pages Phase 5 had
   not reached, and leaving them undefined was the fix. Phase 5 finished:
   Phase 7 grepped all six pages for every one of those names and found
   none. Nothing below defines them because nothing uses them — do not add
   rules for them, and do not treat their absence as a gap.

   ── PERMANENT ALIASES ───────────────────────────────────────────────────
   Four legal pages are frozen markup (two of their URLs are what Google
   Play reads), so V1 names are kept as aliases of their V2 replacement
   rather than renamed in HTML: .chip → .eyebrow, .chip__dot → the eyebrow
   dot, .rule-label → .rule, and .steps--railed kept as a no-op because the
   rail it named is now the hairline between steps. Each is marked ALIAS at
   its definition. Do not "clean these up".
   ═══════════════════════════════════════════════════════════════════════ */

/* ══ CHROME ═════════════════════════════════════════════════════════════
   The header is the first thing V1 got wrong: it was frosted glass over a
   glowing backdrop, which announced "app landing page" before a word was
   read. V2's header is a 64px line of text that gets out of the way. At
   rest it has no background and no border at all, so the poster headline
   under it reads as one uninterrupted block. Once the page has moved 24px
   it becomes 56px tall, fills with the page's own black, and grows a
   single hairline — the "shrink" primitive, and the only chrome state
   change on the site. No blur, no translucency, no @supports fork: a
   solid fill is honest at every scroll position and costs nothing on a
   phone that cannot composite a blur cheaply. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  height: var(--mgd-site-header-h);
  border-bottom: var(--mgd-layout-hairline) solid transparent;
  background-color: transparent;
  transition:
    height var(--mgd-site-dur-fast) linear,
    background-color var(--mgd-site-dur-fast) linear,
    border-color var(--mgd-site-dur-fast) linear;
}

.site-header.is-stuck {
  height: var(--mgd-site-header-h-scrolled);
  border-bottom-color: var(--mgd-color-border);
  background-color: var(--mgd-color-bg);
}

.site-header__inner {
  display: flex;
  height: 100%;
  align-items: center;
  gap: var(--mgd-space-xl);
}

/* The nav takes the middle and the actions the right edge, so the lockup
   is not pushed around by a link label changing length. */
.site-header__nav { margin-inline-start: auto; }
.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--mgd-space-md);
}

/* The lockup is the drawn MyGymDay word, exported by tools/brand-logotype.html
   section 9 to assets/brand/mgd-lockup.png and installed by INSTALL-LOGOS.cmd
   at the repo root. It is 600 × 102 — already the true wide proportion of the
   word (5.88:1), already lime "My" plus warm-white "GymDay" on transparency,
   already sized for a 3× screen at the largest place the site paints it.

   So this rule is now three lines instead of six. Set the width, let the height
   follow, done. The V1 art was a 4320 × 4320 square of near-black type, which
   needed object-fit: cover to find the band, object-position: 50% 49% to centre
   it and filter: invert(1) to make it visible on charcoal. All three are gone,
   and none of them may come back: applied to a correctly proportioned file they
   would crop the word and invert lime to magenta.

   The intrinsic size lives on the img tag as width="600" height="102", which is
   what reserves the box before the file arrives — that is why height: auto here
   costs nothing in layout shift. */
.lockup {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}

.lockup__img {
  display: block;
  width: 168px;
  height: auto;
}

/* One step larger: the footer is where the brand is allowed to be the biggest
   thing in its block. 196px rather than 208 because at the 820px breakpoint the
   brand column is the 1.6fr of a 4.6fr grid — about 215px — and a lockup wider
   than its column overflows a grid cell in silence. Height follows the width
   from the base rule, so there is no second number to keep in step. */
.site-footer .lockup__img {
  width: 196px;
}

.nav__list {
  display: flex;
  align-items: center;
  gap: var(--mgd-space-xl);
}

/* THE TARGET IS 44px AT EVERY WIDTH, and it used to be 44px only under 640px.
   A 13px caption link has a ~18px line box, and between 641px and 819px the
   header is still the single-row layout with no padding on the link — so a
   tablet, or a phone in landscape, got an 18px-tall target where 2.5.8 asks
   for 24px. The one-row/two-row question is a layout question; the size of the
   thing you tap is not, so the guarantee belongs on the base rule.

   flex, not padding: the label is centred inside a box of a known height, so
   the target is 44px whatever the label's line box happens to be. It cannot
   grow the header, either — .site-header sets an explicit 64px (56px stuck)
   and .site-header__inner is height: 100%, so the header's height is declared,
   not measured from its contents, and 44 fits inside 56. */
.nav__link {
  display: flex;
  align-items: center;
  min-height: var(--mgd-layout-min-touch);
  font: var(--mgd-site-caption);
  color: var(--mgd-color-text-secondary);
  text-decoration: none;
  transition: color var(--mgd-site-dur-fast) linear;
}

.nav__link:hover { color: var(--mgd-color-text); }

/* The current page is named by weight and full-strength text, not by a
   lime underline. Two lime elements per viewport is the budget, and the
   header should never spend one of them on something the page title
   already says. */
.nav__link[aria-current='page'] {
  color: var(--mgd-color-text);
  font-weight: 600;
}

/* Pressed, and declared after the aria-current rule on purpose: both are
   two classes deep, so source order is the only thing that lets the current
   page's own link acknowledge a tap.

   A nav link has no box to fill — no padding at rest, and it is now a 44px
   flex cell at every width, spread edge to edge on a phone, so a background
   would hug the glyphs and read as a mistake. The mechanism is therefore
   the label's own colour, stepping DOWN one rung for as long as the thumb
   is down. That is the same idiom the phone's own controls use, and it is
   the one press state that needs no geometry, so it behaves identically in
   the desktop row and the mobile strip.

   Down rather than up, because on Android a tap fires :hover as well and
   leaves it stuck: press dims, release brightens to the hover rung. Two
   different colours from one tap is what makes it legible.

   Tertiary grey is 4.2:1, under the 4.5:1 that 1.4.3 asks of body text —
   which is why it is the pressed state and nothing else. It exists only
   while a finger is on the glass, it fades back over
   --mgd-site-dur-fast, and the resting rung above is 8.4:1. */
.nav__link:active { color: var(--mgd-color-text-tertiary); }

/* ── The mobile header: two rows, no JavaScript, no drawer ────────────
   V3 CHANGED THIS BLOCK, AND THE REASON IS ARITHMETIC. The comment that
   used to live here was right at the time: three labels of six to nine
   characters do fit one line beside a 112px lockup. A fourth label —
   Careers — makes that false. Measured at the site's caption rung with its
   tracking: Product ≈ 58px, For gyms ≈ 62px, Careers ≈ 56px, Support ≈
   58px, so 234px of text; a 360px viewport minus two 20px gutters leaves
   320px, and the lockup takes 112px of it. One row cannot hold four links
   and a logo. It never could.

   So the header becomes TWO rows below 640px. Row one is the lockup alone.
   Row two is a hairline-separated strip holding all four links, spread
   edge to edge of the shell, each one a 44px tap target. Above 640px it
   returns to one flex row, which is why THE SITE STILL HAS EXACTLY ONE
   HEADER BREAKPOINT — this query grew, it did not multiply.

   A drawer was rejected on four counts: it is new JavaScript on all seven
   pages, it is a button plus a panel plus a focus trap plus an
   aria-expanded state to keep correct, it hides navigation behind a tap,
   and four words genuinely fit — they just do not fit on one line with a
   logo. site.js is untouched by this, and its own comment about "no mobile
   drawer" is still true.

   WHY THE HEIGHT TOKENS ARE REDEFINED RATHER THAN THE HEADER RESTYLED.
   .site-header takes its height from --mgd-site-header-h and its stuck
   height from --mgd-site-header-h-scrolled, and base.css computes
   scroll-padding-top from the first of those so a linked anchor never
   lands under the chrome. Overriding the two tokens here therefore fixes
   the header, the shrink transition and the anchor offset from one place.
   Setting a height on .site-header instead would have left
   scroll-padding-top quietly wrong on the pages that use fragments most —
   support.html and delete-account.html.

   The arithmetic those two numbers come from, so a future edit can redo it
   rather than guess:
     at rest    row 1  40px  (the 112px lockup is 19px tall, centred)
                rule    1px
                row 2  44px  = 85px
     stuck      row 1  34px  (the lockup steps to 96px, 16px tall)
                rule    1px
                row 2  44px  = 79px
   Row two does NOT shrink on scroll. The whole point of it is that it is a
   thumb target, and shrinking a target at the exact moment the page is
   being scrolled and tapped would be the wrong six pixels to save.

   THE 44px IN THAT SUM IS NO LONGER SET IN THIS QUERY. It is
   --mgd-layout-min-touch on the base .nav__link rule, because the target had
   to hold between 641px and 819px as well, where the header is still one row
   and the link had no box at all. Row two's height falls out of it; change
   the token, not this arithmetic.

   Label size is clamp(11px, 3.1vw, 13px) so no second query is needed: at
   320px it resolves to 11px, the four labels measure about 174px, and
   space-between has 106px left to spread across three gaps. At 639px it is
   13px, about 209px of text in 575px of shell. Nothing wraps at any width
   in the range, and nowrap makes "For gyms" prove it.

   Scoped to .site-header throughout: the footer is a single column down
   here and keeps its full-size lockup. */
@media (max-width: 640px) {
  :root {
    --mgd-site-header-h: 85px;
    --mgd-site-header-h-scrolled: 79px;
  }

  /* Three children, but .site-header__actions is display:none below, and a
     display:none grid item is not placed — so this is a two-row grid with
     the lockup in row one and the nav in row two. 1fr on row one lets the
     6px the header loses when it sticks come out of the lockup's row and
     leaves row two untouched. */
  .site-header__inner {
    display: grid;
    grid-template-rows: 1fr auto;
    align-items: center;
    gap: 0;
  }

  /* The duplicate call to action is what goes. Every page links to the
     partner app and to the app itself from its own body and from the
     footer, so nothing becomes unreachable. */
  .site-header__actions { display: none; }

  .site-header__nav {
    margin-inline-start: 0;
    align-self: end;
    border-top: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
  }

  .site-header .nav__list {
    justify-content: space-between;
    gap: var(--mgd-space-sm);
  }

  /* The 44px box, the centring and the flex that does it now live on the base
     .nav__link rule, because a touch target should not depend on a breakpoint.
     What is genuinely narrow-only is left: the labels have to survive four
     across a 320px row, so the caption steps down to 11px and is forbidden
     from wrapping — "For gyms" breaking after "For" is what makes a four-link
     row look broken. */
  .site-header .nav__link {
    font-size: clamp(11px, 3.1vw, 13px);
    white-space: nowrap;
  }

  .site-header .lockup__img { width: 112px; }
  .site-header.is-stuck .lockup__img { width: 96px; }
}

/* ══ STRIP ══════════════════════════════════════════════════════════════
   The launch-status line: the only piece of chrome that sits ABOVE the
   header, and the one component Phase 3 had to add because §5.1 §1 asks for
   it and Phase 2's inventory did not include it. It earns the place. The
   first thing a visitor needs to know on this site is that neither app is
   in a store yet — learning that from a greyed-out button after reading a
   headline and a call to action feels like a bait; learning it in the first
   line does not.

   Three deliberate restraints. It is NOT sticky, so it scrolls away once it
   has been read and never competes with the header for the top of the
   window. It is NOT lime-filled — a lime bar across the full width would
   spend the entire two-lime-per-viewport budget on chrome, so the fill is
   the deepest ink in the ramp and it carries no accent at all. That last
   part is an amendment: this comment used to promise a 6px eyebrow dot, and
   neither page ships one. Both posters spend the first viewport's two lime
   elements on the "Soon" tag and the one primary button, so a dot up here
   would have been a third — see index.html §3 and partners.html §1.
   And it holds caption type at 13px, because a line of chrome that reads at
   body size is not chrome any more. One hairline underneath separates it
   from the transparent header, which is rule 1 of this stylesheet. */
.strip {
  border-bottom: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
  background-color: var(--mgd-color-ink);
}

/* The row gap is 16px and the column gap is 12px, which looks lopsided
   written down and is not. A flex line gap is only ever drawn when the
   container wraps, and this strip wraps at exactly one place in its range —
   under about 560px, where the three fragments stop fitting one line. So
   the row value is invisible on every desktop and the only thing it governs
   is the phone.

   It is 16px rather than 8px because of the bare link at the end of the
   strip. That link is 18px of text; at 8px of line gap it carried 26px of
   vertical separation, which clears 2.5.8 AA's 24px by two pixels and
   nothing else. 16px takes it to 34px.

   IT IS DELIBERATELY NOT A 44px TARGET, and this is the site's one recorded
   2.5.5 AAA exception. Boxing it to 44px would add 26px to a chrome stack
   that already spends 85px on the header before a phone sees a word of the
   page, and this strip is a status announcement, not navigation. 2.5.5's
   own equivalent-control clause covers it: the link points at
   #get-the-app, which is also reached from a 44px footer link on all seven
   pages and from a 52px .btn--primary one viewport below. Nothing is
   reachable only from here. */
.strip__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--mgd-space-lg) var(--mgd-space-md);
  padding-block: var(--mgd-space-sm);
  font: var(--mgd-site-caption);
  color: var(--mgd-color-text-secondary);
}

/* ══ BUTTONS ════════════════════════════════════════════════════════════
   One shape for the whole site: a pill, 52px tall, with a real label at
   17px. V1 gave the primary button a lime gradient and a lime glow, which
   is what made the page look like it was selling a game. V2's primary is a
   flat lime fill with near-black ink on it — the highest-contrast pair in
   the palette (about 14:1) and the loudest thing on any page precisely
   because nothing else glows.

   52px is deliberately one step off the app's control ramp (48px medium,
   56px large): a marketing call to action is a bigger target than a form
   field but should not read as a slab. It is declared once here as a local
   custom property so the --lg and --sm variants override one value. */
.btn {
  --btn-h: 52px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--mgd-space-sm);
  min-height: var(--btn-h);
  padding-inline: var(--mgd-space-xl-2);
  border: var(--mgd-layout-hairline) solid transparent;
  border-radius: var(--mgd-radius-pill);
  font: var(--mgd-type-subheading);
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--mgd-site-dur-fast) linear,
    border-color var(--mgd-site-dur-fast) linear,
    color var(--mgd-site-dur-fast) linear;
}

/* Flat fill, ink label, no glow, no gradient, no lift on hover. Pressed
   uses the darker lime the product uses, so the two surfaces agree. */
.btn--primary {
  background-color: var(--mgd-color-primary);
  color: var(--mgd-color-text-inverse);
  font-weight: 700;
}

.btn--primary:hover { background-color: var(--mgd-color-primary-pressed); }

/* The secondary is a hairline and a label. On hover the hairline goes to
   full-strength text colour and the fill steps up one rung on the surface
   ramp — the same two mechanisms the rest of the file uses. */
.btn--ghost {
  border-color: var(--mgd-color-border);
  background-color: transparent;
  color: var(--mgd-color-text);
}

.btn--ghost:hover {
  border-color: var(--mgd-color-text-tertiary);
  background-color: var(--mgd-color-surface);
}

/* Third rank: a text link that keeps the button's target height. Used
   where a real button beside it would create two equal calls to action. */
.btn--quiet {
  padding-inline: var(--mgd-space-md);
  background-color: transparent;
  color: var(--mgd-color-text-secondary);
}

.btn--quiet:hover { color: var(--mgd-color-text); }

/* ── Pressed ──────────────────────────────────────────────────────────
   V3 ADDED THIS, AND IT IS NOT DECORATION. Until now every button on the
   site had a hover state and nothing else, which is a desktop assumption:
   a phone has no hover, so the only feedback a tap produced came from
   Android Chrome's own grey flash — and base.css now switches that off,
   because on a #080B0A page it looks like a rendering fault. Something has
   to take its place, or four fifths of the audience taps a 52px pill and
   watches nothing happen for the length of a page load.

   Both rungs use mechanisms rule 1 already allows — one step on the surface
   ramp and one hairline — so there is no shadow, no lift, no transform and
   no new hex. They also cost nothing to animate: background-color and
   border-color are already in .btn's transition list, so the press fades in
   and out over --mgd-site-dur-fast like everything else.

   :not([aria-disabled='true']) is load-bearing, not defensive. The store
   button ships as <a class="btn btn--store btn--soon" aria-disabled="true">
   with no href, and an <a> with no href still matches :active on tap. These
   rules are declared before the disabled block below and would otherwise
   out-specify it for exactly as long as a thumb is down, making an
   unavailable button feel pressable. */
.btn--ghost:active:not([aria-disabled='true']),
.btn--quiet:active:not([aria-disabled='true']),
.btn--store:active:not([aria-disabled='true']) {
  border-color: var(--mgd-color-text-secondary);
  background-color: var(--mgd-color-surface-strong);
}

/* Lime cannot step up — it is already the brightest value in the palette —
   so the primary presses DOWN to the pressed lime the product uses, and an
   ink hairline appears inside its edge. The border is declared transparent
   at 1px on .btn, so this changes a colour and never a box. */
.btn--primary:active:not([aria-disabled='true']) {
  background-color: var(--mgd-color-primary-pressed);
  border-color: var(--mgd-color-text-inverse);
}

.btn--lg {
  --btn-h: var(--mgd-layout-control-lg);
  padding-inline: var(--mgd-space-xl-3);
}

/* --sm is 44px, NOT the 40px control-sm rung it used to mirror, and the
   reason is that this variant only ever appears as the header's "Get the
   app" button. 40px is a correct size for a control inside a form on a
   desktop; it is four pixels short of 2.5.5 for a thumb. The override lives
   here rather than in tokens.css because --mgd-layout-control-sm is
   mirrored from the app's layout scale, where 40px is right — rule 1 of
   tokens.css forbids editing a mirrored value to suit one page. Nothing
   else on the site uses --sm, so the blast radius is that one button.

   It is worth keeping at 44px even though the 640px query hides the header
   actions entirely: a tablet at 700px, or a touchscreen laptop, sees this
   button and has a thumb. */
.btn--sm {
  --btn-h: var(--mgd-layout-min-touch);
  padding-inline: var(--mgd-space-lg);
  font: var(--mgd-type-label);
}

.btn--block {
  display: flex;
  width: 100%;
}

/* ── Store button ─────────────────────────────────────────────────────
   Neither app is published, so there is no badge to show, and we would not
   be entitled to ship Apple's or Google's artwork if there were. The store
   button is therefore our own object: a hairline rectangle — a badge shape,
   not a pill, so it never competes with a real call to action — carrying an
   optional 20px mark, a small kicker line and the store name as plain
   text. Anatomy Phase 6 writes:

     <a class="btn btn--store btn--soon" aria-disabled="true">
       <img class="btn__icon" …>                       (optional)
       <span class="btn__label">
         <span class="btn__kicker">Coming soon to</span>App Store</span>
       <span class="btn__soon">Soon</span>
     </a>

   TWO OF THESE SHIP TOGETHER, EVERYWHERE. Both apps go to the App Store and
   to Google Play on the same day, so a row that named one store would be
   read as a sequence — and the pages say in words that there is no
   sequence. App Store is written first and Google Play second, in every
   one of the four rows, for no reason beyond consistency; a reader who
   scans two of the four should not have to wonder whether the order means
   something.

   NO ARTWORK ON EITHER, AND THE REASON IS A LICENCE, NOT TASTE. The Apple
   logo and the Play triangle are trademarked artwork we hold no licence to
   redraw, and an approximation of a logo is a worse liability than a plain
   word. The store name as text is permitted with attribution, which is why
   every footer carries one line naming Apple's service mark and Google's
   trademark, and no page ships either logo. There is a mechanical reason
   as well: this site has no inline SVG — a mark would arrive as
   <img class="btn__icon">, and an <img> cannot be recoloured by CSS, so
   inside .band--lime it would keep its own ink while the label around it
   inverted. Text-only badges cost no asset, add no external origin, and
   invert correctly for free.

   The height is set by the two-line label rather than by a number, which
   is why --btn-h is not touched here. */
.btn--store {
  --btn-h: auto;
  gap: var(--mgd-space-md);
  padding: var(--mgd-space-md) var(--mgd-space-lg);
  border-radius: var(--mgd-site-radius-card);
  border-color: var(--mgd-color-border);
  background-color: transparent;
  color: var(--mgd-color-text);
  text-align: start;
}

.btn--store:hover { border-color: var(--mgd-color-text-tertiary); }

.btn__icon {
  display: block;
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

.btn__label {
  display: grid;
  font: var(--mgd-type-subheading);
}

/* 12px uppercase is body-size text as far as WCAG is concerned, so this
   takes the site's quiet rung rather than the mirrored tertiary grey — the
   same correction as .eyebrow below, and the arithmetic is in tokens.css. */
.btn__kicker {
  font: var(--mgd-site-eyebrow);
  letter-spacing: var(--mgd-site-track-eyebrow);
  text-transform: uppercase;
  color: var(--mgd-site-text-quiet);
}

/* The "Soon" tag earns lime: it is the only place on the site that admits
   the apps are not downloadable yet, so it should be the thing the eye
   lands on. Flat fill, ink text, no glow — the rule that applies
   everywhere.

   IT COMES IN PAIRS NOW, AND THAT IS THE WHOLE LIME BUDGET. A viewport is
   allowed two lime elements; a store row ships two badges, each with one of
   these. So the arithmetic in every row that contains a .storebar is
   already closed, and the third child beside the pair — How it works,
   Contact support — is a .btn--ghost rather than the .btn--primary it would
   otherwise be. One lime fill more and the count is three. The same
   correction is written out at length in the launched-store-button block
   below, because launching does not remove the constraint, it only moves it
   from the tags to the fills.

   Two exceptions, both real. Inside .band--lime the rule further down
   inverts these tags to ink-on-lime, so §11's pair costs that band's budget
   nothing and its neighbour may keep an inverted fill. And a lime mark
   inside a photograph — the wall plaque in index.html §5 — is photography,
   not a page element, and is not counted. */
.btn__soon {
  padding: 2px var(--mgd-space-sm);
  border-radius: var(--mgd-radius-pill);
  background-color: var(--mgd-color-primary);
  color: var(--mgd-color-text-inverse);
  font: var(--mgd-site-eyebrow);
  letter-spacing: var(--mgd-site-track-eyebrow);
  text-transform: uppercase;
}

/* --soon exists so a store button can be marked unavailable without also
   being greyed into illegibility: the label stays full strength, the
   pointer stops promising a destination. */
.btn--soon { cursor: default; }

/* ── The launched store button ────────────────────────────────────────
   THE CANONICAL LAUNCH ANATOMY LIVES HERE. There is no config object and
   no JS switch on this site: publishing is a markup edit to eight <a>
   elements — two per row, four rows, being index.html's poster and lime
   band and partners.html's poster and download block. Each one goes from

     <a class="btn btn--store btn--soon" aria-disabled="true">
       <span class="btn__label">
         <span class="btn__kicker">Coming soon to</span>App Store</span>
       <span class="btn__soon">Soon</span>
     </a>

   to

     <a class="btn btn--store btn--primary" href="https://apps.apple.com/…">
       <span class="btn__label">
         <span class="btn__kicker">Download on the</span>App Store</span>
     </a>

   — four attributes and one deleted span each, and the .btn__soon span
   must go, not just its text: it is a lime pill that means "not yet". The
   Google Play sibling beside it takes the identical edit with "Get it on"
   as its kicker and a play.google.com href; both stores go live on the
   same day, so the two are always edited together and a row left half
   launched is a bug, not a state.

   THE EDIT THIS BLOCK USED TO DEMAND HAS ALREADY BEEN MADE. It read: both
   posters sit their store buttons next to <a class="btn btn--primary"
   href="#how-it-works">, and on launch day that neighbour must drop to
   .btn--ghost, because a launched badge is a lime fill and two fills of
   equal weight let "scroll down a bit" tie with "install the app". The
   second store settled it early — two badges carry two .btn__soon tags,
   which is the viewport's whole allowance, so every neighbour in all four
   rows is already a ghost and has been since the pair shipped. Nothing to
   change there, and nothing to remember on the day.

   WHAT THAT LEAVES IS AN INVARIANT WORTH STATING. The lime count does not
   move at launch: it migrates. Two tags become two fills, both of them
   store buttons, both the same instruction. Do not promote the neighbour
   back to .btn--primary in the same edit out of a feeling that the row
   looks flat — that is the third lime element, and it would put the scroll
   cue back in a tie with the thing the page exists to ask for. Inside
   .band--lime the pair inverts instead, so §11 keeps working untouched.

   Why this rule has to exist rather than being left to the two classes:
   .btn--primary is declared at the top of this section and .btn--store
   below it, both at one class of specificity, so on source order alone
   --store's transparent background and default text colour would win and
   the launched button would render as a hairline badge that looks exactly
   like the unavailable one minus its tag. Two classes (0,2,0) settle it
   in either order.

   This is not the .stat/.quote case in rule 5 of the header. Those
   components would display a number, a rating or a customer that does not
   exist; this one styles an anchor whose destination the launcher
   supplies, so it cannot state anything untrue — and the cost of leaving
   it out is a launch that has to be debugged in a hurry. The kicker takes
   the same quiet-ink value that .t-dim takes on lime, so the pair reads
   as one object rather than as lime with grey text on it. */
.btn--store.btn--primary {
  border-color: transparent;
  background-color: var(--mgd-color-primary);
  color: var(--mgd-color-text-inverse);
}

.btn--store.btn--primary:hover {
  border-color: transparent;
  background-color: var(--mgd-color-primary-pressed);
}

.btn--store.btn--primary .btn__kicker { color: var(--mgd-color-surface-strong); }

.btn[aria-disabled='true'],
.btn.is-disabled {
  cursor: default;
}

.btn--primary[aria-disabled='true'],
.btn--primary.is-disabled {
  background-color: var(--mgd-color-surface-strong);
  color: var(--mgd-color-text-tertiary);
}

/* ── Store row ────────────────────────────────────────────────────────
   THREE CHILDREN, ALWAYS THE SAME THREE, IN THE SAME ORDER: the App Store
   badge, the Google Play badge, then one .btn--ghost — How it works on the
   two posters, Contact support in the two download blocks. Both stores
   appear because both apps ship to both on the same day, and the pages say
   so in words; a row naming one store would contradict its own page.

   WHY NEITHER BADGE CARRIES A MARK is recorded in full in the .btn--store
   block above — no licence to redraw the Apple logo or the Play triangle,
   and an <img> glyph could not invert inside .band--lime even if there
   were. Two HTML comments point here for that reason; the answer is thirty
   lines up.

   WHY THE THIRD CHILD IS A GHOST is the .btn__soon block above: two badges
   carry two lime tags, which is the viewport's whole allowance, so the
   anchor beside them gives up its fill and keeps everything else.

   WRAPPING IS FLEX-WRAP AND NOTHING ELSE. No max-width and no basis on the
   row or on any .btn, so each child is exactly as wide as its own label:
   at desktop widths all three sit on one line, and as the column narrows
   the ghost drops first, then the two badges separate. That is the right
   order to lose — the pair stays together longest, and the anchor that
   merely scrolls is the one that moves. align-items: center is what keeps
   the ghost's single-line label on the optical centre of the two-line
   badges beside it; on a phone, where each child owns its line, the
   property costs nothing. Aligned to start by default, and re-centred in the
   three places whose container centres its own children — .poster, .cta-band
   and .stack--center, all three rules sitting under .poster below. */
.storebar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--mgd-space-lg);
}

/* ══ TWO UTILITIES ══════════════════════════════════════════════════════
   .eyebrow and .rule are the only decoration the site owns. Everything V1
   used to separate ideas — glass panels, glowing chips, gradient rails,
   drawn glyph tiles — is replaced by these two and by the surface ramp.

   .eyebrow labels a section in 12px uppercase with wide tracking. It may
   carry a 6px lime dot as its first child; that dot is the smallest legal
   use of lime and the only ornament in the file.

   ALIAS: .chip and .chip__dot are V1 names still present in the frozen
   markup of all six pages. They are the same object.

   THE COLOUR IS A PHASE 7 CORRECTION, and it is the reason the quiet token
   exists. This rung ships on every page; it is 12px at 700 with 0.18em of
   tracking, which WCAG treats as normal text at 4.5 : 1, and the mirrored
   --mgd-color-text-tertiary measures 4.24 : 1 on this ground. Not "about
   right" — a fail, on the most-repeated label on the site. The dot below
   stays on the tertiary token on purpose: 6px of solid colour is a
   graphical object at 3 : 1, so it may sit one step darker, and keeping it
   there is what preserves the ramp between the label and its mark. */
.eyebrow,
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--mgd-space-sm);
  font: var(--mgd-site-eyebrow);
  letter-spacing: var(--mgd-site-track-eyebrow);
  text-transform: uppercase;
  color: var(--mgd-site-text-quiet);
}

.eyebrow__dot,
.chip__dot {
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  border-radius: var(--mgd-radius-pill);
  background-color: var(--mgd-color-primary);
}

/* An eyebrow sitting on the lime band inverts to ink. base.css keeps
   .t-eyebrow colourless for exactly this reason. */
.band--lime .eyebrow,
.band--lime .chip { color: var(--mgd-color-text-inverse); }

.band--lime .eyebrow__dot,
.band--lime .chip__dot { background-color: var(--mgd-color-text-inverse); }

/* .rule is a labelled hairline: the label at the start, the line running
   out to the right margin. V1 centred it between two lines, which turned a
   separator into a title. Anchoring it left makes it read as the caption of
   the block beneath — which is what every one of its uses actually is.

   Markup, unchanged from V1 so the frozen pages keep working:
     <p class="rule"><span>Write to us</span></p>

   ALIAS: .rule-label is the same object under its V1 name. */
.rule,
.rule-label {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--mgd-space-lg);
  margin-bottom: var(--mgd-space-xl-3);
  font: var(--mgd-site-eyebrow);
  letter-spacing: var(--mgd-site-track-eyebrow);
  text-transform: uppercase;
  color: var(--mgd-site-text-quiet);
}

/* The line itself is a divider, not text, so it keeps its own token.

   min-width is not decoration: the label column is `auto` and the line is
   `1fr`, so a label longer than its container takes the whole track and the
   line collapses to zero — a labelled hairline with no hairline in it. It
   happens on the real page, not in theory: index.html:280 reads "Works at the
   gym you already go to", which at 12px with 0.18em of tracking is wider than
   the 260px column .grid--3 gives it on a phone. 40px is the floor at which
   the object still reads as a rule; the label wraps to a second line instead,
   which is the correct thing to lose. */
.rule::after,
.rule-label::after {
  content: '';
  height: var(--mgd-layout-hairline);
  min-width: var(--mgd-space-xl-4);
  background-color: var(--mgd-color-divider);
}

/* A rule inside a .grid captions nothing, so it must not reserve room for a
   block beneath it. The 32px above is the gap between a rule and the thing it
   labels; in a grid the grid's own gap already does that job, and the two
   stack. Three one-line claims on a phone sat 56px apart (24 + 32) and the
   section ended on 32px of dead air. Reaches index.html:279–281 and
   partners.html:183–185, the only two places a rule is a grid child. */
.grid > .rule,
.grid > .rule-label { margin-bottom: var(--mgd-space-none); }

/* ══ SECTION HEAD ═══════════════════════════════════════════════════════
   Eyebrow, headline, one line of lead. It sets no max-width of its own on
   purpose: every type rung in base.css already carries its correct measure
   (poster 16ch, display 20ch, title 24ch, heading 34ch, lead 46ch), so a
   width here would clamp a 20ch headline to a 46ch lead and undo the
   ramp.

   --center is the only variant, and NOTHING ON THE SEVEN REAL PAGES USES IT
   ANY MORE. It had four uses — index.html:320 and partners.html:201 over a
   .steps rail, support.html:245 and delete-account.html:459 over a .faq — and
   every one of them centred a head above content that is hard left, so the
   two axes never met. index.html's own FAQ head already used the plain block
   over the identical markup, which is what settled it: left is this object's
   default and the centred variant was the inconsistency, not the base. The
   rule stays because _reference/components.html:299 documents it and because
   a future centred block (a stack--center band, say) is the case it is right
   for — but do not put it back over a list. */
.section-head {
  display: grid;
  gap: var(--mgd-space-lg);
  margin-bottom: var(--mgd-space-xl-4);
}

.section-head--center {
  justify-items: center;
  text-align: center;
}

/* ══ POSTER ═════════════════════════════════════════════════════════════
   The one borrowed device the teardown says to take wholesale: a page that
   opens with type at poster scale and nothing else. Anton at up to 148px
   on near-black, three or four words, one line of explanation, then the
   store row. No image, no panel, no frame — the headline IS the artwork,
   which is the entire reason this site can be world-class without a
   photograph budget.

   Layout only: vertical rhythm belongs to the .section or .band around it,
   so the same block can open a page or close one. */
.poster {
  display: grid;
  justify-items: center;
  gap: var(--mgd-space-xl-2);
  text-align: center;
}

.poster > * { margin-inline: auto; }

.poster .storebar {
  justify-content: center;
  margin-top: var(--mgd-space-md);
}

/* THE OTHER TWO CENTRED CONTEXTS, and missing them is what left a store row
   hugging the left edge under centred type on two pages.

   A .storebar packs to flex-start by default, which is correct beside
   left-aligned copy and wrong inside a container that centres its children.
   .poster was handled above; these two were not:

     index.html:496    .shell--narrow.stack.stack--center  (the lime band)
     partners.html:407 .cta-band                           (the download card)

   Both are grids with justify-items:center and text-align:center, so the row
   BOX is centred — and that hid the bug on a wide screen, because a box
   shrink-wrapped to its content has no spare width for justify-content to
   distribute. It appears the moment the row wraps: on a phone the flex
   container fills the column, the three children stack onto three lines, and
   every line packs left while the eyebrow, headline and lead above them stay
   centred. That is one of the misalignments in the client's screenshots.

   Specificity keeps the hero out of it: .band--split .poster .storebar is
   (0,3,0) and re-asserts flex-start inside the ≥900px query. */
.cta-band .storebar,
.stack--center .storebar { justify-content: center; }

/* ══ BAND ═══════════════════════════════════════════════════════════════
   The site's unit of rhythm. Ladder's page is a stack of full-viewport-
   width bands with only the text constrained inside them, separated by far
   more space than feels reasonable — that spacing is most of why it reads
   as expensive. A .band is a <section> placed directly in <main>, so it is
   already full width; the .shell inside it does the constraining. Vertical
   padding is --mgd-site-band-y (112→240px fluid), which is deliberately
   larger than --mgd-site-section-y used by ordinary sections.

   Variants change the surface, never the shape:
     --ink    one step darker than the page (the quiet band)
     --lime   flat lime with ink type (loudest, once per page, never twice)
     --media  photograph behind, scrim over, type on top
     --split  copy on one side, a device or frame on the other
     --flip   reverses a --split at desktop width */
.band {
  padding-block: var(--mgd-site-band-y);
  background-color: var(--mgd-color-bg);
}

.band--ink { background-color: var(--mgd-color-ink); }

/* Flat lime under near-black ink: about 14:1, the highest-contrast pair in
   the palette. Nothing inside a lime band may be lime, so the four colour
   utilities are re-pointed to dark values here rather than left grey — a
   grey secondary line on lime is the one contrast failure this band can
   produce, and it is produced by markup that is correct everywhere else. */
.band--lime {
  background-color: var(--mgd-color-primary);
  color: var(--mgd-color-text-inverse);
}

.band--lime .t-ink,
.band--lime .t-lime { color: var(--mgd-color-text-inverse); }

.band--lime .t-dim,
.band--lime .t-faint { color: var(--mgd-color-surface-strong); }

/* THE ONE GROUND WHERE THE FOCUS RING CANNOT BE LIME. base.css draws every
   ring in --mgd-color-primary, which is 16.4 : 1 on the page and 1 : 1 here —
   a keyboard visitor tabbing onto "Contact support" inside index.html's lime
   band would get no visible ring at all. Ink on lime measures 16.7 : 1, so
   only the colour changes; the 2px width, the 3px offset and the radius stay
   in base.css where every other ring on the site reads them.

   Kept in this block rather than qualified in base.css because it is one of
   this band's inversions, exactly like the button, kicker and Soon-tag rules
   below it. If a second lime surface is ever introduced, it inherits this by
   carrying .band--lime — that is the point of putting it here. */
.band--lime :focus-visible { outline-color: var(--mgd-color-ink); }

/* A primary button on lime would be invisible, so it inverts: ink fill,
   lime label. The ghost keeps its hairline and takes it in ink. */
.band--lime .btn--primary {
  background-color: var(--mgd-color-ink);
  color: var(--mgd-color-primary);
}

.band--lime .btn--primary:hover { background-color: var(--mgd-color-bg-alt); }

.band--lime .btn--ghost {
  border-color: var(--mgd-color-surface-strong);
  color: var(--mgd-color-text-inverse);
}

.band--lime .btn--ghost:hover {
  border-color: var(--mgd-color-ink);
  background-color: transparent;
}

/* The same rule reaches the store button, and it has to: the band above the
   footer is where "Get the app" lands, so the Coming-soon object is inside
   the one band that forbids lime. Three things invert. The hairline, which is
   drawn in --mgd-color-border — a near-black value that vanishes on lime.
   The kicker, which is tertiary grey. And the "Soon" tag, which is lime
   filled and would otherwise be a lime pill on a lime field: the exact
   failure the comment fourteen lines up rules out. Ink, in all three. */
.band--lime .btn--store,
.band--lime .btn--store:hover {
  border-color: var(--mgd-color-ink);
  color: var(--mgd-color-text-inverse);
}

.band--lime .btn__kicker { color: var(--mgd-color-surface-strong); }

.band--lime .btn__soon {
  background-color: var(--mgd-color-ink);
  color: var(--mgd-color-primary);
}

/* The launched state needs the same inversion, and for the same reason the
   Coming-soon one did: index.html's second store button sits inside the one
   band that forbids lime, so on launch day a lime fill would put lime on
   lime. Ink fill, lime label, and the kicker steps up to the page's own
   secondary text — 8.4:1 on this ground, where the tertiary grey it uses
   elsewhere would not clear 4.5:1. Hover matches .band--lime .btn--primary
   above rather than inventing a third behaviour. */
.band--lime .btn--store.btn--primary {
  border-color: transparent;
  background-color: var(--mgd-color-ink);
  color: var(--mgd-color-primary);
}

.band--lime .btn--store.btn--primary:hover { background-color: var(--mgd-color-bg-alt); }

.band--lime .btn--store.btn--primary .btn__kicker { color: var(--mgd-color-text-secondary); }

/* The press rungs invert too, and they have to: this band is the one place
   a thumb lands on lime. Above, on the dark ground, a press steps the fill
   UP the surface ramp. Here the ground is already the brightest value in
   the palette, so a ghost or store button presses the lime itself DOWN to
   pressed lime, and the ink-filled primary steps up to the surface rung —
   the same two mechanisms, read in the opposite direction.

   The specificity is deliberate, not accidental. Every selector here adds
   .band--lime to what the base rules match, which puts them one class
   ahead; the launched store button adds .btn--primary on top of that, so it
   lands on the ink rung and not the lime one even though both selectors
   name it. */
.band--lime .btn--ghost:active:not([aria-disabled='true']),
.band--lime .btn--store:active:not([aria-disabled='true']) {
  border-color: var(--mgd-color-ink);
  background-color: var(--mgd-color-primary-pressed);
}

.band--lime .btn--primary:active:not([aria-disabled='true']),
.band--lime .btn--store.btn--primary:active:not([aria-disabled='true']) {
  background-color: var(--mgd-color-surface-strong);
}

/* ── Band with a photograph ───────────────────────────────────────────
   The receiving surface for real photography. This comment used to say the
   component shipped unused "because no photograph of a real gym exists" —
   that stopped being true when the client delivered five. index.html used the
   band twice until V3 rebuilt the poster around two device frames on flat
   ink; it now uses it once, on the auto-checkout band (walking out at golden
   hour), which means the one gradient below is spent once per page rather
   than twice. Anatomy:

     <section class="band band--media">
       <img class="band__media" src="…" alt="">
       <div class="shell"> … </div>
     </section>

   The <img> is a real element rather than a background-image for three
   reasons that all matter: it can be lazy-loaded or marked fetchpriority,
   it participates in the document so a broken path is visible instead of
   silent, and alt="" states honestly that it carries no information the
   type does not already carry.

   var(--mgd-site-scrim) is the ONE gradient permitted in this file. It is
   not decoration: type over a photograph is illegible without it, and a
   scrim is the honest fix where a shadow behind the letters is not. */
.band--media {
  position: relative;
  isolation: isolate;
}

.band__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.band--media::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--mgd-site-scrim);
}

/* Type over a photograph gets one step more contrast than type on the page,
   and this is a contrast fix rather than a second theme — exactly the same
   mechanism, and the same two-rule shape, as the .band--lime block above.

   The arithmetic, re-measured in Phase 7 because the figures this comment
   used to quote were wrong (it claimed 18.6 / 8.4 / 5.0 : 1). Against the flat
   #080B0A page the ramp measures 18.3 : 1 for text, 8.4 : 1 for secondary and
   5.16 : 1 for the quiet rung that .eyebrow and .t-faint now take. Under the
   scrim a photograph is lighter than that page everywhere except the very
   bottom, so every rung loses contrast and the quietest ones lose enough to
   fall under 4.5 : 1 — a 12px tracked eyebrow is the first thing to go,
   whichever grey it holds. Stepping the eyebrow to secondary and .t-dim to
   full text restores the margin without touching a line of markup, so a
   section keeps the same classes whether it sits on the page or on a picture.

   These three rules still win after Phase 7 re-pointed .eyebrow and .t-faint
   to the quiet token: .band--media .eyebrow is (0,2,0) against the
   component's (0,1,0), and .band--media .t-faint likewise outranks base.css's
   utility. Checked rather than assumed — a silently lost override here is
   invisible until someone photographs a white wall. */
.band--media .eyebrow,
.band--media .chip { color: var(--mgd-color-text-secondary); }

.band--media .t-dim { color: var(--mgd-color-text); }

.band--media .t-faint,
.band--media figcaption { color: var(--mgd-color-text-secondary); }

/* ── The poster hero's two extra contrast rules ────────────────────────
   .band--media.band--split reaches exactly one band on the site — index.html's
   hero, the only place a photograph sits behind a headline AND two phones. It
   needs a little more than the shared scrim for one reason that is a property
   of the photograph and not of the design: training.jpg's subject is lit from
   behind, her shoulder is the brightest thing in the frame, and it falls in the
   left column where the h1 goes.

   ONE. A flat wash beneath the scrim, not a second gradient. The shorthand's
   last layer is a <color>, so it paints as this ::before's background-colour
   underneath the same --mgd-site-scrim every media band uses: one gradient
   still exists in this stylesheet and it is still the token. 0.26 was chosen
   against the arithmetic already recorded on the token: the scrim alone reaches
   0.62 at the 52% stop, which is a measured 5.3 : 1 for warm-white type over a
   PURE WHITE photograph, so the headline never needed help. The eyebrow did —
   it sits around 27% down the band, where the ramp is only ≈0.39, and the
   scrim's own comment names a 12px tracked eyebrow as the first thing to go.
   0.26 lifts that to ≈0.55 composite while still letting 61% of the light
   through at the top of the frame, which is where her face is. A heavier wash
   reads as a dark panel and the client asked for a photograph.

   TWO. The eyebrow steps to full text colour here, one rung above the
   secondary that .band--media already gives it. Same mechanism as every other
   contrast fix in this file — a colour, in one place, reaching one band.

   Neither rule touches markup: the hero carries .band--ink as well, so a
   missing or slow training.jpg degrades to the flat surface this band had
   before the photograph arrived rather than to bare page colour. */
.band--media.band--split::before {
  background: var(--mgd-site-scrim), rgba(8, 11, 10, 0.26);
}

.band--media .poster .eyebrow { color: var(--mgd-color-text); }

/* ── The hero's two edges ─────────────────────────────────────────────
   One geometry override on that same single band, and it is what makes
   .duo's clip read as a BLEED rather than as an accident.

   .duo carries overflow:hidden over an aspect-ratio shorter than the phones
   inside it, so both devices are cut off on purpose, and its own comment
   explains why the clip has to live there instead of on this section. What
   that comment could not fix from inside .duo is WHERE the cut lands. With
   the shared --mgd-site-band-y on both sides, the phones stopped 112–240px
   short of the band's own bottom edge: a straight horizontal line across two
   screens, with nothing on it, and dark air underneath. A bleed needs an edge
   to bleed off, and there wasn't one.

   Zeroing the bottom padding puts the clip exactly on the boundary with §4,
   so the phones slide behind the next section — the edge of an image, which
   is one of the three depth sources this site is allowed. Nothing gets
   crowded: at ≥900px the .shell is a two-column grid with align-items:center
   and .duo is the taller column, so the copy stays optically centred and
   keeps the air the padding used to hold. Below 900px the single column ends
   on .duo, and §4 is a .section--tight carrying its own
   clamp(46px, 6.4vw, 102px) of top padding.

   The top drops to --mgd-site-section-y for a different reason: the fold.
   .site-header is sticky, so it sits in flow and its 64px is spent before
   this band's padding begins. At band-y the arithmetic was 64 + 240 + 587
   (.duo at its 544px cap × 108/100) + 240 ≈ 1131px, which is taller than the
   viewport it opens in — the client asked for a hero photograph that looks
   good on first sight, and a third of it was below the fold. At section-y and
   no bottom padding it measures ≈ 827px, so the whole poster, both phones and
   the crop line land in the first screen of a laptop.

   Reaches index.html:221 and nothing else — .band--media.band--split is used
   exactly once on the site, which is also why this can be a geometry rule and
   not a new variant class. */
.band--media.band--split {
  padding-block: var(--mgd-site-section-y) var(--mgd-space-none);
}

/* ── Split band ───────────────────────────────────────────────────────
   Copy on one side, a device or a frame on the other. The grid lives on the
   .shell so the two columns line up with every other constrained block on
   the page. One breakpoint, at 900px, because two columns of this width are
   unreadable below it — and no breakpoint anywhere else in this file, since
   .grid in base.css is auto-fit. Anatomy:

     <section class="band band--split">
       <div class="shell">
         <div class="band__copy"> eyebrow · h2 · p · cta </div>
         <figure class="device"> … </figure>
       </div>
     </section> */
.band--split .shell {
  display: grid;
  gap: var(--mgd-space-xl-4);
  align-items: center;
}

.band__copy {
  display: grid;
  gap: var(--mgd-space-lg);
  align-content: start;
}

.band__copy .storebar { margin-top: var(--mgd-space-md); }

@media (min-width: 900px) {
  .band--split .shell {
    grid-template-columns: 1fr 1fr;
    gap: var(--mgd-space-xl-6);
  }

  /* --flip reverses the pair on wide screens only. The DOM order stays
     copy-first so a screen reader and a phone both get the sentence before
     the picture of the thing it describes. */
  .band--flip .shell > :first-child { order: 2; }

  /* The hero is the one split band whose copy column is a .poster instead of
     a .band__copy, and a poster is centred by definition. Centred is right
     when the type owns the full width — which is what it does below this
     breakpoint, and what the reference does on a phone — but beside a
     picture it has to sit on the column's own edge or the two halves read as
     two unrelated objects. The reference's wide layout is left copy with the
     phones on the right, which is exactly this. Every other split band on
     the site uses .band__copy, so these three rules can reach nothing but
     the hero. */
  .band--split .poster {
    justify-items: start;
    text-align: left;
  }

  .band--split .poster > * { margin-inline: 0; }

  .band--split .poster .storebar { justify-content: flex-start; }
}

/* ══ FRAME ══════════════════════════════════════════════════════════════
   A photograph and its caption. The ratio is a custom property rather than
   a set of modifier classes, because a photograph's crop is a property of
   that photograph and not a design decision worth naming:

     <figure class="frame">
       <img src="…" alt="…" data-settle>
       <figcaption class="t-caption t-faint">…</figcaption>
     </figure>

   The default 3/2 is the ratio of the five delivered photographs, so none of
   them sets --frame-ratio at all; a screenshot or a wider crop is what the
   property is for.

   ON width AND height — the rule on this site is that they are mandatory,
   because they reserve the box before the file arrives and that is the
   difference between a page that settles and a page that jumps. The five
   photographs in index.html are the ONE documented exception, and the reason
   is that no tool in the build environment can read their intrinsic size:
   a guessed pair of numbers is a fact nobody can check, which is worse than
   an absence. Nothing is lost, because the line below reserves the box from
   CSS instead — aspect-ratio plus width:100% fixes the height before the
   first byte, exactly as the attributes would have. Any img whose size IS
   knowable still carries them: both screenshots (1080 × 2340, stated in the
   app manifest) and both lockups (600 × 102, the exact size section 9 of
   tools/brand-logotype.html writes) do. */
.frame {
  display: grid;
  gap: var(--mgd-space-md);
  margin: 0;
}

.frame > img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--frame-ratio, 3 / 2);
  object-fit: cover;
  border-radius: var(--mgd-site-radius-media);
  background-color: var(--mgd-color-surface);
}

/* --bleed is a photograph that runs to both edges of the viewport. It must
   be a direct child of the .band, NOT of the .shell: the band is already
   full width, so nothing has to be un-constrained with 100vw arithmetic
   that breaks the moment a scrollbar appears. Full-bleed media has no
   radius — a rounded corner at the screen edge is a card pretending to be
   a photograph. */
.frame--bleed > img {
  border-radius: 0;
  aspect-ratio: var(--frame-ratio, 21 / 9);
  max-height: 78vh;
}

.frame--bleed > figcaption {
  max-width: var(--mgd-site-page-max);
  margin-inline: auto;
  padding-inline: var(--mgd-site-gutter);
}

/* --empty is the declared slot for a photograph that does not exist yet. It
   is built, documented, and used by no page — the client closed the image
   budget, and the plan is explicit that no empty slot ships. It is written
   anyway, unlike .stat and .quote, for one reason: an empty frame cannot
   state a falsehood. It says "a photograph belongs here and there isn't
   one", which is true. A .stat would say "12,000 members", which is not.
   When a real photograph arrives, this class is what tells whoever adds it
   where it goes; the dashed hairline exists so it can never be mistaken for
   finished work if it is left in by accident. */
.frame--empty {
  display: grid;
  place-items: center;
  min-height: 240px;
  padding: var(--mgd-space-xl-2);
  border: var(--mgd-layout-hairline) dashed var(--mgd-color-border);
  border-radius: var(--mgd-site-radius-media);
  text-align: center;
}

/* ══ DEVICE ═════════════════════════════════════════════════════════════
   A pure-CSS phone shell around an unmodified screenshot. This is the
   substitution that lets the site have product photography without a photo
   shoot and without a single derived image file: no exported mockup, no
   drop-shadowed PNG, nothing to re-render when a screen changes.

   The radii are concentric, which is the whole trick to a device that looks
   drawn rather than assembled: outer 44px, bezel 8px, screen 36px, and
   44 − 8 = 36 exactly. Get that arithmetic wrong by 2px and the eye reads
   the corner as a mistake without being able to say why.

   The screen box is 1080/2340 — the aspect ratio of the two screenshots in
   assets/photo — so object-fit has nothing to crop and the app is shown
   whole. The ratio is what matters here, not the pixel count: V3 halved both
   files to 540 × 1170, which is the same 6:13 and changes nothing below.
   Anatomy:

     <figure class="device">
       <div class="device__screen">
         <img src="./assets/photo/screen-plan.jpg" width="540" height="1170"
              alt="…" data-settle>
       </div>
     </figure> */
.device {
  width: min(100%, 340px);
  margin: 0;
  padding: var(--mgd-site-bezel);
  border: var(--mgd-layout-hairline) solid var(--mgd-color-border);
  border-radius: var(--mgd-site-radius-device);
  background-color: var(--mgd-color-ink);
}

.device__screen {
  aspect-ratio: 1080 / 2340;
  overflow: hidden;
  border-radius: var(--mgd-site-radius-screen);
  background-color: var(--mgd-color-bg);
}

.device__screen > img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% var(--device-y, 50%);
}

/* One size variant: the same shell narrow enough to sit beside a paragraph
   rather than opposite it. --pair from the plan is deliberately NOT written
   — its definition was "tilts two", and a rotated pair of floating phones
   is precisely the flourish this rebuild exists to delete. V3 left this
   variant unused by any content page: §9 was the one band that stacked a
   small phone under a photograph, and that phone became the front frame of
   the hero. It stays because it costs one line, it is demonstrated in
   _reference/components.html, and the next band that has to put a screen
   beside a paragraph needs exactly this and nothing new. */
.device--sm { width: min(100%, 232px); }

/* A device is a fixed 340px object dropped into a 1fr column, so without this
   it hugs whichever edge the column starts at — and --flip moves that edge
   from left to right halfway down the page, which reads as a wobble rather
   than as a decision. Centred in its column, it lands the same way in both. */
.band--split .device { justify-self: center; }

/* ══ DUO — the two-phone hero ═══════════════════════════════════════════
   The reference's first viewport, and the one thing the V3 brief asked for
   by name: two upright phones, the back one larger, higher and further
   left, the front one smaller, lower and further right, overlapping it by
   about a third of itself, both bleeding off the bottom edge.

   THE OBJECTION, ANSWERED. The DEVICE block above refuses a --pair variant
   in writing, because its definition was "tilts two" and "a rotated pair of
   floating phones is precisely the flourish this rebuild exists to delete".
   That objection is to ROTATION AND FLOAT, not to two phones. This
   component applies no transform and no box-shadow; its depth is the front
   frame's hairline bezel sitting against the back frame's lit screen, which
   is the image edge Rule 1 names as a licensed source of depth. Hence a new
   name rather than a variant of the thing that was refused.

   THE GEOMETRY, in units where the container's width is 100 — closed form,
   because nothing here can measure a rendered pixel, and written only in
   percentages, so the pair holds from 320px to 1600px with no media query:

     back    spans  0 → 62     width 62%
     front   spans 46 → 100    width 54%, right: 0
     overlap 62 − 46 = 16 units = 30% of the front frame
     front top 18 units; container 108 units tall, so the tallest extent
     (18 + 117 = 135) is 80% visible and the rest bleeds off the bottom.

   THE ONE TRAP, and it is silent: a percentage `top` resolves against the
   container's HEIGHT, not its width. 18 units of a 108-unit-tall box is
   16.67%, not 18%. Writing top: 18% would push the front phone 19.4
   width-units down instead of 18 and quietly break the overlap. Footnote to
   the heights: the 1080/2340 ratio belongs to .device__screen, so a frame is
   18px shorter than width × 2.1667 — two 8px bezels and two hairlines — and
   the real bleed is nearer 15% than 20%. Less crop than the table predicts,
   never more, so it can never turn into a scrollbar.

   THE CAP. This container is a grid column of .band--split .shell, which
   never exceeds 544px at ≥900px — (1280 − 2 × 64 gutter − 64 gap) / 2 — and
   62% of that is 337px, within three pixels of the 340px a lone .device is
   capped to, so the hero's phones and the page's other phones read at one
   physical scale. Between 640px and 899px the shell is a single column up to
   809px wide, which would inflate the pair into an 874px-tall object on a
   tablet; min() holds it at the desktop size there and changes nothing at
   any other width. min() is not a media query.

   THE CLIP LIVES HERE, not on the band, and that is what makes the 80%
   above true: clipping the <section> would crop at the far side of 112–240px
   of band padding, so nothing would crop at all and both phones would hang
   in the gap under the fold instead.

   SOURCE ORDER IS LOAD-BEARING. .duo__frame--back's width and .device's
   width: min(100%, 340px) are both single-class selectors, so nothing but
   position in this file decides which one wins. These rules must stay AFTER
   the DEVICE block — the same hazard as .btn--store after .btn--primary,
   which cost several phases the first time it was missed. */
.duo {
  position: relative;
  width: min(100%, 544px);
  margin-inline: auto;
  aspect-ratio: 100 / 108;
  overflow: hidden;
}

.duo__frame {
  position: absolute;
  margin: 0;
}

.duo__frame--back {
  left: 0;
  top: 0;
  width: 62%;
}

.duo__frame--front {
  right: 0;
  top: 16.67%;
  width: 54%;
  z-index: 1;
}

/* ══ SHOT — honest crops of the two app screenshots ═════════════════════
   The second half of the product-as-photography substitution. Where .device
   shows a whole screen, .shot shows one part of it: a fixed-aspect window
   over the SAME unmodified PNG, positioned with object-position. Nothing is
   edited, nothing is exported, no derived file exists — a crop is CSS
   geometry, so a new screenshot updates every crop on the site at once.

   The window heights come from the arithmetic recorded in
   assets/photo/README.md: for a window of ratio r over a screenshot 2.167
   times taller than it is wide, the visible fraction is v = r / 2.167, and
   to centre a feature sitting at fraction f down the screen you set
   object-position Y = (f − v/2) / (1 − v). The seven values below are that
   formula applied to the seven features the site actually talks about; they
   are not eyeballed and must not be nudged by hand.

   THE FILENAME SWAP IS GONE, and this note stays only so nobody reinstates
   it. The masters arrived as Plan_page.png holding the HOME screen and
   Home_Page.png holding the PLAN screen — each named for the other. Three
   separate audits had to re-derive that, so V3's optimiser renames them by
   content: screen-home.png is the HOME screen, screen-plan.png is the PLAN
   screen, and the originals sit untouched in assets/photo/_original/ under
   their misleading names. The rules below read the same as they always did;
   what changed is that the name and the file finally agree.

   Nothing here is resolution-dependent either, which is why halving both
   files was safe: every value is a fraction. */
.shot {
  display: grid;
  gap: var(--mgd-space-md);
  margin: 0;
}

.shot > img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--shot-ratio, 16 / 9);
  object-fit: cover;
  object-position: 50% var(--shot-y, 50%);
  border-radius: var(--mgd-site-radius-media);
  background-color: var(--mgd-color-surface);
}

/* screen-home.png — the HOME screen: the check-in button, the gym card, the
   week strip, the quick actions row. */
.shot--checkin { --shot-ratio: 16 / 9; --shot-y: 14%; }
.shot--gym     { --shot-ratio: 16 / 9; --shot-y: 45%; }
.shot--week    { --shot-ratio: 16 / 9; --shot-y: 74%; }
.shot--actions { --shot-ratio: 16 / 9; --shot-y: 97%; }

/* screen-plan.png — the PLAN screen: today's focus, the seven-day plan, the
   consistency panel at the bottom. */
.shot--today   { --shot-ratio: 16 / 9; --shot-y: 10%; }
.shot--plan    { --shot-ratio: 4 / 3;  --shot-y: 59%; }
.shot--trophy  { --shot-ratio: 16 / 9; --shot-y: 97%; }

/* ══ STEPS ══════════════════════════════════════════════════════════════
   A numbered sequence: how check-in works, how a gym registers, how a
   deletion is requested. V1 drew a 44px lime-wash circle per step and a
   vertical gradient rail linking them — a gradient, a wash and a piece of
   ornament, three separate V2 violations in one component. V2 sets the
   numeral in the display face at title scale and separates steps with a
   hairline, which is the same information carried by the two mechanisms the
   site already has.

   The numeral is the markup's own text, never a CSS counter: the frozen
   legal pages number 1–5 and new markup writes 01, and a counter would
   silently overwrite whichever of those two the page chose.

   ALIAS: .steps--railed needs no rule of its own. It survives in frozen
   markup — delete-account.html, lines 144 and 305, verified — but always
   written as class="steps steps--railed", so .steps already styles it. New
   markup does not repeat the name: the rail it referred to is gone. */
.steps {
  display: grid;
  gap: var(--mgd-space-xl-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.step {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--mgd-space-lg);
  align-items: start;
}

.step + .step {
  border-top: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
  padding-top: var(--mgd-space-xl-2);
}

.step__num {
  font: var(--mgd-site-title);
  letter-spacing: var(--mgd-site-track-title);
  color: var(--mgd-color-text-tertiary);
  font-variant-numeric: tabular-nums;
  /* Optical alignment: Anton's cap height sits high in its line box, so the
     numeral needs pulling down a hair to sit on the heading's baseline. */
  margin-top: 0.06em;
  min-width: 1.6ch;
}

.step__text {
  display: grid;
  gap: var(--mgd-space-sm);
}

/* ══ CARD ═══════════════════════════════════════════════════════════════
   Reduced to what a card actually is: a hairline, a step up the surface
   ramp, and 24px of air. V1 had eight variants including a frosted-glass
   one with an @supports fork; V2 has four, and .card--glass is left
   undefined so the pages still carrying that name fall back to this block.

   Flex column rather than grid, so .card__foot can be pushed to the bottom
   edge with margin-top:auto and a row of cards ends level. */
.card {
  display: flex;
  flex-direction: column;
  gap: var(--mgd-space-md);
  padding: var(--mgd-space-xl-2);
  border: var(--mgd-layout-hairline) solid var(--mgd-color-border);
  border-radius: var(--mgd-site-radius-card);
  background-color: var(--mgd-color-surface);
}

/* 32px is right on a desktop and wrong on a phone. Inside .shell at a 320px
   viewport the card is 280px wide, so 32px a side plus the two hairlines
   leaves a 214px content box — 52px of inset before the first character, on
   the narrowest measure the site ever sets. The clamp spends 24px (the base
   .card inset, so nothing looks out of family) until 6vw overtakes it at
   about a 400px viewport, and reaches the full 32px at about 533px. Every
   width the padding was designed for keeps it; only the phone gets the 16px
   of measure back. Used by support.html and delete-account.html. */
.card--pad-lg { padding: clamp(var(--mgd-space-xl-2), 6vw, var(--mgd-space-xl-3)); }

.card--tall { height: 100%; }

/* No border, no fill, no padding: a column of text that only needs the
   card's internal type rules. Used where a grid wants alignment, not boxes. */
.card--flush {
  padding: 0;
  border: 0;
  background-color: transparent;
}

/* A whole card that is one link. Hover moves the hairline, nothing else —
   no lift, no shadow, no scale. */
.card--link {
  color: inherit;
  text-decoration: none;
}

/* THE HOVER EASE HAS TO BE RESTATED AT TWO CLASSES, and the reason is pure
   specificity. All six shipped .card--link anchors (privacy.html ×1,
   delete-account.html ×2, 404.html ×3) also carry data-reveal, and
   base.css's `.js-reveal [data-reveal]` is (0,2,0) — it outranks a bare
   .card--link at
   (0,1,0) and takes the whole `transition` shorthand with it. So for the
   entire life of this component the border-color transition above never
   applied: the hairline snapped. `.card.card--link` is also (0,2,0) and this
   file loads after base.css, so it wins on order.

   LONGHANDS, NOT THE SHORTHAND, and this is the part that is easy to get
   wrong. Writing `transition: border-color …, opacity …, transform …` here
   would reset transition-delay to 0s, and base.css sets
   `transition-delay: var(--reveal-delay)` — which is how delete-account's two
   cards stagger 60ms apart inside their [data-reveal-stagger]. Setting the
   three properties individually leaves the delay alone. Re-declaring opacity
   and transform is not optional either: this rule now owns transition-property
   for these elements, so anything left out simply stops transitioning. */
.card.card--link {
  transition-property: border-color, opacity, transform;
  transition-duration: var(--mgd-site-dur-fast), var(--mgd-site-dur-slow),
    var(--mgd-site-dur-slow);
  transition-timing-function: linear, var(--mgd-site-ease), var(--mgd-site-ease);
}

.card--link:hover { border-color: var(--mgd-color-text-tertiary); }

.card__head {
  display: flex;
  align-items: center;
  gap: var(--mgd-space-md);
}

/* The frozen pages write <h3 class="card__title"> with no type utility on
   it, so these two carry their own type. Both stay in the TEXT face: a card
   title is read, and putting Anton at 22px would make every card shout.

   min-width: 0 because .card__head is a flex row and this is a flex child: a
   child's default min-width is auto, so a title holding an email address, a
   URL or a gym code would set the card's min-content width and push the grid
   past the gutter on a phone. Every title on the site is short prose today,
   which is exactly the kind of thing that stays true until it does not. */
.card__title {
  min-width: 0;
  overflow-wrap: anywhere;
  font: var(--mgd-site-heading);
  letter-spacing: var(--mgd-site-track-heading);
  color: var(--mgd-color-text);
}

.card__body {
  font: var(--mgd-site-body);
  color: var(--mgd-color-text-secondary);
  max-width: var(--mgd-site-measure-body);
}

/* 13px on a --mgd-color-surface card is the worst small-text ground on the
   site: the mirrored tertiary grey measures 3.86 : 1 there, a step worse than
   on the page itself. The quiet rung clears it at 4.69 : 1. */
.card__foot {
  margin-top: auto;
  padding-top: var(--mgd-space-md);
  border-top: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
  font: var(--mgd-site-caption);
  color: var(--mgd-site-text-quiet);
}

/* ══ TILE AND GLYPH ═════════════════════════════════════════════════════
   Surviving, reduced. The plan deletes .tile, but §5.3 keeps the two marks
   on support.html §4 and the partner mark on delete-account, and those are
   frozen documents Google Play reads — so the class survives as a hairline
   box that holds a 24px mark and nothing more. Every V1 flourish (the
   gradient fill, the inner highlight, the 2px lime ring) is gone.

   --lime is the ONE place a lime wash appears on this site, and it is not a
   style choice: the marks in assets/svg are <img> files carrying their own
   lime, and an <img> cannot be recoloured by CSS. A solid-lime tile would
   erase the mark inside it. So the tile goes dark-green and the lime mark
   reads against it. If the marks are ever inlined as SVG with currentColor,
   this variant should become a flat lime fill with an ink mark and the wash
   should leave the codebase with it. */
.tile {
  display: inline-grid;
  place-items: center;
  width: var(--mgd-layout-control-sm);
  height: var(--mgd-layout-control-sm);
  flex: 0 0 auto;
  border: var(--mgd-layout-hairline) solid var(--mgd-color-border);
  border-radius: var(--mgd-site-radius-card);
  background-color: var(--mgd-color-surface);
}

.tile--lime {
  border-color: transparent;
  background-color: var(--mgd-color-primary-wash);
}

.tile--lg {
  width: var(--mgd-layout-control-lg);
  height: var(--mgd-layout-control-lg);
}

/* --brand holds a raster brand mark rather than a line glyph, so it needs
   the padding a bitmap wants and object-fit to keep the logo's own ratio. */
.tile--brand {
  padding: var(--mgd-space-sm);
  background-color: var(--mgd-color-bg-alt);
}

.tile--brand > img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.glyph {
  display: block;
  width: 24px;
  height: 24px;
}

/* ══ NOTICE ═════════════════════════════════════════════════════════════
   A called-out paragraph: "paid plans are not live yet", "this cannot be
   undone", "do not use MyGymDay as proof of attendance". It exists in two
   anatomies across the frozen legal pages and BOTH must keep working —
   Phase 5 is forbidden from tidying either into the other:

     out of prose  <div class="notice notice--danger">
                     <p class="notice__title">…</p><p>…</p></div>
     inside prose  <p class="notice notice--lime">
                     <span class="notice__title">…</span><span>…</span></p>

   display:grid makes both behave identically: block children stack, and the
   inline spans of the second form become rows instead of running together.

   V1 filled these with a coloured wash. V2 states the severity with a 2px
   rule on the leading edge and one step up the surface ramp, so the four
   variants differ by a single custom property and never by a background. */
.notice {
  --notice-accent: var(--mgd-color-border);
  display: grid;
  gap: var(--mgd-space-sm);
  padding: var(--mgd-space-lg) var(--mgd-space-xl);
  border-inline-start: 2px solid var(--notice-accent);
  border-radius: 0 var(--mgd-site-radius-card) var(--mgd-site-radius-card) 0;
  background-color: var(--mgd-color-surface);
  font: var(--mgd-site-body);
  color: var(--mgd-color-text-secondary);
}

.notice--lime   { --notice-accent: var(--mgd-color-primary); }
.notice--warn   { --notice-accent: var(--mgd-color-warning); }
.notice--danger { --notice-accent: var(--mgd-color-danger); }

.notice__title {
  font-weight: 700;
  color: var(--mgd-color-text);
}

/* Inside .prose, base.css gives every paragraph a top margin; the notice
   sets its own rhythm because it is a block, not a sentence. */
.prose > .notice {
  margin-block: var(--mgd-space-xl-2);
  max-width: var(--mgd-site-measure-prose);
}

/* ══ CHECKS ═════════════════════════════════════════════════════════════
   A list where each item is a sentence, not a fragment. The marker is a
   10px lime hairline rather than a tick or a dot: a drawn tick is an icon
   we would have to ship, and a row of dots reads as bullet points from a
   slide deck. A repeated hairline is texture rather than an accent, so it
   does not spend the two-lime-elements-per-viewport budget — the budget
   governs things that pull the eye, and six identical 1px rules do not.

   Anatomy, frozen: <ul class="checks" role="list"><li><span>…</span></li>

   THE ROW GAP HAS TO BEAT THE LINE GAP. Each item is a sentence at
   --mgd-site-body, 16px on 1.60, which leaves about 9.6px of white between
   its own wrapped lines. At the 12px this list used to carry, the boundary
   between two items was barely two pixels looser than a line break inside
   one, so a six-item list read as one grey paragraph with hairlines in it.
   16px restores the ordering — item 16 > line 9.6 — and is the same
   correction base.css took in .prose. The 12px on the item below is a
   different axis (marker to text) and stays. */
.checks {
  display: grid;
  gap: var(--mgd-space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

.checks > li {
  display: grid;
  grid-template-columns: 10px 1fr;
  gap: var(--mgd-space-md);
  align-items: start;
  font: var(--mgd-site-body);
  color: var(--mgd-color-text-secondary);
  max-width: var(--mgd-site-measure-body);
}

.checks > li::before {
  content: '';
  height: var(--mgd-layout-hairline);
  /* Sits on the first line's optical centre: half of body line-height. */
  margin-top: 0.8em;
  background-color: var(--mgd-color-primary);
}

/* The limits list on support.html. Same shape, no lime: these are the
   things we cannot do, and they should not be presented as features. */
.checks--muted > li::before { background-color: var(--mgd-color-text-tertiary); }

/* ══ FAQ ════════════════════════════════════════════════════════════════
   Native <details>, so every answer is findable by find-in-page before any
   JavaScript runs and printable whether or not it is open. Hairline-ruled
   rows, no boxes, no chevron image: the marker is a + that becomes a −.

   This comment used to claim the marker "is aria-hidden by being a
   pseudo-element". That is false, and it is a common enough belief to be
   worth correcting in place: generated content is rendered text, and the
   engines that render it fold it into the accessible name — so a screen
   reader announced "You use the member app plus". The @supports block below
   is the fix. */
.faq {
  border-top: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
}

.faq__item {
  border-bottom: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
}

.faq__q {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--mgd-space-lg);
  align-items: baseline;
  padding-block: var(--mgd-space-xl);
  font: var(--mgd-site-heading);
  letter-spacing: var(--mgd-site-track-heading);
  color: var(--mgd-color-text);
  cursor: pointer;
  list-style: none;
  transition: color var(--mgd-site-dur-fast) linear;
}

.faq__q::-webkit-details-marker { display: none; }

.faq__q:hover { color: var(--mgd-color-primary); }

/* The glyph keeps the tertiary token deliberately: it is set in
   --mgd-site-title, so 26px at its smallest and Anton at 400 — large text,
   where 4.24 : 1 clears the 3 : 1 bar with room. It is the state indicator,
   not a label, and one step quieter than the question is the point. */
.faq__q::after {
  content: '+';
  font: var(--mgd-site-title);
  line-height: 1;
  color: var(--mgd-color-text-tertiary);
}

.faq__item[open] .faq__q::after { content: '\2212'; }

/* Alternative text for generated content: the `/ ''` half tells the engine
   the glyph has no textual equivalent, so the summary's accessible name is
   the question and nothing else.

   IT MUST BE FEATURE-DETECTED. A declaration an engine cannot parse is
   dropped whole — not partially — so writing `content: '+' / ''` in the rule
   above would leave older browsers with no marker at all: a FAQ row that
   gives no sign it opens. Inside @supports, the plain glyph above stands
   everywhere and only engines that understand the two-part syntax take the
   override. Same selectors, later in the file, so it wins on order. */
@supports (content: '+' / '') {
  .faq__q::after { content: '+' / ''; }

  .faq__item[open] .faq__q::after { content: '\2212' / ''; }
}

.faq__a {
  padding-bottom: var(--mgd-space-xl);
  font: var(--mgd-site-body);
  color: var(--mgd-color-text-secondary);
  max-width: var(--mgd-site-measure-body);
}

.faq__a p + p { margin-top: var(--mgd-space-md); }

/* ══ CTA BAND ═══════════════════════════════════════════════════════════
   Kept, against the plan's Phase 2 bullet, and the reasoning is on the
   record: §7 lists .cta-band for deletion but §3.2 — the authoritative
   deletion inventory, the one with per-page removal sites — never mentions
   it, §3.1 does not list it either, and it is in live use on
   delete-account.html inside markup §5.3 does not authorise editing. Delete
   the class and a Play-facing legal document is left with a permanently
   unstyled block. So it survives, restyled: a hairline box, a flat fill,
   no radial gradient, no glow, no overflow tricks. */
.cta-band {
  display: grid;
  justify-items: center;
  gap: var(--mgd-space-xl-2);
  padding: var(--mgd-space-xl-4) var(--mgd-space-xl-2);
  border: var(--mgd-layout-hairline) solid var(--mgd-color-border);
  border-radius: var(--mgd-site-radius-card);
  background-color: var(--mgd-color-surface);
  text-align: center;
}

/* THE ONE PLACE A BUTTON IS ALLOWED TO WRAP. Every .btn on the site is
   `white-space: nowrap`, which is correct for the short labels: a two-line
   pill reads as a mistake. This band is the exception, because the label
   inside it is an address — "Email support@mygymday.app" is 26 characters,
   about 260px of 17px Inter, and nowrap makes min-content equal max-content
   so the pill cannot shrink. At 320px the band's content box is 230px
   (280px shell − 48px padding − 2px border) and `justify-items: center`
   split the 80px of overflow evenly, so the button crossed the hairline on
   BOTH sides and stopped ~8px from each screen edge. It cleared its own
   frame only from 414px up.

   Two 24px lines fit inside the existing --btn-h of 52px, so nothing moves
   vertically. overflow-wrap: anywhere is for the address itself: it has no
   space to break at, and a long domain would otherwise set the min-content
   width all over again. This also settles the store badge in partners.html's
   band, whose kicker + tag + padding come to ~250px against the same box. */
.cta-band .btn {
  white-space: normal;
  overflow-wrap: anywhere;
}

/* ══ FOOTER ═════════════════════════════════════════════════════════════ */
.site-footer {
  border-top: var(--mgd-layout-hairline) solid var(--mgd-color-border);
  padding-block: var(--mgd-space-xl-5) var(--mgd-space-xl-3);
}

.site-footer__top {
  display: grid;
  gap: var(--mgd-space-xl-3);
}

.site-footer__brand {
  display: grid;
  gap: var(--mgd-space-lg);
  align-content: start;
  max-width: var(--mgd-site-measure-lead);
}

@media (min-width: 820px) {
  .site-footer__top {
    grid-template-columns: 1.6fr repeat(3, 1fr);
    gap: var(--mgd-space-xl-4);
  }
}

/* The footer sets no background of its own, so both of these sit on the
   page's #080B0A and the quiet rung measures 5.16 : 1 there. They are 12px
   and 13px text — the two sizes the mirrored tertiary grey cannot carry. */
.footer-group__title {
  margin-bottom: var(--mgd-space-md);
  font: var(--mgd-site-eyebrow);
  letter-spacing: var(--mgd-site-track-eyebrow);
  text-transform: uppercase;
  color: var(--mgd-site-text-quiet);
}

/* gap: 0 with a 44px link, rather than 8px with a 16px one. The pitch works
   out at 44px either way you write it; the difference is whether the 44px
   belongs to the target or to the space beside it, and only one of those is
   tappable. The old spacing gave a 24px pitch — a 16px line box plus 8px —
   which is a mouse measurement.

   These are the links a phone actually needs to hit. Privacy, Terms and
   Delete account are here and nowhere else, they are the three URLs Google
   Play requires, and a mis-tap in a stack of 24px rows lands on the
   neighbour. */
.footer-list {
  display: grid;
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* display:flex, not inline-flex, so the target fills the column instead of
   hugging the glyphs: "Terms" is about 40px of text at this rung and would
   miss 2.5.5 on its short axis if the box stopped at the label. Nothing
   visible changes — the hover state is a colour, not an underline or a
   fill — so a wider hit area costs nothing and reads as nothing. On a phone
   the footer is one column, which makes every one of these a full-width
   row. */
.footer-list a {
  display: flex;
  align-items: center;
  min-height: var(--mgd-layout-min-touch);
  font: var(--mgd-site-body);
  color: var(--mgd-color-text-secondary);
  text-decoration: none;
  transition: color var(--mgd-site-dur-fast) linear;
}

.footer-list a:hover { color: var(--mgd-color-text); }

.footer-list a[aria-current='page'] { color: var(--mgd-color-text); }

/* After aria-current, for the same reason .nav__link:active is: equal
   specificity, and the current page's own footer link should still answer a
   tap. */
.footer-list a:active { color: var(--mgd-color-text-tertiary); }

.site-footer__legal {
  display: grid;
  gap: var(--mgd-space-xs);
  margin-top: var(--mgd-space-xl-4);
  padding-top: var(--mgd-space-xl);
  border-top: var(--mgd-layout-hairline) solid var(--mgd-color-divider);
  font: var(--mgd-site-caption);
  color: var(--mgd-site-text-quiet);
}

/* ══ SETTLE — the third motion primitive ════════════════════════════════
   An image that arrives at 106% and comes to rest at 100% over 1200ms. It
   is the only motion on the site that is not a fade or a rise, and it is
   worth having because it makes a screenshot feel photographed rather than
   pasted. Note what it does NOT need: a single line of JavaScript. site.js
   already adds .is-in to each [data-reveal] as it enters the viewport, so
   settle is a descendant selector on that class — which also means
   tokens.css's prefers-reduced-motion block stills it for free.

   Put data-settle on the <img>, INSIDE a [data-reveal] block — never on the
   same element, because the selector below is a descendant one and an
   element cannot be its own descendant. Inside .device__screen the scale is
   clipped by the screen's rounded box; on a .shot or .frame it bleeds a few
   pixels past the figure for the duration and then sits down, which is the
   effect. Longhand transform is used rather than a scale() shorthand so
   nothing here can collide with the translate the reveal owns.

   The reduced-motion query below is one of the three hand-maintained
   exceptions named in rule 4 at the top of this file. It cannot be left to
   the token layer: 1ms is enough to make a fade imperceptible, but a 1ms
   transition from scale(1.06) to 1 is a visible snap of a whole image. The
   scale has to be dropped, not shortened. */
[data-settle] { transform-origin: center; }

.js-reveal [data-reveal] [data-settle] {
  transform: scale(1.06);
  transition: transform var(--mgd-site-dur-settle) var(--mgd-site-ease);
}

.js-reveal [data-reveal].is-in [data-settle] { transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js-reveal [data-reveal] [data-settle] {
    transform: none;
    transition: none;
  }
}

/* ══ TAIL ═══════════════════════════════════════════════════════════════ */
/* A safety net, and deliberately the last declaration in the last stylesheet.
   No shipped page uses the attribute: Phase 6 established that there is no
   Coming-Soon switch and no hidden twin of any store link, and an earlier
   version of this comment claimed there was. It stays anyway, because the
   browser's own [hidden] rule is display: none at the weakest specificity
   there is, and almost every card, band and storebar above sets its own
   display: flex or grid. Put hidden on one of those without this line and
   the element goes on painting. */
[hidden] { display: none !important; }

/* base.css already turns the page to ink on paper and un-clamps .prose.
   This adds only the chrome a printed policy has no use for.

   One coupling to know before editing this block: the lockup export is drawn
   light-on-dark — warm-white "GymDay" beside lime "My" — so on white paper the
   larger half of it disappears. Both places that hold a lockup are hidden here,
   which is the only reason print is safe. Un-hide either one and use
   assets/brand/mgd-lockup-ink.svg in the same edit; that is the mono variant
   section 9 exports for exactly this case. */
@media print {
  .site-header,
  .site-footer__top,
  .storebar,
  .btn {
    display: none;
  }

  .card,
  .notice,
  .cta-band {
    background: transparent;
    border-color: #999999;
  }
}

