/*
 * The design foundations: type, colour, space, motif, motion.
 *
 * This is the "one block" of DESIGN_SYSTEM.md §12.1, and it exists because
 * three reviewers landed three overlapping palettes independently and the
 * merge of three palettes is worse than any one of them. Nothing here styles
 * a component. It declares vocabulary, and `styles.css` and the public site
 * both consume it.
 *
 * **Three vocabularies that never mix**: identity (`--who-*`, *who* a mark
 * belongs to), valence (`--valence-*`, whether a number is good), state
 * (`--state-*`, which kind of nothing you are looking at). A token from one
 * may never stand in for a token from another, because the moment `--accent`
 * means "selected" and "recommended" and "captain" at once, no call site can
 * be changed without auditing twenty-two others. That is the state this file
 * replaces: `--accent` had 23 call sites across eleven meanings.
 *
 * **Every number in the comments below was computed, not chosen.** The script
 * is `scripts/check_palette.py`; run it after touching any value here and it
 * will tell you which clause you broke. Contrast is WCAG relative luminance;
 * separation is CIELAB dE76, reported as the *worst* of normal vision,
 * protanopia, deuteranopia and tritanopia under Vienot-Brettel-Mollon 1999.
 * The model was anchored against two known pairs before any of this was
 * trusted: tab10 red vs green returns 8.2 (correctly bad) and tab10 blue vs
 * orange returns 100.4 (correctly good).
 *
 * Both themes are *designed*, not flipped. The full light palette is on bare
 * `:root`; only what changes is redefined under `prefers-color-scheme: dark`
 * and again under `[data-theme="dark"]`, so a manual toggle beats the OS in
 * both directions. Never define a colour for the first time inside a media
 * query — a reader on the other theme then has no value at all.
 */

/* ------------------------------------------------------------------ *
 * Faces
 *
 * Self-hosted, because a webfont CDN is a third party on the critical path
 * of a page whose whole claim is that it shows its working.
 *
 * **Archivo and "Archivo Expanded" are one file.** Archivo ships a variable
 * `wdth` axis from 62 to 125, so the expanded display face is the same font
 * at `font-stretch: 125%`. Declaring a second @font-face family for it would
 * download the same bytes twice. `wght` runs 100-900 and `wdth` 62-125 —
 * confirmed from the file's own fvar table, not from the specimen page.
 *
 * Latin and latin-ext are both here: latin-ext costs 86KB and carries the
 * accented characters in a Premier League squad list, which is not optional
 * in a product that prints player names.
 *
 * Both faces are SIL OFL 1.1; the licences travel with the files in
 * `ui/public/fonts/`. `font-display: swap` because a stat that arrives late
 * is worse than a stat in the fallback face for 200ms.
 * ------------------------------------------------------------------ */

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("/fonts/archivo-latin.woff2") format("woff2");
  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;
}

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("/fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-latin.woff2") format("woff2");
  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;
}

