/* ==========================================================================
   tokens.css — the ONLY file a fork repaints.

   Palette, type, rhythm and the theme/mode blocks live here. Nothing below
   this file knows a colour name or a font family: main.css and the markup
   speak in tokens only.

   HOW A TOKEN IS DECLARED — the trailing CSS comment on the token's own line
   is machine-read by palette.py:

       --ua-<name>: <opaque hex>;   [[ <role> · on: <surface>=<ratio> … ]]

   where [[ … ]] stands for an ordinary CSS comment. It is written that way
   here, and not literally, because CSS COMMENTS DO NOT NEST: a literal comment
   inside this header terminates it, the whole :root block is then parsed as
   one malformed rule and dropped, and the stylesheet loads with 200 OK and
   ZERO rules (validate.py, output check `css_parses`, exists because of it).

   role      `surface` | `DECOR ONLY` | anything else, which means TEXT.
             Fail closed: a token with no role is checked as text against
             every declared surface. DECOR ONLY exempts a token from contrast
             and simultaneously FORBIDS it from carrying text.
   on:       every surface this text token may sit on, each with its ratio
             COMPUTED BY THE WCAG FORMULA (python3 palette.py). A comment that
             claims a pass where the computation fails is an error (Л27).

   TRAP — HEX ONLY IN THE :root BLOCK. palette.py reads every `--ua-*: #hex`
   line in this file with no notion of blocks, last one wins. So the dark
   theme below is written in ALIASES ONLY — `--ua-paper: var(--ua-night)` —
   and every hex it needs is declared once, here, with its own `on:` list
   (test_palette.py: test_hex_only_in_root_block,
   test_dark_aliases_keep_their_contrast_pairs).

   TRAP — alpha is refused, not ignored: every token here is opaque.
   Translucency belongs to gradients in main.css, never to a token.

   TRAP — a text token can pass AA on a light ground or on a dark one, never
   on both. Tokens for dark grounds pin themselves with `on: deep=…`.
   ========================================================================== */

/* ---- webfont: ONE variable grotesque, Latin + Cyrillic (24.09.2026) -------
   Geist, self-hosted, subset by tools/media/subset_fonts.py (SOURCES.md).
   Display and text are the same face at two weights (Л28: a strict grotesque
   reads expensive, a rounded one cheap); figures that stand in columns turn
   on `tnum`. The old serif + Plex set was Latin-only and painted /ru/ in
   Georgia and Arial. The fallback is metric-matched (font_metrics.py), so the
   swap repaints without a layout shift. */
