/* Luasc marketing site — hand-rolled, no framework.

   The page is a hard court in daylight. Every colour below is lifted from
   Design/icon-hard.svg (the app icon), not invented: the playing surface, the
   surround apron, the line paint, and the two stops of the ball gradient.

   Two grounds alternate. "On court" sections carry court blue with white line
   paint; "off court" sections carry chalk. What decides which is whether the
   section is about playing (on) or about reading and deciding (off).

   Type has three roles and they never swap:
     --f-display  Archivo, a grotesque drawn from American gothics. Sturdy
                  and athletic rather than friendly: headings sit at normal
                  width, and every numeral is condensed and heavy so the
                  score reads like a board at the end of a court. Its width
                  axis is what earns it a download; don't swap in a static
                  face and lose that.
     --f-body     System sans. Quiet, gets out of the way.
     --f-mono     Labels, formats, prices' cadence. The voice of an
                  order-of-play sheet pinned up at a club. */

/* Archivo by Omnibus-Type, SIL Open Font License 1.1 (assets/archivo-OFL.txt).
   One variable file, latin subset, carrying weight 400-900 and width 62-125%.
   Self-hosted: no third-party font host, so the page makes no request the
   privacy policy would have to describe. */
@font-face {
  font-family: "Archivo";
  src: url("archivo-var.woff2?v=2026-08-27b") format("woff2");
  font-weight: 400 900;
  font-stretch: 62% 125%;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* Surface (icon courtGrad stops) */
  --court: #2f79b0;
  --court-deep: #1e5a88;
  --court-shade: #17456a;
  /* Surround / apron */
  --surround: #2e7d46;
  --surround-deep: #236036;
  /* Ball (icon ballGrad stops) */
  --ball: #c4e23a;
  --ball-hot: #ecff78;
  --ball-shade: #8fad2c;
  /* Line paint, and the chalk ground it reads against */
  --paint: #ffffff;
  --chalk: #eff4f6;
  --chalk-2: #e2ebef;
  --ink: #0e2233;
  /* Two steps down from --ink, both AA on --chalk: body copy and fine print. */
  --ink-2: #405a6b;
  --ink-3: #5a7688;
  /* The other two surfaces. Used only where the copy is about them. */
  --clay: #c56a3b;
  --grass: #4e9c54;

  /* Court paint is 14 units on a 1024 icon: a hairline that never thickens. */
  --paint-w: 2px;

  /* The doubles alley is 4.5ft against a 27ft singles court. That 1:6 is the
     page's rail-to-court split, so the layout sits on real ITF proportions
     rather than an arbitrary sidebar. */
  --alley: minmax(0, 1fr);
  --singles: minmax(0, 6fr);
  --measure: 1180px;

  --f-display: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
  /* Numerals compress; words don't. Applied through font-stretch so one
     variable file covers both. */
  --width-score: 76%;
  --f-body: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --f-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  font-family: var(--f-body);
  background: var(--chalk);
  color: var(--ink);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg { display: block; }

a { color: var(--court-deep); text-underline-offset: 0.18em; }

h1, h2, h3 {
  font-family: var(--f-display);
  font-weight: 700;
  font-stretch: 100%;
  line-height: 1.08;
  letter-spacing: -0.021em;
}

/* Every numeral on the page is a score, a price or a count, so they all line
   up in columns and none of them jitter as they change. Condensed and heavy:
   the proportions of a scoreboard, which is where a player meets these
   numbers in the first place. */
.num, .score-pts, .score-games, .step-count, .tier-fig, .fig {
  font-family: var(--f-display);
  font-stretch: var(--width-score);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

:focus-visible {
  outline: 3px solid var(--ball);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--ball);
  color: var(--ink);
  padding: 0.7rem 1.1rem;
  font-weight: 700;
  z-index: 100;
}
.skip:focus { left: 0; }

/* ============ Shared label ============ */

/* The mono label is the only place small caps-and-tracking is allowed. It
   marks a section the way a court sheet marks a column: never decoration. */
.label {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.16em;
}

/* ============ Nav ============ */

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.9rem clamp(1rem, 4vw, 3rem);
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--court-deep);
  /* The apron line under the court: the same green band the icon has. */
  border-bottom: 3px solid var(--surround);
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--f-display);
  font-weight: 800;
  font-size: 1.25rem;
  letter-spacing: -0.02em;
  color: var(--paint);
  text-decoration: none;
}

.nav-links { display: flex; gap: clamp(0.75rem, 2vw, 1.6rem); flex-wrap: wrap; }
.nav-links a {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: rgba(255, 255, 255, 0.72);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 2px solid transparent;
}
.nav-links a:hover { color: var(--paint); border-bottom-color: var(--ball); }

/* ============ Court sections ============ */

