/* ═══════════════════════════════════════════════════════════════════════════
   GRIDLOCK — industrial system.
   Brushed steel and dark concrete, near-black type, one hot safety orange.
   Implements GRIDLOCK-BRAND.md. Every number below is from that spec; the
   contrast ratios in the comments are measured, not estimated.

   Two rules are load-bearing and easy to break later:
     1. Orange NEVER sits as type on steel (1.13:1 — invisible). On steel it
        only appears inside a solid ink block. Encoded here as .gl-tag and
        .ink-slab, not left to memory.
     2. Radius is 0. Everywhere. Square corners are the industrial read.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 0. Typefaces, served from this origin ────────────────────────────────
   Anton, Archivo and Space Mono, SIL Open Font License 1.1, which expressly
   permits hosting them ourselves.

   Not a micro-optimisation. Going through Google Fonts meant a stylesheet
   from googleapis.com and then the faces from gstatic.com: two extra DNS
   lookups and TLS handshakes standing between the visitor and the first
   line of real type, on a page whose own HTML, CSS and JS total 18 KB. The
   faces were four times the weight of the site they were setting.

   unicode-range is kept exactly as Google served it, so a visitor reading
   plain English still fetches only the five latin files. The latin-ext half
   exists because the readout and the release rail carry real titles and real
   names from the catalogue, and a diacritic must not fall back mid-word.

   Archivo 500 and 700 are gone. They were being requested and never rendered
   — the browser reported both faces unloaded on the live page. */
