/* ============================================================
   LLYR — THE BOARD
   One shared system for all seven pages.

   The world is the board every one of these sports already has on
   its wall: the pace clock on a pool deck, the split board
   trackside, the card at a range, the chalkboard in a bike shop.
   Cold laminate ground, ruled tick grid, dense black ink, and the
   four app accents used the way lane tabs and a grease pencil are
   used — as marks ON the board, never as glowing text.

   Why it carries the product: a board shows what was written and
   leaves blank what wasn't. An empty ruled cell is a visible,
   honest gap. That is Llyr's whole thesis rendered as a material
   fact instead of a caption.

   Class names are load-bearing. assets/llyr.js binds to .btn,
   .btn-primary, .btn-ghost, .sport-pill, .pick, .reveal, .signup,
   .wl-*, #waitlist-form, #hamburger, #mobile-menu, [data-track]
   and <section data-section>. tool/landing-check/check.py asserts
   every colour token below. Rebuild appearances freely; do not
   rename these hooks.
   ============================================================ */

:root {
  /* ---- Palette tokens mirror lib/ui/palette.dart. -------------
     check.py::check_palette_tokens asserts these exact values.
     Do not change one without changing the app. ---------------- */
  --bg: #070B18;
  --surface: #121A30;
  --surface-high: #1D2845;

  --aqua: #2B8FFF;
  --cyan: #22D3EE;
  --lime-cycling: #C6FF3D;
  --lime-cycling-alt: #34E5A0;
  --orange-running: #FF7A2F;
  --orange-running-alt: #FFC93C;
  --violet-golf: #A855F7;
  --violet-golf-alt: #F472B6;
  --lime: #FFB020;
  --orange: #FF9F1C;

  --optimal: #22E07A;
  --fault: #FF4757;
  --caution: #FFB020;

  --brand-navy: #022D50;

  /* ---- Channel forms of the accents. -------------------------
     The old sheet wrote rgba(43,143,255,.35) literally in nine
     places, so a palette change updated the tokens, passed the
     gate, and left forty stale glows behind (audit P2). Every
     translucent use now derives from these. ------------------- */
  --aqua-rgb: 43,143,255;
  --ink-rgb: 18,20,15;
  --fault-rgb: 255,71,87;
  --optimal-rgb: 34,224,122;

  /* ---- The board itself. -------------------------------------
     Not cream and not paper. This is enamel/laminate: a cold
     off-white with a faint green-grey cast, the colour of a real
     lap board bleached by pool light. The use scene decides it —
     these surfaces are read outdoors, in sun, at arm's length,
     so the ground is bright and the ink is dense. ------------- */
  --board: #E8EAE4;
  --board-2: #F3F4F0;
  --board-3: #DCDFD7;
  --ink: #12140F;
  --ink-2: #51564B;
  /* Every --ink-3 use is 12-15px stencil lettering, so it has to clear 4.5:1
     on the DARKEST ground it ever sits on (--board-3). The old sheet's
     equivalent token failed at 3.19:1; this one is 4.58:1 there and 5.10:1
     on --board. Do not lighten it. */
  --ink-3: #5E6357;

  --rule: rgba(var(--ink-rgb), .13);
  --rule-mid: rgba(var(--ink-rgb), .26);
  --rule-strong: rgba(var(--ink-rgb), .52);

  /* Pinned plates — the app's own dark UI, sitting on the board. */
  --plate: var(--bg);
  --plate-edge: rgba(var(--ink-rgb), .30);

  --maxw: 1240px;
  --gut: 26px;

  /* Physical depth: offset down, soft blur. A board casts a real
     shadow; it never emits a halo. */
  --lift-1: 0 2px 4px rgba(var(--ink-rgb), .10), 0 8px 18px rgba(var(--ink-rgb), .09);
  --lift-2: 0 4px 8px rgba(var(--ink-rgb), .12), 0 18px 40px rgba(var(--ink-rgb), .16);
  --lift-3: 0 6px 12px rgba(var(--ink-rgb), .14), 0 30px 64px rgba(var(--ink-rgb), .20);

  --ease: cubic-bezier(.16,1,.3,1);
}

/* ---- Reset + ground ---------------------------------------- */
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--board);
  color: var(--ink);
  font-family: "Barlow", system-ui, -apple-system, sans-serif;
  font-size: 17.5px;
  line-height: 1.62;
  overflow-x: hidden;
  /* The ruled sheet. Two hairlines on a 34px module — the faint
     grid of a lap board, visible only when you look for it. */
  background-image:
    linear-gradient(to bottom, var(--rule) 1px, transparent 1px);
  background-size: 100% 34px;
  background-position: 0 -1px;
}

img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }

::selection { background: var(--ink); color: var(--board); }

/* ---- Type -------------------------------------------------- */
h1, h2, h3, h4, .display {
  font-family: "Barlow Condensed", "Barlow Semi Condensed", sans-serif;
  text-transform: uppercase;
  font-weight: 900;
  line-height: .92;
  letter-spacing: -.005em;
  margin: 0;
  text-wrap: balance;
}
h1 { letter-spacing: -.018em; }

p { text-wrap: pretty; }
.tnum { font-variant-numeric: tabular-nums; }
.muted { color: var(--ink-2); }

/* Measurement lettering: the small stencilled labels a board
   carries. Real tabular figures of the brand face, not a mono
   costume. */
.stencil {
  font-family: "Barlow Condensed", sans-serif;
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: .2em;
  font-size: 12.5px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gut); }
section { position: relative; }

/* ---- Focus ------------------------------------------------- */
a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ---- Skip link (audit P3) ---------------------------------- */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--ink); color: var(--board);
  padding: 12px 20px; font-weight: 700;
}
.skip:focus { left: 12px; top: 12px; }

/* ============================================================
   THE TICK RULE
   The bezel of a pace clock, the edge of a ruler. This is the
   system's signature mark and the only divider the site uses.
   ============================================================ */
.tickrule {
  height: 13px;
  border-bottom: 1.5px solid var(--rule-strong);
  background-image: repeating-linear-gradient(
    to right,
    var(--rule-strong) 0 1.5px,
    transparent 1.5px 100%
  );
  background-size: 13px 7px;
  background-repeat: repeat-x;
  background-position: 0 100%;
}
.tickrule.tall { height: 22px; background-size: 13px 12px; }
/* Every fifth tick is a long one, exactly like a clock face. */
.tickrule.tall::after {
  content: ""; display: block; height: 100%;
  background-image: repeating-linear-gradient(
    to right, var(--rule-strong) 0 1.5px, transparent 1.5px 100%);
  background-size: 65px 20px;
  background-repeat: repeat-x;
  background-position: 0 100%;
}

/* ============================================================
   BUTTONS — stencilled plates, not pills
   ============================================================ */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 54px; padding: 0 30px;
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 800; font-size: 19px;
  text-transform: uppercase; letter-spacing: .07em;
  border: 0; border-radius: 2px; cursor: pointer;
  transition: transform .3s var(--ease), box-shadow .3s var(--ease),
              background-color .2s ease, color .2s ease;
}
.btn-primary {
  background: var(--ink); color: var(--board);
  box-shadow: var(--lift-1);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--lift-2); }
.btn-ghost {
  background: transparent; color: var(--ink);
  box-shadow: inset 0 0 0 1.5px var(--rule-strong);
}
.btn-ghost:hover { background: rgba(var(--ink-rgb), .06); transform: translateY(-2px); }
.btn[aria-disabled="true"] { opacity: .55; cursor: default; transform: none; }
@media (prefers-reduced-motion: reduce) { .btn:hover { transform: none; } }

/* ============================================================
   NAV — the header strip of a board
   ============================================================ */
header.nav {
  position: sticky; top: 0; z-index: 100;
  background: var(--board);
  border-bottom: 1.5px solid var(--ink);
}
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: 70px; }
.brand { display: flex; align-items: center; gap: 12px; }
.brand .logo {
  width: 36px; height: 36px; border-radius: 3px;
  /* The mark is white line art, so it needs its dark plate. This
     is logo identity, not a UI surface — see PRODUCT.md. */
  background: var(--brand-navy);
  display: grid; place-items: center; overflow: hidden;
  box-shadow: var(--lift-1);
}
.brand .logo img { width: 31px; height: 31px; object-fit: contain; }
.brand .word {
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 900; font-size: 30px; letter-spacing: .14em;
}
.nav-links { display: flex; align-items: center; gap: 4px; }
.nav-links a.link {
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 700; font-size: 17px;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink-2);
  padding: 12px 14px; min-height: 44px; display: inline-flex; align-items: center;
  border-bottom: 2px solid transparent;
  transition: color .18s ease, border-color .18s ease;
}
.nav-links a.link:hover { color: var(--ink); border-bottom-color: var(--ink); }
.nav-links a.link[aria-current="page"] { color: var(--ink); border-bottom-color: var(--ink); }
.nav-cta { display: flex; align-items: center; gap: 12px; }
.nav-cta .btn { min-height: 46px; font-size: 16px; padding: 0 20px; }
.hamburger {
  display: none; width: 46px; height: 46px; border-radius: 2px;
  background: transparent; box-shadow: inset 0 0 0 1.5px var(--rule-strong);
  cursor: pointer; align-items: center; justify-content: center;
}
.hamburger svg { width: 23px; height: 23px; stroke: var(--ink); stroke-width: 2; }
#mobile-menu { display: none; }