@font-face { font-family: "Geist"; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url("/fonts/geist-latin.woff2?v=74491a74") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+20BD, U+2122, U+2190-2193, U+2212, U+2215, U+2248, U+FEFF, U+FFFD; }
@font-face { font-family: "Geist"; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url("/fonts/geist-cyrillic.woff2?v=ff36ba2a") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
@font-face { font-family: "Geist Fallback"; src: local("Arial"); ascent-override: 100.26%; descent-override: 29.43%; line-gap-override: 0%; size-adjust: 100.24%; }

:root {
  /* ---- surfaces: every ground the site paints, declared ---------------- */
  /* A silently excluded background is a surface on which nobody computed
     the contrast. If you add a background colour anywhere, add it here. */
  --ua-paper:       #FAF8F4;  /* surface · page ground */
  --ua-card:        #FFFFFF;  /* surface · tile, form panel, raised card */
  --ua-sand:        #F1ECE3;  /* surface · alternating band */
  --ua-deep:        #0E1A18;  /* surface · dark ground: the veil under a hero frame, footer, sticky bar */
  --ua-accent-deep: #153F39;  /* surface · primary button hover and active ground */
  --ua-night:       #0F1B19;  /* surface · dark theme: page ground */
  --ua-night-card:  #172622;  /* surface · dark theme: card, tile, form panel */
  --ua-night-inset: #0A1412;  /* surface · dark theme: the inset the sand band becomes */

  /* ---- text on the light grounds -------------------------------------- */
  --ua-ink:      #14181B;   /* text · on: paper=16.83 card=17.85 sand=15.17 */
  --ua-ink-soft: #50565A;   /* text · on: paper=7.02 card=7.45 sand=6.33 */
  --ua-accent:   #1F5F57;   /* text · on: paper=6.99 card=7.41 sand=6.30 */
  --ua-warm:     #7A5A2E;   /* text · on: paper=5.95 card=6.31 sand=5.36 */

  /* ---- text on the dark grounds, both themes -------------------------- */
  --ua-on-deep:      #F7F4EE;  /* text · on: deep=16.22 accent-deep=10.62 night=16.06 night-card=14.31 night-inset=17.05 */
  --ua-on-deep-soft: #D2CCBF;  /* text · on: deep=11.13 accent-deep=7.29 night=11.02 night-card=9.82 night-inset=11.71 */
  --ua-on-deep-lit:  #8FCBBB;  /* text · on: deep=9.69 accent-deep=6.34 night=9.59 night-card=8.55 night-inset=10.19 */

  /* ---- text of the dark theme: what ink, ink-soft and warm become ------ */
  --ua-bone:      #EEEAE0;  /* text · on: night=14.68 night-card=13.08 night-inset=15.58 */
  --ua-bone-soft: #B9C0B8;  /* text · on: night=9.48 night-card=8.45 night-inset=10.07 */
  --ua-warm-lit:  #D6B47E;  /* text · on: night=8.97 night-card=8.00 night-inset=9.53 */

  /* ---- decoration: hairlines, rules, motifs, plan line-work ------------ */
  /* DECOR ONLY is not a note. palette.py reads main.css and fails the build
     if any of these ever ends up in a `color:` declaration. */
  --ua-brass:      #B08A55;  /* DECOR ONLY · accent rule, source dot, motif stroke, plan line-work (2.99 on paper, 5.55 on night) */
  --ua-rule:       #E4DED2;  /* DECOR ONLY · rules and borders (1.26 on paper) */
  --ua-mist:       #EDE8DE;  /* DECOR ONLY · empty-frame fill, bar track (1.15 on paper) */
  --ua-rule-night: #2A3934;  /* DECOR ONLY · rules and borders in the dark theme (1.46 on night) */
  --ua-mist-night: #1C2B27;  /* DECOR ONLY · decoration fill in the dark theme (1.20 on night) */

  /* ---- type ------------------------------------------------------------
     One family at two weights. Display tracking is NEGATIVE because this is
     a grotesque — the Moscow mistake was the reverse transfer, a serif's
     zero carried onto a grotesque (Л28). The family is named here and
     nowhere else. */
  --ua-font-display: "Geist", "Geist Fallback", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  --ua-font-text: var(--ua-font-display);
  --ua-font-mono: var(--ua-font-display);
  --ua-weight-display: 620;
  --ua-track-display: -0.025em;
  --ua-track-label: 0.07em;
  --ua-lead: 1.6;
  --ua-lead-display: 1.06;

  --ua-step-0: clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
  --ua-step-1: clamp(1.1rem, 1.02rem + 0.4vw, 1.3rem);
  --ua-step-2: clamp(1.3rem, 1.15rem + 0.75vw, 1.625rem);
  --ua-step-3: clamp(1.7rem, 1.3rem + 1.9vw, 2.5rem);      /* h2: 40 px at 1280 */
  --ua-step-4: clamp(2.3rem, 1.4rem + 4vw, 4.25rem);       /* h1 */
  --ua-step-index: clamp(1.35rem, 1.1rem + 1.2vw, 2rem);   /* a figure in a ledger or tile */
  --ua-step-mono: 0.78rem;                                  /* labels, eyebrows, source lines */
  --ua-step-small: 0.9375rem;   /* the page's footnote size — the legal block
                                   is never allowed below this (see main.css) */

  /* ---- rhythm ---------------------------------------------------------- */
  --ua-gap: clamp(1rem, 0.8rem + 1vw, 1.75rem);
  --ua-section: clamp(2.5rem, 1.25rem + 2.4vw, 4.5rem);
  --ua-measure: 33em;   /* ~75 characters of Geist; its «0» is wide, so 64ch set ~90 */
  --ua-page: 80rem;
  --ua-toc-w: 0rem;
  --ua-radius: 6px;
  --ua-radius-lg: 12px;
  --ua-sticky-h: 4rem;   /* reserved BEFORE the bar exists — CLS 0 */
  --ua-ease: cubic-bezier(0.22, 0.61, 0.36, 1);

  /* ---- motif ------------------------------------------------------------
     Favicon, wordmark, the «you are here» mark on every plan, and the honest
     stand-in wherever a page has no photograph of its own (Л25). */
  --ua-motif: url("/img/motif.svg?v=d1f6c78d");
}

/* ---- dark theme: ALIASES ONLY (see the header) ---------------------------
   Designed, not inverted: the page ground is the network's night green, cards
   lift one step, the sand band becomes a deeper inset. Brass stays brass. */
:root[data-ua-theme="dark"] {
  color-scheme: dark;
  --ua-paper: var(--ua-night);
  --ua-card: var(--ua-night-card);
  --ua-sand: var(--ua-night-inset);
  --ua-deep: var(--ua-night-card);
  --ua-ink: var(--ua-bone);
  --ua-ink-soft: var(--ua-bone-soft);
  --ua-accent: var(--ua-on-deep-lit);
  --ua-warm: var(--ua-warm-lit);
  --ua-rule: var(--ua-rule-night);
  --ua-mist: var(--ua-mist-night);
}

/* ---- brief mode: composition only, never a colour ------------------------
   The reading mode for people who scan. Same HTML, same palette; the type
   compresses, the rhythm halves and the measure widens. Everything else
   brief does is a layout rule in main.css keyed on the same attribute. */
:root[data-ua-mode="brief"] {
  --ua-weight-display: 600;
  --ua-track-display: -0.01em;
  --ua-track-label: 0.06em;
  --ua-lead: 1.45;
  --ua-lead-display: 1.15;
  --ua-step-0: 0.95rem;
  --ua-step-1: 1.05rem;
  --ua-step-2: 1.2rem;
  --ua-step-3: 1.35rem;
  --ua-step-4: 1.6rem;
  --ua-step-index: 1.05rem;
  --ua-step-mono: 0.75rem;
  --ua-section: clamp(1.25rem, 0.75rem + 1vw, 1.75rem);
  --ua-gap: 0.75rem;
  --ua-measure: 70ch;
}
@media (min-width: 1100px) {
  :root[data-ua-mode="brief"] { --ua-toc-w: 13rem; }
}