@font-face {
  font-family: 'Anton';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/anton-400-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: 'Anton';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/anton-400-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: 'Archivo';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/archivo-400-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: 400;
  font-display: swap;
  src: url('/assets/fonts/archivo-400-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: 'Archivo';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/archivo-600-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: 600;
  font-display: swap;
  src: url('/assets/fonts/archivo-600-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: 'Space Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/space-mono-400-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: 'Space Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/space-mono-400-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: 'Space Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/space-mono-700-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: 'Space Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/space-mono-700-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;
}

/* ── 1. Tokens ─────────────────────────────────────────────────────────── */
:root {
  /* Core */
  --gl-orange:        #FF5A14;  /* the only accent. Never introduce a second. */
  --gl-ink:           #17181A;  /* near-black. Type on steel, ground on concrete. */

  /* Steel surface (light) */
  --gl-steel-light:   #9CA7B2;
  --gl-steel:         #7E8A96;
  --gl-steel-dark:    #74808C;

  /* Concrete surface (dark) */
  --gl-concrete:      #17181A;
  --gl-text-dark-bg:  #C7D0D8;
  --gl-muted-dark-bg: #99A4AF;

  /* Derived */
  --gl-ink-soft:      rgba(23, 24, 26, 0.68);
  --gl-hairline:      rgba(23, 24, 26, 0.26);
  --gl-hairline-dark: rgba(199, 208, 216, 0.24);
  --gl-panel:         rgba(23, 24, 26, 0.07);
  --gl-panel-dark:    rgba(199, 208, 216, 0.05);

  /* Families */
  --gl-display: 'Anton', Impact, Haettenschweiler, 'Arial Narrow', sans-serif;
  --gl-mono:    'Space Mono', 'DejaVu Sans Mono', 'Courier New', monospace;
  --gl-body:    'Archivo', 'Helvetica Neue', Arial, sans-serif;

  /* Spacing scale — 4 8 12 16 24 32 48 64 96 152 */
  --s4: 4px;   --s8: 8px;   --s12: 12px; --s16: 16px; --s24: 24px;
  --s32: 32px; --s48: 48px; --s64: 64px; --s96: 96px; --s152: 152px;

  --nav-h: 72px;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  font-family: var(--gl-body);
  font-size: 18px;
  line-height: 1.5;
  background: var(--gl-concrete);
  color: var(--gl-text-dark-bg);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

/* Radius 0 is load-bearing. Belt and braces against a stray UA style. */
button, input, textarea, select, img, a, div, section { border-radius: 0; }

/* ── 2. Surfaces ───────────────────────────────────────────────────────────
   One ground, plus orange where a section should shout. The steel gradient,
   its flattened variant and the brushed grain that came with them are gone:
   steel is a text family now, and dead surface CSS is how a retired system
   quietly comes back. See docs/BRAND.md §1. */

.gl-concrete { background-color: #17181A; color: #C7D0D8; }

/* THE BLOOM MARKS A PAGE OPENING, AND NOTHING ELSE.
   It is a light source above the top edge. It used to live on .gl-concrete,
   which put it on five sections, so every dark band announced itself as a
   new page top — worst under the ink readout, where band-then-glow-then-
   heading read as a second homepage starting. Two things are page openings:
   the hero, and the 404. Nothing else may claim it. */
.has-bloom { background-image: var(--bloom); }
:root {
  --bloom: radial-gradient(130% 80% at 50% -10%,
             rgba(255,90,20,0.11) 0%, rgba(23,24,26,0) 62%);
  --tooth: repeating-linear-gradient(0deg,
             rgba(255,255,255,0.018) 0px, rgba(255,255,255,0.018) 1px,
             rgba(0,0,0,0) 1px,           rgba(0,0,0,0) 4px);
}
@media (min-resolution: 2dppx) {
  .gl-concrete { background-image: var(--tooth); }
  .has-bloom   { background-image: var(--bloom), var(--tooth); }
}

/* ── 3. Layout ─────────────────────────────────────────────────────────── */
.wrap {
  width: 100%;
  max-width: 1472px;          /* 1280 content + 96 page margin each side */
  margin-inline: auto;
  padding-inline: clamp(24px, 6vw, 96px);
}

section { position: relative; }
/* Asymmetric on purpose: generous above a heading, tighter below a section.
   Symmetrical 130px meant 260px of nothing at every boundary. */
.band { padding-block: clamp(56px, 7vw, 112px) clamp(40px, 5vw, 80px); }

/* ── 4. Typography ─────────────────────────────────────────────────────── */
.d1, .d2, .d3 {
  font-family: var(--gl-display);
  font-weight: 400;
  text-transform: uppercase;      /* structural headings only — never a name */
  letter-spacing: 0.005em;
  line-height: 0.88;
  margin: 0;
}
/* Anton is narrower than Impact / Arial Narrow. These clamps are sized so the
   line still fits in the FALLBACK face, which is what renders during swap and
   whenever Google Fonts is blocked. */
.d1 { font-size: clamp(44px, 11.5vw, 132px); }
.d2 { font-size: clamp(34px, 6vw,   76px); }
.d3 { font-size: clamp(28px, 4.2vw, 46px); }

h3, .h3 {
  font-family: var(--gl-body); font-weight: 600;
  font-size: clamp(20px, 2vw, 26px); line-height: 1.25;
  letter-spacing: -0.005em; margin: 0;
}

p { margin: 0 0 var(--s16); }
.lede { font-size: clamp(17px, 1.5vw, 19px); line-height: 1.5; }

/* Space Mono does the industrial work. Uppercase, tracked, 700. */
.label {
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase;
  margin: 0;
}
@media (min-width: 768px) { .label { font-size: 14px; } }

/* A proper noun keeps its own casing. ALiV has a lowercase i on purpose;
   uppercasing someone's name is a typo in their brand, not a style. */
.verbatim { text-transform: none; letter-spacing: 0.1em; }

.caption { font-size: 14px; line-height: 1.45; }

/* ── 5. The orange rule, encoded ───────────────────────────────────────────
   On concrete, orange runs free. On steel it only ever appears inside a solid
   ink block. Both directions below measure 5.69:1.                          */
.gl-tag {
  display: inline-block;
  background: var(--gl-ink); color: var(--gl-orange);
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  padding: 6px 10px 5px;
}
:is(.gl-concrete, .on-dark) .gl-tag { background: var(--gl-orange); color: var(--gl-ink); }

/* Orange type, safe on either surface: on concrete it sits on the ground, on
   steel it is wrapped in ink. Use .accent inside concrete only. */
.accent { color: var(--gl-orange); }

/* A display-size ink slab — how orange gets to be loud on a steel section. */
.ink-slab {
  background: var(--gl-ink); color: var(--gl-orange);
  display: inline-block; padding: 0.02em 0.12em 0.08em;
}

/* ── 6. Rules and dividers ─────────────────────────────────────────────── */
.rule-3   { border: 0; border-top: 3px solid var(--gl-ink); margin: 0; }
:is(.gl-concrete, .on-dark) .rule-3 { border-top-color: var(--gl-text-dark-bg); }
.rule-hair { border: 0; border-top: 2px solid var(--gl-hairline); margin: 0; }
:is(.gl-concrete, .on-dark) .rule-hair { border-top-color: var(--gl-hairline-dark); }

/* Header rail — two mono labels, baseline-aligned, pushed apart. */
.rail {
  --rule: var(--gl-ink);
  position: relative;
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s24); padding-top: calc(var(--s12) + 6px);
  border-top: 0;
  margin-bottom: var(--s32);
}
/* THE DIVIDER IS A BAND OF HAZARD STRIPES, page-wide, and it does not move.
   The roster cards' belt turned out to be the page's divider language, so
   every section rail takes the same stripe as a static texture: 6px, the
   same 45° / 5-on-7-off period as the belt, in --rule so it is ink on the
   orange ground and orange on every dark one. Static on purpose — motion on
   this page marks where something is live (the roster, the footer's edge),
   and a rule that crawled under every heading would be noise within a
   minute. The one-time draw-in it always had stays; see §26. Under a rail
   the labels sit 6px lower than they did, which is the band's height. */
.rail::before {
  content: '';
  position: absolute; left: 0; right: 0; top: 0;
  height: 6px;
  background: repeating-linear-gradient(-45deg, var(--rule) 0 5px, transparent 5px 12px);
}
:is(.gl-concrete, .on-dark) .rail { --rule: var(--gl-orange); }
.rail .r-left  { color: var(--gl-ink); }
:is(.gl-concrete, .on-dark) .rail .r-left { color: var(--gl-orange); }
.rail .r-right { color: var(--gl-ink-soft); text-align: right; }
:is(.gl-concrete, .on-dark) .rail .r-right { color: var(--gl-muted-dark-bg); }

/* ── 7. Buttons ────────────────────────────────────────────────────────── */
.gl-btn-primary, .gl-btn-secondary {
  display: inline-flex; align-items: center; gap: 10px;
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 15px 24px; border-radius: 0; text-decoration: none;
  cursor: pointer; transition: background .18s linear, color .18s linear, border-color .18s linear;
}
.gl-btn-primary  { background: var(--gl-ink); color: var(--gl-orange); border: 2px solid var(--gl-ink); }
:is(.gl-concrete, .on-dark) .gl-btn-primary { background: var(--gl-orange); color: var(--gl-ink); border-color: var(--gl-orange); }
.gl-btn-primary:hover { background: transparent; color: var(--gl-ink); }
:is(.gl-concrete, .on-dark) .gl-btn-primary:hover { background: transparent; color: var(--gl-orange); }

.gl-btn-secondary { background: transparent; color: var(--gl-ink); border: 2px solid var(--gl-ink); }
:is(.gl-concrete, .on-dark) .gl-btn-secondary { color: var(--gl-text-dark-bg); border-color: var(--gl-hairline-dark); }
.gl-btn-secondary:hover { background: var(--gl-ink); color: var(--gl-steel-light); }
:is(.gl-concrete, .on-dark) .gl-btn-secondary:hover { background: var(--gl-text-dark-bg); color: var(--gl-ink); border-color: var(--gl-text-dark-bg); }

:where(a, button, input, textarea):focus-visible {
  outline: 3px solid var(--gl-orange); outline-offset: 2px;
}

/* ── 8. Icons — stroke only, 24 grid, inherit the surface ──────────────── */
.ico {
  width: 24px; height: 24px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.ico-lg { width: 32px; height: 32px; }

/* ── 9. Logo lockup ────────────────────────────────────────────────────────
   Aspect is 560x484 (1.157:1) — both dimensions are always set, or the mark
   stretches in a flex column. */
.lockup { display: inline-flex; align-items: center; gap: 12px; text-decoration: none; }
.lockup img { width: 54px; height: 47px; object-fit: contain; }
.lockup .word {
  font-family: var(--gl-display); font-size: 30px; line-height: 1;
  letter-spacing: 0.1em; text-transform: uppercase; color: currentColor;
}
.lockup--sm img { width: 40px; height: 35px; }
.lockup--sm .word { font-size: 22px; }

/* ── 10. Nav ───────────────────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: 60;
  height: var(--nav-h);
  display: flex; align-items: center; justify-content: space-between; gap: var(--s24);
  background: rgba(23,24,26,0.92);
  border-bottom: 2px solid var(--gl-hairline-dark);
  backdrop-filter: saturate(140%) blur(8px);
}
.nav .wrap { display: flex; align-items: center; justify-content: space-between; gap: var(--s24); }
.nav-links { display: none; gap: var(--s32); list-style: none; margin: 0; padding: 0; }
.nav-links a {
  font-family: var(--gl-mono); font-weight: 700; font-size: 13px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gl-text-dark-bg); text-decoration: none;
  padding-bottom: 3px; border-bottom: 2px solid transparent;
  transition: color .16s linear, border-color .16s linear;
}
.nav-links a:hover { color: var(--gl-orange); border-bottom-color: var(--gl-orange); }
.nav-right { display: flex; align-items: center; gap: var(--s24); }
.nav .gl-btn-primary { display: none; padding: 11px 18px; }
/* THE NAV HAS ITS OWN BREAKPOINT, and it is not 900 like the rest of the
   page. The bar carries a two-word lockup, five links and a CTA, and at 900
   that is over-stuffed: measured, adding the fifth link (Roster, 21 Sep
   2026) pushed the lockup onto two lines and squeezed "Work with us" from
   148px to 119px, which wrapped the button too. Everything still fit the
   viewport — nothing overflowed — so the only symptom was a bar that looked
   broken, which is the kind of failure a width check does not catch.
   1024 is where all seven items sat on one line each with room to spare;
   below it the burger and the sheet do the job, and the sheet carries the
   same links. The page's OTHER 900px rules — the Fig. 01 schematic and
   the philosophy grid — are about column space, not this bar, so they stay
   where they are.

   MOVED TO 1120 on 23 Sep 2026, when Growth became the sixth link.
   Measured in the real page with fonts loaded: at 1024 a sixth link
   reproduced the old failure exactly — the lockup wrapped from 35px to 44px
   tall, "Work with us" squeezed from 148px to 132px, and the space between
   lockup and links fell from 94px to 24px, with nothing overflowing. At
   1100 there was 79px left. 1120 buys about 100px, which is the margin the
   fallback faces need: Impact for the lockup and the monospace fallback for
   the links both render wider than the web fonts during the swap. Between
   1024 and 1119 the burger shows, which is already the designed state. */
@media (min-width: 1120px) {
  .nav-links { display: flex; }
  .nav .gl-btn-primary { display: inline-flex; }
}

.burger {
  display: inline-flex; flex-direction: column; justify-content: center; gap: 5px;
  width: 44px; height: 44px; padding: 0 10px;
  background: transparent; border: 0; cursor: pointer;
}
.burger span { display: block; height: 2px; background: var(--gl-text-dark-bg); }
@media (min-width: 1120px) { .burger { display: none; } }

.sheet {
  position: fixed; inset: 0; z-index: 70;
  display: none; flex-direction: column; justify-content: center; gap: var(--s24);
  padding: var(--s96) var(--s24) var(--s48);
  /* Six links and the button compute to about 626px, against 640-667px on a
     small phone held upright and far less held sideways. Plain centering
     plus scrolling would cut the TOP off an overflowing list and make it
     unreachable; `safe center` falls back to the start edge instead, and a
     browser without it keeps the line above. */
  justify-content: safe center;
  overflow-y: auto; overscroll-behavior: contain;
}
.sheet[data-open="true"] { display: flex; }
.sheet a {
  font-family: var(--gl-display); font-size: clamp(34px, 9vw, 54px); line-height: 1;
  letter-spacing: 0.02em; text-transform: uppercase;
  color: var(--gl-text-dark-bg); text-decoration: none;
  padding-bottom: var(--s12); border-bottom: 2px solid var(--gl-hairline-dark);
}
.sheet a:hover, .sheet a:focus-visible { color: var(--gl-orange); }
.sheet .gl-btn-primary { align-self: flex-start; font-size: 14px; }
.sheet-close {
  position: absolute; top: var(--s24); right: var(--s24);
  width: 44px; height: 44px; display: grid; place-items: center;
  background: transparent; border: 2px solid var(--gl-hairline-dark);
  color: var(--gl-text-dark-bg); cursor: pointer;
}

/* ── 11. Hero ──────────────────────────────────────────────────────────── */
.hero { padding-block: clamp(36px, 4.5vw, 64px) clamp(32px, 4vw, 56px); overflow: hidden; }
.hero-eyebrow { color: var(--gl-orange); margin-bottom: var(--s16); }
.hero-title {
  /* Reserve two lines so the Anton swap does not shove the page down. */
  min-height: calc(2 * 0.88em);
  margin-bottom: var(--s24);
}
.hero-sub { max-width: 58ch; color: var(--gl-text-dark-bg); margin-bottom: var(--s24); }
.hero-actions { display: flex; flex-wrap: wrap; gap: var(--s16); margin-bottom: clamp(32px, 4vw, 56px); }

/* ── 12. Schematic ─────────────────────────────────────────────────────────
   The org diagram, rebuilt as a technical drawing that carries a signal.

   Six columns on desktop; below 900px it becomes a left-spine list, because
   connector lines across a reflowed grid are the thing that broke the old
   hero on a phone. Both layouts get the same signal, so the idea survives
   the breakpoint even though the geometry does not.

   Three layers, in this order, and each one is optional:
     base      readable with no JS and no motion at all
     .drawn    a one-shot draw-in — lines grow, nodes arrive
     .is-live  the repeating pulse from the artist out to the branches

   Everything that starts hidden is gated behind html.js, so a visitor with
   no JavaScript gets the finished drawing rather than an empty box. The
   loop is removed when the figure scrolls out of view.                      */

.schema { border-top: 3px solid var(--gl-text-dark-bg); padding-top: var(--s16); --cyc: 4s; }
.schema-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s16); margin-bottom: var(--s20, 20px); }
.schema-head .r-left { color: var(--gl-orange); }
.schema-head .r-right { color: var(--gl-muted-dark-bg); }

.sch-root { display: flex; justify-content: center; }
.sch-root span {
  font-family: var(--gl-mono); font-weight: 700; font-size: 13px;
  letter-spacing: 0.18em; text-transform: uppercase;
  background: var(--gl-orange); color: var(--gl-ink); padding: 10px 20px;
}
.sch-drop { width: 2px; height: 20px; background: var(--gl-hairline-dark); margin: 0 auto; }
/* Half a column in from each edge: with 6 equal tracks that is 100%/12. */
.sch-bar { height: 2px; background: var(--gl-hairline-dark); margin: 0 calc(100% / 12); }
.sch-ticks { display: grid; grid-template-columns: repeat(6, 1fr); }
.sch-ticks i { display: block; width: 2px; height: 18px; background: var(--gl-hairline-dark); margin: 0 auto; }
.sch-ticks-2 i { height: 14px; }
.sch-nodes { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--s12); }
.sch-node {
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.12em; text-transform: uppercase; text-align: center;
  color: var(--gl-text-dark-bg);
  border: 2px solid var(--gl-hairline-dark); padding: 12px 6px;
}
.sch-node[data-accent] { border-color: var(--gl-orange); color: var(--gl-orange); }
.sch-subs { display: grid; grid-template-columns: repeat(6, 1fr); gap: var(--s12); }
.sch-sub { list-style: none; margin: 0; padding: 0; }
.sch-sub li {
  font-family: var(--gl-body); font-size: 13px; line-height: 1.3;
  color: var(--gl-muted-dark-bg);
  padding: 9px 0 8px; border-top: 2px solid var(--gl-hairline-dark);
  text-align: center;
}
.schema-foot { margin: var(--s32) 0 0; color: var(--gl-muted-dark-bg); }

.sch-tree { display: none; }
@media (min-width: 900px) { .sch-tree { display: block; } }
@media (max-width: 899px) {
  .sch-root { justify-content: flex-start; }
  .sch-drop { margin: 0 0 0 13px; height: 18px; }
}

/* ── 12a. The draw-in ─────────────────────────────────────────────────── */
html.js .sch-root span { opacity: 0; transform: translateY(-8px); transition: opacity .4s ease, transform .4s ease; }
html.js .sch-drop,
html.js .sch-ticks i   { transform: scaleY(0); transform-origin: top center; transition: transform .36s ease; }
html.js .sch-bar       { transform: scaleX(0); transition: transform .5s ease; }
html.js .sch-node      { opacity: 0; transform: translateY(8px); transition: opacity .4s ease, transform .4s ease; }
html.js .sch-sub li    { opacity: 0; transition: opacity .4s ease; }

/* These carry the html.js prefix for the same reason the rules above do, not
   as decoration: `html.js .sch-node` is one tag more specific than a bare
   `.drawn .sch-node`, so without it the hidden base state wins and the whole
   diagram stays invisible for everyone running JavaScript. */
html.js .drawn .sch-root span { opacity: 1; transform: none; }
html.js .drawn .sch-drop      { transform: scaleY(1); transition-delay: .26s; }
html.js .drawn .sch-bar       { transform: scaleX(1); transition-delay: .56s; }
html.js .drawn .sch-ticks i   { transform: scaleY(1); transition-delay: .80s; }
html.js .drawn .sch-ticks-2 i { transition-delay: 1.02s; }
html.js .drawn .sch-node      { opacity: 1; transform: none; transition-delay: .92s; }
html.js .drawn .sch-sub li    { opacity: 1; transition-delay: 1.12s; }

/* ── 12b. The signal ───────────────────────────────────────────────────────
   One travel keyframe per axis, active for the first ~11% of the cycle and
   idle for the rest. Every element shares the cycle and differs only by
   animation-delay, so the phases hold their spacing for ever instead of
   drifting apart the way independent durations would.                       */
.sch-drop, .sch-bar, .sch-ticks i { position: relative; overflow: hidden; }
.sch-drop::after, .sch-ticks i::after {
  content: ''; position: absolute; left: 0; right: 0; height: 80%; top: -80%; opacity: 0;
  background: linear-gradient(to bottom, rgba(255, 90, 20, 0), var(--gl-orange));
}
.sch-bar::before, .sch-bar::after {
  content: ''; position: absolute; top: 0; bottom: 0; width: 26%; opacity: 0;
}
.sch-bar::before { left: 50%;  background: linear-gradient(to left,  rgba(255, 90, 20, 0), var(--gl-orange)); }
.sch-bar::after  { right: 50%; background: linear-gradient(to right, rgba(255, 90, 20, 0), var(--gl-orange)); }

@keyframes sig-y    { 0% { top: -80%; opacity: 0 } 2% { opacity: 1 } 11% { top: 100%; opacity: 1 } 13%, 100% { top: 100%; opacity: 0 } }
@keyframes sig-left { 0% { left: 50%; opacity: 0 } 3% { opacity: 1 } 12% { left: -26%; opacity: 1 } 14%, 100% { left: -26%; opacity: 0 } }
@keyframes sig-right{ 0% { right: 50%; opacity: 0 } 3% { opacity: 1 } 12% { right: -26%; opacity: 1 } 14%, 100% { right: -26%; opacity: 0 } }

/* The source fires: the block inverts for a beat. Ink on orange and orange
   on ink both measure 5.69:1, so the flash never dips below AA. */
@keyframes sig-root { 0%, 100% { background: var(--gl-orange); color: var(--gl-ink) }
                      1%, 7%   { background: var(--gl-ink); color: var(--gl-orange) }
                      14%      { background: var(--gl-orange); color: var(--gl-ink) } }
@keyframes sig-node { 0%, 100% { border-color: var(--gl-hairline-dark); color: var(--gl-text-dark-bg) }
                      2%, 9%   { border-color: var(--gl-orange); color: var(--gl-orange) }
                      20%      { border-color: var(--gl-hairline-dark); color: var(--gl-text-dark-bg) } }
@keyframes sig-node-accent { 0%, 100% { background: transparent; color: var(--gl-orange) }
                      2%, 9%   { background: var(--gl-orange); color: var(--gl-ink) }
                      20%      { background: transparent; color: var(--gl-orange) } }
@keyframes sig-sub  { 0%, 100% { color: var(--gl-muted-dark-bg) }
                      3%, 12%  { color: var(--gl-text-dark-bg) }
                      24%      { color: var(--gl-muted-dark-bg) } }

.is-live .sch-root span           { animation: sig-root var(--cyc) linear infinite; }
.is-live .sch-drop::after         { animation: sig-y var(--cyc) linear infinite .06s; }
.is-live .sch-bar::before         { animation: sig-left var(--cyc) linear infinite .34s; }
.is-live .sch-bar::after          { animation: sig-right var(--cyc) linear infinite .34s; }
.is-live .sch-ticks i::after      { animation: sig-y var(--cyc) linear infinite .70s; }
.is-live .sch-node                { animation: sig-node var(--cyc) linear infinite .94s; }
.is-live .sch-node[data-accent]   { animation: sig-node-accent var(--cyc) linear infinite .94s; }
.is-live .sch-ticks-2 i::after    { animation: sig-y var(--cyc) linear infinite 1.26s; }
.is-live .sch-sub li              { animation: sig-sub var(--cyc) linear infinite 1.52s; }

/* The pulse leaves the centre of the bar, so the outer columns are reached
   last. Stagger centre-out or the six branches light as one flat row. */
.is-live :is(.sch-ticks, .sch-nodes, .sch-subs) > :nth-child(3),
.is-live :is(.sch-ticks, .sch-nodes, .sch-subs) > :nth-child(4) { --col: 0s; }
.is-live :is(.sch-ticks, .sch-nodes, .sch-subs) > :nth-child(2),
.is-live :is(.sch-ticks, .sch-nodes, .sch-subs) > :nth-child(5) { --col: .07s; }
.is-live :is(.sch-ticks, .sch-nodes, .sch-subs) > :nth-child(1),
.is-live :is(.sch-ticks, .sch-nodes, .sch-subs) > :nth-child(6) { --col: .14s; }
.is-live .sch-ticks i::after   { animation-delay: calc(.70s + var(--col, 0s)); }
.is-live .sch-node             { animation-delay: calc(.94s + var(--col, 0s)); }
.is-live .sch-ticks-2 i::after { animation-delay: calc(1.26s + var(--col, 0s)); }
.is-live .sch-sub li           { animation-delay: calc(1.52s + var(--col, 0s)); }
.is-live .sch-sub li:nth-child(2) { animation-delay: calc(1.60s + var(--col, 0s)); }
.is-live .sch-sub li:nth-child(3) { animation-delay: calc(1.68s + var(--col, 0s)); }

/* ── 12c. Phone: a spine instead of a tree, carrying the same signal ───── */
/* ── The phone diagram ─────────────────────────────────────────────────────
   It was six boxes with an L-shaped rule each, disconnected from one another
   and from anything above them — a list, not a structure — and THE ARTIST
   was inside the desktop-only tree, so the phone lost the source the whole
   figure is about.

   Now it is a real tree turned on its side: one continuous trunk descending
   from the artist, each division branching off it, and the trunk stopping at
   the last branch instead of running past it. Same drawing as the desktop
   six-column version, rotated to the shape a phone can hold. */
/* A <ul> carries 40px of default inline padding, which pushed the trunk out
   of line with the drop coming off THE ARTIST — the two read as separate
   marks instead of one continuous line. */
.sch-spine { display: block; margin: 0; padding: 0; }
.sch-spine > li {
  list-style: none; position: relative;
  margin-left: 13px;
  padding: 0 0 var(--s20, 20px) 24px;
  border-left: 2px solid var(--gl-hairline-dark);
}
.sch-spine > li::before {                      /* the branch off the trunk */
  content: ''; position: absolute; left: 0; top: 11px;
  width: 18px; height: 2px; background: var(--gl-hairline-dark);
}
.sch-spine > li[data-accent]::before { background: var(--gl-orange); }
/* The trunk ends at the last branch. An orange trunk segment would read as a
   fault in the line rather than as an accent, so only the branch is tinted. */
/* The trunk used to stop at the last branch, which is correct for a drawn
   tree and wrong here: each division's functions sit BELOW its branch, so
   the last one hung outside the structure and the line read as cut off
   rather than finished. It runs the full height now and terminates in a
   foot, which is the drawing saying "this is the end" out loud. */
.sch-spine > li:last-child { padding-bottom: 0; }
.sch-spine::after {
  content: ''; display: block; width: 18px; height: 2px;
  margin-left: 13px; background: var(--gl-hairline-dark);
}
@media (min-width: 900px) { .sch-spine::after { display: none; } }
/* The pulse travels the trunk. It lives on ::after because ::before is the
   static branch above — they were both on ::before, the later rule won, and
   the branches silently never drew. */
.sch-spine > li::after {
  content: ''; position: absolute; left: -2px; width: 2px; height: 60%; top: -60%; opacity: 0;
  background: linear-gradient(to bottom, rgba(255, 90, 20, 0), var(--gl-orange));
}
.is-live .sch-spine > li::after { animation: sig-y var(--cyc) linear infinite; }
.is-live .sch-spine > li:nth-child(1)::after { animation-delay: .10s }
.is-live .sch-spine > li:nth-child(2)::after { animation-delay: .34s }
.is-live .sch-spine > li:nth-child(3)::after { animation-delay: .58s }
.is-live .sch-spine > li:nth-child(4)::after { animation-delay: .82s }
.is-live .sch-spine > li:nth-child(5)::after { animation-delay: 1.06s }
.is-live .sch-spine > li:nth-child(6)::after { animation-delay: 1.30s }
.sch-spine .sp-name {
  font-family: var(--gl-mono); font-weight: 700; font-size: 13px;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--gl-text-dark-bg);
  display: block; margin-bottom: 4px;
}
.sch-spine > li[data-accent] .sp-name { color: var(--gl-orange); }
.sch-spine .sp-items { font-size: 14px; color: var(--gl-muted-dark-bg); }
@media (min-width: 900px) { .sch-spine { display: none; } }