@media (max-width: 900px) {
  .nav-links, .nav-cta .btn { display: none; }
  .hamburger { display: inline-flex; }
  #mobile-menu.open {
    display: flex; flex-direction: column;
    padding: 8px var(--gut) 20px;
    background: var(--board);
    border-bottom: 1.5px solid var(--ink);
  }
  #mobile-menu a.link {
    font-family: "Barlow Condensed", sans-serif;
    padding: 15px 2px; font-size: 22px; font-weight: 800;
    text-transform: uppercase; letter-spacing: .08em;
    color: var(--ink); border-bottom: 1px solid var(--rule);
    min-height: 44px; display: flex; align-items: center;
  }
  #mobile-menu .btn { display: inline-flex; margin-top: 16px; }
}

/* ============================================================
   THE CTA DOCK — the primary action, on a phone
   ============================================================
   Below 900px the nav drops BOTH its links and its button, so from the moment
   a phone visitor scrolls past the hero there is no way to act without
   scrolling to the bottom of a long page. That is the single worst conversion
   defect on the site and it only exists at phone width, which is where a
   product about propping your phone up is most likely to be read.

   So: a slim plate that docks to the bottom edge once the hero has gone, and
   undocks again when the real form comes into view — it must never sit on top
   of the thing it points at. llyr.js owns both transitions.

   Board grammar, not an app bar: laminate ground, one heavy rule along the
   top edge, stencil lettering, a stamped rectangle for the action. */