/* .oncourt paints the playing surface: lit towards the net end, falling away
   towards the baseline, the way a court reads under low sun. The court's
   line geometry is not painted here as decoration — the one line that
   matters is the singles sideline, and it is drawn on .court-col below,
   where it genuinely divides the alley from the court. */
.oncourt {
  position: relative;
  background: linear-gradient(180deg, var(--court) 0%, var(--court-deep) 78%, var(--court-shade) 100%);
  color: var(--paint);
}

/* The App Store badge is excluded because it is artwork, not text, and Apple
   forbids recolouring it. Note the specificity while editing: a bare
   `.oncourt a` outranks any single-class rule further down the file. */
.oncourt a:not(.appstore-badge) { color: var(--ball); }
.oncourt .label { color: var(--ball); }

/* ============ Layout: the alley rail and the court ============ */

.bay {
  display: grid;
  grid-template-columns: var(--alley) var(--singles);
  gap: clamp(1.25rem, 4vw, 3rem);
  max-width: var(--measure);
  margin: 0 auto;
  padding: clamp(3rem, 7vw, 5.5rem) clamp(1rem, 4vw, 3rem);
}

/* The rail carries the section's label and nothing else, the way the alley
   carries no play. It sticks so the label stays with the content it names. */
.rail { position: sticky; top: 5.5rem; align-self: start; }
.rail .label { display: block; padding-top: 0.35rem; }

/* The singles sideline. The rail is the doubles alley, where no play happens
   and only the section's label sits; this line is the edge of the court
   proper. It is the page's only structural rule, and it falls on a real
   boundary rather than at a decorative offset. */
.court-col {
  border-left: var(--paint-w) solid var(--chalk-2);
  padding-left: clamp(1.25rem, 3vw, 2.5rem);
}
.oncourt .court-col { border-left-color: rgba(255, 255, 255, 0.28); }

/* h1 sits here only on the press page, whose lead section is a bay rather
   than the hero. Same size as a section heading: it is a page title, not a
   second thesis competing with the homepage's. */
.court-col > h1,
.court-col > h2 { font-size: clamp(1.9rem, 4.2vw, 2.85rem); margin-bottom: 0.9rem; max-width: 20ch; }
.court-col > .lede { max-width: 48ch; }
/* A section whose intro runs to two paragraphs, rather than one wall. */
.court-col > .lede + .lede { margin-top: 1rem; }

.lede { font-size: 1.075rem; opacity: 0.86; }
.oncourt .lede { opacity: 0.9; }

.center { text-align: center; }
.text-link { font-weight: 600; }

/* ============ Hero ============ */

/* The hero is two bays in one section: the board and the watch, then the
   sequence. They share the section's ground, so the padding between them
   collapses rather than reading as two separate blocks. */
.hero > .bay { padding-bottom: clamp(1.5rem, 3vw, 2.25rem); }
.hero > .bay + .bay { padding-top: 0; }

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: center;
}

/* ---- The scoreboard: the page's thesis ---- */

/* This is the courtside board the app draws, rendered as the page's own
   headline rather than as a picture of one. It shows exactly the score in
   the watch screenshot beside it, and assets/motion.js advances the point
   column in step with the watch frames — so the page can never show a score
   the screenshot does not. */
.board {
  border-top: var(--paint-w) solid rgba(255, 255, 255, 0.55);
  border-bottom: var(--paint-w) solid rgba(255, 255, 255, 0.55);
  max-width: 28rem;
}

/* Fixed numeric columns rather than auto, so the header words and the
   numerals under them share an edge whatever the score reads. */
.board {
  --col-games: clamp(2.25rem, 7vw, 3rem);
  --col-pts: clamp(3.25rem, 12vw, 5rem);
}

.score-head,
.score-row {
  display: grid;
  grid-template-columns: 1fr var(--col-games) var(--col-pts);
  gap: clamp(0.75rem, 3vw, 1.75rem);
}

.score-head {
  font-family: var(--f-mono);
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  opacity: 0.6;
  padding-bottom: 0.15rem;
}
.score-head span { text-align: right; }

.score-row {
  align-items: baseline;
  padding: 0.35rem 0;
}
.score-row + .score-row { border-top: 1px solid rgba(255, 255, 255, 0.28); }