/* Motion is the whole point of this block, so reduced motion gets the
   finished drawing and nothing that moves — not a slower version of it. */
@media (prefers-reduced-motion: reduce) {
  html.js :is(.sch-root span, .sch-drop, .sch-bar, .sch-ticks i, .sch-node, .sch-sub li) {
    opacity: 1; transform: none; transition: none;
  }
  .is-live :is(.sch-root span, .sch-node, .sch-sub li),
  .is-live :is(.sch-drop, .sch-ticks i)::before,
  .is-live .sch-spine > li::after,
  .is-live :is(.sch-drop, .sch-ticks i)::after,
  .is-live .sch-bar::before, .is-live .sch-bar::after { animation: none; }
}

/* ── 13. Marquee — the ink readout that divides concrete from steel ────── */
/* Orange ground, ink type — 5.69:1, the same ratio as the ink-and-orange it
   replaces, just the other way round. Note this is now the page's SECOND
   orange area after the ecosystem block. A 75px band and a full section are
   different weights so it holds, but a third one would make orange the page
   rather than the signal on it. */
.marquee {
  background: var(--gl-orange); color: var(--gl-ink);
  border-block: 3px solid var(--gl-orange);
  overflow: hidden; white-space: nowrap; padding-block: 21px;
}
.marquee-track { display: inline-block; will-change: transform; animation: slide 46s linear infinite; }
.marquee-track span {
  font-family: var(--gl-mono); font-weight: 700; font-size: 13px;
  letter-spacing: 0.18em; text-transform: uppercase; padding-inline: 18px;
}
.marquee-track span.verbatim { text-transform: none; }
.marquee-track i { font-style: normal; opacity: 0.42; }
@keyframes slide { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .marquee-track { animation: none; } }

/* ── 14. Section heads ─────────────────────────────────────────────────── */
.sec-head { margin-bottom: clamp(32px, 4vw, 64px); }
.sec-head .d2 { margin-top: var(--s16); }
.sec-intro { max-width: 62ch; margin-top: var(--s24); color: var(--gl-ink); }
:is(.gl-concrete, .on-dark) .sec-intro { color: var(--gl-text-dark-bg); }

/* ── 15. Cards — panel fill, 3px ink TOP border only. No radius, no shadow.
   The top border alone reads as a panel.                                   */
.cards { display: grid; gap: var(--s24); grid-template-columns: 1fr; }
@media (min-width: 620px)  { .cards { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1040px) { .cards { grid-template-columns: repeat(3, 1fr); } }

.card {
  background: var(--gl-panel);
  border-top: 3px solid var(--gl-ink);
  padding: var(--s24);
  display: flex; flex-direction: column; gap: var(--s12);
  transition: background .18s linear;
}
:is(.gl-concrete, .on-dark) .card { background: var(--gl-panel-dark); border-top-color: var(--gl-text-dark-bg); }
.card:hover { background: rgba(23,24,26,0.13); }
/* Concrete is the ground now, so the panel lifts rather than deepens, and
   the ink type that read on steel has to become light. */
:is(.gl-concrete, .on-dark) .card:hover { background: rgba(199,208,216,0.10); }
:is(.gl-concrete, .on-dark) .card-body { color: var(--gl-text-dark-bg); }
:is(.gl-concrete, .on-dark) :is(.card-num, .card-tag) { color: var(--gl-muted-dark-bg); }
.card-top { display: flex; align-items: center; justify-content: space-between; gap: var(--s12); }
/* The icons take the accent. They are stroke-only and inherit currentColor,
   so this is a colour change and nothing else. Orange on concrete is 5.69:1;
   on an orange ground it would be 1:1, hence the second rule — the cards are
   only on concrete today but the first person to move them should not have
   to discover that. */
.card-top .ico { color: var(--gl-orange); }
.loud .card-top .ico { color: var(--gl-ink); }
.card-num { font-family: var(--gl-mono); font-weight: 700; font-size: 13px; letter-spacing: 0.16em; color: var(--gl-ink-soft); }
.card-tag { font-family: var(--gl-mono); font-weight: 700; font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--gl-ink-soft); }
.card p:last-child { margin-bottom: 0; }
.card-body { color: var(--gl-ink); font-size: 16px; line-height: 1.5; }
.card-link { margin-top: auto; padding-top: var(--s8); }
.card-link a { text-decoration: none; }
.card-link a:hover .gl-tag { background: var(--gl-orange); color: var(--gl-ink); }

/* ── THE ONE ORANGE SURFACE ──────────────────────────────────────────────
   Full bleed and ink-only. Measured, because this is the pairing people get
   wrong:

       ink #17181A   on orange   5.69:1   the only one that passes
       white         on orange   3.12:1   large text only
       body #C7D0D8  on orange   2.00:1   fails
       steel #7E8A96 on orange   1.13:1   fails

   Orange and steel sit at almost the same relative luminance, 0.286 against
   0.248, which is the same reason orange type was banned on a steel ground.
   They are a forbidden pair in both directions.

   So everything here is ink at full strength. Hierarchy comes from size, not
   from a lighter tone, because there is no lighter tone available. */
.loud {
  background: var(--gl-orange); color: var(--gl-ink);
  padding-block: clamp(48px, 7vw, 104px);
  margin-top: clamp(48px, 6vw, 96px);
}
.loud-inner { display: grid; gap: var(--s24); align-items: center; }
@media (min-width: 860px) { .loud-inner { grid-template-columns: minmax(0,1fr) minmax(0,1.05fr); gap: var(--s64); } }
.loud .label, .loud .d2, .loud .lede { color: var(--gl-ink); }
.loud .label { margin-bottom: var(--s16); }
.loud .lede { margin-bottom: 0; }

/* ── 16. Latest releases rail ──────────────────────────────────────────── */
/* Bleeds off the right edge of the screen. A horizontal scroll rail that
   stops neatly at the page margin does not look scrollable — it looks like
   the list ends there. The left edge stays aligned with the heading, so the
   only thing that changes is that the row visibly runs out of the frame.
   50% of the wrap's content box minus 50vw cancels the wrap's own padding at
   any width. */