.cta-dock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
  display: none; align-items: center; justify-content: space-between; gap: 14px;
  padding: 11px var(--gut) calc(11px + env(safe-area-inset-bottom, 0px));
  background: var(--board-2);
  border-top: 1.5px solid var(--ink);
  box-shadow: 0 -6px 22px rgba(var(--ink-rgb), .13);
}
.cta-dock .seat {
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 700; font-size: 13px; line-height: 1.25;
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-3);
}
.cta-dock .seat b {
  display: block; font-weight: 900; font-size: 19px;
  letter-spacing: .06em; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.cta-dock .btn { min-height: 48px; font-size: 17px; padding: 0 20px; flex: none; }
@media (max-width: 900px) {
  /* Present in the tree but off the bottom edge until llyr.js docks it, so a
     no-JS visitor is never left with a bar they cannot dismiss.
     `visibility` is what keeps the undocked bar out of the tab order — a
     translated-off-screen link is still focusable, and a keyboard visitor
     would otherwise tab into a button they cannot see. It is deferred by the
     length of the transition so the slide still plays on the way out. */
  .js .cta-dock {
    display: flex;
    visibility: hidden;
    transform: translateY(105%);
    transition: transform .42s var(--ease), visibility 0s linear .42s;
  }
  .js .cta-dock.docked {
    visibility: visible;
    transform: none;
    transition: transform .42s var(--ease), visibility 0s;
  }
}
@media (prefers-reduced-motion: reduce) {
  .js .cta-dock { transition: none; }
  .js .cta-dock.docked { transition: none; }
}

/* ============================================================
   HERO — the board's headline, marked by hand
   ============================================================ */
/* Top pad reduced from a 92px ceiling to 74px on 2026-08-13: the lede added a
   two-line block to the copy column and a 900px-tall laptop was cutting the
   primary action in half. The action stays in the first viewport. */
.hero { padding: clamp(44px, 8vw, 74px) 0 0; }
/* The read is landscape and carries the argument, so it takes the larger
   column — the old split was sized for a 340px dial. */
.hero-grid {
  display: grid; grid-template-columns: 1fr 1.12fr;
  gap: clamp(28px, 5vw, 56px); align-items: center;
}
/* Display ramp caps at 96px (6rem), the craft floor's ceiling. */
.hero h1 {
  font-size: clamp(50px, 8.4vw, 96px);
  max-width: 13ch;
}
/* The second line is marked, not coloured: a grease-pencil
   underline drawn beneath it. Emphasis by weight and mark —
   never gradient text. */
/* Painted as a background on the text itself rather than an absolutely
   positioned child: box-decoration-break carries it across a line wrap, and
   it cannot fall behind the page ground the way a z-index:-1 child did.
   The mark defaults to fully drawn and only the ENTRANCE is gated on .js, so
   a no-script visitor gets the mark rather than an invisible bar.
   --mark is set per page so each sport is underlined in its own lane colour. */
.hero h1 .accent {
  --mark: var(--aqua);
  background-image: linear-gradient(var(--mark), var(--mark));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% .13em;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
  padding-bottom: .05em;
  /* The mark needs somewhere to sit when the marked line WRAPS. At .92
     leading a wrapped mark landed on the cap height of the line beneath it and
     read as a strike-through — visible on every sport page at phone width,
     where the marked half of the headline runs to two lines. Leading is set on
     the span, not the h1, so only the lines the mark actually occupies open
     up and the unmarked first line keeps the display face's density. */
  line-height: 1.06;
}
.js .hero h1 .accent { background-size: 0 .13em; animation: mark-in .95s var(--ease) .3s forwards; }
@keyframes mark-in { to { background-size: 100% .13em; } }
@media (prefers-reduced-motion: reduce) {
  .js .hero h1 .accent { animation: none; background-size: 100% .13em; }
}
[data-sport-page="bike"] .hero h1 .accent { --mark: var(--lime-cycling); }
[data-sport-page="run"]  .hero h1 .accent { --mark: var(--orange-running); }
[data-sport-page="golf"] .hero h1 .accent { --mark: var(--violet-golf); }

/* ---- THE HERO LEDE (2026-08-13) ----------------------------
   The product framing, directly beneath the headline, on every hero.

   The H1 is pinned verbatim by tool/landing-check/check.py, so the framing
   cannot live in it. This is the same slot and the same solution the E27
   comprehension pass used: the line the headline is not allowed to say gets
   its own element immediately under the headline, where a scanning visitor
   reads it as the headline's second half.

   TYPOGRAPHY. The board's own face (Barlow Condensed, ink, with the 9x1.5
   tick scaled up to 26x3) but NOT uppercase — h1/h2/h3/h4 are uppercased
   globally, so an uppercase line here would read as a second headline and
   fight the first. Sentence case at 700 with the object at 900 keeps the
   display voice reserved for headings while still landing in full --ink,
   two steps above the --ink-2 prose beneath it. No accent colour and no
   second grease-pencil mark: the H1 owns the only mark in the viewport. */
/* The tick is absolutely placed, NOT a flex item. Flexing the paragraph
   promoted the emphasised <b> to a flex item of its own and broke the
   sentence into three boxes that wrapped independently. */
.hero-lede {
  position: relative; padding-left: 40px;
  margin: 20px 0 0;
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 700;
  font-size: clamp(23px, 2.6vw, 31px);
  line-height: 1.12;
  letter-spacing: .002em;
  color: var(--ink);
  /* 30ch, not 26: partners.html carries the longer third-person sentence and
     26ch broke it into three lines with "into an" alone on the second. */
  max-width: 30ch;
}
.hero-lede::before {
  content: ""; position: absolute;
  left: 0; top: .52em;
  width: 26px; height: 3px;
  background: var(--ink);
}
/* The framing is the object of the sentence and has to be read as one thing.
   Left to wrap naturally the column broke it after "AI", which reads as two
   half-phrases; nowrap moves the break in front of it instead. It is 19
   characters of condensed type, so it still fits the 375px column. */
.hero-lede b { font-weight: 900; white-space: nowrap; }
@media (max-width: 620px) {
  .hero-lede { padding-left: 30px; max-width: none; }
  .hero-lede::before { width: 20px; height: 2.5px; }
}

.hero .sub {
  margin: 20px 0 0; font-size: clamp(17.5px, 1.6vw, 21px);
  color: var(--ink-2); max-width: 62ch;
}
.hero .cta-row { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 26px; }

/* ---- THE SPEC STRIP (E27, 2026-08-13) ----------------------
   What the product IS, scannable, in the first screen.

   The teardown of this category (docs/growth/competitor-landing-teardown.md)
   found that four of five competitors defer the mechanism below the fold and
   get away with it because a celebrity or a press wall in the hero already
   told the visitor what they were looking at. We have neither and nobody has
   heard of us, so the mechanism cannot be deferred — and it also cannot be a
   sixth sentence of hero prose, which is what it was: a visitor scanning the
   old first screen learned that something reads a body, and not that it is a
   phone camera doing it with nothing strapped on.

   Four measured facts, ruled like a spec plate on the board. Deliberately NOT
   badges, pills or icons: this is the same tick-and-rule vocabulary .act-list
   uses, so it reads as something written on the board rather than a widget
   imported from another world. No number here is a claim about accuracy —
   every line names hardware or behaviour, never performance. */
.spec-strip {
  /* 20px, trimmed from 26 with the same 2026-08-13 budget as .hero's top pad:
     the strip is already fenced off by its own 1.5px rule, so it needs less
     air above it than the unruled prose does. */
  margin: 20px 0 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  border-top: 1.5px solid var(--ink);
  border-bottom: 1px solid var(--rule-mid);
  max-width: 62ch;
}
.spec {
  position: relative;
  display: flex; align-items: baseline; gap: 10px;
  padding: 11px 16px 11px 0;
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 700; font-size: 15px;
  letter-spacing: .055em; text-transform: uppercase;
  color: var(--ink);
}
/* Two columns of two: a hairline between the rows, a hairline between the
   columns. Ruled cells, not a gap-separated grid. */
.spec:nth-child(n + 3) { border-top: 1px solid var(--rule); }
.spec:nth-child(even) { border-left: 1px solid var(--rule); padding-left: 16px; }
/* The board's own mark, same 9x1.5 tick as .act-list. */
.spec::before {
  content: ""; flex: none;
  width: 9px; height: 1.5px; margin-top: .62em;
  background: var(--ink);
}
/* Stays two-up on a phone. Stacked it ran to four full rows and pushed the
   read — the one piece of evidence on the page — clean out of the first
   viewport, which costs more than the extra line length here saves. */
@media (max-width: 620px) {
  .spec-strip { max-width: none; }
  .spec {
    font-size: 13.5px; letter-spacing: .04em; gap: 8px;
    padding: 10px 12px 10px 0;
  }
  .spec:nth-child(even) { padding-left: 12px; }
  .spec::before { width: 7px; margin-top: .55em; }
}

/* THE READ. An athlete filmed side-on with Llyr's reading drawn onto her.
   This replaced a pace-clock dial: a clock face reads as elapsed time, which
   is the one thing this product does not do, and it taught the visitor
   nothing they could not have guessed. A body with the skeleton on it needs
   no key to understand — the key underneath only names the three marks.

   The skeleton geometry is BlazePose output measured on this exact file, so
   the angle tags are read values rather than decoration. */
.hero-read { margin: 0; }
.read-frame {
  position: relative;
  border: 1.5px solid var(--ink);
  box-shadow: var(--lift-2);
  background: var(--board-2);
  overflow: hidden;
  line-height: 0;
}
.read-frame img { width: 100%; height: auto; display: block; }
.skel {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
}

/* Two passes: a dark halo under a light line. The skeleton crosses bright sky
   behind her trailing arm and near-black fabric across her torso, and a single
   stroke colour cannot hold contrast over both. */
.skel-halo line {
  stroke: rgba(8, 10, 6, .52);
  stroke-width: 8; stroke-linecap: round;
}
.skel-bone line {
  stroke: #F7F8F4;
  stroke-width: 3.4; stroke-linecap: round;
}
.skel-joint circle {
  fill: var(--orange-running);
  stroke: #F7F8F4; stroke-width: 2.4;
}

/* The model's line. Dashed, never --fault: this is where the standard puts the
   forearm, not an accusation that hers is broken. */
.skel-ghost line {
  stroke: #F7F8F4; stroke-width: 3;
  stroke-dasharray: 9 7; stroke-linecap: round;
  filter: drop-shadow(0 0 3px rgba(8, 10, 6, .75));
}
.skel-ghost circle {
  fill: none; stroke: #F7F8F4; stroke-width: 2.6;
  stroke-dasharray: 5 4;
  filter: drop-shadow(0 0 3px rgba(8, 10, 6, .75));
}

/* The gap. Same 135° hatch the ledger's blank row uses, so the two places that
   say "nothing is written here" say it the same way. */
.skel-gap rect {
  fill: url(#skelgap);
  stroke: #F7F8F4; stroke-width: 2; stroke-dasharray: 6 5;
  stroke-opacity: .85;
}

.skel-tag rect { fill: rgba(8, 10, 6, .82); }
.skel-tag text {
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 800; font-size: 21px; letter-spacing: .04em;
  font-variant-numeric: tabular-nums;
  fill: #F7F8F4; text-anchor: middle;
}
/* The dashed line needs naming or it reads as noise. Outlined, not filled, so
   it belongs to the ghost rather than to the two measured tags. */
.skel-ghost .ghost-tag rect {
  fill: rgba(8, 10, 6, .72);
  stroke: #F7F8F4; stroke-width: 1.5; stroke-dasharray: 5 4;
}
.skel-ghost .ghost-tag text {
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 800; font-size: 15px; letter-spacing: .16em;
  fill: #F7F8F4; text-anchor: middle;
}

/* The key. Three lines, one per mark, in the order the eye meets them. */
.read-key {
  display: grid; gap: 7px;
  margin: 16px 0 0; font-size: 14.5px; color: var(--ink-2);
}
.read-key .ki { display: flex; gap: 9px; align-items: baseline; }
.read-key .ki b {
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 800; text-transform: uppercase; letter-spacing: .1em;
  font-size: 13px; color: var(--ink); flex: none; min-width: 62px;
}
/* Each key line carries its own mark so the label is not the only channel:
   a solid rule, a dashed rule, a hatched swatch. */
.read-key .ki::before {
  content: ""; flex: none; width: 26px; height: 12px; margin-top: 3px;
  border-bottom: 3px solid var(--ink);
}
.read-key .ki-ghost::before { border-bottom-style: dashed; }
/* For a key line whose mark in the image is the written value itself, not a
   line style. The spacer stays so the labels keep their column. */
.read-key .ki-plain::before { border-bottom-color: transparent; }
.read-key .ki-gap::before {
  border-bottom: 0; height: 14px;
  background-image: repeating-linear-gradient(135deg,
    var(--rule-strong) 0 2px, transparent 2px 7px);
  border: 1px solid var(--rule-mid);
}

@media (max-width: 920px) {
  /* One column, and the read moves above the buttons. Stacked in source order
     the figure lands below the fold on a phone, which loses the one thing the
     first viewport exists to show. `display: contents` promotes the copy's
     children to grid items so the figure can be ordered between them. */
  .hero-grid { grid-template-columns: 1fr; align-items: start; }
  .hero-copy { display: contents; }
  .hero-grid > .hero-copy > h1 { order: 1; }
  /* Same order group as the H1, deliberately: equal `order` values fall back
     to source order, so the lede stays welded under the headline it finishes.
     Without an order it would default to 0 and jump above the H1. */
  .hero-grid > .hero-copy > .hero-lede { order: 1; }
  .hero-grid > .hero-copy > .sub { order: 2; }
  /* The spec strip is the caption the headline needs on a phone, so it sits
     between the prose and the evidence rather than after it. */
  .hero-grid > .hero-copy > .spec-strip { order: 3; }
  .hero-grid > .hero-read { order: 4; margin-top: 4px; }
  .hero-grid > .hero-copy > .cta-row { order: 5; }
  /* `gap` now applies between every promoted child, so the generous desktop
     gutter has to come back down to a vertical rhythm — and the promoted
     children's own top margins have to go with it, or every space in the hero
     is paid for twice (26px margin + 22px gap). Left as they were, the
     doubling pushed the read most of the way out of the first viewport, and
     the read is the only evidence the page has. */
  .hero-grid { gap: 20px; }
  .hero { padding-top: 34px; }
  .hero h1 { margin-bottom: 0; }
  .hero .hero-lede, .hero .sub, .hero .spec-strip { margin-top: 0; }
  .hero .cta-row { margin-top: 4px; }
}

/* ============================================================
   SPORT STRIP — lane tabs along the board's edge
   ============================================================ */
.sport-pills { border-bottom: 1.5px solid var(--ink); background: var(--board-3); }
.sport-pills .wrap {
  display: flex; align-items: stretch; gap: 0; flex-wrap: wrap;
}
.sport-pills .lbl {
  display: inline-flex; align-items: center;
  font-family: "Barlow Condensed", sans-serif;
  color: var(--ink-3); font-size: 13px; font-weight: 700;
  letter-spacing: .2em; text-transform: uppercase;
  padding-right: 20px;
}
.sport-pill {
  position: relative;
  display: inline-flex; align-items: center;
  min-height: 48px; padding: 0 24px;
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 800; font-size: 19px;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--ink);
  border-left: 1px solid var(--rule-mid);
  transition: background-color .2s ease;
}
.sport-pill:last-of-type { border-right: 1px solid var(--rule-mid); }
/* The lane key: a solid colour square, the way a board codes its lanes.
   On hover the whole tab lights up in that lane's colour. */
.sport-pill .key {
  width: 11px; height: 11px; margin-right: 11px; flex: none;
  background: var(--tab, var(--ink));
  transition: transform .25s var(--ease);
}
.sport-pill:hover { background: var(--tab, var(--ink)); }
.sport-pill:hover .key { transform: scale(1.45); }
@media (prefers-reduced-motion: reduce) { .sport-pill .key { transition: none; } }
.sport-pill.s-swim { --tab: var(--aqua); }
.sport-pill.s-bike { --tab: var(--lime-cycling); }
.sport-pill.s-run  { --tab: var(--orange-running); }
.sport-pill.s-golf { --tab: var(--violet-golf); }
/* Four are built; more are being built. Stated as a quiet trailing note on the
   lane strip rather than as a fifth tab, because a tab is a place you can go
   and there is nowhere to go yet. It names no sport, no order and no date —
   check_launch_timing exists precisely to stop that sentence growing one. */
.sport-pills .more {
  display: inline-flex; align-items: center;
  padding-left: 20px;
  font-family: "Barlow Condensed", sans-serif;
  color: var(--ink-3); font-size: 13px; font-weight: 700;
  letter-spacing: .14em; text-transform: uppercase;
}
@media (max-width: 620px) {
  .sport-pills .wrap { padding-top: 0; padding-bottom: 0; }
  .sport-pills .lbl { flex: 1 1 100%; padding: 12px 0 4px; }
  .sport-pill { flex: 1 1 0; justify-content: center; padding: 0 8px; font-size: 17px; }
  .sport-pill:first-of-type { border-left: 0; }
  .sport-pills .more { flex: 1 1 100%; padding: 10px 0 12px; }
}

/* ============================================================
   SECTION SHELL
   ============================================================ */
.sec { padding: clamp(60px, 8.5vw, 108px) 0; }
.sec + .sec { border-top: 1px solid var(--rule); }
/* Section headings are sized against the COLUMN they live in, not the
   viewport: a 78px face in a 507px board-split column stacks one word per
   line. Full-width sections can afford the larger step. */
.sec-head { margin-bottom: 44px; }
.sec-head h2 { font-size: clamp(34px, 4vw, 56px); }
/* Cap the measure in em ON THE HEADING, never in ch on the wrapper: ch
   resolves against the wrapper's 17.5px body face, so a 22ch cap squeezed a
   72px display heading into ~212px and broke it to one word per line. */
.sec-wide .sec-head { max-width: none; }
.sec-wide .sec-head h2 { font-size: clamp(38px, 5vw, 72px); max-width: 12em; }

/* Prose. Measure held to ~68ch. */
.gap-lines { max-width: 68ch; }
.gap-lines p {
  color: var(--ink-2); font-size: clamp(17.5px, 1.5vw, 21px);
  line-height: 1.6; margin: 0 0 22px;
}
.gap-lines p:last-child { margin-bottom: 0; }
.gap-lines p strong { color: var(--ink); font-weight: 700; }

/* A two-column board layout: heading pinned left, content right. */
/* The heading column is deliberately the narrower one: it holds two or three
   words of display type, and giving it a full half left 600-800px of dead
   board under every sticky heading. */
.board-split {
  display: grid; grid-template-columns: minmax(0,.72fr) minmax(0,1.28fr);
  gap: clamp(26px, 5vw, 72px); align-items: start;
}
/* The grid item must stretch to the row height or the sticky child has no
   range to travel in and the heading scrolls away with align-items:start. */
.board-split > .sec-head { margin-bottom: 0; align-self: stretch; }
.board-split > .sec-head > h2 { position: sticky; top: 100px; }
@media (max-width: 900px) {
  .board-split { grid-template-columns: 1fr; }
  .board-split > .sec-head { align-self: auto; margin-bottom: 30px; }
  .board-split > .sec-head > h2 { position: static; }
}

/* ============================================================
   THE LEDGER — ruled rows. Used for the honesty block, where the
   last row is deliberately left blank.
   ============================================================ */
.ledger { border-top: 1.5px solid var(--ink); }
.ledger-row {
  display: grid; grid-template-columns: 96px 1fr;
  gap: 20px; align-items: baseline;
  padding: 22px 0; border-bottom: 1px solid var(--rule-mid);
}
.ledger-row .k {
  font-family: "Barlow Condensed", sans-serif; font-weight: 800;
  text-transform: uppercase; letter-spacing: .14em; font-size: 13px;
  color: var(--ink-3); padding-top: .35em;
}
.ledger-row .v { font-size: 18px; color: var(--ink); margin: 0; }
.ledger-row .v small { display: block; margin-top: 6px; color: var(--ink-2); font-size: 15.5px; }
/* The blank cell. This row is empty on purpose: it is what the
   page is arguing. Do not fill it. */
.ledger-row.blank .v {
  color: var(--ink-3); font-style: italic;
}
.ledger-row.blank {
  background: repeating-linear-gradient(
    135deg, transparent 0 9px, rgba(var(--ink-rgb), .05) 9px 10px);
}
@media (max-width: 560px) {
  .ledger-row { grid-template-columns: 1fr; gap: 6px; }
  .ledger-row .k { padding-top: 0; }
}

/* ============================================================
   PINNED PLATES — the app's own dark screens, on the board
   ============================================================ */
.plate {
  background: var(--plate);
  border: 1.5px solid var(--ink);
  border-radius: 3px;
  overflow: hidden;
  box-shadow: var(--lift-2);
}
.plate img { width: 100%; height: auto; display: block; }
.plate-cap {
  display: flex; gap: 10px; align-items: baseline;
  padding: 12px 14px; background: var(--board-2);
  border-top: 1.5px solid var(--ink);
  font-size: 14.5px; color: var(--ink-2);
}
.plate-cap b {
  font-family: "Barlow Condensed", sans-serif; font-weight: 800;
  text-transform: uppercase; letter-spacing: .1em; font-size: 13px;
  color: var(--ink); white-space: nowrap;
}
.plate-cap em { font-style: italic; color: var(--ink); }

/* A container only paints the dark plate colour when it is EMPTY. With an
   image inside, a failed load has to read as broken rather than as a panel
   somebody meant to be dark (DESIGN.md). */
.plate:has(img),
.showcase .frame:has(img),
.step .ph:has(img),
.pick .shot:has(img) { background: none; }

/* A DATED OR WITHDRAWN SCREEN. Two shots on this site are true only because
   they say when they are from: the mid-2026 home tabs (Run and Golf still read
   "soon") and the withdrawn model picker. The stamp is the load-bearing part of
   the caption, so it is styled like one — never quiet it down, and never use
   these classes on a current screen. */
.plate.dated .plate-cap b,
.plate.withdrawn .plate-cap b {
  color: var(--board); background: var(--ink);
  padding: 3px 8px; border-radius: 2px;
}
/* Both carry a 640px-wide phone screenshot. Left uncapped in a full-width
   container these blew up to ~1190px — a 1.85x upscale, and a 2300px-tall
   blurry slab. Every other plate on the site renders BELOW native width
   (the swim hero plate sits at 0.67x); this keeps these two to the same
   rule. Never let a screenshot render larger than it was captured. */
.plate.dated,
.plate.withdrawn { max-width: 380px; }
/* Desaturated because it is no longer a thing you can go and see. */
.plate.withdrawn img { filter: grayscale(.55) contrast(.94); }

/* ============================================================
   ILLUSTRATION — a picture that is deliberately NOT a screen.
   No --plate ground, no phone framing: it must not be mistaken
   for something the app renders. See index.html's comment.
   ============================================================ */
/* Deliberately not full width. A stock composite must not out-rank the real
   screens above it, and at container width this thing read as the hero of the
   block it is a footnote to. */
.illus {
  margin: clamp(34px, 5vw, 58px) 0 0;
  max-width: 620px;
  border: 1.5px solid var(--ink); border-radius: 3px;
  background: var(--board-2); box-shadow: var(--lift-2); overflow: hidden;
}
.illus img { width: 100%; height: auto; display: block; }
.illus .plate-cap { align-items: flex-start; }

/* ============================================================
   ONE APP, FOUR SPORTS — the home screen beside its own caption
   ============================================================ */
.one-app {
  margin-top: clamp(34px, 5vw, 56px);
  display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  gap: clamp(26px, 4vw, 54px); align-items: center;
}
.one-app > .plate { margin: 0; }
.one-app-note .sports-note { margin: 0 0 16px; }
.one-app-note .sports-note:last-child { margin-bottom: 0; }
@media (max-width: 720px) {
  .one-app { grid-template-columns: 1fr; max-width: 400px; }
}

/* ============================================================
   BEATS — the procedure. A numbered sequence the reader needs,
   so the numbers stay. Deliberately unequal: the first beat is
   wide, the next two sit beside it.
   ============================================================ */
.beats { display: grid; grid-template-columns: 1.35fr 1fr 1fr; gap: 22px; align-items: start; }
.beat { margin: 0; }
/* One shared height so the three captions sit on a common baseline; the
   hierarchy comes from column width, not from ragged image heights. */
/* No --plate background: an image that fails to load must read as broken,
   not as a deliberately dark panel. */
.beat .beat-shot {
  width: 100%; height: clamp(340px, 36vw, 540px); object-fit: cover;
  object-position: 50% 22%; display: block;
  border: 1.5px solid var(--ink); border-radius: 3px;
  box-shadow: var(--lift-2);
}
.beat figcaption { padding: 20px 2px 0; }
.beat h3 {
  font-size: 27px; display: flex; align-items: baseline; gap: 12px;
}
.beat h3 .n {
  font-size: 15px; letter-spacing: .1em; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.beat p { color: var(--ink-2); font-size: 16px; margin: 10px 0 0; }
@media (max-width: 900px) {
  .beats { grid-template-columns: 1fr 1fr; }
  .beat:first-child { grid-column: 1 / -1; }
  .beat:first-child .beat-shot { height: clamp(300px, 52vw, 460px); object-position: 50% 30%; }
}
@media (max-width: 560px) {
  .beats { grid-template-columns: 1fr; }
  .beat .beat-shot { height: clamp(300px, 88vw, 460px); }
}

/* ---- Capability chips: stamped, not glowing ---------------- */
.cap-chips { display: flex; flex-wrap: wrap; gap: 0; margin-top: 40px; border: 1.5px solid var(--ink); border-radius: 3px; }
.cap-chip {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 15px 22px; min-height: 48px;
  font-family: "Barlow Condensed", sans-serif;
  font-size: 15px; font-weight: 800;
  letter-spacing: .11em; text-transform: uppercase; color: var(--ink-2);
  border-right: 1.5px solid var(--ink); flex: 1 1 auto;
}
.cap-chip:last-child { border-right: 0; }
.cap-chip-honest { color: var(--ink); background: var(--lime-cycling); }
@media (max-width: 760px) {
  .cap-chips { flex-direction: column; }
  .cap-chip { border-right: 0; border-bottom: 1.5px solid var(--ink); }
  .cap-chip:last-child { border-bottom: 0; }
}

/* ============================================================
   SPORT BOARD — four lanes, not four identical cards
   ============================================================ */
.pick-grid { border-top: 1.5px solid var(--ink); }
.pick {
  position: relative; display: grid;
  grid-template-columns: 74px minmax(0,1fr) minmax(0,.9fr) 62px;
  gap: 0; align-items: stretch;
  border-bottom: 1.5px solid var(--ink);
  background: var(--board);
  transition: background-color .3s var(--ease);
  overflow: hidden;
}
.pick:hover { background: var(--board-2); }
/* The lane tab: a full-height colour field carrying its lane number, the way
   a board divides itself. Not a border on a card. */
.pick .lane {
  position: relative; z-index: 3;
  display: grid; place-items: center;
  background: var(--tab); color: var(--ink);
  font-family: "Barlow Condensed", sans-serif; font-weight: 900;
  font-size: 32px; font-variant-numeric: tabular-nums;
}
.pick .meta { padding: 30px 26px; display: flex; flex-direction: column; justify-content: center; }
.pick h3 { font-size: clamp(32px, 4vw, 48px); }
/* Sits BELOW the heading, so it reads as the row's detail rather than as an
   eyebrow label above it. */
.pick .note {
  margin: 8px 0 0; font-size: 15.5px; color: var(--ink-2);
}
.pick .shot {
  position: relative; overflow: hidden; border-left: 1px solid var(--rule);
  background: var(--plate); min-height: 150px;
}
.pick .shot img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  /* Per sport: a single crop point decapitates the standing subjects. The
     swimmer is horizontal and sits low; the golfer and runner are upright. */
  object-fit: cover; object-position: 50% var(--crop, 45%);
  transition: transform .6s var(--ease);
}
.pick-swim { --crop: 46%; }
.pick-bike { --crop: 30%; }
.pick-run  { --crop: 34%; }
.pick-golf { --crop: 26%; }
.pick:hover .shot img { transform: scale(1.05); }
.pick .go {
  display: grid; place-items: center; border-left: 1px solid var(--rule);
}
.pick .go svg { width: 22px; height: 22px; stroke: var(--ink); stroke-width: 2.4; transition: transform .35s var(--ease); }
.pick:hover .go svg { transform: translateX(5px); }
.pick-swim { --tab: var(--aqua); }
.pick-bike { --tab: var(--lime-cycling); }
.pick-run  { --tab: var(--orange-running); }
.pick-golf { --tab: var(--violet-golf); }
@media (prefers-reduced-motion: reduce) {
  .pick .shot img, .pick .go svg { transition: none; }
}
/* The plate stays on mobile: this is a phone app, and hiding every product
   screen on phones was the worst thing the old breakpoint did. It becomes a
   full-width band under the row's copy. */
@media (max-width: 860px) {
  .pick { grid-template-columns: 58px minmax(0,1fr) 54px; grid-template-rows: auto auto; }
  .pick .lane { font-size: 26px; grid-row: 1 / span 2; }
  .pick .meta { padding: 22px 20px 16px; grid-row: 1; }
  .pick .go { grid-row: 1; }
  .pick .shot {
    grid-column: 2 / span 2; grid-row: 2;
    border-left: 0; border-top: 1px solid var(--rule);
    min-height: 0; height: 168px;
  }
}

.sports-note {
  margin: 30px 0 0; color: var(--ink-2); font-size: 16.5px; max-width: 60ch;
}

/* ---- Back to all sports ------------------------------------ */
.allsports {
  display: inline-flex; align-items: center; gap: 9px;
  font-family: "Barlow Condensed", sans-serif; font-weight: 800;
  text-transform: uppercase; letter-spacing: .12em; font-size: 15px;
  color: var(--ink-2); min-height: 44px; padding: 0 4px;
  border-bottom: 2px solid transparent;
  transition: color .18s ease, border-color .18s ease;
}
.allsports:hover { color: var(--ink); border-bottom-color: var(--ink); }
.allsports svg { width: 16px; height: 16px; stroke-width: 2.4; }

/* ============================================================
   STEPS (sport pages, partners)
   ============================================================ */
.steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px; }
.steps.four { grid-template-columns: repeat(4, 1fr); }
.step { position: relative; }
.step .ph {
  position: relative; aspect-ratio: 3 / 4; overflow: hidden;
  background: var(--plate); border: 1.5px solid var(--ink); border-radius: 3px;
  box-shadow: var(--lift-1);
}
.step .ph img { width: 100%; height: 100%; object-fit: cover; object-position: top center; }
.step .ph.wide { aspect-ratio: 4 / 3; }
.step .n {
  position: absolute; top: -1.5px; left: -1.5px; z-index: 2;
  min-width: 42px; height: 34px; padding: 0 10px;
  display: grid; place-items: center;
  font-family: "Barlow Condensed", sans-serif; font-weight: 900; font-size: 19px;
  color: var(--board); background: var(--ink);
  font-variant-numeric: tabular-nums;
}
.step .body { padding: 18px 2px 0; }
.step h3 { font-size: 26px; }
.step p { color: var(--ink-2); font-size: 16px; margin: 9px 0 0; }
@media (max-width: 900px) { .steps, .steps.four { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 540px) { .steps, .steps.four { grid-template-columns: 1fr; max-width: 380px; } }

/* ============================================================
   SHOWCASE — a plate beside its readings
   ============================================================ */
.showcase { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(28px, 5vw, 64px); align-items: center; }
.showcase .frame {
  position: relative; overflow: hidden;
  background: var(--plate); border: 1.5px solid var(--ink); border-radius: 3px;
  box-shadow: var(--lift-3);
}
.showcase .frame img { width: 100%; height: auto; display: block; }
.showcase .frame.duo { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5px; background: var(--ink); }
.showcase .frame.duo img { height: 100%; object-fit: cover; object-position: top center; }

/* Readings list — a board's annotation column. */
.feat-list { list-style: none; margin: 30px 0 0; padding: 0; }
.feat-list li {
  display: grid; grid-template-columns: 34px 1fr; gap: 16px;
  padding: 20px 0; border-top: 1px solid var(--rule-mid);
}
.feat-list li:last-child { border-bottom: 1px solid var(--rule-mid); }
.feat-list .ic { padding-top: 2px; }
.feat-list .ic svg { width: 24px; height: 24px; stroke: var(--ink); stroke-width: 1.8; fill: none; }
.feat-list h4 {
  margin: 0; font-family: "Barlow Condensed", sans-serif; font-weight: 800;
  font-size: 21px; text-transform: uppercase; letter-spacing: .02em;
}
.feat-list p { margin: 5px 0 0; color: var(--ink-2); font-size: 16px; }
@media (max-width: 900px) { .showcase { grid-template-columns: 1fr; } }

/* ============================================================
   NOTE CARDS — honesty statements. Ruled slips, not cards.
   ============================================================ */
.tst-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 clamp(28px, 5vw, 64px); }
.tst { padding: 26px 0; border-top: 1.5px solid var(--ink); }
.tst h4 {
  margin: 0 0 10px; font-family: "Barlow Condensed", sans-serif;
  font-weight: 800; font-size: 21px; text-transform: uppercase; letter-spacing: .02em;
}
.tst p { margin: 0; color: var(--ink-2); font-size: 16.5px; }
@media (max-width: 700px) { .tst-grid { grid-template-columns: 1fr; } }