@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url("/fonts/jetbrains-mono-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

/* ================================================================== *
 * LIGHT — the full palette. Every token in the system has a value here.
 * ================================================================== */

:root {
  color-scheme: light;

  /* ---- Ground and ink (§3.1) ------------------------------------- *
   *
   * Three grounds, and a token's contrast is quoted against the *worst*
   * of the ones it can legally sit on. `--pitch` is the same colour in
   * both themes, which is the point of it: the dark data panel does not
   * change when the page does.
   */
  --paper: #f6f6f3;                          /* page ground              */
  --surface: #ffffff;                        /* cards, panels           */
  --pitch: #142018;                          /* dark data panel, both themes */

  --ink: #121210;                            /* primary text · 17.8:1 on paper */
  --ink-body: rgba(18, 18, 16, 0.7);         /* body · 6.75:1 on paper  */

  /* **The most consequential token in the file.** It carries every
     caption, column header, axis tick and help line — most of the small
     text in the product — so a failure here fails most of the product.
     Measured 5.47:1 on paper, 5.64:1 on surface.

     The tempting value is a 45% ink. It measures **2.98:1** and fails AA
     outright, and the stylesheet this replaces used an equivalent at
     3.41:1, which also fails. ink-64 is visually near-identical in weight
     and there is simply no reason to spend the contrast.

     **Forbidden, measured on paper: ink at 28% (1.87:1), 45% (2.98:1) and
     50% (3.46:1).** Where you want "quieter than body", use --text-help at
     a smaller size. Never a lower alpha. Every one of those three reads as
     a reasonable design choice and every one of them is illegal. */
  --text-help: rgba(18, 18, 16, 0.64);       /* 5.47:1 on paper         */

  --hairline: rgba(18, 18, 16, 0.12);        /* 1px dividers            */

  /* ---- Identity: who (§3.2) -------------------------------------- *
   *
   * Two hues, a near-black, a dashed rule and a hatch — not a colour per
   * entity. **Seven mutually distinguishable hues do not exist in a
   * palette this restrained**, and faking it produces a chart that lies to
   * about one reader in twelve. Many entities on one chart are separated
   * by *naming*: two hued series, everything else near-black, labels on
   * the lines, one crosshair reading out every series at the hovered
   * point. Do not add a third hue to solve this.
   *
   * Measured dE76, worst of four vision models, floor 25:
   *
   *     pair                light   dark
   *     me vs engine         70.8   65.4
   *     me vs rival          28.4   28.3
   *     engine vs rival      30.6   24.9      <- see the note on --who-rival
   */
  --who-me: #1f7a46;                         /* the manager reading · 4.93:1 */

  /* **The second hue is violet, and the obvious choice is wrong.** A dark
     blue against this green measures in the 12-21 band depending which
     blue you pick — every one of them under the floor — because under
     protanopia and deuteranopia a desaturated dark green and a dark blue
     both collapse toward the same blue-grey. Violet clears at 70.8 and
     reads 6.41:1 on paper. */
  --who-engine: #7340a8;                     /* the engine's team · 6.41:1 */

  /* **Nearly black on purpose.** At a mid-grey this measures around 18-22
     against the brand green, because a mid-grey and a mid-green share a
     lightness and separation has to come from lightness once hue is spent.
     It is also semantically right: rivals recede into plain near-black and
     are told apart by direct labels and a crosshair, not by hue. */
  --who-rival: rgba(18, 18, 16, 0.85);       /* other managers · 11.5:1 */

  /* The average, the whole game. A baseline, never a series, and **always
     text-labelled** — a dashed rule with no label is a rule nobody can
     read. Consumed as `1px dashed var(--who-field)`. */
  --who-field: rgba(18, 18, 16, 0.12);

  /* A benchmark cohort. **A pattern, not a hue** — it spends nothing from
     the hue budget and survives every dichromacy absolutely. 45deg, which
     is why --state-missing is 135deg and --state-unreadable is vertical:
     the three patterns in this file must be told apart from each other,
     not merely from a flat fill. */
  --who-elite: repeating-linear-gradient(
    45deg,
    rgba(18, 18, 16, 0.85) 0 1.5px,
    transparent 1.5px 5px
  );

  /* ---- Valence: good, bad, level (§3.3) -------------------------- *
   *
   * **Position carries valence; hue only reinforces it. This is forced,
   * not preferred.** The best red/green pair available measures 18.3 in
   * light and 23.8 in dark, both under the floor, because every red/green
   * pair collapses under deuteranopia — there is no fix, only a different
   * channel. So a signed number always shows its sign, and a bar always
   * grows from a visible zero line. A chart here that carried its meaning
   * in red-versus-green would be unreadable to roughly one man in twelve
   * and would look completely fine to whoever shipped it.
   */
  --valence-good: #1f7a46;                   /* 4.93:1 on paper         */
  --valence-bad: #9a3412;                    /* 6.75:1 on paper         */

  /* **Exactly zero is a result, not an absence.** It is drawn, in the
     help colour, as a `0` with a sign-width of blank in front of it —
     never omitted, never an em dash. An engine that predicts no change
     has predicted something. */
  --valence-flat: rgba(18, 18, 16, 0.64);

  /* ---- State: which kind of nothing (§3.4) ----------------------- *
   *
   * Never a valence, never an identity. Three kinds of nothing plus one
   * demand, and the difference between them is this product's honesty
   * made visible. A blank is drawn as a blank; it is never a zero.
   */
  --state-pending: rgba(18, 18, 16, 0.64);   /* recorded, not yet settled. Normal */

  /* **Not recorded. Unrecoverable.** 135deg, so it cannot be confused
     with --who-elite at 45deg. */
  --state-missing: repeating-linear-gradient(
    135deg,
    rgba(18, 18, 16, 0.64) 0 1px,
    transparent 1px 4px
  );

  /* The source would not serve it — a different failure from "nobody ever
     recorded it", and the reader is owed the difference. Vertical, so it
     is a third distinguishable pattern rather than a second diagonal. */
  --state-unreadable: repeating-linear-gradient(
    90deg,
    rgba(18, 18, 16, 0.64) 0 1px,
    transparent 1px 4px
  );

  --state-blocked: #9a3412;                  /* action required now, full-width */

  /* ---- Accent (§3.5) --------------------------------------------- *
   *
   * **A dark-panel colour and nothing else.** 8.62:1 on --pitch and
   * **1.80:1 on paper**. It may not appear on a light ground at any size,
   * for any reason — not as a 10px label, not as a 1px rule. The number is
   * not a close call; it is a quarter of the floor.
   */
  --accent: #35d46a;

  /* ---- Interaction colour is not meaning colour (§3.7) ------------ *
   *
   * Focus rings, active tab, pressed state, slider thumb. **Chrome, and it
   * carries no meaning.** It resolves to the same hue as --who-me today
   * and it is still not the same token, because collapsing them is exactly
   * how one token ends up meaning "selected", "recommended", "focused" and
   * "captain" at once — which is the state this file was written to end.
   * If identity ever needs to move, this stays put.
   */
  --accent-ui: #1f7a46;

  /* ---- The fixture ramp (§3.6) ----------------------------------- *
   *
   * Seven steps, **the only diverging scale in the product**, used only
   * for fixture difficulty and never borrowed. The principle it encodes:
   * a fixture's kindness is a **magnitude with a sign, not an identity**,
   * which is why it must never reuse an identity slot — "Brighton" and "a
   * hard away day" cannot share a colour language.
   *
   * Each step ships a paired ink because the two ends are dark enough to
   * need light text and the middle three are not. Every pair clears AA;
   * lightness is monotonic outward from the midpoint in both directions
   * and in both themes, which is what keeps the ramp readable when the hue
   * is not. Measured L*: 40.4 60.1 84.9 90.1 85.1 61.8 45.1.
   */
  --fixture-3: #a83a22;  --fixture-3-ink: #ffffff;   /* 6.38:1 */
  --fixture-2: #d17a5c;  --fixture-2-ink: #2a1206;   /* 5.60:1 */
  --fixture-1: #f0cdbe;  --fixture-1-ink: #5c2a15;   /* 7.89:1 */
  --fixture-0: #e4e3dc;  --fixture-0-ink: #52514e;   /* 6.17:1 */
  --fixture1: #bcdccb;   --fixture1-ink: #0d4531;    /* 7.44:1 */
  --fixture2: #4da585;   --fixture2-ink: #062a1d;    /* 5.19:1 */
  --fixture3: #0f7a52;   --fixture3-ink: #ffffff;    /* 5.35:1 */
}

/* ================================================================== *
 * DARK — only what changes.
 *
 * **These two blocks must stay byte-identical.** They exist twice because
 * CSS gives no way to share one declaration body between a media query and
 * a plain selector, and both are needed: the media query so the OS setting
 * works, the [data-theme] stamp so a manual toggle beats it in *both*
 * directions. The `:not([data-theme="light"])` guard is what makes "beats
 * it in both directions" true rather than only true one way.
 *
 * Drift between them is the failure this arrangement invites, and it is
 * silent — the reader on the other theme simply sees a different product.
 * `scripts/check_palette.py` diffs the two and fails if they disagree, so
 * run it after touching either.
 * ================================================================== */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
  color-scheme: dark;

  --paper: #0f1512;
  --surface: #161d19;
  /* --pitch does not change. That is the whole point of it. */

  --ink: #eef1ef;
  --ink-body: rgba(238, 241, 239, 0.8);

  /* 6.76:1 on --pitch, the worst of the three dark grounds; 6.85 on
     surface, 7.13 on paper. **Forbidden: on-dark at 35% (2.97:1) and 45%
     (4.03:1)**, measured on --pitch. */
  --text-help: rgba(238, 241, 239, 0.64);

  --hairline: rgba(238, 241, 239, 0.14);

  --who-me: #4fbf7e;
  --who-engine: #b08ce8;

  /* **on-dark-95, not the 90 the document specifies, and the tenth of a
     dE this buys is the whole reason.** At 90 this pair measures 24.9
     against --paper, 25.2 against --surface and 25.5 against --pitch: the
     document's 25.1 reproduces on --surface, and on the page ground the
     same pair is under the floor. It is the tightest pair in the system,
     so it is the one that has to be measured on the *worst* ground rather
     than a representative one. At 95 it reads 28.2 / 28.4 / 28.6 and the
     margin stops depending on which panel the chart happens to sit in.
     Nothing else moves — no hue changes, and the document's own argument
     for this token ("separation has to come from lightness once hue is
     spent") is the argument for spending a little more of it. */
  --who-rival: rgba(238, 241, 239, 0.95);

  --who-field: rgba(238, 241, 239, 0.14);
  --who-elite: repeating-linear-gradient(
    45deg,
    rgba(238, 241, 239, 0.9) 0 1.5px,
    transparent 1.5px 5px
  );

  --valence-good: #4fbf7e;                   /* 7.27:1 on --pitch       */
  --valence-bad: #e35d4c;                    /* 4.75:1 on --pitch       */
  --valence-flat: rgba(238, 241, 239, 0.64);

  --state-pending: rgba(238, 241, 239, 0.64);
  --state-missing: repeating-linear-gradient(
    135deg,
    rgba(238, 241, 239, 0.64) 0 1px,
    transparent 1px 4px
  );
  --state-unreadable: repeating-linear-gradient(
    90deg,
    rgba(238, 241, 239, 0.64) 0 1px,
    transparent 1px 4px
  );
  --state-blocked: #e35d4c;

  /* --accent does not change either: it is a --pitch colour, and --pitch
     is the same in both themes. */

  --accent-ui: #4fbf7e;

  /* Re-stepped for the dark surface, not inverted: lightness runs outward
     from the midpoint *upward* here and *downward* in light, which is why
     the dark ends take dark ink and the light ends take white. Measured
     L*: 54.0 37.1 25.8 21.6 31.2 45.5 59.1.
     **--fixture-3 was #c2542f and is not any more.** At that value the
     paired ink measured 4.23:1 and failed AA, and so did every other ink:
     the fill sat at the pinch point where neither black nor white clears
     4.5, the best available being white at 4.56. The fill had to move, not
     the ink. #ce6139 is the same hue two steps lighter, reads 4.95:1 with
     the ink already specified, and keeps L* monotonic. */
  --fixture-3: #ce6139;  --fixture-3-ink: #1a0a04;   /* 4.95:1 */
  --fixture-2: #8a4327;  --fixture-2-ink: #fbe6dd;   /* 6.00:1 */
  --fixture-1: #5a3325;  --fixture-1-ink: #f0c4b0;   /* 6.84:1 */
  --fixture-0: #343432;  --fixture-0-ink: #c9c8bd;   /* 7.41:1 */
  --fixture1: #22523f;   --fixture1-ink: #a8dcc4;    /* 5.84:1 */
  --fixture2: #27795c;   --fixture2-ink: #e0f4ec;    /* 4.61:1 */
  --fixture3: #33a077;   --fixture3-ink: #04231a;    /* 5.11:1 */
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --paper: #0f1512;
  --surface: #161d19;
  /* --pitch does not change. That is the whole point of it. */

  --ink: #eef1ef;
  --ink-body: rgba(238, 241, 239, 0.8);

  /* 6.76:1 on --pitch, the worst of the three dark grounds; 6.85 on
     surface, 7.13 on paper. **Forbidden: on-dark at 35% (2.97:1) and 45%
     (4.03:1)**, measured on --pitch. */
  --text-help: rgba(238, 241, 239, 0.64);

  --hairline: rgba(238, 241, 239, 0.14);

  --who-me: #4fbf7e;
  --who-engine: #b08ce8;

  /* dE from --who-engine is **24.9 against --paper**, 25.2 against
     --surface, 25.5 against --pitch. The document records 25.1, which
     reproduces on --surface; on --paper the same pair is a tenth under the
     floor. It is the tightest pair in the system and the one to re-measure
     first if any dark value moves. See scripts/check_palette.py, which
     fails on it and says so. */
  --who-rival: rgba(238, 241, 239, 0.95);

  --who-field: rgba(238, 241, 239, 0.14);
  --who-elite: repeating-linear-gradient(
    45deg,
    rgba(238, 241, 239, 0.9) 0 1.5px,
    transparent 1.5px 5px
  );

  --valence-good: #4fbf7e;                   /* 7.27:1 on --pitch       */
  --valence-bad: #e35d4c;                    /* 4.75:1 on --pitch       */
  --valence-flat: rgba(238, 241, 239, 0.64);

  --state-pending: rgba(238, 241, 239, 0.64);
  --state-missing: repeating-linear-gradient(
    135deg,
    rgba(238, 241, 239, 0.64) 0 1px,
    transparent 1px 4px
  );
  --state-unreadable: repeating-linear-gradient(
    90deg,
    rgba(238, 241, 239, 0.64) 0 1px,
    transparent 1px 4px
  );
  --state-blocked: #e35d4c;

  /* --accent does not change either: it is a --pitch colour, and --pitch
     is the same in both themes. */

  --accent-ui: #4fbf7e;

  /* Re-stepped for the dark surface, not inverted: lightness runs outward
     from the midpoint *upward* here and *downward* in light, which is why
     the dark ends take dark ink and the light ends take white. Measured
     L*: 54.0 37.1 25.8 21.6 31.2 45.5 59.1.
     **--fixture-3 was #c2542f and is not any more.** At that value the
     paired ink measured 4.23:1 and failed AA, and so did every other ink:
     the fill sat at the pinch point where neither black nor white clears
     4.5, the best available being white at 4.56. The fill had to move, not
     the ink. #ce6139 is the same hue two steps lighter, reads 4.95:1 with
     the ink already specified, and keeps L* monotonic. */
  --fixture-3: #ce6139;  --fixture-3-ink: #1a0a04;   /* 4.95:1 */
  --fixture-2: #8a4327;  --fixture-2-ink: #fbe6dd;   /* 6.00:1 */
  --fixture-1: #5a3325;  --fixture-1-ink: #f0c4b0;   /* 6.84:1 */
  --fixture-0: #343432;  --fixture-0-ink: #c9c8bd;   /* 7.41:1 */
  --fixture1: #22523f;   --fixture1-ink: #a8dcc4;    /* 5.84:1 */
  --fixture2: #27795c;   --fixture2-ink: #e0f4ec;    /* 4.61:1 */
  --fixture3: #33a077;   --fixture3-ink: #04231a;    /* 5.11:1 */
}