.latest-rail {
  display: grid; grid-auto-flow: column; grid-auto-columns: 172px;
  gap: var(--s16); overflow-x: auto; padding-bottom: var(--s16);
  scroll-snap-type: x mandatory; scrollbar-width: thin;
  margin-right: calc(50% - 50vw);
  padding-right: clamp(24px, 6vw, 96px);
}
.latest-card { scroll-snap-align: start; text-decoration: none; display: block; }
.latest-card img {
  width: 172px; height: 172px; object-fit: cover;
  border: 2px solid var(--gl-hairline-dark);
  transition: border-color .18s linear;
}
.latest-card:hover img { border-color: var(--gl-orange); }
.latest-card .lt {
  display: block; margin-top: var(--s12);
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.08em; color: var(--gl-text-dark-bg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.latest-card .la {
  display: block; margin-top: 3px; font-size: 13px; color: var(--gl-muted-dark-bg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Two ways out of this section: the whole catalogue, or a demo. The second
   is the only artist-facing call to action on the page — the contact form at
   the bottom is aimed at everybody. */
.latest-actions { display: flex; flex-wrap: wrap; gap: var(--s16); margin-top: var(--s24); }

/* ── 17. Philosophy ────────────────────────────────────────────────────── */
.phil { display: grid; gap: var(--s32); }
@media (min-width: 900px) { .phil { grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: var(--s96); } }
.phil-body p { color: var(--gl-text-dark-bg); }
.phil-body p:last-child { margin-bottom: 0; }

/* ── 18. Contact ───────────────────────────────────────────────────────── */
.contact-grid { display: grid; gap: var(--s48); }
@media (min-width: 960px) { .contact-grid { grid-template-columns: minmax(0,1fr) minmax(0,1.15fr); gap: var(--s96); } }
.contact-sub { color: var(--gl-text-dark-bg); max-width: 46ch; }
.contact-meta { margin-top: var(--s32); display: grid; gap: var(--s12); }
.contact-meta a { color: var(--gl-text-dark-bg); text-decoration: none; display: inline-flex; align-items: center; gap: 10px; }
.contact-meta a:hover { color: var(--gl-orange); }

.form { display: grid; gap: var(--s16); }
.form-row { display: grid; gap: var(--s16); }
@media (min-width: 620px) { .form-row { grid-template-columns: 1fr 1fr; } }
.field {
  width: 100%; font-family: var(--gl-body); font-size: 16px;
  background: transparent; color: var(--gl-text-dark-bg);
  border: 2px solid var(--gl-hairline-dark); border-radius: 0;
  padding: 14px 16px;
  transition: border-color .16s linear;
}
.field::placeholder { color: var(--gl-muted-dark-bg); opacity: 1; }
.field:focus { outline: 0; border-color: var(--gl-orange); }
textarea.field { min-height: 148px; resize: vertical; }
.form .gl-btn-primary { justify-content: center; border: 0; }
.form-note { margin: 0; color: var(--gl-orange); font-family: var(--gl-mono); font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; }

/* ── 19. Footer ────────────────────────────────────────────────────────── */
.foot { position: relative; border-top: 0; padding-block: var(--s48); }
/* THE FOOTER'S EDGE IS THE ONE DIVIDER THAT MOVES besides the roster cards:
   the same belt, full width, where a conveyor ends. Same keyframes as the
   cards (§32), same one-period translate so the loop has no seam, still
   under reduced motion. */
.edge-belt { position: absolute; left: 0; right: 0; top: 0; height: 8px; overflow: hidden; }
.edge-belt::before {
  content: ''; position: absolute; inset: 0; width: 200%;
  background: repeating-linear-gradient(-45deg, var(--gl-orange) 0 5px, transparent 5px 12px);
  animation: belt-run 1.4s linear infinite, belt-pulse 2.6s ease-in-out infinite;
  will-change: transform, opacity;
}
@media (prefers-reduced-motion: reduce) { .edge-belt::before { animation: none; opacity: .8; } }
.foot-grid { display: grid; gap: var(--s32); align-items: start; }
@media (min-width: 760px) { .foot-grid { grid-template-columns: auto 1fr auto; gap: var(--s48); align-items: center; } }
.foot-links { display: flex; flex-wrap: wrap; gap: var(--s24); list-style: none; margin: 0; padding: 0; }
.foot-links a {
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg); text-decoration: none;
}
.foot-links a:hover { color: var(--gl-orange); }
.foot-copy { margin: 0; font-family: var(--gl-mono); font-size: 12px; letter-spacing: 0.1em; color: var(--gl-muted-dark-bg); }

/* ── 20. Reveal — only when JS is present, so no-JS keeps every section ── */
html.js .reveal { opacity: 0; transform: translateY(18px); }
html.js .reveal.visible { opacity: 1; transform: none; transition: opacity .6s ease, transform .6s ease; }
@media (prefers-reduced-motion: reduce) {
  html.js .reveal { opacity: 1; transform: none; }
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--gl-orange); color: var(--gl-ink);
  font-family: var(--gl-mono); font-weight: 700; font-size: 13px;
  letter-spacing: 0.1em; text-transform: uppercase; padding: 12px 18px; text-decoration: none;
}
.skip:focus { left: var(--s24); top: var(--s12); }
[hidden] { display: none !important; }

/* ── 21. Section seams ─────────────────────────────────────────────────────
   #latest is concrete and sits between the two steel bands, but it ships
   hidden and only unhides once the catalogue fetch returns. Both states have
   to look deliberate, so the seam is drawn explicitly rather than left to an
   adjacency selector that a hidden element in the DOM would defeat. */
#services { padding-bottom: 0; }   /* the orange block closes it */
#about { padding-top: clamp(56px, 7vw, 112px); }   /* the ink stats band that
   used to close this section is gone, so .band's own bottom padding applies
   again and the rust plate ends on space rather than on a hard edge. */
.phil { margin-top: var(--s32); }

/* ── 22. Small type on steel ───────────────────────────────────────────────
   --gl-ink-soft composites to roughly 3.1:1 over steel mid. That is fine for
   a large quiet heading and NOT fine for a 12px mono label, which is most of
   what this page uses it for. Hierarchy here comes from size and weight
   instead, and every label clears AA. */
.rail .r-right,
.card-num,
.card-tag { color: var(--gl-ink); }
:is(.gl-concrete, .on-dark) .rail .r-right { color: var(--gl-muted-dark-bg); }

/* ── 23. Orange that survives any ground ──────────────────────────────────
   The video plate that used to live here has been removed — see the commit.
   This stayed, because it is the thing that made the plate safe and it is
   just as useful without one: a solid orange block with ink type is 5.69:1
   whatever sits behind it. */
/* Orange loud enough to survive anything behind it: an opaque block, ink
   type, 5.69:1. The steel sections use the inverse — see .ink-slab. */
.slab-orange {
  background: var(--gl-orange); color: var(--gl-ink);
  display: inline-block; padding: 0.02em 0.12em 0.08em;
}



/* ── 24. Rust — the material ground ────────────────────────────────────────
   See docs/BRAND.md §4a. Generated, not photographed, so its luminance could
   be tuned rather than compensated for: the brightest pixel on the shipped
   plate is 0.0578, which puts body type at 5.85:1 with NO scrim. That is the
   whole reason it works where the video did not.

   What still cannot go on it: muted grey at 3.46:1 and orange at 3.12:1. So
   body colour for type, and orange only inside a solid block. */
.rust {
  background-color: #17181A;
  color: var(--gl-text-dark-bg);
  position: relative; isolation: isolate; overflow: hidden;
}
/* On a pseudo, inset past the edges, so the drift is a compositor transform
   rather than a background repaint every frame. */
.rust::before {
  content: ''; position: absolute; inset: -7%; z-index: -1;
  background: url('/assets/texture/rust.jpg') center / cover;
}
.rust > .wrap { position: relative; }
/* The section carries .on-dark so buttons, tags and rules take their dark
   treatment. Two of those then have to be walked back, because on-dark is
   written for concrete and rust is brighter than concrete:

     the rail's left label is ORANGE on concrete — 3.12:1 here, so it
     takes the body colour instead
     the rail's right label is MUTED on concrete — 3.46:1 here, same

   Everything else on-dark gives is already correct. */
.rust .rail .r-left,
.rust .rail .r-right { color: var(--gl-text-dark-bg); }
.rust .phil-body p   { color: var(--gl-text-dark-bg); }


/* ── 25. Entrance motion ───────────────────────────────────────────────────
   Four gestures, all one vocabulary: something is drawn, wiped or uncovered
   in a straight line. Nothing scales, bounces or fades in place — the
   schematic already established that this page draws itself, and these
   extend that rather than inventing a second language.

   Everything is behind html.js so a visitor without scripting gets the
   finished page, and everything is off under reduced motion. */

/* 1. Section rules draw left to right. */
html.js [data-anim="rule"]::before {
  transform: scaleX(0); transform-origin: left center;
  transition: transform .75s cubic-bezier(.2, .7, .2, 1);
}
html.js [data-anim="rule"].in::before { transform: scaleX(1); }

/* 2. Display headings are uncovered from below, like a plate being lifted. */
html.js [data-anim="mask"] {
  clip-path: inset(-30% 0 100% 0);
  transition: clip-path .85s cubic-bezier(.2, .7, .2, 1) .12s;
}
html.js [data-anim="mask"].in { clip-path: inset(-30% 0 -30% 0); }

/* 3. The orange block wipes in from the left. It is the loudest thing on the
      page, so it arrives rather than appears. */
html.js [data-anim="wipe"] {
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1s cubic-bezier(.2, .7, .2, 1);
}
html.js [data-anim="wipe"].in { clip-path: inset(0 0 0 0); }

/* 5. Photographs are uncovered downward, in sequence — the same straight
      line as the headings, one beat apart so the row lands as four events
      rather than one. */
html.js [data-anim="frame"] {
  clip-path: inset(0 0 100% 0);
  transition: clip-path .9s cubic-bezier(.2, .7, .2, 1);
}
html.js [data-anim="frame"].in { clip-path: inset(0 0 0 0); }
html.js .lf-2[data-anim="frame"] { transition-delay: .09s; }
html.js .lf-3[data-anim="frame"] { transition-delay: .18s; }
html.js .lf-4[data-anim="frame"] { transition-delay: .27s; }
html.js .lf-5[data-anim="frame"] { transition-delay: .36s; }

/* 4. The rust plate drifts. Ninety seconds end to end — slow enough that you
      never catch it moving, only notice the section is not a flat image. */
@media (prefers-reduced-motion: no-preference) {
  .rust::before { animation: rust-drift 90s ease-in-out infinite alternate; }
}
@keyframes rust-drift {
  from { transform: translate3d(-1.4%, -1%, 0) scale(1); }
  to   { transform: translate3d(1.4%, 1%, 0) scale(1.05); }
}

@media (prefers-reduced-motion: reduce) {
  html.js [data-anim] { clip-path: none !important; transition: none !important; }
  html.js [data-anim="rule"]::before { transform: scaleX(1); transition: none; }
  .rust::before { animation: none; }
}


/* ── 26. The mark on the orange block ─────────────────────────────────────
   mark-ink is the file the brand ships for LIGHT surfaces, and orange is the
   light surface. The block is the loudest thing on the page and was the only
   large area carrying no identity at all. 560x484, so both dimensions are
   set or it stretches. */
.loud-mark { width: 58px; height: 50px; object-fit: contain; margin-bottom: var(--s16); }

/* ── 27. Nav: where you are, and how far in ───────────────────────────────
   A rule that fills as you scroll. It is a rule, it is orange, it moves in a
   straight line — the same vocabulary as everything else on the page, rather
   than a progress widget borrowed from somewhere else. */
.nav { position: sticky; }
.nav-progress {
  position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
  background: var(--gl-orange);
  transform: scaleX(var(--progress, 0)); transform-origin: left center;
  will-change: transform;
}

/* ── 28. Honeypot ─────────────────────────────────────────────────────────
   Off-screen rather than display:none — a bot that skips hidden inputs is
   not caught by one, and a positioned field still gets filled. */
.trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* A select has to look like the inputs beside it, and Safari will not style
   the arrow, so the native one is removed and drawn. */
select.field {
  -webkit-appearance: none; appearance: none; cursor: pointer;
  padding-right: 44px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--gl-muted-dark-bg) 50%),
    linear-gradient(135deg, var(--gl-muted-dark-bg) 50%, transparent 50%);
  background-position: calc(100% - 24px) calc(50% - 2px), calc(100% - 18px) calc(50% - 2px);
  background-size: 6px 6px; background-repeat: no-repeat;
}
select.field option { background: var(--gl-concrete); color: var(--gl-text-dark-bg); }


/* ── 29. Narrow rails ─────────────────────────────────────────────────────
   Two tracked mono labels pushed apart is the header rail's whole idea, and
   at 375px it becomes two three-line columns colliding in the middle. Below
   560px they stack, which keeps both readable and still reads as a rail. */
@media (max-width: 559px) {
  .rail, .schema-head { flex-direction: column; align-items: flex-start; gap: 2px; }
  .rail .r-right, .schema-head .r-right { text-align: left; }
}

/* ── 30. Live — the only photography on the page ──────────────────────
   Two problems, one treatment.

   The colour: these four were shot under cyan, magenta and amber. Side by
   side at full strength they read as a rainbow and they fight the orange, so
   at rest every frame is pulled into the page's own accent and comes back to
   full colour one at a time — on hover, or for whichever frame you have
   swiped to on a phone. The page stays two colours; the photography becomes
   something you uncover rather than something that shouts over the brand.

   The crop: three of the four are portrait and one is landscape, so a row of
   equal columns would have to object-fit them and would cut heads off. Each
   column is instead sized to its own file's width ÷ height, which justifies
   the row to a single height with the images at their true proportions and
   nothing cropped at all. SWAPPING A PHOTO MEANS CHANGING ITS fr VALUE to
   match the new file, or the row stops justifying. */

.live { padding-block: clamp(56px, 7vw, 104px) clamp(60px, 7.5vw, 116px); }
.live-head { margin-bottom: clamp(28px, 3.4vw, 52px); }
.live-head .d3 { margin-top: var(--s16); max-width: 20ch; }

.live-strip {
  display: grid;
  /* live-03  live-05   live-04   live-01  live-02 — each fr is that file's w/h */
  grid-template-columns: 0.7456fr 0.8867fr 1.3333fr 0.8fr 0.7956fr;
  gap: clamp(8px, 1.1vw, 18px);
  align-items: start;
  padding-inline: clamp(16px, 4vw, 56px);
}

.lf {
  margin: 0; position: relative; overflow: hidden;
  isolation: isolate;                 /* keeps the blend off the page ground */
  /* This background must be OPAQUE, and that is not a style choice. Isolating
     the group means the orange below blends against this and nothing else, so
     a translucent panel colour here (the --gl-panel-dark this used to use)
     leaves most of the blend uncomposited and a frame whose photo has not
     painted yet is a glowing #FF5A14 plate. Measured, not guessed. */
  background: #1E1F21;
}
.lf img { width: 100%; height: auto; display: block; }

/* The stagger. Two frames drop, so the row reads as prints on a light table
   rather than a filmstrip. Rows 2 and 4, so the drop alternates. */
.lf-2 { margin-top: clamp(18px, 3.4vw, 56px); }
.lf-4 { margin-top: clamp(34px, 6.2vw, 96px); }