/* ============================================================
   FINAL CTA — the sign-up sheet, pinned to the board
   ============================================================ */
.final { background: var(--ink); color: var(--board); border-top: 1.5px solid var(--ink); }
/* Horizontal gutter must be restated: this rule outranks .wrap's padding, and
   a `0` here clipped the CTA to the viewport edge on every page below 1240px. */
.final .content { padding: clamp(64px, 9vw, 120px) var(--gut); }
/* Two columns so the sign-up sheet sits beside the notice instead of leaving
   the right half of a full-bleed dark band empty. */
.final .content {
  display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr);
  gap: 0 clamp(32px, 5vw, 84px); align-items: start;
}
/* The ARGUMENT stacks down the left column; the ACTION is one object in the
   right one. Placing the form and its small print as independent grid items
   put them in independent rows, which is how "No spam…" came to sit a hundred
   pixels adrift of the field it qualifies on a short page — and, on a phone,
   above the form entirely. `.final-act` holds them together. */
.final h2 { grid-column: 1; grid-row: 1; font-size: clamp(42px, 6.2vw, 84px); max-width: 13ch; color: var(--board); }
.final .content > p { grid-column: 1; grid-row: 2; color: #B9BFB2; font-size: 19px; max-width: 46ch; margin: 22px 0 0; }
.final-act {
  grid-column: 2; grid-row: 1 / span 3; align-self: start;
  display: flex; flex-direction: column;
  /* NOT flex-start: shrink-to-fit collapses .signup below the 260px basis its
     email field asks for and the button drops to its own line. */
  align-items: stretch;
}
/* `.cta-row` only ever had a flex rule scoped to `.hero`, so partners.html's
   pair of buttons were laid out as bare inline-blocks: no gap, and at 1.62
   line-height the second one crowded the first the moment they wrapped. */
.final .cta-row { display: flex; flex-wrap: wrap; gap: 14px; }
@media (max-width: 900px) {
  .final .content { grid-template-columns: 1fr; }
  .final h2, .final .content > p, .final .seats, .final-act {
    grid-column: 1; grid-row: auto;
  }
  .final-act { margin-top: 34px; }
}

/* ---- THE SEAT COUNT (2026-08-13) ---------------------------
   The one piece of genuine scarcity this product has, and the only thing on
   the page that gives a visitor a reason to hand over an address today rather
   than bookmark and forget. The first tester group is capped at 100 people —
   that is a real, checkable ceiling, not a growth-hack.

   RULES THIS BLOCK LIVES BY, because scarcity is exactly where a site starts
   lying: the number is the CAP, never a live count of seats remaining. This
   page cannot know how many are left, and a fabricated counter is the same
   offence as a fabricated testimonial (E25/E26) wearing a different hat.
   No countdown, no timer, no "only N left", no date. If the cap ever changes,
   change this number; do not animate it.

   Drawn as a stamped plate rather than a badge: heavy rule above, the figure
   in the display face at ledger scale, the sentence underneath in prose. */
.final .seats {
  grid-column: 1; grid-row: 3;
  margin: 32px 0 0; padding-top: 20px;
  max-width: 46ch;
  border-top: 1.5px solid rgba(232, 234, 228, .38);
}
.final .seats .n {
  display: block;
  font-family: "Barlow Condensed", sans-serif;
  font-weight: 900; font-size: clamp(40px, 5vw, 58px); line-height: .9;
  letter-spacing: -.01em; color: var(--board);
  font-variant-numeric: tabular-nums;
}
.final .seats .n small {
  display: block; margin-top: 7px;
  font-size: 13px; font-weight: 700; letter-spacing: .2em; line-height: 1.2;
  color: #9AA192;
}
.final .seats p { margin: 14px 0 0; color: #B9BFB2; font-size: 16.5px; }

.signup { display: flex; flex-wrap: wrap; gap: 14px; max-width: 620px; }
.signup input[type="email"] {
  flex: 1 1 260px; min-height: 56px; padding: 0 20px;
  background: transparent; color: var(--board);
  border: 0; border-bottom: 2px solid rgba(232,234,228,.4);
  font-family: "Barlow", sans-serif; font-size: 18px;
  transition: border-color .2s ease;
}
.signup input[type="email"]:focus { outline: none; border-bottom-color: var(--board); }
.signup input[type="email"]:focus-visible { outline: 3px solid var(--board); outline-offset: 4px; }
.signup input::placeholder { color: #868C7C; }
/* Scoped to the BAND, not to the form. `.btn-primary` paints --ink on --ink
   inside `.final`, which made partners.html's "Email contact@llyrlabs.com" a
   black plate on a black ground — the primary action on that page was
   effectively invisible, and only the sign-up pages escaped because `.signup`
   carried its own override. The band inverts every primary button in it. */
.final .btn-primary { background: var(--board); color: var(--ink); }
.final .btn-primary:hover { background: #fff; }
.signup .err {
  flex: 1 1 100%; margin: 2px 0 0; min-height: 1.2em;
  color: #FFB4AC; font-size: 15px; font-weight: 600;
}
/* Inside .final-act, directly under the action it qualifies. */
.final .fine { margin: 26px 0 0; color: #9AA192; font-size: 14.5px; max-width: 46ch; }

/* Sport chooser — lane tabs again, on the dark plate */
.signup .wl-sport { flex: 1 1 100%; min-width: 0; border: 0; margin: 0; padding: 0; }
.signup .wl-sport legend {
  padding: 0; margin: 0 0 12px;
  font-family: "Barlow Condensed", sans-serif; color: #9AA192;
  font-size: 13px; font-weight: 700; letter-spacing: .2em; text-transform: uppercase;
}
.signup .wl-opts { display: flex; flex-wrap: wrap; gap: 10px; }
.signup .wl-opt { position: relative; display: inline-flex; }
.signup .wl-opt input {
  position: absolute; inset: 0; width: 100%; height: 100%; min-height: 0;
  margin: 0; padding: 0; opacity: 0; border: 0; background: none; cursor: pointer;
}
.signup .wl-opt span {
  display: inline-flex; align-items: center;
  min-height: 46px; padding: 0 20px; border-radius: 2px;
  font-family: "Barlow Condensed", sans-serif;
  font-size: 17px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: #C9CFC1; box-shadow: inset 0 0 0 1.5px rgba(232,234,228,.28);
  white-space: nowrap;
  transition: color .2s ease, background-color .2s ease, box-shadow .2s ease;
}
.signup .wl-opt:hover span { color: var(--board); box-shadow: inset 0 0 0 1.5px var(--board); }
.signup .wl-opt input:focus-visible + span { outline: 3px solid var(--board); outline-offset: 3px; }
.signup .wl-opt input:checked + span { color: var(--ink); box-shadow: none; }
.signup .wl-opt.s-swim input:checked + span { background: var(--aqua); }
.signup .wl-opt.s-bike input:checked + span { background: var(--lime-cycling); }
.signup .wl-opt.s-run  input:checked + span { background: var(--orange-running); }
.signup .wl-opt.s-golf input:checked + span { background: var(--violet-golf); }
.signup .wl-opt.s-any  input:checked + span { background: var(--board); }
.signup .wl-hint {
  flex: 1 1 100%; margin: 12px 0 0; min-height: 1.2em;
  color: #9AA192; font-size: 15px; font-weight: 600;
}
.signup .wl-hint.nudge { color: var(--orange-running-alt); }
.signup .wl-sport.nudge .wl-opts { animation: wl-nudge .45s ease; }
@keyframes wl-nudge { 0%,100% { transform: none; } 25% { transform: translateX(-5px); } 75% { transform: translateX(5px); } }
@media (prefers-reduced-motion: reduce) { .signup .wl-sport.nudge .wl-opts { animation: none; } }

/* Confirmation */
.signup-done { max-width: 560px; padding: 30px 28px; border: 1.5px solid var(--optimal); border-radius: 3px; }
.signup-done:focus-visible { outline: 3px solid var(--board); outline-offset: 3px; }
.signup-done .ok {
  display: grid; place-items: center; width: 46px; height: 46px; margin: 0 0 16px;
  border-radius: 2px; background: var(--optimal);
}
.signup-done .ok svg { width: 24px; height: 24px; stroke: var(--ink); fill: none; }
.signup-done h3 { font-size: 34px; color: var(--board); }
.signup-done p { color: #B9BFB2; font-size: 16.5px; margin: 12px 0 0; }

/* ============================================================
   FOOTER
   ============================================================ */
footer { background: var(--board-3); border-top: 1.5px solid var(--ink); padding: 54px 0 34px; }
.foot { display: flex; justify-content: space-between; gap: 40px; flex-wrap: wrap; }
.foot .tag { color: var(--ink-2); max-width: 40ch; margin-top: 16px; font-size: 16px; }
.foot-links { display: flex; gap: 56px; flex-wrap: wrap; }
.foot-col h5 {
  font-family: "Barlow Condensed", sans-serif; text-transform: uppercase;
  letter-spacing: .18em; font-size: 13px; color: var(--ink-3); margin: 0 0 10px; font-weight: 700;
}
.foot-col a {
  display: flex; align-items: center; min-height: 44px;
  color: var(--ink-2); font-size: 16px;
}
.foot-col a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 4px; }
.foot-bottom {
  margin-top: 44px; padding-top: 22px; border-top: 1px solid var(--rule-mid);
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  color: var(--ink-3); font-size: 14.5px;
}

/* ============================================================
   MOTION — one authored moment: the mark being made.
   Content is visible by default; .reveal only adds the draw.
   ============================================================ */
/* Every hidden-until-seen rule is gated on .js, which the inline snippet in
   each page's <head> sets. Without scripting the content is simply there:
   a visitor with JS off, a crawler, a reader-mode pass and a print all get
   the whole page instead of a blank sheet. */
.js .reveal { opacity: 0; transform: translateY(18px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.js .reveal.in { opacity: 1; transform: none; }
.js .reveal[data-d="1"] { transition-delay: .08s; }
.js .reveal[data-d="2"] { transition-delay: .16s; }
.js .reveal[data-d="3"] { transition-delay: .24s; }

/* Rules draw left to right as they arrive. */
.js .drawrule { transform: scaleX(0); transform-origin: left center; transition: transform .9s var(--ease); }
.js .drawrule.in { transform: scaleX(1); }

/* THE READ LANDING ON THE BODY — the same gesture as the rules, performed on
   an athlete instead of a clock face. The joints are found, the bones connect
   them, the angles are written, the model's line arrives, and the sequence
   ends on the gap: the last thing that happens is the part it could not see.
   Roughly 1.8s end to end, once, no loop. --i is the per-element index. */
/* The frame itself does not fade or rise: the photo is present, and the read
   arrives on top of it. `.reveal` is here only to receive `.in` from the
   shared observer. */
.js .read-frame.reveal { opacity: 1; transform: none; transition: none; }

.js .skel-bone line {
  stroke-dasharray: 340; stroke-dashoffset: 340;
}
.js .read-frame.in .skel-bone line {
  animation: bone-draw .5s var(--ease) forwards;
  animation-delay: calc(.18s + var(--i) * .055s);
}
@keyframes bone-draw { to { stroke-dashoffset: 0; } }

/* The halo is drawn with the bone it sits under, or it would flash first. */
.js .skel-halo { opacity: 0; }
.js .read-frame.in .skel-halo { animation: fade-in .5s var(--ease) .18s forwards; }

.js .skel-joint circle { opacity: 0; transform: scale(.4); transform-origin: center; transform-box: fill-box; }
.js .read-frame.in .skel-joint circle {
  animation: joint-in .34s var(--ease) forwards;
  animation-delay: calc(.1s + var(--i) * .055s);
}
@keyframes joint-in { to { opacity: 1; transform: scale(1); } }

.js .skel-tag .tag { opacity: 0; }
.js .read-frame.in .skel-tag .tag {
  animation: fade-in .34s var(--ease) forwards;
  animation-delay: calc(.92s + var(--i) * .1s);
}

.js .skel-ghost { opacity: 0; }
.js .read-frame.in .skel-ghost { animation: fade-in .42s var(--ease) 1.24s forwards; }

/* Last. The page ends its one authored moment on the blank. */
.js .skel-gap { opacity: 0; }
.js .read-frame.in .skel-gap { animation: fade-in .46s var(--ease) 1.5s forwards; }

@keyframes fade-in { to { opacity: 1; } }

/* THE HEARTBEAT. The read used to draw once and stop, which meant the best
   thing on the site was over in 1.8s and never came back. It now dissolves and
   lands again on a ~9s cycle for as long as the hero is on screen.
   WHAT LOOPS IS THE READING, NOT THE BODY. Every coordinate stays the measured
   BlazePose output DESIGN.md pins; there is no pose B. Animating the athlete
   between invented positions would be exactly the "eyeball new coordinates onto
   a new photo" that DESIGN.md forbids, and on this page of all pages a body
   that never moved must not appear to move.
   llyr.js owns the cycle (and stops it off-screen and under reduced motion);
   CSS owns the dissolve. */
.js .read-frame .skel { transition: opacity .5s var(--ease); }
.js .read-frame.recycling .skel { opacity: 0; }

/* ---- The supporting vocabulary ------------------------------
   Before this, one 18px fade carried headings, prose, ledgers, tiles and
   figures alike, so nothing the page did told you what kind of thing was
   arriving. Each entrance below explains something specific; anything that
   explains nothing keeps the plain .reveal. */

/* Ledger rows settle in order, and the refusal lands last and slowest. That
   ordering is the argument: what it CAN see is preamble, the blank is the
   point. */
.js .reveal.in .ledger-row {
  --rd: 0s;
  animation: row-settle .52s var(--ease) backwards;
  animation-delay: calc(var(--rd) + .10s);
}
.js .reveal.in .ledger-row:nth-child(2) { --rd: .09s; }
.js .reveal.in .ledger-row:nth-child(3) { --rd: .18s; }
.js .reveal.in .ledger-row:nth-child(4) { --rd: .27s; }
.js .reveal.in .ledger-row:nth-child(5) { --rd: .36s; }
.js .reveal.in .ledger-row:nth-child(6) { --rd: .45s; }
.js .reveal.in .ledger-row.blank {
  animation-duration: .82s;
  animation-delay: calc(var(--rd) + .34s);
}
@keyframes row-settle { from { opacity: 0; transform: translateY(9px); } }

/* The lane tab fills from the bottom, writing its own number as it goes — a
   colour being laid down on the board rather than a card fading in. */
.js .pick.reveal.in .lane { animation: lane-fill .62s var(--ease) .05s backwards; }
@keyframes lane-fill { from { clip-path: inset(100% 0 0 0); } }

/* Step numbers are stamped, not faded: the sequence is information. */
.js .step.reveal.in .n { animation: stamp .34s var(--ease) .16s backwards; }
@keyframes stamp { from { opacity: 0; transform: scale(.84); } }

/* Chips stamp left to right so the honest one arrives last. */
.js .cap-chips.reveal.in .cap-chip { animation: stamp .3s var(--ease) backwards; }
.js .cap-chips.reveal.in .cap-chip:nth-child(1) { animation-delay: .12s; }
.js .cap-chips.reveal.in .cap-chip:nth-child(2) { animation-delay: .23s; }
.js .cap-chips.reveal.in .cap-chip:nth-child(3) { animation-delay: .34s; }

/* prefers-reduced-motion gets an INTENTIONAL END STATE, never a blanket kill:
   the mark is drawn, the read is complete and static on the body, every reveal
   is visible, and no loop ever starts. Every entrance above uses `backwards`
   fill only, so switching the animation off already leaves the resting state —
   nothing here can strand content mid-way. */
@media (prefers-reduced-motion: reduce) {
  .js .reveal { opacity: 1 !important; transform: none !important; transition: none; }
  .js .drawrule { transform: scaleX(1) !important; transition: none; }
  /* The read is already complete and static; nothing is hidden behind an
     animation that will not run. */
  .js .skel-bone line { stroke-dashoffset: 0; }
  .js .skel-halo, .js .skel-ghost, .js .skel-gap, .js .skel-tag .tag { opacity: 1; }
  .js .skel-joint circle { opacity: 1; transform: none; }
  .js .read-frame .skel { transition: none; }
  .js .read-frame.recycling .skel { opacity: 1; }
  .js .read-frame.in .skel-bone line,
  .js .read-frame.in .skel-halo,
  .js .read-frame.in .skel-joint circle,
  .js .read-frame.in .skel-tag .tag,
  .js .read-frame.in .skel-ghost,
  .js .read-frame.in .skel-gap,
  .js .reveal.in .ledger-row,
  .js .pick.reveal.in .lane,
  .js .step.reveal.in .n,
  .js .cap-chips.reveal.in .cap-chip { animation: none; }
}

/* ============================================================
   THE TWO READS  (#capabilities, 2026-08-09)
   ============================================================
   Llyr does two different things to a body: it reads it held still, and it
   reads it moving. The block that says so is the site's spine, so the two acts
   are built to be STRUCTURALLY equal — same grid track, same rule weight, same
   type ramp — rather than equal-by-eye. check_capabilities_block asserts the
   pair; this is what makes the pair look like a pair.

   The board grammar is unchanged: a heavy rule over each act, a stencil
   numeral, dense condensed ink. No cards, no glow, no floating phones. */
.sec-lead {
  margin: 16px 0 0;
  max-width: 46ch;
  font-size: clamp(17px, 1.45vw, 20px);
  color: var(--ink-2);
}
.sec-head .sec-lead { margin-top: 14px; }

.acts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(30px, 4.4vw, 72px);
  align-items: start;
}
.act { margin: 0; padding-top: 20px; border-top: 2.5px solid var(--ink); }
.act-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px; }
.act-n {
  font-family: 'Barlow Condensed', system-ui, sans-serif;
  font-weight: 900;
  font-size: clamp(30px, 3vw, 42px);
  line-height: 1;
  color: var(--ink);
}
.act-kick {
  font-size: 12.5px;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding-bottom: 2px;
}
.act-head h3 {
  flex: 1 0 100%;
  margin: 6px 0 0;
  font-size: clamp(28px, 3.1vw, 42px);
  line-height: 1.02;
}
.act-lead {
  margin: 16px 0 0;
  font-size: clamp(16.5px, 1.35vw, 18.5px);
  color: var(--ink-2);
}
.act-list { list-style: none; margin: 26px 0 0; padding: 0; }
.act-list li {
  position: relative;
  padding: 13px 0 13px 20px;
  border-top: 1px solid var(--rule);
  font-size: 16px;
  color: var(--ink-2);
}
.act-list li:last-child { border-bottom: 1px solid var(--rule); }
/* The tick is the board's own mark, not a bullet glyph. */
.act-list li::before {
  content: "";
  position: absolute;
  left: 0; top: 21px;
  width: 9px; height: 1.5px;
  background: var(--ink);
}
.act-list b { color: var(--ink); font-weight: 700; }
.act-shot { margin: 26px 0 0; }
.act-shot.plate { max-width: 300px; }
.act-go { margin: 26px 0 0; }

@media (max-width: 900px) {
  .acts { grid-template-columns: 1fr; gap: 46px; }
  .act-shot.plate { max-width: 260px; }
}

/* The posture read is DRAWN, not photographed: there is no still-stance
   capture to screenshot yet, and inventing one would be a fabricated screen.
   So the frame carries its own dark ruled ground and the skeleton sits
   straight on it. The aspect ratio matches the SVG viewBox (700x780) because
   .skel is absolutely positioned and would otherwise collapse to zero height
   without an <img> to give the frame its size. */
.posture-frame {
  aspect-ratio: 440 / 722;   /* must match the SVG viewBox in posture.html */
  /* A standing figure is a tall drawing. Unconstrained it runs the hero well
     past the fold on a laptop, so the frame is capped and centred in its
     column rather than filling it. */
  max-width: 396px;
  margin-inline: auto;
  background:
    linear-gradient(rgba(232, 234, 228, .065) 1px, transparent 1px) 0 0 / 100% 39px,
    linear-gradient(90deg, rgba(232, 234, 228, .065) 1px, transparent 1px) 0 0 / 35px 100%,
    #14170F;
}

/* The ghost tag (MODEL on the sport heroes, PLUMB here) was drawn to sit on a
   bright photograph. On the posture read's near-black ground it goes almost
   invisible, so it is lifted here only — keeping the dashed border, which is
   what says "this is the reference line, not your body". */
.posture-frame .skel-ghost .ghost-tag rect { fill: rgba(8, 10, 6, .5); stroke-opacity: .95; }
.posture-frame .skel-ghost .ghost-tag text { fill: #F3F4F0; fill-opacity: .96; }

/* The anatomical body under the posture read — the stand-in for the photograph
   every other page has. It is the SUBJECT, so it sits well below the read in
   contrast and never animates: present from the first paint, with the read
   arriving on top. Deliberately uniform: no muscle group is tinted by what the
   read found, because Llyr reads joint geometry and does not see muscle. */
.posture-frame .anat .form {
  fill: rgba(243, 244, 240, .05);
  stroke: #F3F4F0;
  stroke-opacity: .34;
  stroke-width: 2;
  stroke-linejoin: round;
}
.posture-frame + .read-key .read-note,
.read-key .read-note {
  grid-column: 1 / -1;
  margin: 12px 0 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--ink-3);
  max-width: 52ch;
}
.read-key .read-note b { color: var(--ink); font-weight: 700; }

/* The heartbeat dissolves `.skel` wholesale, which is correct everywhere else
   because the body there is an <img> outside the SVG and never moves. Here the
   body is INSIDE the SVG, so the stock fade took the subject with it and the
   frame blinked empty. Scope the dissolve to the read groups and pin `.anat`:
   the body stays present, the read comes and goes over it — same relationship
   as the photo pages, just drawn instead of photographed. */
.js .read-frame.posture-frame .skel { transition: none; }
.js .read-frame.posture-frame .skel > g:not(.anat) {
  transition: opacity .5s var(--ease);
}
.js .read-frame.posture-frame.recycling .skel { opacity: 1; }
.js .read-frame.posture-frame.recycling .skel > g:not(.anat) { opacity: 0; }

.posture-frame .anat .m {
  fill: none;
  stroke: #F3F4F0;
  stroke-opacity: .28;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Two more routing destinations on the waitlist, not two more analysis
   modules — see the comment on the radios in index.html. */
.signup .wl-opt.s-tri input:checked + span { background: var(--cyan); }
.signup .wl-opt.s-strength input:checked + span { background: var(--lime); }

/* ---- The two reads: motion --------------------------------- */
/* Each act lands as a unit, then its list rules in line by line. Same
   vocabulary as the ledger rows below it, so the page has one motion system
   rather than a new idea per section. */
.js .act-list li { opacity: 0; transform: translateY(9px); }
.js .reveal.in .act-list li {
  animation: act-line .5s var(--ease) var(--rd, 0s) forwards;
}
.js .reveal.in .act-list li:nth-child(2) { --rd: .07s; }
.js .reveal.in .act-list li:nth-child(3) { --rd: .14s; }
.js .reveal.in .act-list li:nth-child(4) { --rd: .21s; }
@keyframes act-line { to { opacity: 1; transform: none; } }

@media (prefers-reduced-motion: reduce) {
  .js .act-list li { opacity: 1; transform: none; }
  .js .reveal.in .act-list li { animation: none; }
}

/* ---- The spec strip: stamped, in reading order -------------
   Same `stamp` keyframe the capability chips use, for the same reason — these
   are facts being pressed onto the board, not content fading in. It runs on
   the hero's own clock: the mark under the headline draws at .3s, so the spec
   lands behind it rather than competing with it, and the whole strip is
   finished long before the read completes on the body.

   Not gated on .reveal: the hero is above the fold on every page and its
   observer would fire immediately anyway, so the strip is animated on load
   like the headline's mark. */
/* The lede settles in on the hero's own clock, between the headline's mark
   (.3s) and the first spec (.46s), so the first viewport reads in the order
   it argues: headline, framing, facts. `row-settle` and not `stamp` — a
   two-line sentence is a row being written, not a small object being pressed
   on, and stamp's scale is wrong at 31px. from-only keyframe with `backwards`
   and no `forwards`, so switching the animation off leaves the resting state. */
.js .hero .hero-lede { animation: row-settle .42s var(--ease) .36s backwards; }
.js .hero .spec { animation: stamp .32s var(--ease) backwards; }
.js .hero .spec:nth-child(1) { animation-delay: .46s; }
.js .hero .spec:nth-child(2) { animation-delay: .55s; }
.js .hero .spec:nth-child(3) { animation-delay: .64s; }
.js .hero .spec:nth-child(4) { animation-delay: .73s; }

/* The readings column on the sport pages rules itself in line by line —
   the same gesture as .act-list and the ledger rows, because it is the same
   kind of object: a list being written onto the board a row at a time. */
.js .feat-list li { opacity: 0; transform: translateY(9px); }
.js .reveal.in .feat-list li {
  animation: act-line .5s var(--ease) var(--rd, 0s) forwards;
}
.js .reveal.in .feat-list li:nth-child(2) { --rd: .07s; }
.js .reveal.in .feat-list li:nth-child(3) { --rd: .14s; }
.js .reveal.in .feat-list li:nth-child(4) { --rd: .21s; }

@media (prefers-reduced-motion: reduce) {
  .js .hero .hero-lede, .js .hero .spec { animation: none; }
  .js .feat-list li { opacity: 1; transform: none; }
  .js .reveal.in .feat-list li { animation: none; }
}

/* ---- Forced colors (audit P3) ------------------------------ */
@media (forced-colors: active) {
  .btn-primary, .cap-chip-honest, .step .n { forced-color-adjust: none; }
  .pick::before, .sport-pill::after { forced-color-adjust: none; }
}

/* ---- Print ------------------------------------------------- */
@media print {
  header.nav, .final, .hamburger { display: none; }
  body { background: #fff; }
}