.score-name {
  font-family: var(--f-mono);
  font-size: clamp(0.75rem, 1.6vw, 0.9rem);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  align-self: center;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

/* The serve indicator is the ball, at the size a ball reads at. It marks the
   server the way the app's own scoreboard does. */
.serve {
  width: 0.62em;
  height: 0.62em;
  border-radius: 50%;
  background: var(--ball);
  flex: none;
  opacity: 0;
}
.score-row.is-serving .serve { opacity: 1; }

.score-games {
  font-size: clamp(1.6rem, 4vw, 2.4rem);
  font-weight: 700;
  opacity: 0.72;
  line-height: 1;
  text-align: right;
}

.score-pts {
  font-size: clamp(3rem, 8vw, 5rem);
  font-weight: 800;
  line-height: 0.95;
  letter-spacing: -0.05em;
  min-width: 2.2ch;
  text-align: right;
}
.score-row.is-serving .score-pts { color: var(--ball); }

/* Constrained to the board it belongs to, not to the copy column: these are
   the board's own footer line, the way the app draws it. */
.board-foot {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  max-width: 28rem;
  font-family: var(--f-mono);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  opacity: 0.7;
  padding: 0.5rem 0 0;
  margin-bottom: 1.75rem;
}

.hero h1 {
  font-size: clamp(1.9rem, 4.2vw, 2.9rem);
  margin-bottom: 1rem;
  max-width: 18ch;
}

.hero .lede { max-width: 44ch; margin-bottom: 2rem; }

/* Pulled left by the badge's own clear-space margin so the artwork's edge
   still lines up with the column, without eating into that clear space. */
.hero-actions {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  margin-left: -11px;
}
.hero-note {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  opacity: 0.72;
  margin-top: 1.1rem;
  /* Uppercase mono is wide, and this line only just fits the column. Slightly
     tighter tracking than .label keeps it on one line; balance shares it
     evenly if a narrower viewport does break it. */
  letter-spacing: 0.075em;
  text-wrap: balance;
}

/* ---- App Store button ---- */

/* Apple's official badge artwork (assets/app-store-badge.svg, the preferred
   black variant), used unaltered per
   developer.apple.com/app-store/marketing/guidelines/. The rules this markup
   has to keep, and why there is almost no styling here:

     - No modifying, angling, animating, or applying effects. So no shadow,
       no hover transform, no recolour, no border-radius of our own.
     - Minimum 40px tall onscreen. 44px here, at the artwork's own
       119.664:40 aspect ratio.
     - Clear space of one quarter the badge height (11px at 44px) with no
       type or graphics inside it. `margin` reserves it; the flex `gap` on
       .hero-actions is wider still.

   It is an <img>, not inline SVG, so page CSS cannot reach in and restyle
   the artwork, and so the file's own id="livetype" isn't duplicated by the
   second badge further down the page. */
.appstore-badge {
  display: inline-block;
  margin: 11px;
  text-decoration: none;
  line-height: 0;
}
.appstore-badge img { width: 132px; height: 44px; }
/* The link is focusable, so it must show focus. The ring sits at the edge of
   the reserved clear space rather than across the artwork. */
.appstore-badge:focus-visible { outline-offset: 6px; }

.notify-link {
  font-family: var(--f-mono);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-weight: 600;
}

/* ============ Devices ============ */

/* Real device frames (assets/frames/, see site/README.md) rather than a CSS
   approximation of a phone. The bezel PNG has a transparent screen cutout and
   sits on top; the screenshot is positioned behind it at the cutout's exact
   offsets, expressed as percentages so the whole thing scales with the
   container. The cutout's rounded corners mask the screenshot's square ones. */
.device {
  position: relative;
  margin: 0 auto;
  container-type: inline-size;
  filter: drop-shadow(0 24px 44px rgba(8, 30, 48, 0.45));
}

.device-bezel { position: relative; z-index: 2; width: 100%; height: auto; }

/* A filter applies to the whole subtree, so anything that repaints inside a
   .device forces its drop-shadow to be redrawn — and WebKit redraws it with a
   slightly different blur each time, which reads as a halo pulsing around the
   whole device. Nothing moves inside the phones, so they keep the filter
   above. The watch swaps screenshots, so it casts its shadow from a second,
   static copy of the frame art instead (`.device-shadow`, its first child).
   That copy never changes, so the shadow never redraws.

   The copy is invisible in its own right: the real frame sits exactly on top
   of it at z-index 2, and .watch-screen covers the shadow that falls inside
   the cutout. Only what falls outside the case is ever seen. */
.device-shadow {
  position: absolute;
  inset: 0;
  z-index: 0;
  filter: drop-shadow(0 24px 44px rgba(8, 30, 48, 0.45));
}
.device-shadow img { display: block; width: 100%; height: auto; }

/* The frame art is transparent across the cutout, so on its own it casts a
   shadow with a hole in it — half the density the old one had, since the
   filter used to see the screen's black box as part of the silhouette. This
   fills the cutout again, from the same figures .watch-screen uses. */
.device-shadow::before {
  content: "";
  position: absolute;
  left: var(--cutout-x);
  top: var(--cutout-y);
  width: var(--cutout-w);
  height: var(--cutout-h);
  border-radius: var(--cutout-r);
  background: #000;
}

/* Cutout is 1320x2868 at +75+66 inside the 1470x3000 frame. ~160px of a
   1470px-wide frame (10.9%) clips the screenshot's square corners inside the
   frame's opaque body without pulling back off the hole. cqw, not %, so it
   tracks rendered width — a % radius goes elliptical on an element this much
   taller than it is wide. */
.device-screen {
  position: absolute;
  z-index: 1;
  left: 5.102%;
  top: 2.2%;
  width: 89.796%;
  height: 95.6%;
  border-radius: 10.9cqw;
  background: #000;
}

/* The landscape frame is the same asset rotated, so the offsets transpose and
   the radius is the same 160px against a 3000px-wide frame. */
.device-landscape .device-screen {
  left: 2.2%;
  top: 5.102%;
  width: 95.6%;
  height: 89.796%;
  border-radius: 5.33cqw;
}

.device-sm { width: 210px; }
.device-landscape { width: min(820px, 100%); margin: 0 auto; }

/* ---- Watch ---- */

.hero-device { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
/* Cutout figures live here because two rules need to agree on them: the
   screen itself, and the fill that keeps .device-shadow's silhouette solid. */
.watch {
  width: 250px;
  filter: none; /* the shadow comes from .device-shadow */
  --cutout-x: 12.857%;
  --cutout-y: 21.818%;
  --cutout-w: 74.286%;
  --cutout-h: 56.364%;
  --cutout-r: 12.5cqw;
}

/* Cutout is 416x496 at +72+192 inside the 560x880 bezel — exactly the size of
   the watch screenshots, so they drop in without resampling. overflow +
   radius clip here because this box also clips the gesture overlay; the
   frames additionally carry the radius themselves (see below), which the
   phone's single screenshot gets from .device-screen. */
.watch-screen {
  position: absolute;
  left: var(--cutout-x);
  top: var(--cutout-y);
  width: var(--cutout-w);
  height: var(--cutout-h);
  background: #000;
  border-radius: var(--cutout-r);
  overflow: hidden;
  /* The box lands exactly on the cutout, so both antialiased edges land on
     the same device pixel and the page showed through the seam as a faint
     blue hairline down the glass. The ring carries the black a fraction
     further out, under the frame's opaque rim, without moving the box or
     resampling the screenshots. Keep it well under the ~3px (frame-space)
     of opaque frame that surrounds the cutout, or it reappears outside the
     case. */
  box-shadow: 0 0 0 0.4cqw #000;
}

/* Real screenshots stacked so assets/motion.js can swap them. Exactly one
   carries .is-shown; the markup ships it on the first frame so the watch is
   never blank. Both .is-shown rules are scoped to .js-motion, so with the
   script absent the stack always collapses to the first frame alone.

   The swap is a cut, not a cross-fade. Dissolving two real frames shows both
   at once: for half a second the point column read as a 30/40 hybrid numeral,
   the serve glyph lit all four boxes, and the whole screen dipped dark (two
   part-transparent layers over black never sum back to one opaque one) —
   three states the app never draws. The watch changes the score in one frame,
   so the page does too. Any fade added back here has to solve the
   double-exposure first.

   border-radius: inherit, not just the parent's overflow clip: the frames are
   composited layers, and WebKit clips a composited child to the parent's
   *rectangular* bounds, so their square black corners escaped the case and
   showed as four dark nubs on the watch band. */
.watch-screen img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: inherit; }
.watch-screen img + img { opacity: 0; }
.js-motion .watch-screen img:not(.is-shown) { opacity: 0; }
.js-motion .watch-screen img.is-shown { opacity: 1; }

.device-caption {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-align: center;
  max-width: 19rem;
  opacity: 0.78;
  /* Captions swap with the screenshot and the hero centres this column in its
     grid row, so any change in caption height moves the watch by half that
     amount. Reserve the tallest caption: 2 lines at this line-height. Keep
     every caption inside two lines, or raise the reserve to match. */
  min-height: 3.4em;
  line-height: 1.7;
}
.js-motion .device-caption { transition: opacity 0.25s ease; }
.device-caption.is-fading { opacity: 0; }

/* ============ Gesture demo ============ */

/* An annotation drawn *over* the real watch screenshot — never a mock-up of
   the app's own UI. It shows the one thing a static PNG can't: that you score
   by swiping and double-tapping the board. assets/motion.js sets
   data-gesture on .watch-screen. */

.gesture-layer { position: absolute; inset: 0; pointer-events: none; }
.gesture-trail, .gesture-dot, .gesture-ripple { position: absolute; opacity: 0; }

/* The gesture column sits left of centre, in the gap between the serve
   indicators and the games numeral, so the finger never covers a score it is
   meant to be demonstrating. */
.gesture-trail {
  left: 33%;
  top: 34%;
  width: 6px;
  height: 36%;
  margin-left: -3px;
  border-radius: 3px;
  background: linear-gradient(180deg, rgba(236, 255, 120, 0), rgba(236, 255, 120, 0.75));
  transform-origin: top center;
}

.gesture-dot {
  left: 33%;
  top: 34%;
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border-radius: 50%;
  background: rgba(236, 255, 120, 0.25);
  border: 2px solid rgba(236, 255, 120, 0.95);
  box-shadow: 0 0 18px rgba(236, 255, 120, 0.5);
}

.gesture-ripple {
  left: 33%;
  top: 50%;
  width: 40px;
  height: 40px;
  margin: -20px 0 0 -20px;
  border-radius: 50%;
  border: 2px solid rgba(236, 255, 120, 0.8);
}

/* --- Swipe down: the finger travels towards you, taking the point. --- */
[data-gesture="swipe"] .gesture-trail { animation: trail-grow 1.5s ease-out; }
[data-gesture="swipe"] .gesture-dot { animation: swipe-down 1.5s cubic-bezier(0.4, 0, 0.3, 1); }

@keyframes trail-grow {
  0% { opacity: 0; transform: scaleY(0); }
  25% { opacity: 1; }
  70% { opacity: 1; transform: scaleY(1); }
  100% { opacity: 0; transform: scaleY(1); }
}
/* Travel is expressed in `top` percentages, not a pixel translate, so the
   finger lands on the same part of the board at any rendered watch size —
   from 34% (their red band) down to 70% (your green band), matching
   .gesture-trail. Re-check against assets/screenshots/watch-score.png if the
   watch scoreboard layout changes again. */
@keyframes swipe-down {
  0% { opacity: 0; top: 34%; transform: scale(0.7); }
  15% { opacity: 1; top: 34%; transform: scale(1); }
  70% { opacity: 1; top: 70%; transform: scale(1); }
  100% { opacity: 0; top: 75%; transform: scale(0.85); }
}

/* --- Double-tap: two quick taps in the middle of the board. --- */
[data-gesture="tap"] .gesture-dot { top: 50%; animation: tap-twice 1.5s ease-out; }
[data-gesture="tap"] .gesture-ripple { animation: ripple-out 0.55s ease-out; }
[data-gesture="tap"] .gesture-ripple:nth-of-type(2) { animation-delay: 0.32s; }

@keyframes tap-twice {
  0%, 100% { opacity: 0; transform: scale(0.6); }
  8%, 40% { opacity: 1; }
  12% { transform: scale(0.82); }
  22% { transform: scale(1); }
  32% { transform: scale(0.82); }
  46% { transform: scale(1); }
  85% { opacity: 1; transform: scale(1); }
}
@keyframes ripple-out {
  0% { opacity: 0.9; transform: scale(0.6); }
  100% { opacity: 0; transform: scale(2.6); }
}

/* Long-press/undo is deliberately not demonstrated — see the note on `steps`
   in assets/motion.js. Adding it back needs a screenshot showing the score
   actually reverting, not just an annotation over the after-frame. */

/* ============ Sequence (score, mirror, keep) ============ */

/* Numbered because this genuinely is an order: you score the point, then the
   phone mirrors it, then it is kept. Counted in the sport's own numerals
   rather than 01/02/03, because tennis already has a way to count to three. */
.sequence { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; }

.step { padding: clamp(1.5rem, 3vw, 2rem) clamp(1rem, 2.5vw, 1.75rem); }
.step + .step { border-left: var(--paint-w) solid rgba(255, 255, 255, 0.22); }

.step-count {
  font-family: var(--f-display);
  font-variant-numeric: tabular-nums;
  font-size: clamp(2rem, 4.5vw, 2.9rem);
  font-weight: 800;
  letter-spacing: -0.04em;
  color: var(--ball);
  line-height: 1;
  display: block;
  margin-bottom: 0.6rem;
}
.step h3 { font-size: 1.1rem; margin-bottom: 0.4rem; }
.step p { font-size: 0.95rem; opacity: 0.85; }

/* ============ Ruleset ============ */

/* Term-and-definition, because that is what these are: the rules the app
   keeps for you. Laid out as a rulebook rather than as icon cards — the
   promise is "the rules are handled", so the section should look like rules. */
.ruleset { margin-top: 2.25rem; }

.rule {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: 0.4rem clamp(1rem, 3vw, 2.5rem);
  padding: 1.05rem 0;
  border-top: 1px solid var(--chalk-2);
}
.rule:last-child { border-bottom: 1px solid var(--chalk-2); }

.rule dt {
  font-family: var(--f-display);
  font-weight: 800;
  font-size: 1.02rem;
  letter-spacing: -0.02em;
}
.rule dd { color: var(--ink-2); font-size: 0.96rem; }

/* ============ Formats table ============ */

/* A real table, because formats genuinely are rows and columns: the name, how
   it is scored, and when you'd pick it. It reads like the sheet on a club
   noticeboard, which is where a player meets these words in the first place. */
.formats { width: 100%; border-collapse: collapse; margin-top: 2.25rem; }

.formats th {
  font-family: var(--f-mono);
  font-size: 0.66rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  text-align: left;
  padding: 0 0 0.6rem;
  color: var(--ball);
  border-bottom: var(--paint-w) solid rgba(255, 255, 255, 0.55);
}

.formats td { padding: 0.95rem 1.25rem 0.95rem 0; vertical-align: top; font-size: 0.95rem; }
.formats td:last-child { padding-right: 0; }
.formats tbody tr + tr td { border-top: 1px solid rgba(255, 255, 255, 0.22); }

.formats th + th, .formats td + td { padding-left: 1.25rem; }

.format-name { font-family: var(--f-display); font-weight: 800; letter-spacing: -0.02em; white-space: nowrap; }
.formats .col-when { opacity: 0.78; }
/* The link out to /rules/ sits under the table, not in it. */
.formats + .lede { margin-top: 1.75rem; }

/* ============ Split (phone) ============ */

.split {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  margin-top: 3rem;
}

.split h3 { font-size: 1.45rem; margin-bottom: 0.6rem; }
.split p { color: var(--ink-2); margin-bottom: 1rem; }

/* Each line is a court line: a hairline rule, no tick marks. */
.plainlist { list-style: none; margin-top: 1.4rem; }
.plainlist li {
  padding: 0.55rem 0;
  border-top: 1px solid var(--chalk-2);
  font-size: 0.95rem;
}
.plainlist li:last-child { border-bottom: 1px solid var(--chalk-2); }
.oncourt .plainlist li { border-color: rgba(255, 255, 255, 0.22); }

/* The fine print that qualifies a block above it: the pricing footnote, the
   line under the stats list, the caveats on the press page. Quieter than body
   copy, and AA on both grounds — --ink-3 disappears into the court, so the
   on-court variant carries white at the same remove instead. */
.note { font-size: 0.87rem; margin-top: 1.75rem; max-width: 52ch; color: var(--ink-3); }
.oncourt .note { color: rgba(255, 255, 255, 0.72); }

/* ============ Screens gallery ============ */

.shots {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.5rem, 4vw, 3rem);
  margin-top: 2.5rem;
}
.shot { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.shot figcaption {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  line-height: 1.7;
  letter-spacing: 0.04em;
  text-align: center;
  opacity: 0.8;
  max-width: 17rem;
}

/* ============ Share cards ============ */

/* The cards are the app's exported PNGs at their real 4:5, and they sit flat:
   no rounded corners of ours, because the app draws square ones and every
   place a card actually lands shows it that way. Two of them, never one — the
   app offers five styles and a single image reads as the only one there is. */
.cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.5rem, 4vw, 3rem);
  margin-top: 2.5rem;
}
.card {
  display: block;
  width: 100%;
  height: auto;
  /* The cards are nearly as dark as the far end of the court gradient, so they
     need the shadow to sit on the surface rather than sink into it. */
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.38);
}