/* Duotone: greyscale the image first so the accent lands evenly across it,
   then blend the accent over it for hue and saturation only. A filter chain
   of sepia + hue-rotate would be one declaration shorter and drifts off the
   brand orange on a wide-gamut display; this does not. */
.lf img { filter: grayscale(1) contrast(1.05) brightness(.94); transition: filter .6s cubic-bezier(.2,.7,.2,1); }
.lf::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: var(--gl-orange); mix-blend-mode: color; opacity: .82;
  transition: opacity .6s cubic-bezier(.2,.7,.2,1);
  /* A BLENDED LAYER DOES NOT ALWAYS REPAINT WHEN ITS OPACITY ANIMATES.
     WebKit composites a mix-blend-mode layer with its backdrop group and can
     miss the re-render, which leaves the orange sitting there at full
     strength while the greyscale lifts off the photo underneath — the tint
     appears not to come off at all. Naming opacity here puts the layer on
     the compositor up front, so the fade is a compositor animation with
     nothing to invalidate. It costs one layer per frame and the belt is
     already promoted the same way. Chromium never needed it: measured
     identical at rest and on hover, before and after. */
  will-change: opacity;
}
/* Seats the frame into the dark page instead of letting it float off it. */
.lf::after {
  content: ''; position: absolute; inset: 0; z-index: 2; pointer-events: none;
  background: linear-gradient(to bottom, rgba(23,24,26,.24) 0%, rgba(23,24,26,0) 34%, rgba(23,24,26,.46) 100%);
}
/* The identity list, NOT `filter: none` — see the roster's copy of this
   rule. A filter list interpolating to `none` is not animated reliably, so
   the greyscale stepped off while ::before's wash faded over the same
   duration beside it, and the tint appeared to snap rather than lift. The
   two lists have to stay compatible function-for-function. */
.lf.lit img { filter: grayscale(0) contrast(1) brightness(1); }
.lf.lit::before { opacity: 0; visibility: hidden; transition: opacity .6s cubic-bezier(.2,.7,.2,1), visibility 0s linear .6s; }

/* The index plate rides up out of the bottom edge with the colour. */
.lf figcaption {
  position: absolute; left: 0; bottom: 0; z-index: 3;
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.16em; line-height: 1;
  color: var(--gl-ink); background: var(--gl-orange);
  padding: 8px 12px;
  transform: translateY(100%);
  transition: transform .45s cubic-bezier(.2,.7,.2,1);
}
.lf.lit figcaption { transform: none; }

/* NOT gated on (hover: hover) and (pointer: fine) any more. The roster
   cards' identical treatment never was, so on a touchscreen laptop — which
   reports a coarse pointer — the cards lit and the strip did not, which
   reads as the strip being broken rather than as a deliberate difference.
   A stuck :hover after a tap is the cost, and the scroll-driven .lit class
   already moves through these frames on a phone regardless. */
.lf:hover img        { filter: grayscale(0) contrast(1) brightness(1); }
.lf:hover::before    { opacity: 0; visibility: hidden; transition: opacity .6s cubic-bezier(.2,.7,.2,1), visibility 0s linear .6s; }
.lf:hover figcaption { transform: none; }

/* On a phone four frames side by side are four slivers. It becomes a swipe
   rail, and there a ragged bottom edge looks like a mistake rather than a
   stagger — so the rail takes a fixed height and each frame gets an
   object-position tuned to where its subject actually sits. */
@media (max-width: 899px) {
  .live-strip {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: 78vw;
    gap: 10px;
    grid-auto-rows: min(62vh, 440px);   /* height here, so the scrollbar
                                           lives in the padding rather than
                                           eating into the frames */
    padding-bottom: 10px;
    overflow-x: auto;
    scrollbar-width: thin;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: clamp(16px, 4vw, 56px);
    -webkit-overflow-scrolling: touch;
  }
  .lf { height: 100%; scroll-snap-align: center; }
  .lf-2, .lf-4 { margin-top: 0; }        /* no stagger inside a rail */
  .lf img { height: 100%; object-fit: cover; }
  .lf-1 img { object-position: 50% 28%; }
  .lf-2 img { object-position: 50% 38%; }
  .lf-3 img { object-position: 48% 44%; }
  .lf-4 img { object-position: 50% 26%; }
  .lf-5 img { object-position: 50% 46%; }
}

@media (prefers-reduced-motion: reduce) {
  .lf img, .lf::before, .lf figcaption { transition: none; }
}


/* ── 31. Social ─────────────────────────────────────────────────
   The real brand marks, not redrawn ones. Each is the official glyph on its
   own 24 grid, so they are FILLED and must not inherit .ico, which sets
   `fill: none; stroke: currentColor` and would render every one of them as
   an outline of itself.

   Hit area is 44px against a 22px glyph — below that these are a coin toss
   on a phone. Hover inverts to ink on an orange square rather than shifting
   the orange, because there is nowhere brighter for the accent to go. */
.foot-social {
  display: flex; align-items: center; gap: var(--s24);
  padding-top: var(--s24); margin-top: var(--s24);
  border-top: 2px solid var(--gl-hairline-dark);
}
.soc-row { display: flex; gap: var(--s4); list-style: none; margin: 0; padding: 0; }
.soc-row a {
  display: grid; place-items: center; width: 44px; height: 44px;
  color: var(--gl-orange); text-decoration: none;
  transition: color .16s linear, background-color .16s linear;
}
.soc-row a:hover, .soc-row a:focus-visible {
  background: var(--gl-orange); color: var(--gl-ink);
}
.soc { width: 22px; height: 22px; flex: none; fill: currentColor; stroke: none; }
/* Optical: line the first glyph up with the text above it, not the 44px
   box drawn around it. */
.contact-soc { margin-top: var(--s16); margin-left: -11px; }


/* ── 32. Roster cards ─────────────────────────────────────────────────────
   Each artist is a collectible trading card, and the proportion is the whole
   argument: 5:7 is the physical card (63 × 88 mm), so the artboard the yearly
   printed card is laid out on is the artboard this one is laid out on. The
   front is the artist — picture plate, wordmark, name — and the back is the
   record that makes the front worth believing: every figure and the status
   are one turn away, each next to the base it was read against. The front
   carries none of them. The flip is ported from the catalogue at /gmg — same
   geometry, same
   .6s curve, same inert handling — so the two pages read as one system rather
   than two sites that happen to share a palette.

   THE SPLIT IN THIS SECTION, because it is the thing to preserve. Everything
   down to "── The flip ──" is the card WITHOUT script: two faces stacked in a
   grid, both readable, the artist link a tab stop. Everything under html.js
   turns that stack into a flip. The gate is not decoration — the flip hides
   one face at a time, and anything that can hide content has to be unable to
   run when the thing that unhides it did not load. Same reason §20 and the
   schematic in §14 carry the prefix.

   Every colour is a token BRAND.md has already measured against concrete —
   orange 5.69:1, body 11.37:1, muted 7.01:1. One exception, the plate, and it
   is argued for where it is declared. */

/* The grid is unchanged from the list this replaced. auto-fill over a
   280–340px track keeps two cards card-width instead of ballooning to half
   the page, and a third signing fills the next slot with no CSS at all;
   justify-content: start lines the first card up with the heading above it.

   align-items: start is new and it is load-bearing. Grid items stretch by
   default, so an OPEN card — which grows past 5:7 when its back needs the
   room — would drag its closed neighbour's height up with it and leave that
   card's faces floating in a taller box. */
.roster {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 340px;
  gap: var(--s16);
  align-items: start;
  list-style: none;
  /* Bleeds off the right edge for the reason §16's release rail does: a rail
     that stops neatly at the page margin does not look scrollable, it looks
     like the list ends there. The left edge stays on the heading. */
  /* overflow-x: auto computes overflow-y to auto as well, so this box CLIPS
     vertically even though nothing overflows it. The focus ring does: §.front
     draws a 3px outline at 2px offset, and with no headroom the top 5px of it
     was cut off on a keyboard tab. 6px of padding on the top and left with a
     matching negative margin buys the ring room and moves nothing — measured
     headroomAbove was 0 before this. */
  margin: -6px calc(50% - 50vw) 0 -6px;
  padding: 6px clamp(24px, 6vw, 96px) var(--s16) 6px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 6px;
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
/* Below 560px a 280px track plus the page margin stops fitting, so it
   becomes one column — the same width the rails already stack at. */
/* 340px TRACKS, NOT A SHARE OF THE VIEWPORT, at every width. The back's
   content was measured at 340 and its height budget allows exactly ONE
   wrapped line down by the sources; a narrower card wraps a second and
   pushes the back off 5:7. So the card never shrinks — the rail scrolls
   instead, which is the whole point of it. */
.roster .card { scroll-snap-align: start; }

/* ONE CLASS NAME IN THIS SECTION IS ALREADY TAKEN, and it would break the
   card silently rather than loudly: .card, which §15 gives a 3px ink top
   border, a panel fill and 24px of padding — on the roster, a stripe across
   the top of every card and 24px of dead concrete inside the 5:7 box.

   There were two. `.hero` was §11's band, up to 128px of vertical padding
   around the headline figure, and it left this section when the figure did:
   no card front carries one now, so there is no longer anything here for §11
   to reach and nothing to keep neutralised in step with it.

   Renaming .card here would have been the other fix, but it is the name the
   catalogue's cards use and the name the printed card's layers use, and the
   whole point of this section is that the three stay one vocabulary. So it is
   neutralised instead, scoped under .roster. The :hover is listed separately
   because §15's hover fill is (0,3,0) and would otherwise come back the
   moment a pointer touched the card. */
.roster .card {
  display: block;
  position: relative;
  min-width: 0;
  list-style: none;
  background: none;
  border-top: 0;
  padding: 0;
  gap: 0;
  transition: none;
}
.roster .card:hover { background: none; }

/* ── The card without script ──────────────────────────────────────────────
   Two faces, stacked, both legible. This is the finished no-JS design and not
   a degraded one: a visitor with script blocked gets the front and the stat
   line underneath it, and the artist link is an ordinary tab stop. The two
   controls that only mean something in a flip — the close button and the
   "Details" affordance — are the only things hidden. */
.roster .faces { display: grid; gap: var(--s12); }

.roster .face {
  border: 2px solid var(--gl-hairline-dark);
  background: var(--gl-ink);
}

/* The front is a <button>, so it is reset to nothing and built back up. Two
   of the re-declarations are not obvious:

     box-sizing  `all: unset` resets it to content-box, overriding the
                 universal border-box at the top of this file, because this
                 rule is more specific than `*`. Harmless today only because
                 the front carries no padding of its own. Declared so it stays
                 harmless.
     outline     for the same reason: `all: unset` beats the global
                 :where(button):focus-visible ring at (0,1,0), so without the
                 rule below the front is the one control on the page a
                 keyboard user cannot see. */
.roster .front {
  all: unset;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  cursor: pointer;
  border: 2px solid var(--gl-hairline-dark);
  background: var(--gl-ink);
  color: var(--gl-text-dark-bg);
  transition: border-color .18s linear;
}
.roster .front:hover,
.roster .front:focus-visible { border-color: var(--gl-orange); }
.roster .front:focus-visible { outline: 3px solid var(--gl-orange); outline-offset: 2px; }

/* The serial band. Same rule on both faces, so the two read as one object
   turned over rather than two designs. */
.roster .strip {
  display: flex;
  justify-content: space-between;
  gap: var(--s8);
  padding: 8px 12px;
  border-bottom: 2px solid var(--gl-hairline-dark);
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg);
}
/* Only the left half of the strip takes the accent, and only on the front.
   The series is the year the artist signed and what the card belongs to and it is worth the orange; the
   serial beside it is an index and reads as loudly again if it is given the
   same colour, which is an orange band with no hierarchy in it. The class is
   on the front's strip alone — the back's serial carries no .series and stays
   muted, because the back is the record and the record is not selling. */
.roster .series { color: var(--gl-orange); }

