/* --- Fonts --------------------------------------------------- */
@font-face { font-family:"Fira Code"; src:url("/fonts/FiraCode-Light.woff2") format("woff2"); font-weight:300; font-display:swap; }
@font-face { font-family:"Fira Code"; src:url("/fonts/FiraCode-Regular.woff2") format("woff2"); font-weight:400; font-display:swap; }
@font-face { font-family:"Fira Code"; src:url("/fonts/FiraCode-Medium.woff2") format("woff2"); font-weight:500; font-display:swap; }
@font-face { font-family:"Fira Code"; src:url("/fonts/FiraCode-SemiBold.woff2") format("woff2"); font-weight:600; font-display:swap; }
@font-face { font-family:"Fira Code"; src:url("/fonts/FiraCode-Bold.woff2") format("woff2"); font-weight:700; font-display:swap; }
@font-face { font-family:"Fira Mono"; src:url("/fonts/FiraMono-Regular.woff2") format("woff2"); font-weight:400; font-display:swap; }
@font-face { font-family:"Fira Mono"; src:url("/fonts/FiraMono-Medium.woff2") format("woff2"); font-weight:500; font-display:swap; }
@font-face { font-family:"Fira Mono"; src:url("/fonts/FiraMono-Bold.woff2") format("woff2"); font-weight:700; font-display:swap; }
@font-face {
  font-family: "Amarna";
  src: url("/fonts/Amarna-VariableFont_wght.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
/* DSEG14 Classic Mini — triathlon.tech wordmark (and any brand-face--dseg).
   Face is already slanted; declare normal/normal so WebKit matches. */
@font-face {
  font-family: "DSEG14 Classic Mini";
  src:
    url("/fonts/DSEG14ClassicMini-BoldItalic.woff2") format("woff2"),
    url("/fonts/DSEG14ClassicMini-BoldItalic.woff") format("woff"),
    url("/fonts/DSEG14ClassicMini-BoldItalic.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: block;
}
/* DSEG7 Modern Mini — the 7-segment LCD digits (private line boot count, and the
   studio-seat register count). Slant is baked in; declare normal so WebKit matches. */
@font-face {
  font-family: "DSEG7 Modern Mini";
  src: url("/fonts/DSEG7ModernMini-BoldItalic.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: block;
}
/* DSEG14 Modern Mini — the same Modern LCD digits as DSEG7, but 14-segment so it
   carries a diagonal "/". Used where an LCD figure needs the slash (studio-seat
   card register: 001/333). Slant is baked in; declare normal so WebKit matches. */
@font-face {
  font-family: "DSEG14 Modern Mini";
  src:
    url("/fonts/DSEG14ModernMini-BoldItalic.woff2") format("woff2"),
    url("/fonts/DSEG14ModernMini-BoldItalic.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: block;
}

/* --- Brand wordmarks (data-brand-name system) -------------------
   Single source: surfaces-nav.js BRAND_WORDMARKS. Pages mark slots with
   data-brand-name="triathlon" (etc.); mountBrandNames() fills text + face.
   Edit the registry once — every lockup, menu label face, and page title
   that uses the attribute stays in step. */
.brand-name {
  margin: 0;
  font-weight: 500;
  line-height: 1;
  color: var(--ink);
  white-space: nowrap;
}
/* Face classes win over page .brand-name font-family (higher specificity). */
.brand-name.brand-face--fira,
.brand-face--fira {
  font-family: "Fira Code", ui-monospace, monospace;
  letter-spacing: 0;
  text-transform: none;
}
.brand-name.brand-face--fira-caps,
.brand-face--fira-caps {
  font-family: "Fira Code", ui-monospace, monospace;
  letter-spacing: 0;
  text-transform: lowercase;
}
.brand-name.brand-face--amama,
.brand-face--amama {
  font-family: "Amarna", "EB Garamond", ui-serif, Georgia, serif;
  letter-spacing: var(--track-wordmark);
  text-transform: none;
}
.brand-name.brand-face--dseg14-classic-mini,
.brand-face--dseg14-classic-mini,
h2.brand-face--dseg14-classic-mini {
  font-family: "DSEG14 Classic Mini", "Fira Code", ui-monospace, monospace;
  font-style: normal;
  font-weight: 500;
  font-synthesis: none;
  letter-spacing: var(--track-title);
  text-transform: none;
  -webkit-font-smoothing: antialiased;
}

/* --- Tokens — LOCKED --------------------------------------------
   These values are the brand. Do NOT change, "correct", or re-derive any
   of them without explicit instruction.
   Mirrors (DESIGN.md, data/design-tokens.json, design skill) follow THIS
   block — sync them when a change is authorised, never the reverse. */
/* The vanta guard: on the light ground NO dark ink
   ever sits translucent. Text recedes ride the ink ladder (all-black in
   light, above); glyph hardware keeps its dim-at-rest language on the dark
   ground only — in light every mark and the receded nav mark pin solid. */
html.is-light .tabgrid-glyph,
html.is-light .tabgrid-cell svg[data-wx="all-segments"].tabgrid-glyph,
html.is-light .top.is-open .brand-btn,
html.is-light .top.is-open .brand-btn:hover {
  opacity: 1;
}
/* Type ramp + tracking: /shell/tokens.css (--t-*, --track-*). --t-body is
   also the live control the palette system adjusts (surfaces-nav.js fontBody). */

/* --- Base ---------------------------------------------------- */
* { box-sizing: border-box; }
/* The ground rides on HTML only: the html element paints the canvas — behind
   the field, in the overscroll rubber-band, and before body lays down — so a
   fresh window never shows a bare white gap, and the stars are not buried
   under an opaque body ground. */
html, body { margin: 0; min-height: 100%; overflow: visible; }
html { background: var(--g); }
/* Ground on html only. Body stays transparent. Never isolation:isolate on
   html: that compositor plate in WebKit clips the document to the viewport
   so .site-footer cannot be scrolled to (William, 2026-08-27). Never
   z-index -1 on the field: WKWebView paints that behind the iframe backdrop.
   Never mix-blend on #shellField: plus-lighter of an opaque canvas zeroes
   the stars. min-height, never height: 100%. A height-locked root inside
   the Desk iframe (and WKWebView live) has no scrollport, so the footer
   sits below the fold. Do not overflow-x clip html, body, or .shell: clip
   on a locked-height root computes overflow-y to clip as well. Do not give
   .shell a z-index: that full-page stacking context is a compositor plate
   over the stars. Card rails shrink via min-width 0 on .surface. */
/* Dark by default — declare the scheme so a new window/tab paints a DARK canvas on
   the very first frame instead of the browser's white default (the ground CSS
   above applies a beat later in the @import cascade, so without this a fresh window
   flashes white). Light mode (html.is-light) flips it. (William bug, 2026-08-12.) */
:root { color-scheme: dark; }
html.is-light { color-scheme: light; }
html { scroll-behavior: smooth; }   /* lifted from pages; honours reduced-motion below */
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
/* ── Browser scrollbar — a slim, moody dark-gradient thumb that recedes into the
   ground; the track stays invisible. Firefox gets the thin dark thumb (no
   gradient there). The swipe rails hide their own bars via more-specific rules,
   so this only dresses the page + panel scrollbars. (William, 2026-08-02) */
html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--ink) 18%, var(--vanta)) transparent; }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
  border: 3px solid transparent;                 /* transparent border insets the thumb so it reads slim */
  background: linear-gradient(180deg, color-mix(in srgb, var(--ink) 22%, var(--vanta)), var(--unlit));
  background-clip: padding-box;
  border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
  background: linear-gradient(180deg, color-mix(in srgb, var(--ink) 34%, var(--vanta)), color-mix(in srgb, var(--ink) 10%, var(--vanta)));
  background-clip: padding-box;
}
body {
  position: relative;
  background: transparent;
  color: var(--ink);
  font-family: "Fira Code", ui-monospace, monospace;
  font-size: var(--t-body);
  line-height: 1.55;
  word-spacing: -0.18em; /* page-wide: tight between words */
  -webkit-font-smoothing: antialiased;
}
/* Explicit on headings so titles pick it up even if a rule resets spacing */
h1, h2, h3, h4, .brand-word {
  word-spacing: -0.28em;
}
/* --- Heading system — the house hierarchy ---------------------------
   Semantic defaults: use the element and you get the system. Space
   between headings comes from layout gaps, never heading margins.
   H1 natural casing, tight track. H2–H3 lowercase titles at 0.
   H4 is the eyebrow-size heading (--track-eyebrow). 500 is the house
   heavy — never 600/700 in UI. Pages may size a heading UP a rung with
   a token, never with raw px. */
h1 {
  margin: 0;
  font-size: var(--t-h1);
  font-weight: 500;
  letter-spacing: var(--track-tight);
  line-height: 1.25;
  color: var(--ink);
}
h2 {
  margin: 0;
  font-size: var(--t-h2);
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.3;
  text-transform: lowercase;
  color: var(--ink);
}
h3 {
  margin: 0;
  font-size: var(--t-h3);
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.35;
  text-transform: lowercase;
  color: var(--ink);
}
h4 {
  margin: 0;
  font-size: var(--t-h4);
  font-weight: 400;
  letter-spacing: var(--track-eyebrow);
  line-height: 1.4;
  text-transform: lowercase;
  color: var(--ink-3);
}
/* An acronym inside a title stays uppercase and does not take eyebrow
   tracking (William, 2026-08-26). Beats h2/h3 lowercase. */
.rs-acronym {
  text-transform: uppercase;
  letter-spacing: 0;
}
:is(h1, h2, h3, h4) .rs-acronym,
.news-title .rs-acronym,
.paper-subtitle .rs-acronym,
.shc-name .rs-acronym,
.hero-sentence .rs-acronym {
  text-transform: uppercase;
  letter-spacing: 0;
}
button, textarea, input { font: inherit; color: inherit; }
button { cursor: pointer; }
a { color: inherit; text-decoration: none; }
::selection { background: rgba(0, 157, 255, 0.38); color: var(--ink); }

/* --- The page grid — Swiss, 8 segments ------------------------
   The wordmark is built on module = cap/8; pages follow the same grammar.
   Horizontal: content aligns to 8 equal columns (.grid-8) inside the page
   measure — spans of 2/4/8 are the house rhythm (no thirds).
   Vertical posters: zone rows — grid-template-rows alternating 1fr (free
   band) and auto (fixed cluster), align-items center — so every floating
   line sits EXACTLY halfway between its fixed neighbours. Never position
   poster elements with magic offsets. */
.grid-8 {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 0 20px;
}

/* ── Optical wrapping, app-wide ──────────────────────────────────────────
   Running copy re-rags so a lone word never sits on the last line
   (William, 2026-08-05: "us." alone made no sense). pretty is the right
   tool where the engine has it (Chrome; newer Safari); THIS app's WKWebView
   only has balance, so display copy that must always rag well carries
   .rs-balance (or is listed here) as the working fallback. Headings balance
   everywhere. */
p, li, dd, figcaption, blockquote { text-wrap: pretty; }
h1, h2, h3 { text-wrap: balance; }
.rs-balance, .cloud-copy, .page-hero-statement { text-wrap: balance; }
/* Hand-set lines beat the machine: copy that carries its own br must not be
   re-ragged by the balancer (it splits the forced segments into extra lines).
   Any balanced element holding a br falls back to plain wrapping. */
.rs-balance:has(br), .cloud-copy:has(br), .page-hero-statement:has(br),
h1:has(br), h2:has(br), h3:has(br) { text-wrap: initial; }