/* ============ Pricing ============ */

.tiers {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0;
  margin-top: 2.25rem;
  border-top: var(--paint-w) solid var(--ink);
}

.tier { padding: 1.75rem clamp(1rem, 3vw, 2rem) 1.75rem 0; }
/* The net: one line between the two sides. */
.tier + .tier { border-left: var(--paint-w) solid var(--ink); padding-left: clamp(1.25rem, 3vw, 2.25rem); padding-right: 0; }

.tier h3 { font-size: 1.15rem; margin-bottom: 0.75rem; }

.tier-fig { font-size: clamp(2.6rem, 6vw, 3.6rem); font-weight: 800; letter-spacing: -0.04em; line-height: 1; }
.tier-cadence {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-3);
  margin-top: 0.4rem;
}
.tier-terms { font-size: 0.85rem; color: var(--ink-3); margin-top: 0.7rem; }
.tier-plus {
  font-family: var(--f-mono);
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  margin: 1.4rem 0 0.2rem;
  color: var(--surround-deep);
}

/* ============ Closer ============ */

/* The surround: the apron you walk off onto when the match is over. */
.closer {
  background: var(--surround-deep);
  color: var(--paint);
  text-align: center;
  padding: clamp(3.5rem, 8vw, 5.5rem) 1.25rem;
}
.closer img { margin: 0 auto 1.25rem; }
.closer h2 { font-size: clamp(2rem, 5vw, 2.75rem); margin-bottom: 0.5rem; }
.closer p { opacity: 0.85; margin-bottom: 1.5rem; }
.closer .appstore-badge { margin: 11px auto; } /* keeps Apple’s clear space */
.closer-note {
  font-family: var(--f-mono);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin: 1.25rem 0 0;
  opacity: 0.75;
}