/* The picture plate. It is the card's picture AREA now, not a band above the
   type: flex: 1 inside the front's column, so it takes everything the strip,
   the name block and the footer line leave — roughly a square on a 340 × 476
   card. The `aspect-ratio: 3 / 1` it shipped with is gone with that change,
   because a ratio and a flex length are two sources for one height and the
   ratio would win the argument in the wrong direction: a 3:1 band on a card
   whose front is otherwise empty.

   The rest is carried over from .act-mark unchanged, because the recipe was
   measured in the pane and every line of it is still load-bearing:

     min-height: 0    a flex item's automatic minimum is its min-content
                      height, so without this the plate refuses to shrink
                      below whatever the image inside it works out to — 192px
                      and 185px for these two files, two different plates and
                      the equal weight gone. It guarded the same failure under
                      aspect-ratio; only the reason it is reachable changed.
     grid-template-rows  the mark's height: 100% needs a definite row to
                      resolve against or it falls back to auto, the image
                      paints at its OWN proportions and object-fit never gets
                      to do anything. Capping the row at the content box is
                      what hands the sizing back to the plate.
     padding: 5%      percentage padding resolves against the WIDTH on both
                      axes, so the 14% this was drafted with is 47px top and
                      bottom on a 336px card. 5% is the same optical margin.

   The mark is CAPPED rather than fitted, and that is the one line the square
   plate made necessary. On a 3:1 band object-fit: contain did the capping by
   itself — the band was short enough that the height ran out first. Given a
   square it does not: the same declaration paints the wordmark the full width
   of the card, which is a logo sheet, not a card front. 72% × 40% holds it at
   about the size it has always been.

   The marks are white, drawn for a dark ground, and they are presented as the
   artist drew them: never tinted, never recoloured to match a section.

   THE ONE NON-TOKEN COLOUR ON THIS PAGE is the plate ground. #0F1012 is
   --gl-ink with 8 subtracted from each channel (23,24,26 → 15,16,18) — the
   ink darkened, not a second dark hue entering the system — and it is here so
   the plate reads as a window cut into the card rather than as the same
   surface with a rule under it. A card is a printed object and the picture
   area sits deeper than the stock. */
.roster .plate {
  display: grid;
  place-items: center;
  grid-template-rows: minmax(0, 1fr);
  flex: 1;
  min-height: 0;
  padding: 5% 10%;
  background: #0F1012;
  border-bottom: 2px solid var(--gl-hairline-dark);
}
.roster .mark {
  width: 100%; height: 100%;
  max-width: 72%; max-height: 40%;
  object-fit: contain;
  display: block;
}

/* THE MILESTONE FINISH. A card earns rust when that artist's streams since
   signing reach 1,000,000, and it is applied by hand — add `milestone` to the
   card's class. OSSIEN (1,747,607) and Bassurgence (1,535,785) both carry
   it as of 21 Sep 2026. It is a milestone and never a ranking: the finish
   says an artist passed a number, not that they beat the artist next to
   them, so a second card taking it changes nothing about the first. Rust is the brand's material
   ground (§4a) and the roster may use it only where no type sits on it.

   THE PLATE RULE BELOW ONLY REACHES A CARD WITHOUT A PHOTOGRAPH, and that is
   not a bug to fix by raising its specificity. On a .has-photo card the photo
   is absolutely positioned over the plate at inset 0, so a ground behind it
   is invisible whatever the cascade says — and .has-photo .plate repaints
   that ground ink at equal specificity further down, so the rule loses twice.
   Every card on the page carries a photograph today, which is why the finish
   is the FRAME. */
.roster .milestone .plate { background: url('/assets/texture/rust.jpg') center / cover; }

/* The frame is where rust lands on a photographed card. It is also the
   trading-card idiom for a rare one, and the only surface on this object
   with no type on it at all — the condition §4a puts on bare rust. Two
   layers: ink clipped to the padding box for the card itself, the texture
   clipped to the border box for the edge.

   The FRONT takes 4px so it reads as a different card across the grid rather
   than as a hover state; its .plate is flex:1 and absorbs the 4px. The BACK
   keeps 2px and changes colour only, because its content clears the 5:7 box
   by 4px and taking 4px of height away would put it over. */
.roster .milestone .front,
.roster .milestone .back {
  border-color: transparent;
  background-image:
    linear-gradient(var(--gl-ink), var(--gl-ink)),
    url('/assets/texture/rust.jpg');
  background-clip: padding-box, border-box;
  background-origin: padding-box, border-box;
  background-size: auto, cover;
  background-repeat: no-repeat, no-repeat;
}
.roster .milestone .front { border-width: 4px; }
/* The hover and focus cue is not lost to the transparent border: an opaque
   border paints OVER the background's border-box layer, so colouring the
   border covers the rust without touching the background at all. That keeps
   the cue on .18s with every other card — swapping the background layer
   instead, as this first did, made OSSIEN the one card whose edge snapped,
   because a url() and a gradient have nothing to interpolate between. The
   :hover rule earlier in this sheet cannot do it alone: .milestone .front
   sets border-color at equal specificity and later, so it needs restating
   here at one class more. */
.roster .milestone .front:hover,
.roster .milestone .front:focus-visible { border-color: var(--gl-orange); }

/* ── THE PHOTOGRAPH ───────────────────────────────────────────────────────
   The front of a collectible card is the artist, so the plate carries their
   picture behind the wordmark: photo, orange wash, scrim, mark, in that
   order, bottom to top. A card takes one by adding `has-photo` and dropping
   an `<img class="photo">` in as the first child of its .plate.

   BOTH CARDS CARRY ONE TODAY, AND THE PHOTO-LESS PATH IS NOT DEAD CODE.
   Everything here is scoped under .has-photo, so a card without the class is
   the ink plate with the mark centred on it and none of this reaches it —
   which is a finished front and the state the next signing ships in. A
   photograph goes on a card the day the person in it is identified and not
   an hour before, because deciding by eye which face belongs to which artist
   is the error §4b names, and a new card will sit on ink for as long as that
   answer takes. Deleting this fallback to tidy up would turn that wait into
   a broken plate.

   The treatment is §4b's, declaration for declaration, because the site gets
   ONE photo rule and not two: greyscale first so the accent lands evenly,
   then the orange blended over for hue and saturation only, full colour on
   hover or keyboard focus, §30's .6s curve on both halves of it. A roster
   card that duotoned differently from the live strip would be a second
   photographic language on a two-colour site. */
.roster .has-photo .plate {
  position: relative;
  isolation: isolate;          /* the blend stays inside the plate */
  /* OPAQUE, and it is not a style choice — see §30 and §4b. Isolating the
     group means the orange blends against this ground and nothing else, so a
     translucent colour here leaves most of the blend uncomposited and a plate
     whose photo has not painted yet is a glowing #FF5A14 slab. */
  background: #0F1012;
}
.roster .has-photo .photo {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Faces sit high in a frame and the mark sits low over it, so the crop is
     pulled up off centre: 50% would put a chin behind the wordmark. */
  object-position: 50% 30%;
  filter: grayscale(1) contrast(1.05) brightness(.94);
  transition: filter .6s cubic-bezier(.2,.7,.2,1);
}
/* The crop belongs to the FILE; the card is only how this sheet can address
   it. 50% 30% above assumes a wide frame with the subject high in it, which
   is the common case and the safe default. Bassurgence's file is a square crop
   of a standing portrait, cut 21 Sep 2026 with his face about a third of the
   way down, and this line was re-measured when it was cut. It is written
   here rather than as a style attribute in index.html for two reasons: a
   crop hidden in a style attribute is invisible to anyone reading this
   section for how the photography works, and this is the page's second set
   of per-file object-positions — §30 tunes five the same way, for the same
   reason. REPLACING A PHOTOGRAPH MEANS RE-MEASURING ITS LINE HERE, exactly
   as swapping a live frame means re-measuring its fr in §30. */
.roster [data-artist="bassurgence"] .photo { object-position: 50% 35%; }
/* The wash: hue and saturation only, never a tint laid over live colour. */
.roster .has-photo .plate::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: var(--gl-orange); mix-blend-mode: color; opacity: .82;
  transition: opacity .6s cubic-bezier(.2,.7,.2,1);
  /* A BLENDED LAYER DOES NOT ALWAYS REPAINT WHEN ITS OPACITY ANIMATES.
     WebKit composites a mix-blend-mode layer with its backdrop group and can
     miss the re-render, which leaves the orange sitting there at full
     strength while the greyscale lifts off the photo underneath — the tint
     appears not to come off at all. Naming opacity here puts the layer on
     the compositor up front, so the fade is a compositor animation with
     nothing to invalidate. It costs one layer per frame and the belt is
     already promoted the same way. Chromium never needed it: measured
     identical at rest and on hover, before and after. */
  will-change: opacity;
}
/* The scrim, and it is measured rather than judged: a white mark has to stay
   legible over whatever the picture does at the foot of the frame, and the
   two frames it was checked against are not the ones the next card will
   bring. It sits ABOVE the wash so it darkens the finished duotone and not
   the raw photo, and it is the plate ground fading up rather than neutral
   grey, so the mark reads as sitting on the plate and not on a box drawn
   over it. */
.roster .has-photo .plate::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to top, rgba(15,16,18,.86) 0%, rgba(15,16,18,.35) 38%, rgba(15,16,18,0) 70%);
}
/* The mark drops to the bottom centre of the picture once there is a picture
   to sit on, which is where the name plate goes on a printed card. Smaller
   than the centred version because it is now type over a picture rather than
   the only thing in the frame. The two numbers under it are both measured and
   both worth reading before changing either. */
.roster .has-photo .mark {
  align-self: end;
  justify-self: center;
  position: relative; z-index: 2;
  /* height: auto, against the height: 100% the centred mark takes. Both marks
     are wider than they are tall, so they hit the width cap first and then
     letterbox inside a box that is still 30% of the plate tall — 92px of box
     around 62px of painted wordmark, which floats the mark 15px off the
     bottom edge it is supposed to be sitting on. Hugging the image makes the
     element box the mark and the measurement mean what it says. The height
     cap stays as the guard it was, for a squarer mark than these two. */
  height: auto;
  max-width: 64%; max-height: 30%;
  /* Clearance off the bottom rule, and the measured number is NOT 6% of the
     plate: percentage margins resolve against the containing block's WIDTH,
     and the plate's own 5% padding and 2px rule are under it. 6% of a 268.8px
     content box is 16.1px, plus 16.8px of padding and the rule = 34.9px, or
     10.2% of a 341px plate. Change the plate's proportion and this is the
     line to re-measure. */
  margin-bottom: 6%;
}
/* The whole front is one button, so the front is the hover target — the plate
   is not, and cannot be: it has no pointer of its own inside a control. Same
   behaviour as §30 all the same, one card lit at a time, and focus-visible
   gets it too so a keyboard reaches the colour the pointer does. */
/* The identity list, NOT `filter: none`. A filter list interpolating to
   `none` is the one case engines do not animate reliably: the greyscale
   drops off in a single step while the wash beside it fades over .6s, and
   the tint appears to snap rather than lift. Naming grayscale(0) contrast(1)
   brightness(1) keeps the two lists function-for-function compatible, so
   both halves of the duotone come off on the same curve. */
.roster .has-photo .front:hover .photo,
.roster .has-photo .front:focus-visible .photo { filter: grayscale(0) contrast(1) brightness(1); }
/* visibility is the guarantee behind the fade. will-change puts this layer
   on the compositor so the opacity animation repaints, but if an engine
   still misses it the wash would sit there at full strength for ever.
   Stepping visibility to hidden at the END of the fade (0s, delayed the
   full .6s) means the worst case is a late snap to untinted rather than a
   tint that never lifts — and on the way back it returns instantly, because
   the delay lives on the hover rule and not on the base one. */
.roster .has-photo .front:hover .plate::before,
.roster .has-photo .front:focus-visible .plate::before {
  opacity: 0;
  visibility: hidden;
  transition: opacity .6s cubic-bezier(.2,.7,.2,1), visibility 0s linear .6s;
}

@media (prefers-reduced-motion: reduce) {
  .roster .has-photo .photo,
  .roster .has-photo .plate::before { transition: none; }
}

/* The name block, and the flex went the other way round with the redesign:
   the plate takes the slack now and this is flex: none, only as tall as the
   three lines in it. Leaving both at flex: 1 would have split the leftover
   height between a picture and three lines of type that do not want any, and
   the plate would have stopped being the card. A 4px gap rather than margins
   on each row: three lines spaced by one declaration is one decision to
   change instead of three to keep in step. */
.roster .body {
  display: flex;
  flex-direction: column;
  flex: none;
  gap: 4px;
  padding: 12px 14px;
  min-height: 0;
}

/* Anton, and NO text-transform on either face. OSSIEN is the artist's own
   all-caps, Bassurgence is title case, and ALiV — elsewhere on this roster —
   has a deliberate lowercase i. The page never decides how a name is cased.
   BRAND.md §5, and it is a rule about people rather than about type. */
.roster .name {
  font-family: var(--gl-display); font-weight: 400;
  font-size: 28px; line-height: 0.95; letter-spacing: 0.005em;
  color: var(--gl-text-dark-bg);
}
/* Uppercase is fine here. A genre is a category, not somebody's name. */
.roster .genre {
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gl-orange);
}
/* THE THIRD CLASS NAME THAT WAS ALREADY TAKEN. This row shipped for one build
   as `.foot`, and the page footer is `<footer class="foot">` — §20 gives that
   a 3px orange top border and 48px of vertical padding, which landed on every
   card as an orange rule above the belt and a block of dead space under it.
   Same failure as `.card` and `.hero`, and the same lesson: on this page a
   new class is grepped before it is named. The row is `.tray` now — the only
   thing on the site that answers to it. */