/* ================================================================== *
 * Theme-invariant: type, space, motif, motion.
 *
 * Nothing below changes between themes. It is separated from the palette
 * so the dark blocks stay short enough to diff by eye.
 * ================================================================== */

:root {
  /* ---- Faces and steps (§4) --------------------------------------
   *
   * **Mono for every label, stat, verdict, timestamp and button is the
   * rule that makes this look like an instrument rather than a blog.** It
   * is free, and it is the strongest single decision in the system.
   *
   * "Archivo Expanded" is Archivo at `font-stretch: 125%` — the same file,
   * the same download. --font-display carries the stretch with it so a
   * call site cannot get the face right and the width wrong.
   */
  --font-display: 125% 600 var(--step-display) / 1.05 "Archivo",
    ui-sans-serif, system-ui, sans-serif;
  --font-body: "Archivo", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* **Two display ceilings, deliberately.** The public site goes to 64px,
     where a page has room and one headline is the whole screen. **Inside
     the app the ceiling is 32px**, used once per screen — a 38px name is
     unusable at fifteen rows and eight columns, which is what the app
     actually renders. */
  --step-display-public: 64px;
  --step-display: 32px;                      /* app ceiling, once per screen */
  --step-display-2: 26px;
  --step-display-3: 20px;

  --step-body-public: 17px;
  --step-body: 15px;
  --step-body-dense: 14px;

  --step-mono: 13px;
  --step-mono-2: 12px;
  --step-mono-3: 11px;
  --step-mono-4: 10px;                       /* table headers           */

  /* Tracking scales *down* as size goes up, which is the whole reason it
     is a token set and not one value: -.04em at 64px is invisible and
     -.04em at 12px is illegible. */
  --track-64: -0.04em;
  --track-32: -0.03em;
  --track-26: -0.02em;
  --track-20: -0.01em;
  --track-base: 0;

  /* Mono is set uppercase and open. .16em at 10px, closing to .1em at
     13px — the smaller the type, the more air the letterforms need. */
  --track-mono-tight: 0.1em;
  --track-mono: 0.12em;
  --track-mono-wide: 0.16em;

  /* **Non-negotiable in a product whose content is columns of numbers.**
     Every table, every stat, every axis tick. Applied wherever digits
     stack, which in this product is nearly everywhere. */
  --numeric: tabular-nums;

  /* ---- Ink on --pitch -------------------------------------------
   *
   * `--pitch` is the one ground that does not change between themes, so the
   * ink that sits on it does not either — which is why these four live here,
   * with the theme-invariant tokens, and are not repeated in the dark block.
   *
   * **They exist because a dark panel on a light page is a hole in the
   * cascade.** Inside `.panel-dark` the page's `--ink` is near-black and the
   * ground is near-black with it; every token that resolves against the theme
   * rather than against the panel comes out invisible. It is not a
   * hypothetical: the style guide's own accent specimen rendered its measured
   * ratio in dark-green-on-dark and the number could not be read at all.
   *
   * Measured on --pitch: ink 14.78:1, body 9.84:1, help 6.76:1. The hairline
   * is decorative at 1.49:1, like every hairline in the system.
   */
  --pitch-ink: #eef1ef;
  --pitch-ink-body: rgba(238, 241, 239, 0.8);
  --pitch-text-help: rgba(238, 241, 239, 0.64);
  --pitch-hairline: rgba(238, 241, 239, 0.14);

  /* ---- Space (§6) -------------------------------------------------
   *
   * **Spacing is a scale and a token, never a hand-written margin.** A
   * stylesheet with eleven distinct margin values has no system, and that
   * is the diagnosis of the one this replaces.
   *
   * Four steps carry the app; three more carry the public site, where a
   * section break has to read as a break rather than as a gap.
   *
   * **Layout does the spacing**: flex or grid with `gap`, never
   * per-element margins that collapse or double.
   */
  --space-1: 8px;
  --space-2: 12px;
  --space-3: 16px;
  --space-4: 24px;
  --space-5: 32px;                           /* public                  */
  --space-6: 48px;                           /* public                  */
  --space-7: 72px;                           /* public                  */
  --space-8: 96px;                           /* public — see note below */

  /* Section rhythm. The app's two are on the scale; the public site's
     upper value is not, which is a small disagreement with the document:
     §6 says "three more" steps for the public site but then names four
     distinct public values (32 / 48 / 72 / 96). 96 is 2x48 and is kept as
     --space-8 rather than written inline, because an inline 96 is exactly
     the ninth margin value the rule exists to prevent. */
  --rhythm-app: var(--space-5);              /* 32px */
  --rhythm-app-lg: 40px;                     /* the one app value off-scale */
  --rhythm-public: var(--space-7);           /* 72px */
  --rhythm-public-lg: var(--space-8);        /* 96px */

  /* Gutters are their own tokens, not scale steps: a gutter answers to the
     viewport and the scale answers to the content. The narrow values are
     the ones that matter — the app has *no* rule below 560px today, which
     is why its pitch view renders two cards wide on a phone. */
  --gutter-app: 24px;
  --gutter-app-narrow: 16px;                 /* <=560px */
  --gutter-public: 44px;
  --gutter-public-narrow: 20px;              /* <=560px */

  --pad-card: var(--space-3);                /* 16px, app  */
  --pad-card-public: var(--space-4);         /* 24px, site */

  /* **Touch targets are 44px minimum below 560px** (§9). It is a token
     because it is a floor, and a floor written inline is a floor somebody
     rounds down. */
  --touch-min: 44px;

  /* ---- Geometry and motif (§5) ------------------------------------
   *
   * **Square corners everywhere. No border radius, no shadows.** Depth
   * comes from ground changes and hairlines. --radius exists and is 0 so
   * that a call site asking for a radius gets the system's answer rather
   * than inventing one.
   */
  --radius: 0;

  /* Pointy-top. **height must be width x 1.1547 or it stops being
     regular** — a 1:1 hexagon is a visibly squashed hexagon and the
     wordmark is where it shows. **Never put a border on a clipped
     hexagon**: clip-path cuts the border on every diagonal and it renders
     as two vertical bars. If you need an outline, draw an SVG. */
  --hex-clip: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  --hex-ratio: 1.1547;

  /* **At most one texture per screen, and never behind a table or a
     chart.** Texture you notice on the second look, not decoration
     competing on the first.
     Honeycomb: light sections that are mostly type. ~5.5% stroke, which is
     the difference between a texture and a pattern. */
  --texture-honeycomb-alpha: 0.055;

  /* Netting: **the dark panel only**, two out-of-phase layers, squares
     kept square and orthogonal. **A 45deg version reads as a fence, not a
     net** — the orthogonal grid is doing the recognition work. */
  --texture-net-size: 22px;
  --texture-net-alpha: 0.1;

  /* Touchline: a section rule where a hairline is too quiet. **The uneven
     alpha is the point** — worn paint, not a border. A clean 3px line is
     just a thick border and reads as a mistake.

     **This is an alpha profile, not a colour.** §5 describes the touchline as
     a white gradient, and white is right on the dark panel and invisible on
     paper — the public site's footer rule disappeared entirely. So the token
     carries only the wear, the element carries the ink through a mask, and
     the same rule works on either ground. The colour stops are black here
     purely because a mask reads alpha and ignores everything else. */
  --texture-touchline: linear-gradient(
    90deg,
    rgba(0, 0, 0, 0.5) 0%,
    rgba(0, 0, 0, 0.85) 7%,
    rgba(0, 0, 0, 0.6) 19%,
    rgba(0, 0, 0, 0.9) 34%,
    rgba(0, 0, 0, 0.55) 46%,
    rgba(0, 0, 0, 0.88) 61%,
    rgba(0, 0, 0, 0.62) 78%,
    rgba(0, 0, 0, 0.8) 90%,
    rgba(0, 0, 0, 0.5) 100%
  );

  /* ---- Motion (§8) ------------------------------------------------
   *
   * Almost none, and all of it ambient. **The netting sway is the only
   * continuous animation in the product** — two out-of-phase loops, long
   * enough that it is never noticed directly. Out-of-phase is what stops
   * the two layers beating against each other into a visible pulse.
   *
   * **Data never animates on load.** A chart that draws itself is a chart
   * you cannot read for 400ms, and this product is read in a hurry on a
   * Friday. No lift, no shadow, no transform on hover — buttons darken by
   * about `calc(l - .06)` and secondary fills to ink-6, and that is all.
   */
  --sway-a: 16s;
  --sway-b: 22s;
  --ease: ease-in-out;
  --motion-ui: 120ms;
}

/* Honour it properly: pause the netting, remove every transition. A
   reduced-motion setting that leaves a 16s loop running has not been
   honoured, it has been acknowledged. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --sway-a: 0s;
    --sway-b: 0s;
    --motion-ui: 0s;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/*
 * Breakpoints are 560px and 900px (§9), and they are written out at every
 * media query rather than tokenised, because **a custom property cannot be
 * used in a media query condition** — `@media (max-width: var(--bp))` does
 * not work in any browser and fails silently, which is the worst way for a
 * breakpoint to fail. Two numbers, memorised, and this comment is where
 * they are written down.
 */