/* ============ Footer ============ */

.footer {
  background: var(--ink);
  color: rgba(255, 255, 255, 0.65);
  padding: 2.25rem clamp(1rem, 4vw, 3rem);
  text-align: center;
  font-size: 0.85rem;
}
.footer a { color: rgba(255, 255, 255, 0.85); }
.footer-made { font-size: 0.8rem; margin-top: 0.4rem; opacity: 0.8; }
.footer-fine { font-size: 0.72rem; margin-top: 0.6rem; opacity: 0.6; }

/* ============ Inner pages (privacy / support / 404) ============ */

.page {
  max-width: 46rem;
  margin: 0 auto;
  padding: clamp(2.5rem, 6vw, 4rem) clamp(1rem, 4vw, 2rem) 4.5rem;
}
.page h1 { font-size: clamp(2rem, 5vw, 2.75rem); margin-bottom: 0.5rem; }
.page .updated {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-3);
  margin-bottom: 2.25rem;
}
.page h2 {
  font-size: 1.3rem;
  margin: 2.25rem 0 0.6rem;
  padding-top: 1.1rem;
  border-top: var(--paint-w) solid var(--chalk-2);
}
/* Support-page FAQ questions sit under the "Frequently asked" h2. */
.page h3 { font-size: 1.05rem; margin: 1.7rem 0 0.4rem; }
.page p, .page li { color: var(--ink-2); }
.page strong { color: var(--ink); }
.page ul { padding-left: 1.3rem; margin: 0.5rem 0 1rem; }
.page li { margin-bottom: 0.4rem; }
/* Nothing here sets a paragraph margin, so runs of prose used to butt
   together. The rules pages are all runs of prose. */