.roster .tray {
  margin-top: 8px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
.roster .more {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg);
  transition: color .18s linear;
}
/* ── The belt ─────────────────────────────────────────────────────────
   Hazard stripes running left to right the full width of the card, the
   divider between the name block and the Details line, and breathing. The page's motion vocabulary is straight lines in motion — the
   pulse that runs down the schematic's trunk, the marquee — and this is the
   same vocabulary at card scale: a conveyor, which is what a roster is to a
   management company. Two animations, both cheap: the stripes are a
   repeating gradient on a layer twice the belt's width, translated by
   exactly one stripe period so the loop has no seam, and the opacity
   breathes on a slower cycle so the two never phase-lock into a visible
   beat. The period is 12px measured perpendicular to a 45° stripe, which is
   12 × √2 = 16.97px along the axis of travel — translate by anything else
   and the loop jumps. Transform and opacity only, so it composites.
   Hover on the front runs the belt faster: the card is being picked up.
   Reduced motion holds the stripes still. No script anywhere in it. */
.roster .belt {
  display: block;
  width: 100%;
  height: 8px;
  margin-top: 10px;
  position: relative;
  overflow: hidden;
}
.roster .belt::before {
  content: '';
  position: absolute; inset: 0;
  width: 200%;
  background: repeating-linear-gradient(-45deg, var(--gl-orange) 0 5px, transparent 5px 12px);
  animation: belt-run 1.4s linear infinite, belt-pulse 2.6s ease-in-out infinite;
  will-change: transform, opacity;
}
.roster .front:hover .belt::before,
.roster .front:focus-visible .belt::before { animation-duration: .7s, 2.6s; }
@keyframes belt-run   { from { transform: translateX(-16.97px); } to { transform: translateX(0); } }
@keyframes belt-pulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .roster .belt::before { animation: none; opacity: .8; }
}
.roster .front:hover .more,
.roster .front:focus-visible .more { color: var(--gl-orange); }

/* ── The back ─────────────────────────────────────────────────────────── */
.roster .back {
  display: flex;
  flex-direction: column;
  gap: var(--s8);
  padding: var(--s16);
}
/* §6 gives every <p> 16px of bottom margin. The back spaces itself with the
   flex gap above, so the margin is a second, uneven gap on every row. */
.roster .back p { margin: 0; }

.roster .back .name { font-size: 24px; }
/* The strip on the back clears the close button rather than running under it,
   so the serial never sits behind a 40px chip. */
.roster .back .strip { padding-right: 46px; }

.roster .close {
  position: absolute;
  top: 12px; right: 12px;
  width: 40px; height: 40px;
  display: grid;
  place-items: center;
  background: var(--gl-ink);
  border: 2px solid var(--gl-hairline-dark);
  color: var(--gl-text-dark-bg);
  cursor: pointer;
  transition: color .16s linear, border-color .16s linear;
}
.roster .close:hover,
.roster .close:focus-visible { border-color: var(--gl-orange); color: var(--gl-orange); }

.roster .career {
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--gl-orange);
  padding-bottom: 8px;
  border-bottom: 2px solid var(--gl-hairline-dark);
}

/* The stat line, and it is a real <table> because it is real tabular data:
   two measures across two seasons — listeners, and followers summed across the
   platforms the .basis line under it names — and a screen reader should be
   able to say "Listeners, 2026 · now, 26,214" rather than read a pile of spans.

   table-layout: fixed keeps the columns from re-flowing between the two cards
   — with auto layout OSSIEN's three rows and Bassurgence's one would compute
   different column widths and the two backs would stop matching. The first
   column is widened off the equal quarter because "2025 · signed" is 13
   monospace characters and the three number columns need seven.

   SEVEN, not the six the figures needed, because growth is a percentage now:
   `+1,987%` is the widest string the table carries and it paints 47.1px in a
   63.8px column at 340 — 16.7px of slack, and it still clears at the 345px
   phone column. Measured rather than eyeballed, because the failure is quiet:
   a growth cell that wraps turns one figure into two lines and drops that row
   out of line with the two above it. */
.roster .seasons {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
  font-family: var(--gl-mono);
  font-size: 11px;
}
.roster .seasons th,
.roster .seasons td {
  padding: 6px 0;
  text-align: right;
  border-bottom: 1px solid var(--gl-hairline-dark);
  font-weight: 400;
}
.roster .seasons th:first-child,
.roster .seasons td:first-child { text-align: left; }
.roster .seasons thead th:first-child { width: 37%; }
.roster .seasons thead th {
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg);
}
.roster .seasons tbody th { color: var(--gl-muted-dark-bg); }
.roster .seasons tbody td { color: var(--gl-text-dark-bg); }
/* The growth row IS the card, so it takes the accent. Orange on ink is
   5.69:1. A card with no at-signing reading has no growth row, by rule. */
.roster .seasons .growth td { color: var(--gl-orange); font-weight: 700; }
/* A cell with no reading behind it. A dash, muted, in body weight even on
   the growth row — the accent belongs to a number that was measured, and a
   dash is the card saying "not recorded", which is a different thing from
   zero and must never be typeset as one. */
.roster .seasons tbody td.na,
.roster .seasons .growth td.na { color: var(--gl-muted-dark-bg); font-weight: 400; }
/* The platforms a card's Followers sum is drawn from — the same set on the
   signed row and the now row, so the growth row divides like with like. Set
   like the column heads, because it is the column head's footnote. */
.roster .basis {
  margin: 6px 0 0;
  font-family: var(--gl-mono); font-weight: 400;
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg);
}

/* The label carries the platform set the figure sums, so it takes the same
   0.1em tracking as .basis, which does that job under the table. At 0.14em
   "STREAMS SINCE SIGNING = SPOTIFY + SOUNDCLOUD" ran 326px in a 313px column
   and wrapped, and on the one card whose sources line ALSO wraps that pushed
   the back to 0.707 — off 5:7, which §4d's first rule does not allow. The
   back's height budget allows exactly ONE wrapped line down here, which is
   why the fine print's row labels read "Signed / Now / Streams" and not
   "Signed row": eleven characters shorter fits the longest sources line on
   one row. Measured at 393px, all three backs are 345 x 483 = 0.7143. */
.roster .streams span {
  display: block;
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg);
}
.roster .streams b {
  font-family: var(--gl-mono); font-weight: 700;
  font-size: 16px; letter-spacing: 0.06em;
  color: var(--gl-orange);
}
.roster .role { font-size: 13px; line-height: 1.4; color: var(--gl-text-dark-bg); }
/* Hand-entered numbers go stale silently. The date and the two sources travel
   ON the card, not only under the grid, because the card is the thing that
   gets screenshotted. */
.roster .fine {
  font-family: var(--gl-mono); font-weight: 400;
  font-size: 9px; letter-spacing: 0.06em; line-height: 1.5;
  color: var(--gl-muted-dark-bg);
}
/* One fact per line — the date, the signed row's sources, the now row's —
   with the row label in bold, because a run-on line of sources at 9px is
   not readable on a card. */
.roster .fine span { display: block; }
.roster .fine b { font-weight: 700; color: inherit; }

/* THE ARTIST'S OWN PLATFORMS, where an "Artist page" pill used to be. The
   OneLink build is not finished, so a card that sent people to it was
   pointing at something unready; these point at accounts that exist. They
   are exactly the platforms the table above measures — Spotify, Instagram,
   SoundCloud — plus TikTok, so the card links what it counts.

   Same Simple Icons glyphs and the same orange-inverting hover as §.soc-row
   at the foot of the page, at 34px rather than 44. The 44px box is right
   down there where there is room; here it would cost 12px the back does not
   have at 5:7. 34 still clears the 24px minimum target and these sit in
   their own row with nothing to mis-hit. margin-left pulls the first glyph's
   box off the text edge so the icons line up with the type above — the same
   optical correction .contact-soc makes. margin-top: auto seats the row on
   the bottom of the back however tall it ends up, as the pill did. */
.roster .socials {
  margin: auto 0 0 -8px;
  display: flex;
  gap: 2px;
  list-style: none;
  padding: 0;
}
.roster .socials a {
  display: grid; place-items: center; width: 34px; height: 34px;
  color: var(--gl-orange); text-decoration: none;
  transition: color .16s linear, background-color .16s linear;
}
.roster .socials a:hover,
.roster .socials a:focus-visible { background: var(--gl-orange); color: var(--gl-ink); }
.roster .socials .soc { width: 18px; height: 18px; }

/* §8's icons are a 24 grid sized for body copy. Inside a 10px footer line and
   an 11px pill they have to come down to the type, not the other way round. */
.roster .more .ico { width: 14px; height: 14px; }
.roster .close .ico { width: 16px; height: 16px; }

/* The two controls that only exist because of the flip. Without script there
   is nothing to close and nothing to reveal, so they are hidden here and
   handed back under html.js below. */
.roster .close,
.roster .more { display: none; }

/* ── The flip — only when JS is present ───────────────────────────────────
   Everything below either positions a face absolutely, rotates it, or hides
   it, so all of it is gated. With the class missing the rules above stand on
   their own and the card is two stacked faces, both readable, both reachable.

   The back ships WITHOUT inert in index.html for exactly that reason — an
   inert back in the static markup would make the artist link unreachable for
   a visitor with script off, which is the failure the gate exists to prevent.
   site.v2.js sets inert on every .card .back at init instead, and moves it
   between the faces as the card turns. */
html.js .roster .card { perspective: 1400px; }
html.js .roster .faces {
  display: block;
  position: relative;
  width: 100%;
  aspect-ratio: 5 / 7;
  transform-style: preserve-3d;
  transition: transform .6s cubic-bezier(.2, .7, .2, 1);
}
html.js .roster .card.open .faces { transform: rotateY(180deg); aspect-ratio: auto; }

html.js .roster .face,
html.js .roster .front {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  overflow: hidden;
}
html.js .roster .back { transform: rotateY(180deg); }

/* BACKFACE CULLING IS NOT ENOUGH ON THESE CARDS, and the reason is in the
   plate: .has-photo .plate isolates a stacking context and its ::before
   blends with mix-blend-mode: color. A face whose subtree blends is
   composited as a group, and the group loses the cull — so through the back
   half of the turn the PHOTOGRAPH paints mirrored, under the stats, before
   the back settles. Both faces still declare backface-visibility above;
   this hides the one turning away as well, which fixes it without touching
   the duotone.

   The switch is a 0s step delayed to half the .6s turn. At .3s the card is
   edge-on and the outgoing face is a zero-width line, so there is nothing on
   screen at the moment it changes — the step is invisible by construction,
   where a fade would cross-dissolve two faces in full view. The same delay
   sits on both faces, so closing switches at the same angle as opening.
   Interaction needs nothing here: site.v2.js moves `inert` between the faces.

   The front keeps its border-color transition in the same shorthand — drop
   it and the hover edge snaps. */
html.js .roster .front { transition: border-color .18s linear, opacity 0s linear .3s; }
html.js .roster .back { transition: opacity 0s linear .3s; }
html.js .roster .card.open .front { opacity: 0; }
html.js .roster .card:not(.open) .back { opacity: 0; }

/* Open, the back leaves the absolute stack and becomes the thing that sizes
   the card: at least the 5:7 it flipped from, taller when the table and the
   fine print need it, and no inner scrollbar on a phone.

   overflow must stay VISIBLE here, and this is the trap the catalogue records
   in the same rule: a scroll container's automatic minimum height is zero, so
   an aspect-ratio box that is one never grows past its ratio — the back would
   silently clip its own last rows instead of pushing the card taller.
   clip-path does the job overflow: hidden was doing, keeping the faces inside
   the border while leaving the height free. */
html.js .roster .card.open .back {
  position: relative;
  aspect-ratio: 5 / 7;
  overflow: visible;
  clip-path: inset(0);
}

html.js .roster .more { display: inline-flex; }
html.js .roster .close { display: grid; }

/* Reduced motion: the card still turns over, it just does not rotate to do
   it. The face that is not showing crossfades out and stops taking the
   pointer, mirroring the catalogue's rule exactly. */
@media (prefers-reduced-motion: reduce) {
  html.js .roster .faces { transition: none; transform: none !important; }
  html.js .roster .back { transform: none; }
  html.js .roster .face { transition: opacity .2s linear; }
  html.js .roster .card:not(.open) .back { opacity: 0; pointer-events: none; }
  html.js .roster .card.open .front { opacity: 0; pointer-events: none; }
}

/* Hand-entered numbers go stale silently. Dating them turns an old figure
   into a historical fact instead of a false current claim. */
.as-of { margin-top: var(--s24); color: var(--gl-muted-dark-bg); }

/* ── 33. Growth Capital page ─────────────────────────────────────────────
   /growth is a document, not a brochure: an orange cover sheet, then a
   reading column with a contents index in the margin. Every class here is
   gc-; `.growth` belongs to §32's roster table. Kept from the first build:
   .ledger, .ps-note and the current-page nav rule. Gone with the card grids
   and the orange block they served: .cards-2 and the #campaigns seam. */

