/* ============================================================
   XF IDENTITY 2.0 — DESIGN TOKENS
   The system holds the line, not designer vigilance.
   Author: Ava · 4 July 2026 · Decision set D1–D5
   Rules: no !important except the prefers-reduced-motion reset (§6.1).
          No hex outside this file. Two families.
   ============================================================ */

@font-face {
  font-family: 'Anybody XC';
  src: url('fonts/Anybody_ExtraCondensed-ExtraBoldItalic.woff2') format('woff2'),
       url('fonts/Anybody_ExtraCondensed-ExtraBoldItalic.ttf') format('truetype');
  font-weight: 800;
  font-style: italic;
  font-display: swap;   /* preloaded in <head> — the swap window closes before paint */
}

/* Register 02 — self-hosted like Anybody: no CDN dependency, renders offline
   and on locked-down club PCs, no third-party request (Saxon, UK GDPR). */
@font-face {
  font-family: 'Inter';
  src: url('fonts/Inter-Variable.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* The whisper — §3.4, restricted. Italic 400 only; no other cut is hosted. */
@font-face {
  font-family: 'Cormorant Garamond';
  src: url('fonts/CormorantGaramond-Italic.woff2') format('woff2');
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

:root {
  /* ---- Colour · D3 -------------------------------------- */
  --xf-black:      #0A0A0A;  /* ground */
  --xf-white:      #FFFFFF;  /* message */
  --xf-off-white:  #F5F5F3;  /* light sections */
  --xf-green-hover: #009E28; /* primary button hover — the master darkened at
                                 constant hue 135.2° (value only moves), never a
                                 glow. Inside the §2.3 ±10° green-pixel band, so
                                 hovered buttons still count toward the ration.
                                 Black text 5.6:1 — AA, accepted for transient
                                 states only (guide §6.3). */
  --xf-green-press: #009526; /* primary button active — same constant-hue
                                 derivation, hue 135.3° (value only moves). Black
                                 text 5.0:1 — AA with headroom; lifted from
                                 #009024/4.7:1 on the round-5 revision so no
                                 shipped black-on-green state sits within 0.25 of
                                 the 4.5 floor, covering keyboard/switch dwell on
                                 :active. Inside the §2.3 ±10° band (guide §6.3). */
  --xf-green:      #00B62E;  /* THE signal — rationed. The only colour.
                                 XF GREEN 2.0, ratified by Lorenzo 2026-07-04,
                                 superseding #00C800 (RGB-pure, dead in new
                                 artwork). Hue shifted toward emerald; estate
                                 paint migrates at natural refresh. Black text
                                 on green 7.3:1 AAA. Never white on green.
                                 Derived from guide §2.1/§2.2 — the guide wins;
                                 the §8.4 diff reads these figures, not just hex. */
  /* #036016 deep green REMOVED 2026-07-04 (Lorenzo: "neither energy nor
     recovery"). Recovery territory = same black, warmer light, smaller scale.
     Territory is expressed through photography and pace, never a second green. */

  /* Documentation ink — NOT palette (§2.1 is complete). Scoped strictly to
     counter-example / misuse plates (P2, P8): the corner-to-corner kill stroke
     and MISUSE badge on a dead mark. Never a brand surface, never member-facing.
     Named here so the §8.4(j) hex-diff allow-list matches what ships, rather than
     an undocumented inline red setting precedent. */
  --doc-kill: #E10600;

  --grey-100: #E9E9E7;
  --grey-200: #C7C7C5;
  --grey-300: #8E8E8C;
  --grey-400: #55554F;
  --grey-500: #1C1C1A;

  --hairline-dark:  rgba(255,255,255,0.12);
  --hairline-light: rgba(10,10,10,0.10);
  --overlay: rgba(10,10,10,0.52);

  /* ---- Scrim · §4.5 — type over photography is a governed surface -------
     Never a flat 50% box (the caption-bar budget tell). Bottom-anchored, the
     52% mid-stop is the floor and only darkens on bright frames. White
     Register 01 over the scrimmed zone clears 4.5:1 on the darkest sampled
     pixel, checked per hero in the §8.4 render pass. */
  --scrim-training: linear-gradient(to top,
      var(--xf-black) 0%, rgba(10,10,10,0.52) 45%, rgba(10,10,10,0) 100%);
  /* Recovery scrim — same geometry, warmed so it never cools a warm frame. */
  --scrim-recovery: linear-gradient(to top,
      #0a0a0a 0%, rgba(28,18,10,0.52) 45%, rgba(28,18,10,0) 100%);

  /* ---- Header / footer · §6.11 — the frame every page hangs in --------- */
  --header-h: 64px;      /* desktop */
  --header-h-mobile: 56px;

  /* ---- Type · D1/D2 -------------------------------------- */
  /* Register 01 — THE SHOUT. Campaign surfaces only. Never body. */
  --font-display: 'Anybody XC', 'Arial Narrow', sans-serif;
  /* Register 02 — THE VOICE. All reading. (Inter interim → Neue Haas Grotesk) */
  --font-text: 'Inter', 'Helvetica Neue', -apple-system, sans-serif;
  /* The whisper — §3.4, restricted. Cormorant Garamond Italic 400 only.
     Named as a token so the one recovery-social carve-out (§3.4, §7.2) can be
     exercised on P13 tile 08 without inlining the family name each time.
     FONT-LOAD CONTRACT (§3.4, §8.4(o)): the whisper is the register that drifts
     precious first, and Georgia has no matched italic to Cormorant — so on a
     woff2 load failure the payoff must fall back to Register 02 Regular (Inter),
     NEVER to a mismatched system serif italic. The `.whisper` rule below reads
     --font-whisper; the load guard (script contract, documented under .whisper)
     swaps the element to Register 02 if Cormorant fails to load. The stack keeps
     Inter (not Georgia) as the immediate fallback so even before the guard fires
     the surface never renders a system serif italic. */
  --font-whisper: 'Cormorant Garamond', 'Inter', -apple-system, sans-serif;

  --display-lh: 0.92;
  --body-size: 17px;
  --body-lh: 1.7;
  --label-size: 12px;

  /* ---- Tracking · closed palette (§3.3) — the one axis left half-disciplined
     until round 6. Letter-spacing is a fixed named set, not a range: every
     register maps to exactly one value, and checks.yml(t) asserts no raw
     letter-spacing outside this file except the sanctioned set. The fragments
     were drifting across 0.02/0.06/0.08/0.10/0.12/0.14/-0.02em plus format
     duplicates (.11em vs 0.11em vs .1em) — closed here so drift can't recur,
     exactly as the type scale and hex are closed. --label-track retained as
     the alias --track-eyebrow's prior name so existing .label consumers hold. */
  --track-display:  0.004em;  /* Register 01, the shout — never tracked out (§3.1) */
  --track-label:    0.08em;   /* Register 03 buttons + nav (§6.3 button tracking) */
  --track-meta:     0.06em;   /* Register 03 captions/meta — the softer label track */
  --track-eyebrow:  0.11em;   /* Register 03 eyebrows, the "expensive small print" */
  --track-tight:   -0.02em;   /* large Register 02 display-scale headings only */
  --display-track: var(--track-display);  /* .display consumes the token */
  --label-track:   var(--track-eyebrow);  /* .label consumes the token */

  /* ---- Motion · D5 — one curve, three speeds -------------- */
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
  --t-fast: 200ms;
  --t-med:  350ms;
  --t-slow: 500ms;

  /* ---- Space · 8px scale ---------------------------------- */
  --s-2:  8px;   --s-4: 16px;  --s-6: 24px;  --s-8: 32px;
  --s-12: 48px;  --s-16: 64px; --s-24: 96px; --s-36: 144px;

  /* ---- Layout --------------------------------------------- */
  --container: 1200px;
  --gutter: 40px;
  --radius: 4px;
  --radius-card: 8px;

  /* Prose reading-measure (§6.2) — the token that stops body copy running wide
     even when photography goes full-bleed. Every prose block consumes this;
     checks.yml(u) asserts no hand-authored prose max-width outside the token,
     mirroring (a) for hex and (p1) for the type scale. ~58–72ch ≈ 620–720px. */
  --measure: clamp(58ch, 66ch, 72ch);

  /* Email body wrapper (§7.5) — a dedicated 600px literal, NOT the prose --measure
     clamp. --measure is a ch-based range (~620–720px) that exceeds Outlook's
     safe column; the booking + lifecycle emails wrap at 600px hard. checks.yml(u)
     reads the email wrapper against this target, not the prose measure. */
  --email-measure: 600px;

  /* Column / gap system (§6.2, §7.1) — card rails and section grids read as one
     editorial strip everywhere, copied off a token like the section order is.
     The named set is the only grid rhythm a plate branches to; --gap replaces
     hand-authored grid gaps. Fragments improvising ~19 grid-template-columns
     definitions collapse onto these. */
  --gap: 24px;                                   /* the one grid gap */
  --cols-2: repeat(2, minmax(0, 1fr));
  --cols-3: repeat(3, minmax(0, 1fr));
  --cols-4: repeat(4, minmax(0, 1fr));
  --cols-cards: repeat(auto-fit, minmax(260px, 1fr));  /* card rails */

  /* ---- Modular type scale (§6.1) — plates consume the token, never inline
     a raw clamp(). The §8.4(p1) diff asserts no font-size clamp outside this
     file, mirroring check (a) for hex. Heading sizes were drifting per-plate
     (P5 128, P11 88, hero 210) — tokenised here so drift can't recur. -------- */
  --fs-hero:          clamp(64px, 22vw, 210px);  /* web hero (§3.6) */
  --fs-shout-train:   clamp(56px, 10vw, 128px);  /* Register 01 training shout */
  --fs-shout-recover: clamp(48px,  7vw,  96px);  /* recovery shout, capped 96 (§3.5) */
  --fs-h2:            clamp(40px,  6vw,  88px);
  --fs-h3:            clamp(24px,  3vw,  40px);
  /* Plate-title scale — the reference-plate H2 runs a size above the web --fs-h2
     because a plate is a printed reference viewed larger than a web section
     (the Equinox carve-out: a plate's proportions are deliberately not the web
     type scale). Two tokenised sizes so plate titles stop being hand-authored
     clamps. Any raw font-size clamp() that is NOT one of these and NOT inside a
     data-scaled-mock block fails check (p1). */
  --fs-plate-title:   clamp(48px,  6vw,  84px);  /* the recurring plate H2 */
  --fs-plate-title-l: clamp(48px,  7vw, 104px);  /* the taller plate H2 (P3/P8) */

  /* ---- Named breakpoints (§6.1) — one controlled set, not per-plate media
     queries. Consumed via @custom-media where the toolchain supports it;
     otherwise these are the only widths a plate branches at. ---------------- */
  --bp-s:  600px;
  --bp-m:  900px;
  --bp-l:  1200px;
  --bp-xl: 1440px;
}

/* ---- Global type discipline · §3.6 — scale-free, mandatory ----------------
   Two one-line disciplines the Register 01 shout depends on and colour
   arithmetic can't reach (Equinox parity, checks.yml(r)):
   1. Every heading balance-wraps — a single orphaned word on a 210px condensed
      uppercase shout at line-height 0.92 destroys the composition, and
      text-wrap:balance is a scale-free fix, not a per-plate judgement.
   2. text-size-adjust is pinned so mobile Safari never auto-inflates body type
      off the tokenised scale (the club-PC/email path pins it hard). */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
h1, h2, h3, h4, h5, h6, .display { text-wrap: balance; }

/* ---- The three registers ---------------------------------- */

.display {
  font-family: var(--font-display);
  font-weight: 800;
  font-style: italic;
  text-transform: uppercase;      /* enforced in CSS — Nike model */
  line-height: var(--display-lh);
  letter-spacing: var(--display-track);
}

.body-text {
  font-family: var(--font-text);
  font-weight: 400;
  font-size: var(--body-size);
  line-height: var(--body-lh);
  text-transform: none;
}

/* Prose measure (§6.2) — apply to every prose block; copy runs to --measure,
   never a hand-authored max-width. checks.yml(u) reads this. */
.prose { max-width: var(--measure); }

/* Grid rhythm (§6.2/§7.1) — card rails and section grids consume the named
   column set and the one --gap, so three club pages share internal column
   feel and not just section order. checks.yml(v) reads grid-template-columns. */
.rail    { display: grid; gap: var(--gap); grid-template-columns: var(--cols-cards); }
.grid-2  { display: grid; gap: var(--gap); grid-template-columns: var(--cols-2); }
.grid-3  { display: grid; gap: var(--gap); grid-template-columns: var(--cols-3); }
.grid-4  { display: grid; gap: var(--gap); grid-template-columns: var(--cols-4); }

.label {
  font-family: var(--font-text);
  font-weight: 600;
  font-size: var(--label-size);
  letter-spacing: var(--label-track);
  text-transform: uppercase;
}

/* The green full stop — THE distinctive asset. Recalibrated 2026-07-04: the
   mark reads bigger than the type it follows so it registers as a signature at
   a glance, not a subtle dot. Signature, not field — still under the ration. */
.stop { color: var(--xf-green); font-style: italic; font-size: 1.15em; }

/* The whisper — §3.4. Cormorant Italic 400, one sentence, beside Reg 01/02 only.
   The stack falls back to Inter, never Georgia italic (§8.4(o)). */
.whisper { font-family: var(--font-whisper); font-style: italic; font-weight: 400; }

/* Load guard (the §8.4(o) contract, made technical): if Cormorant fails to load,
   demote every .whisper to Register 02 Regular so the register never renders as a
   mismatched serif italic. Mirrors §3.4's "Register 02 is the default when the
   whisper is unavailable." Ships inline in <head> on any page using .whisper:

     if ('fonts' in document) {
       document.fonts.load("italic 400 24px 'Cormorant Garamond'").then(() => {
         if (!document.fonts.check("italic 400 24px 'Cormorant Garamond'"))
           document.documentElement.classList.add('whisper-unavailable');
       }).catch(() => document.documentElement.classList.add('whisper-unavailable'));
     }

   The class below is the demotion target; no Georgia ever paints the whisper. */
.whisper-unavailable .whisper {
  font-family: var(--font-text);
  font-style: normal;
}

/* ---- Accessibility — system level, non-negotiable ---------- */

/* Focus is never green: green is rationed ink, and a green halo is the exact
   effect §6.5 bans. Grey-200 ring on dark grounds; override to grey-400 on
   light sections (guide §6.3). */
:focus-visible {
  outline: 2px solid var(--grey-200);
  outline-offset: 2px;
  border-radius: 2px;
}
.light :focus-visible { outline-color: var(--grey-400); }

/* ---- Button states · guide §6.3 — vendor defaults never fill the gap ---- */
.btn-primary          { background: var(--xf-green); color: var(--xf-black); }
.btn-primary:hover    { background: var(--xf-green-hover); }   /* darken, never a glow */
.btn-primary:active   { background: var(--xf-green-press); }
.btn-primary:disabled { background: var(--grey-100); color: var(--grey-300); }
.btn-secondary:hover  { box-shadow: inset 0 0 0 1px currentColor; } /* hairline thickens to 2px */
.btn-secondary:disabled { border-color: var(--grey-100); color: var(--grey-300); }

/* Motion is a courtesy, never a requirement.
   !important is mandatory here (§6.1): this universal selector sits at
   specificity 0 and silently loses to any element-scoped transition without
   it. This is the one sanctioned !important in the file; the §8.4 token diff
   checks these four declarations still carry it, so the kill-switch always wins. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- The signature web-hero moment · §6.7 · plate P15 -------------------
   The ONE theatrical moment: the three-line hero rises word-by-word, each word
   translateY(24px)+opacity:0 → settled, --ease at --t-slow, 90ms per-word
   stagger, once on load. Pinned frame-by-frame here so it is copied not drifted;
   §8.4(p1) asserts the hero animates only with --ease and the three tokenised
   durations — no raw cubic-bezier, no fourth speed. */
@keyframes hero-word-rise {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}
.hero-signature .hero-word {
  opacity: 0;
  animation: hero-word-rise var(--t-slow) var(--ease) forwards;
}
.hero-signature .hero-word:nth-child(1) { animation-delay: 0ms; }
.hero-signature .hero-word:nth-child(2) { animation-delay: 90ms; }
.hero-signature .hero-word:nth-child(3) { animation-delay: 180ms; }
/* Reduced-motion end-state: settled type, no transform, opacity 1, zero
   duration — the §6.1 kill-switch already forces this via !important, but the
   final visual is stated so the end-state is inspectable, not inferred. */
@media (prefers-reduced-motion: reduce) {
  .hero-signature .hero-word { opacity: 1; transform: none; }
}

/* ---- Forced colours / Windows high-contrast · §6.10 · checks.yml(q) -------
   The club-PC estate is Windows and email clients honour forced-colors, so
   structure must survive the system palette: hairlines stay visible, the green
   CTA keeps a boundary rather than dissolving into the system button colour,
   focus maps to Highlight. Shipped web templates carry this block. */
@media (forced-colors: active) {
  .btn-primary { border: 2px solid ButtonText; forced-color-adjust: none;
                 background: ButtonFace; color: ButtonText; }
  .card, .ticker, .offer-tag { border-color: CanvasText; }
  :focus-visible { outline-color: Highlight; }
}

/* ---- Touch-target floor · §6.10 — 44px minimum on interactive app/booking
   elements; buttons already ship 48/52px (§6.3), this is the floor beneath
   which nothing interactive drops on the most-opened surface. */
.tappable { min-height: 44px; min-width: 44px; }