.page p + p, .page blockquote + p, .page p + blockquote { margin-top: 0.9rem; }

/* ============ Scoring rules (/rules/) ============ */

/* Where the page sits, above its own title. Same mono voice as .updated,
   which means "last updated" on the older pages and so isn't reused here. */
.page .crumb {
  font-family: var(--f-mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-3);
  margin-bottom: 0.9rem;
}
.page .crumb a { color: var(--ink-3); }

/* The plain answer, before any explanation. It carries the singles sideline
   rather than a tinted box: the sideline is already the page's one structural
   rule, and a callout panel would be a second idea competing with it. */
.answer {
  border-left: var(--paint-w) solid var(--court);
  padding-left: clamp(1rem, 3vw, 1.5rem);
  margin-bottom: 2.25rem;
}
.page .answer p { color: var(--ink); font-size: 1.12rem; }

/* The rule as the ITF writes it. Quieter line than .answer on purpose: the
   plain answer is the page, the quotation is the proof. */
.page .ruletext {
  border-left: var(--paint-w) solid var(--chalk-2);
  padding-left: clamp(1rem, 3vw, 1.5rem);
  color: var(--ink-2);
}

/* The question list: all six on the hub with a line each, and
   three of the other five at the foot of each one. Rows, the way a club
   noticeboard carries them. */