.ledger { list-style: none; margin: 0; padding: 0; border-bottom: 2px solid var(--gl-hairline-dark); }
.ledger > li { display: grid; gap: var(--s8) var(--s24); padding: var(--s16) 0; border-top: 2px solid var(--gl-hairline-dark); }
@media (min-width: 620px) { .ledger > li { grid-template-columns: minmax(140px, 180px) minmax(0, 1fr); } }
.ledger .lg-k {
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--gl-orange); padding-top: 4px;
}
.ledger h3 { margin-bottom: var(--s8); }
.ledger p { margin: 0; font-size: 16px; line-height: 1.5; color: var(--gl-text-dark-bg); max-width: 62ch; }
.ledger a { color: var(--gl-orange); }
.ps-note { margin-top: var(--s24); color: var(--gl-text-dark-bg); max-width: 62ch; }
.nav-links a[aria-current="page"] { color: var(--gl-orange); border-bottom-color: var(--gl-orange); }

/* A mono tag line under an h3, outside a card. Same measure as .card-tag. */
.gc-tag {
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg); margin: 0 0 var(--s8);
}

/* ── The cover: the page's ONE full-bleed orange section (BRAND §3.2).
   Everything on it is ink — 5.69:1 at every size. No bloom: the bloom is a
   light over concrete, and this page opens with a wall. No band elsewhere
   on the page, on purpose: under an orange cover a stripe would make orange
   the page rather than the signal on it. */
.gc-cover {
  background: var(--gl-orange); color: var(--gl-ink);
  padding-block: var(--s16) clamp(40px, 5vw, 64px);
  overflow: hidden;
  --gc-indent: 0px;
}
.gc-cover .rail { margin-bottom: clamp(32px, 4vw, 56px); }   /* stripes and both labels are ink via §6/§22 */
/* The site's focus ring is orange, which is the ground here. */
.gc-cover :focus-visible { outline-color: var(--gl-ink); }
.gc-title { color: var(--gl-ink); min-height: calc(2 * 0.88em); margin-bottom: clamp(24px, 3vw, 40px); }   /* two lines reserved for the swap */
.gc-title span { display: block; }
.gc-title .gc-l2 { padding-left: var(--gc-indent); }
.gc-cover-row { display: grid; grid-template-columns: var(--gc-indent) minmax(0, 1fr); }
.gc-cover-row > * { grid-column: 2; }              /* the lede starts where line two starts */
.gc-cover .lede { max-width: 58ch; color: var(--gl-ink); margin-bottom: var(--s24); }
.gc-cover-actions { display: flex; flex-wrap: wrap; gap: var(--s16); }
/* Cover metadata: the readout the marquee used to be, printed still. The
   hairline is --gl-hairline, the ink-at-26% token that exists for orange. */
.gc-meta { margin: clamp(32px, 4vw, 56px) 0 0; display: grid; gap: var(--s16) var(--s32); }
.gc-meta > div { border-top: 2px solid var(--gl-hairline); padding-top: var(--s12); }
.gc-meta dt {
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.16em; text-transform: uppercase; margin: 0 0 var(--s4);
}
.gc-meta dd { margin: 0; font-size: 15px; line-height: 1.45; }
@media (min-width: 560px) { .gc-meta { grid-template-columns: 1fr 1fr; } }
@media (min-width: 900px) {
  .gc-cover { --gc-indent: clamp(40px, 8vw, 128px); }
  .gc-meta { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

/* ── Contents: one list, shown in one place at any width. Below 1120 it is
   a block after the cover; from 1120 it is the sticky index beside the
   column and the block is display:none, so assistive tech never meets both. */
.gc-body { padding-block: var(--s16) clamp(40px, 5vw, 80px); }
.gc-toc { padding-block: var(--s24) var(--s8); }
.gc-toc .label, .gc-index .label { color: var(--gl-muted-dark-bg); margin-bottom: var(--s8); }
.gc-list { list-style: none; margin: 0; padding: 0; border-top: 2px solid var(--gl-hairline-dark); }
.gc-list a {
  display: flex; align-items: baseline; gap: var(--s12);
  padding: 14px 0 14px 12px;                       /* 45px rows — a real tap target */
  border-left: 2px solid transparent; border-bottom: 2px solid var(--gl-hairline-dark);
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px; line-height: 1.3;
  letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--gl-text-dark-bg); text-decoration: none;
  transition: color .16s linear, border-color .16s linear;
}
.gc-list b { color: var(--gl-orange); min-width: 2.4ch; }
.gc-list a:hover, .gc-list a:focus-visible, .gc-list a[aria-current] { color: var(--gl-orange); }
.gc-list a[aria-current] { border-left-color: var(--gl-orange); }
.gc-index { display: none; }
.gc-read { max-width: 840px; }
@media (min-width: 1120px) {
  .gc-toc { display: none; }
  .gc-grid { display: grid; grid-template-columns: 200px minmax(0, 1fr); column-gap: var(--s64); align-items: start; }
  .gc-index { display: block; position: sticky; top: calc(var(--nav-h) + var(--s24)); }
  .gc-body { padding-top: clamp(32px, 4vw, 56px); }
}

/* ── Sections: a hairline across the column, a numeral, a name, a title. */
.gc-sec { border-top: 2px solid var(--gl-hairline-dark); padding-top: clamp(32px, 4vw, 56px); scroll-margin-top: calc(var(--nav-h) + var(--s16)); }
.gc-sec + .gc-sec { margin-top: clamp(48px, 6vw, 96px); }
.gc-sec-head { display: flex; align-items: baseline; gap: var(--s16); margin: 0 0 var(--s16); }
.gc-sec-n {
  font-family: var(--gl-display); font-weight: 400; font-size: clamp(40px, 5vw, 64px);
  line-height: 0.88; letter-spacing: 0.005em; color: var(--gl-orange);
}
.gc-sec-t {
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.16em; text-transform: uppercase; color: var(--gl-muted-dark-bg);
}
.gc-sec > .d3 { color: var(--gl-text-dark-bg); margin-bottom: var(--s16); }
.gc-sec > .lede { max-width: 62ch; color: var(--gl-text-dark-bg); margin-bottom: clamp(24px, 3vw, 40px); }

/* ── Fig. G1: the attribution chain. Five boxes on one line at ≥1120, a
   column of boxes below it — a flowchart turned upright, not the homepage's
   trunk-and-branches spine. The connector is ONE pseudo carrying two
   background layers: the static hairline, and an orange pulse parked
   outside the box and driven along it by background-position in pixels
   (percentages cannot move a 100%-wide image). Runs without script; off
   under reduced motion. */
.gc-chain { margin: 0 0 clamp(24px, 3vw, 40px); --cyc: 4s; }
.gc-chain-row { list-style: none; margin: 0 0 var(--s16); padding: 0; display: flex; flex-direction: column; align-items: center; }
.gc-node { position: relative; width: 100%; max-width: 260px; display: flex; flex-direction: column; align-items: center; text-align: center; }
.gc-node + .gc-node { margin-top: 24px; }
.gc-node-k {
  display: block; width: 100%; height: 40px; line-height: 36px; padding: 0 8px;
  border: 2px solid var(--gl-hairline-dark); white-space: nowrap;
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--gl-text-dark-bg);
}
.gc-node[data-accent] .gc-node-k { border-color: var(--gl-orange); color: var(--gl-orange); }
.gc-node-n { display: block; margin-top: var(--s8); font-family: var(--gl-body); font-size: 13px; line-height: 1.3; color: var(--gl-muted-dark-bg); max-width: 22ch; }
.gc-node + .gc-node::before {
  content: ''; position: absolute; left: 50%; top: -24px; width: 2px; height: 24px; margin-left: -1px;
  background-image: linear-gradient(to bottom, rgba(255, 90, 20, 0), var(--gl-orange)),
                    linear-gradient(var(--gl-hairline-dark), var(--gl-hairline-dark));
  background-size: 100% 100%, 100% 100%; background-repeat: no-repeat;
  background-position: 0 24px, 0 0;                /* pulse parked below the box */
  animation: gc-run-y var(--cyc) linear infinite;
}
.gc-chain .gc-node-k { animation: sig-node var(--cyc) linear infinite; }                 /* §12b keyframes, reused */
.gc-chain .gc-node[data-accent] .gc-node-k { animation-name: sig-node-accent; }
.gc-node:nth-child(1) .gc-node-k { animation-delay: 0s; }
.gc-node:nth-child(2)::before { animation-delay: .12s; }  .gc-node:nth-child(2) .gc-node-k { animation-delay: .44s; }
.gc-node:nth-child(3)::before { animation-delay: .46s; }  .gc-node:nth-child(3) .gc-node-k { animation-delay: .78s; }
.gc-node:nth-child(4)::before { animation-delay: .80s; }  .gc-node:nth-child(4) .gc-node-k { animation-delay: 1.12s; }
.gc-node:nth-child(5)::before { animation-delay: 1.14s; } .gc-node:nth-child(5) .gc-node-k { animation-delay: 1.46s; }
@keyframes gc-run-y { 0% { background-position: 0 -24px, 0 0; } 8%, 100% { background-position: 0 24px, 0 0; } }
@keyframes gc-run-x { 0% { background-position: -28px 0, 0 0; } 8%, 100% { background-position: 28px 0, 0 0; } }
@media (min-width: 1120px) {
  .gc-chain-row { flex-direction: row; align-items: flex-start; }
  .gc-node { flex: 1 1 0; min-width: 0; max-width: none; }
  .gc-node + .gc-node { margin-top: 0; margin-left: 28px; }
  .gc-node + .gc-node::before {
    left: -28px; top: 19px; width: 28px; height: 2px; margin-left: 0;   /* box mid-height */
    background-image: linear-gradient(to right, rgba(255, 90, 20, 0), var(--gl-orange)),
                      linear-gradient(var(--gl-hairline-dark), var(--gl-hairline-dark));
    background-position: 28px 0, 0 0;
    animation-name: gc-run-x;
  }
}
@media (prefers-reduced-motion: reduce) {
  .gc-chain .gc-node-k, .gc-node + .gc-node::before { animation: none; }
}

/* ── The docket: the five steps as a printed job ticket. Dashed rules are
   the ticket's own language; everything else is the system's. */
.gc-docket { border: 2px solid var(--gl-hairline-dark); background: var(--gl-panel-dark); }
.gc-docket-head, .gc-docket-foot {
  display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--s4) var(--s16);
  margin: 0; padding: 12px 16px;
  font-family: var(--gl-mono); font-weight: 700; font-size: 12px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--gl-muted-dark-bg);
}
.gc-docket-rows { list-style: none; margin: 0; padding: 0; }
.gc-docket-rows > li {
  display: grid; grid-template-columns: 2.6ch minmax(0, 1fr) auto; grid-template-areas: "n h s" "n p p";
  gap: var(--s8) var(--s16); align-items: start;
  padding: var(--s16); border-top: 2px dashed var(--gl-hairline-dark);
}
.gc-docket-n { grid-area: n; font-family: var(--gl-mono); font-weight: 700; font-size: 12px; letter-spacing: 0.16em; color: var(--gl-orange); padding-top: 5px; }
.gc-docket-rows h3 { grid-area: h; }
.gc-docket-state {
  grid-area: s; justify-self: end; white-space: nowrap;
  font-family: var(--gl-mono); font-weight: 700; font-size: 11px;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--gl-text-dark-bg); border: 2px solid var(--gl-hairline-dark); padding: 6px 8px 5px;
}
.gc-docket-state[data-accent] { color: var(--gl-orange); border-color: var(--gl-orange); }
.gc-docket-rows p { grid-area: p; margin: 0; font-size: 16px; line-height: 1.5; color: var(--gl-text-dark-bg); max-width: 62ch; }
.gc-docket-foot { border-top: 2px dashed var(--gl-hairline-dark); justify-content: flex-start; color: var(--gl-text-dark-bg); }
.gc-docket-foot .gc-arrow { color: var(--gl-orange); }
.gc-rule { margin-top: clamp(32px, 4vw, 56px); }
.gc-rule .d3 { margin-bottom: var(--s16); }        /* .accent gives it orange on concrete, 5.69:1 */
.gc-rule p { max-width: 62ch; color: var(--gl-text-dark-bg); margin: 0; }

/* ── Three kinds of client: columns, not cards — no fill, no top border. */
.gc-three { display: grid; gap: var(--s24); border-top: 2px solid var(--gl-hairline-dark); padding-top: var(--s24); }
.gc-three h3 { margin-bottom: var(--s4); }
.gc-three p:last-child { margin: 0; font-size: 16px; line-height: 1.5; color: var(--gl-text-dark-bg); }
@media (max-width: 759px) { .gc-three > div + div { border-top: 2px solid var(--gl-hairline-dark); padding-top: var(--s24); } }
@media (min-width: 760px) { .gc-three { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s32); } }

/* ── Contact: one column, the form in the reading measure. */
.gc-contact .form { margin-top: clamp(24px, 3vw, 40px); }
.gc-contact-meta { margin-top: clamp(32px, 4vw, 48px); padding-top: var(--s24); border-top: 2px solid var(--gl-hairline-dark); }
.gc-contact-meta .contact-meta { margin-top: 0; }