.page .qlist { list-style: none; padding-left: 0; margin: 0.6rem 0 0; }
.page .qlist li {
  margin-bottom: 0;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--chalk-2);
}
.page .qlist li:first-child { border-top: 1px solid var(--chalk-2); }
.page .qlist a { font-family: var(--f-display); font-weight: 700; font-size: 1.05rem; }
.page .qlist p { font-size: 0.93rem; margin-top: 0.15rem; }

/* The app, once, at the end of the answer. Never a pop-up, never a banner
   halfway down: somebody who came here for the rule gets the rule first. */
.page .app-line {
  color: var(--ink);
  margin-top: 2.25rem;
  padding-top: 1.1rem;
  border-top: var(--paint-w) solid var(--chalk-2);
}
/* On the hub the list closes with its own rule, so a second one under it
   would just be two lines with a gap between them. */
.page .qlist + .app-line { border-top: none; padding-top: 0; }

/* ============ Press ============ */

/* The app icon at the size a journalist would place it, square. The App Store
   rounds it and so does the home screen, but both do it themselves — a
   corner radius of ours would be a modified icon, which is the one thing
   Apple's marketing guidelines rule out. Same reason .card sits flat. */
.press-icon {
  width: clamp(88px, 12vw, 120px);
  height: auto;
  margin-bottom: 1.75rem;
}

/* The facts a journalist checks first. A definition list, not a table: there
   is one column of answers, and at narrow widths a term over its answer
   reads better than a row that has to be scrolled. */
.facts { margin-top: 2.25rem; }
.facts > div {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr);
  gap: 0.2rem 1.5rem;
  padding: 0.75rem 0;
  border-top: 1px solid var(--chalk-2);
}
.facts > div:last-child { border-bottom: 1px solid var(--chalk-2); }
.oncourt .facts > div { border-color: rgba(255, 255, 255, 0.22); }
.facts dt {
  font-family: var(--f-mono);
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding-top: 0.3rem;
  color: var(--ink-3);
}
.oncourt .facts dt { color: var(--ball); }
.facts dd { font-size: 0.95rem; }

/* Each asset's own dimensions, which are the thing being chosen between. In
   the mono face and never inside the link text, so the link is the name of
   the picture rather than a string of numbers. */
.dl-meta {
  font-family: var(--f-mono);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  white-space: nowrap;
  margin-left: 0.7rem;
  opacity: 0.72;
}

/* The boilerplate paragraph, marked as the thing to lift rather than read.
   The ball-coloured edge is the only decorative rule on the site besides the
   sideline, and it earns it by saying "copy from here to here". */
.pullquote {
  margin-top: 2rem;
  padding: 0.35rem 0 0.35rem 1.5rem;
  border-left: 4px solid var(--ball);
  max-width: 62ch;
}
.pullquote p { font-size: 1.05rem; color: var(--ink-2); }

/* A subheading directly inside a bay, which only the press page's boilerplate
   section needs: the ones elsewhere all sit inside .step, .split, .rule or
   .tier and take their spacing from those. */
.court-col > h3 { font-size: 1.2rem; margin: 2.5rem 0 0.6rem; }

.press-landscape { margin-top: 3rem; }

/* The closer sits on the surround green, which is not .oncourt, so a link in
   it would otherwise take the default court blue and disappear. The App Store
   badge is artwork and is deliberately not touched here. */
.closer .text-link { color: var(--ball); }

/* ============ Responsive ============ */

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; }
  .hero-device { order: -1; }
  .board { max-width: none; }
  .bay { grid-template-columns: 1fr; gap: 0.5rem; }
  .rail { position: static; }
  .rail .label { padding-top: 0; margin-bottom: 0.5rem; }
  /* Stacked, the sideline would run down the left of the whole page. */
  .court-col { border-left: none; padding-left: 0; }
  .sequence { grid-template-columns: 1fr; }
  .step { padding-left: 0; padding-right: 0; }
  .step + .step { border-left: none; border-top: var(--paint-w) solid rgba(255, 255, 255, 0.22); }
  .rule { grid-template-columns: 1fr; }
  .split { grid-template-columns: 1fr; }
  /* Copy first, screenshot under it — a stacked image between the heading and
     its own list reads as belonging to neither. */
  .split-device { order: 2; }
  .shots { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* A term beside its answer runs out of room long before the page does. */
  .facts > div { grid-template-columns: 1fr; gap: 0.3rem; }
  .facts dt { padding-top: 0; }
  .tiers { grid-template-columns: 1fr; }
  .tier { padding-right: 0; }
  .tier + .tier {
    border-left: none;
    border-top: var(--paint-w) solid var(--ink);
    padding-left: 0;
    padding-top: 1.75rem;
  }
}

@media (max-width: 620px) {
  .shots { grid-template-columns: 1fr; }
  /* Two-up, a card's own stat rows are set below 6px and stop being readable
     as anything but texture. One at a time keeps them legible. */
  .cards { grid-template-columns: minmax(0, 1fr); }
  .nav { flex-direction: column; gap: 0.7rem; padding: 0.8rem 1rem; }
  .nav-links { justify-content: center; gap: 0.5rem 0.9rem; }
  /* A three-column table can't survive this width. Each format becomes a
     stacked block with its column headings carried by the cells. */
  .formats, .formats tbody, .formats tr, .formats td { display: block; width: 100%; }
  .formats thead { display: none; }
  .formats tr { padding: 1rem 0; border-top: 1px solid rgba(255, 255, 255, 0.22); }
  .formats tbody tr + tr td { border-top: none; }
  .formats td { padding: 0.15rem 0; }
  .formats td + td { padding-left: 0; }
  .format-name { font-size: 1.1rem; white-space: normal; }
}

/* ============ Motion ============ */

/* Every rule that hides something is scoped to .js-motion, which
   assets/motion.js sets on <html> only when it runs *and* the visitor hasn't
   asked for reduced motion. With JS off, JS broken, or reduced motion on,
   none of this applies and the page renders fully visible. */

/* The one point-scoring moment on the page: the numeral changes the way the
   app's own board changes, and nothing else on the page animates on scroll
   hard enough to compete with it. */
.js-motion .score-pts { transition: color 0.25s ease; }
.score-pts.is-scored { animation: point-taken 0.5s cubic-bezier(0.2, 1.4, 0.4, 1); }
@keyframes point-taken {
  0% { transform: scale(1); }
  35% { transform: scale(1.12); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* Belt and braces: motion.js already declines to add .js-motion here, but
     these aren't gated on it. */
  .nav-links a { transition: none; }
  .score-pts.is-scored { animation: none; }
}
