/* open-design-system-bench marketing docs: shared design system.
 *
 * Register: brand (these pages ARE the deliverable), on the product's own
 * dark palette so the docs and the tool it describes read as one thing.
 *
 * The organizing idea is a calibration certificate: the document that ships
 * with a precision instrument stating what was measured, by what method, and
 * with what tolerance. Consequences, applied everywhere below:
 *   - Monospace marks MEASURED VALUES only (scores, counts, code, run ids).
 *     Never labels, never headings, never decoration. Mono as meaning, not
 *     costume.
 *   - Running prose is set in a serif (Newsreader); the interface around it
 *     stays in the grotesque. A calibration certificate reads as a document,
 *     not as a dashboard, and the split makes "argument" and "chrome"
 *     distinguishable at a glance.
 *   - Rules organize real structure. No hairlines as ornament.
 *   - Authority comes from precision and type scale, not from boxes. Rows and
 *     hanging labels instead of uniform card grids.
 *   - One accent, reserved for wayfinding, links, and the verdict.
 *
 * Dark is the default identity (it matches the report.html the tool generates);
 * light is a designed second mode with its own re-picked accent and status
 * colours, selectable by OS preference or the masthead toggle. Print styles at
 * the bottom cover the one-pager going to a leadership meeting.
 */

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

/* Dark is this design's home: it matches the report.html the tool generates, so
 * a reader moving between a report and these docs stays in one world. Light is
 * a designed second mode, not an inversion; its neutrals are tinted toward the
 * same blue hue and its accent and status colours are re-picked because the dark
 * ones fail contrast on a near-white surface (#6C8CFF on white is 2.9:1).
 *
 * Resolution order, so a reader's explicit choice always wins:
 *   :root                                  -> dark (the default identity)
 *   prefers-color-scheme: light             -> light, unless [data-theme="dark"]
 *   [data-theme="light"] / ="dark"          -> the toggle, beats the OS both ways
 * Every value below is verified >= 4.5:1 against bg, surface, and surface-2.
 */
:root {
  color-scheme: dark;
  --bg: #0A0D16;
  --surface: #10151F;
  --surface-2: #161D2C;
  --border: #232B3F;
  --border-strong: #34405C;
  --ink: #E8ECF7;
  --muted: #8B95B3;
  --accent: #6C8CFF;
  --accent-ink: #9FB0FF;
  --green: #3DD68C;
  --yellow: #F5C542;
  --red: #F0576B;

  /* Categorical series colours. A documented exception to "one accent, one
     job": in a chart, colour is the encoding, not decoration, so five hues do
     work no single accent can. They are scoped to .chart and never appear in
     the interface.
     Taken in the fixed order blue/orange/aqua/yellow/violet and validated
     against both chart surfaces with the dataviz validator: every check passes
     in both modes, worst adjacent CVD separation 8.4 dark / 9.1 light against
     a >=8 target. Do not reorder or insert: the guarantee is a property of the
     order, and dropping a hue from the middle collapsed the aqua/magenta pair
     to 1.6 when this was first stepped. Status colours above are reserved and
     never used as a series. */
  --s1: #3987e5;
  --s2: #d95926;
  --s3: #199e70;
  --s4: #c98500;
  --s5: #9085e9;
  /* Tier band behind the chart. Needs more presence on a light surface. */
  --band: 0.07;
  --smoothing: antialiased;

  /* Geist for the interface, Geist Mono for measured values, Geist Pixel for
     the h1, Newsreader for running prose. Each cut has one job; nothing is
     bolted on for decoration. Real Geist instances are 400/500/600, so the
     scale below never asks for 650. */
  --sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono: "Geist Mono", "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  /* Newsreader, for paragraphs, ledes, and prose lists only.
   *
   * Why a serif here: these pages argue a position at length, and a reading
   * face signals "read this" the way a grotesque signals "operate this". It
   * also separates the two registers already on the page: the prose is the
   * claim, the sans-set tables, labels, and nav are the apparatus. Newsreader
   * specifically because it was cut for screens (low stroke contrast, sturdy
   * serifs that survive a dark background) and carries a real optical-size
   * axis, so the 21px lede and the 17px body get different drawings rather
   * than one drawing scaled.
   *
   * Constraints it imposes, handled at the rules below:
   *   - Headings stay on --sans/--display. A serif h2 under a pixel h1 is one
   *     voice too many.
   *   - Small UI text (asides, captions, table cells, catalog rows) stays on
   *     --sans; at 14px and below the serifs muddy on the dark surface.
   *   - Its "0" is narrower than Geist's, so the ch-based measure caps are
   *     re-measured below rather than carried over.
   */
  --serif: "Newsreader", Georgia, "Iowan Old Style", "Times New Roman", serif;

  /* Geist Pixel, used for the page h1 and nothing else.
   *
   * Why it belongs here: the whole argument of this project is that a design
   * system now has a machine reader. Setting the loudest element on the page in
   * a grid-native face says that in type instead of in a sentence. It also
   * pairs on a real contrast axis (pixel display against a neutral grotesque),
   * which is stronger than two similar sans faces.
   *
   * Constraints it imposes, all handled at the h1 rule below:
   *   - ONE static weight (400). Asking for 650 triggers synthetic bold, which
   *     smears the pixel grid. Never set a weight on it.
   *   - No negative tracking. Pixel stems collide.
   *   - Display sizes only. Below roughly 28px the grid turns to mush, so h2,
   *     h3, body, labels, and all UI stay on --sans.
   *   - Wider advance widths than a grotesque, so every change to the display
   *     clamp has to be re-checked against the longest h1 in the set, which is
   *     the landing page's. At the current clamp that headline sets in 3 lines
   *     at 1440px and 6 at 320px, with no word overflowing the column on any
   *     page. Raise it further and that is the measurement to repeat.
   */
  --display: "Geist Pixel", var(--sans);

  /* Hand-picked, not geometric: no single ratio runs through this ramp, and
     the earlier "~1.28" note here did not match the values below. Measured at
     a 16px root: 12 -> 14 -> 17 -> 19px steps at 1.167, 1.214, 1.118, which is
     deliberately tighter than any display scale because those four are four
     jobs at a similar size (micro metadata, captions, sans interface, serif
     prose) rather than rungs of a heading ladder. The jumps that do carry
     hierarchy are prose -> h2 (1.263 to 1.600 across the clamp) and h2 -> h1
     (1.640 to 1.853). No golden ratio anywhere; where a step lands near 1.618
     it is the clamp maximum coinciding, not a rule. */
  --t-display: clamp(2.46rem, 1.8rem + 2.66vw, 3.52rem);
  --t-h2: clamp(1.5rem, 1.25rem + 0.9vw, 1.9rem);
  --t-h3: 1.1875rem;
  --t-body: 1.0625rem;
  /* Prose is set larger than the interface, which is why --t-body (the sans UI
     baseline) and --t-prose are separate tokens. The reason is width: at 17px a
     line that fills the 928px column runs ~100 characters, far past readable,
     so the only way to give paragraphs a column that does not look starved next
     to a full-width table is to make the type bigger and let the measure grow
     with it. 19px Newsreader holds ~80 characters at 670px. --t-h3 moves up to
     match, or a sub-heading ends up smaller than the prose it introduces. */
  --t-prose: 1.1875rem;
  --t-small: 0.875rem;
  --t-micro: 0.75rem;

  --r-sm: 4px;
  --r-md: 8px;

  /* One easing, exponential ease-out. No bounce. Enter/exit and press
     feedback share it; hover colour uses the same curve at --fast. */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --fast: 140ms;
  --med: 220ms;
  --enter: 240ms;

  --z-sticky: 100;
  /* Measured in docs.js; this is the unwrapped masthead for no-JS readers. */
  --masthead-h: 2.9rem;
}

/* The light palette, declared once and applied through two selectors: the OS
   preference (unless the toggle has pinned dark) and the toggle itself. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    color-scheme: light;
    --bg: #F4F6FB;
    --surface: #FFFFFF;
    --surface-2: #EDF1F8;
    --border: #DCE3EF;
    --border-strong: #B9C3D6;
    --ink: #121722;
    --muted: #5A6478;
    --accent: #3350DD;
    --accent-ink: #2942C4;
    --green: #12784A;
    --yellow: #8A6100;
    --red: #C22B3F;
    --s1: #2a78d6;
    --s2: #eb6834;
    --s3: #1baf7a;
    --s4: #eda100;
    --s5: #4a3aa7;
    --band: 0.10;
    /* Antialiased thins type on light backgrounds; only dark needs it. */
    --smoothing: auto;
  }
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #F4F6FB;
  --surface: #FFFFFF;
  --surface-2: #EDF1F8;
  --border: #DCE3EF;
  --border-strong: #B9C3D6;
  --ink: #121722;
  --muted: #5A6478;
  --accent: #3350DD;
  --accent-ink: #2942C4;
  --green: #12784A;
  --yellow: #8A6100;
  --red: #C22B3F;
  --s1: #2a78d6;
  --s2: #eb6834;
  --s3: #1baf7a;
  --s4: #eda100;
  --s5: #4a3aa7;
  --band: 0.10;
  --smoothing: auto;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.7;
  -webkit-font-smoothing: var(--smoothing);
  text-rendering: optimizeLegibility;
}

/* Prose measure note: `ch` is the advance of the "0" glyph, never the average
   character, so a ch cap has to be re-measured whenever the prose face changes.
   In Geist "0" is 11.3px against a 7.8px average prose character, so the old
   52ch cap landed at ~75 real characters. Newsreader's "0" is 0.559em against a
   0.441em average prose character, so a ch cap buys fewer characters here and
   every number below was re-measured in the browser with a range walk over the
   rendered text.

   Prose is NOT capped: paragraphs, prose lists, and the lede fill the full
   928px column. This is a deliberate override of the 65-75 character guideline,
   made because a capped measure left paragraphs ending at 59% of a column whose
   tables and charts ran full width, and that ragged edge read as a bug rather
   than a decision. One edge for every element beats a correct measure that
   looks broken.

   What it costs, stated plainly so nobody has to re-measure it: at the 19px
   --t-prose the full column runs ~110 real characters per line. Two things pay
   that down. Prose is 19px rather than 17px, which removes about 13 characters
   from the line for free; and line-height is 1.75 rather than 1.62, because
   leading is the real mitigation for a long measure -- it is the return sweep
   that fails first, not the line itself. If a future reader wants the
   conventional measure back, the lever is `max-width` on the three rules below
   (63ch was ~82 characters at 76% of the column), not the type size.

   One content width on every page: 58rem == 928px at the default 16px root,
   and it matches .masthead-inner exactly, so the nav edge and the content edge
   line up. Kept in rem rather than hard px so the column still grows with the
   text if a reader raises their base font size.
   The measure is protected separately from the container, so widening the
   container gives figures, tables, and code the full 928px without stretching
   paragraphs past a readable line length. */
.wrap { max-width: 58rem; margin: 0 auto; padding: 0 1.75rem 6rem; }

/* ---------- Type ---------------------------------------------------------- */

h1, h2, h3 { margin: 0; line-height: 1.15; font-weight: 600; }

h1 {
  font-family: var(--display);
  font-size: var(--t-display);
  /* No weight declared: Geist Pixel ships 400 only and synthesises an ugly
     bold. No negative tracking: pixel stems touch. */
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.25;         /* pixel caps need more air than a grotesque */
  margin: 0 0 1rem;
}

h2 {
  font-size: var(--t-h2);
  letter-spacing: -0.02em;
  text-wrap: balance;
  margin: 3.5rem 0 0.75rem;
}

h3 { font-size: var(--t-h3); letter-spacing: -0.005em; }

/* Running prose: Newsreader. The selector list is deliberately narrow -- bare
   p, bare lists, and the lede. Everything with a class is apparatus (asides,
   captions, catalog rows, mono metadata) and is held on --sans by the rules
   further down, so a serif never has to hold up at 14px on this surface. */
p,
ul:not([class]),
ol:not([class]),
.lede {
  font-family: var(--serif);
  /* Newsreader ships the opsz axis; auto lets the 21px lede and the 17px body
     take different drawings instead of one drawing scaled. */
  font-optical-sizing: auto;
}

p {
  margin: 1.125rem 0;
  font-size: var(--t-prose);
  max-width: none;     /* prose fills the column; see the measure note above */
  line-height: 1.75;   /* long measure, so more leading to stop the eye dropping a line */
  text-wrap: pretty;
}

.lede {
  font-size: clamp(1.3125rem, 1.15rem + 0.6vw, 1.5625rem);
  line-height: 1.5;
  color: var(--muted);
  max-width: none;
  margin: 0 0 2rem;
  text-wrap: pretty;
}

.lede + .hero-actions { margin-top: -0.75rem; }

.hero-actions {
  margin: 0 0 2rem;
  font-family: var(--sans);
}

.cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--sans);
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1;
  color: var(--bg);
  background: var(--accent);
  text-decoration: none;
  text-decoration-color: transparent;
  border-radius: var(--r-sm);
  padding: 0.7rem 0.95rem;
  white-space: nowrap;
}
.cta:hover { color: var(--bg); background: var(--accent-ink); }
.cta svg {
  width: 1em;
  height: 1em;
  flex: none;
  display: block;
}

ul, ol { padding-left: 1.25rem; }
ul:not([class]), ol:not([class]) {
  font-size: var(--t-prose);
  max-width: none;
  line-height: 1.75;
}
li { margin: 0.5rem 0; }
li::marker { color: var(--muted); }

/* The three <p> elements that are apparatus rather than prose, pulled back to
   the grotesque: a sidenote, a group note, and a catalog row description are
   read at a glance, not read through, and 14px serif muddies on --surface.
   Captions, index descriptions, and draft bodies need no rule here -- they are
   not <p>, so they never inherit the serif in the first place. */
.aside p,
.group-note,
.evals .e-what {
  font-family: var(--sans);
}

strong { font-weight: 600; color: var(--ink); }
em { font-style: italic; }

/* Keeps hyphenated terms ("AI-ready") from breaking across lines in display type. */
.nobr { white-space: nowrap; }

a { color: var(--accent-ink); text-decoration-color: color-mix(in srgb, var(--accent-ink) 40%, transparent); text-underline-offset: 3px; }
a:hover { text-decoration-color: currentColor; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--r-sm); }

/* Mono is reserved for measured values, identifiers, and code. */
code, .num, .val {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

code {
  font-size: 0.86em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 0.1em 0.35em;
}

pre {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: 1rem 1.125rem;
  overflow-x: auto;
  margin: 1.25rem 0;
}
pre code { background: none; border: 0; padding: 0; font-size: 0.8125rem; line-height: 1.75; }

/* Config listings get three tokens and no new colours. "One accent, one job"
   rules out blue keys and green strings, so structure is carried by weight
   and annotation by the muted tone: keys firm, values plain, comments quiet.
   .cmt is an alias for .c so a snippet written either way highlights. */
pre .c, pre .cmt { color: var(--muted); }
pre .key { color: var(--ink); font-weight: 600; }
pre .str { color: var(--ink); }

/* ---------- Masthead and wayfinding ------------------------------------- */

/* The nav's active state is the only "you are here" signal the pages need.
   No per-page uppercase kicker: that was section grammar, not voice. */
.masthead {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  border-bottom: 1px solid var(--border);
  margin-bottom: 2.5rem;
}
@supports (backdrop-filter: blur(6px)) {
  .masthead { backdrop-filter: blur(8px); }
}

.masthead-inner {
  max-width: 58rem;
  margin: 0 auto;
  padding: 0.625rem 1.75rem;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
}

.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: 1.25rem;
  letter-spacing: 0;
  line-height: 1;
  color: var(--ink);
  text-decoration: none;
  flex: none;
}
.wordmark .mark {
  width: 0.72em;
  height: 0.72em;
  display: block;
  flex: none;
  overflow: visible;
  transform: translateY(0.03em);
}
.wordmark .mark-cursor,
.hero-lockup .mark-cursor {
  fill: currentColor;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.wordmark:hover { color: var(--accent-ink); }

/* Hero lockup: the mark at brand scale above the h1, so a page lifted out of
   this folder still says whose it is. Inline rather than one of the
   visuals/*.svg lockups because those carry baked hex fills and would ignore
   the theme toggle; this shares the masthead mark's path and currentColor
   treatment instead. --ink, not --accent: a logo is not wayfinding. */
.hero-lockup {
  display: flex;
  align-items: center;
  gap: 0.45em;
  font-family: var(--display);
  font-weight: 400;              /* Geist Pixel ships 400 only */
  /* Floored at 28px, below which the pixel grid turns to mush, and capped well
     under the h1 so the headline stays the loudest thing on the page. */
  font-size: clamp(1.75rem, 1.45rem + 1.2vw, 2.25rem);
  line-height: 1;
  letter-spacing: 0;
  color: var(--ink);
  margin: 0 0 1.75rem;
}
.hero-lockup .mark {
  width: 0.95em;
  height: 0.95em;
  flex: none;
  display: block;
  overflow: visible;
  transform: translateY(0.02em);
}

.docnav { display: flex; gap: 0.125rem 1rem; flex-wrap: wrap; font-size: var(--t-micro); }
.docnav a {
  color: var(--muted);
  text-decoration: none;
  padding: 0.25rem 0;
  border-bottom: 1px solid transparent;
}
.docnav a:hover { color: var(--ink); }
.docnav a[aria-current="page"] { color: var(--accent-ink); border-bottom-color: var(--accent); }

.theme-toggle {
  margin-left: auto;
  flex: none;
  font: inherit;
  font-size: var(--t-micro);
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 0.25rem 0.5rem;
  cursor: pointer;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--border-strong); }

/* ---------- Multi-series charts ----------------------------------------- */

/* Naming note: .mark means two unrelated things in this file. Under .wordmark
   and .hero-lockup it is the brand mark's <svg>; under .chart it is a plotted
   bar, which is the dataviz convention. Every rule for either is scoped to its
   parent and must stay that way -- a bare `.mark { }` rule would hit both. The
   chart bars cannot borrow .bar, which the composite chart already uses for
   horizontal bars with a different animation (bar-in scales X, bar-up scales
   Y). */

/* .chart rides on .plot for the box and for the enter-view hook, and adds what
   a five-series grouped bar needs. Grid and axes are solid hairlines one shade
   off the surface, never dashed: dashing reads as "threshold" when it is just a
   grid. All text stays on ink tokens; identity is carried by the swatch beside
   the label, never by colouring the text. */
.chart .chart-note {
  font-family: var(--sans);      /* bare p is serif; this is apparatus */
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--muted);
  margin: 0.25rem 0 0.875rem;
}
.chart svg { display: block; width: 100%; height: auto; min-width: 42.5rem; }
.chart svg text { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.chart .gridline { stroke: var(--border); stroke-width: 1; }
.chart .baseline { stroke: var(--border-strong); stroke-width: 1; }
.chart .tick { font-size: 10px; fill: var(--muted); }
.chart .cat { font-size: 11.5px; fill: var(--ink); font-weight: 600; font-family: var(--sans); }
.chart .val { font-size: 11px; fill: var(--muted); }

/* Legend is mandatory at five series, so identity is never colour-alone. */
.chart-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem 1.125rem;
  margin: 0 0 1rem;
  padding: 0;
  max-width: none;
}
.chart-legend li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: var(--t-micro);
  color: var(--muted);
}
.chart-legend .swatch {
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 2px;
  background: var(--sw);
  flex: none;
}

/* The table twin. Every value in these charts is reachable without colour and
   without hover, which is also what relieves the sub-3:1 contrast of the aqua
   and yellow slots on the light surface. */
.data-table { margin-top: 1rem; border-top: 1px solid var(--border); }
.data-table > summary {
  cursor: pointer;
  list-style: none;
  padding: 0.625rem 0 0;
  font-size: var(--t-micro);
  color: var(--muted);
}
.data-table > summary::-webkit-details-marker { display: none; }
.data-table > summary:hover { color: var(--ink); }
.data-table td.best { color: var(--green); }
.data-table td.bad { color: var(--red); }

/* ---------- Local page navigation --------------------------------------- */

/* landing.html carries five documents in one scroll, so it needs within-page
   wayfinding. The masthead nav is between-document wayfinding and stays as it
   is; this is the other axis. One markup tree, two presentations.

   Wide viewports get the list fixed in the left gutter. It is an overlay in
   the margin, never a grid column, so the 58rem content column and the
   masthead edge stay exactly where they are on every other page: the "one
   edge" rule survives a page that has a sidebar.

   Narrower than that there is no gutter to put it in, so the same list
   becomes a sticky strip under the masthead showing the parts only. It sticks
   to --masthead-h, which docs.js measures on load and resize because the
   masthead wraps to two rows on small screens; the value below is the
   unwrapped fallback for a no-JS reader. */

.pagenav {
  position: sticky;
  top: var(--masthead-h);
  z-index: calc(var(--z-sticky) - 1);
  margin: -2.5rem 0 2.25rem;      /* reclaim the masthead's own margin-bottom */
  padding: 0.375rem 1.75rem;
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  border-bottom: 1px solid var(--border);
}
@supports (backdrop-filter: blur(6px)) {
  .pagenav { backdrop-filter: blur(8px); }
}

.pagenav-list {
  list-style: none;
  display: flex;
  gap: 1.25rem;
  margin: 0 auto;
  padding: 0;
  max-width: 58rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.pagenav-list::-webkit-scrollbar { display: none; }
.pagenav-list > li { margin: 0; flex: none; }
.pagenav-sub { display: none; }

.pagenav a {
  display: block;
  color: var(--muted);
  text-decoration: none;
  font-size: var(--t-micro);
  white-space: nowrap;
  padding: 0.25rem 0;
  border-bottom: 1px solid transparent;
}
.pagenav a:hover { color: var(--ink); }

/* Strip mode hides the section links, so the part carries "you are here"
   whether the reading position is the part itself or a section inside it.
   Without the .is-active half, a reader inside any section (which is most of
   the page) would see no current state at all. */
.pagenav a[aria-current],
.pagenav-list > li.is-active > a { color: var(--accent-ink); border-bottom-color: var(--accent); }

/* Set by docs.js from the measured masthead: when the chrome is already deep,
   the strip gives up stickiness rather than stacking on top of it. Scoped
   under max-width so it can never fight the gutter rules below, which a bare
   class selector would win on specificity. */
@media (max-width: 81.9375rem) {
  :root.chrome-heavy .pagenav { position: static; }
}

/* 82rem is the width at which the gutter finally holds an 11rem column plus a
   1.5rem gap without touching the viewport edge. Below it, the strip. */
@media (min-width: 82rem) {
  .pagenav {
    position: fixed;
    top: calc(var(--masthead-h) + 2.25rem);
    right: calc(50% + 28.5rem);
    width: 11rem;
    max-height: calc(100vh - var(--masthead-h) - 5rem);
    overflow-y: auto;
    margin: 0;
    padding: 0;
    background: none;
    border-bottom: 0;
    backdrop-filter: none;
  }
  .pagenav-list {
    display: block;
    max-width: none;
    overflow: visible;
    border-left: 1px solid var(--border);
  }
  .pagenav a { white-space: normal; line-height: 1.35; border-bottom: 0; }
  .pagenav-list > li > a {
    font-size: var(--t-small);
    padding: 0.3125rem 0 0.3125rem 0.75rem;
    margin-left: -1px;
    border-left: 2px solid transparent;
  }
  /* Both levels are visible here, so they say different things: the part
     containing the reading position gets ink and a quiet rail, the exact line
     gets the accent. Equal specificity, so the accent rule stays second. */
  .pagenav-list > li.is-active > a { color: var(--ink); border-left-color: var(--border-strong); }
  .pagenav-list > li > a[aria-current] { color: var(--accent-ink); border-left-color: var(--accent); }

  /* Only the part being read shows its sections: 29 links at once is a
     directory, not a wayfinder. :focus-within keeps the collapsed ones
     reachable by keyboard. */
  .pagenav-list > li.is-active > .pagenav-sub,
  .pagenav-list > li:focus-within > .pagenav-sub { display: block; }
  .pagenav-sub {
    list-style: none;
    margin: 0.125rem 0 0.5rem;
    padding-left: 1.375rem;
  }
  .pagenav-sub > li { margin: 0; }
  .pagenav-sub a { padding: 0.1875rem 0; }
}

/* ---------- Standalone page ---------------------------------------------- */

/* landing.html ships without the masthead so it can be lifted out of this
   folder whole. Three things follow from having no chrome above the content.
   The page has to supply its own top air, since it was previously getting the
   masthead's height plus its 2.5rem margin. The strip's negative top margin,
   which existed to reclaim that margin, has to go or it pulls the nav off the
   top of the viewport. And the gutter nav aligns to the same --page-top as the
   content, so the sidebar and the lockup start on one line.

   Note what else leaves with the header: the theme toggle lived in it, so this
   page follows the stored or OS preference and offers no in-page switch. That
   is the right trade for a page meant to be pasted into someone else's shell,
   but it is a trade. */
body.standalone { --page-top: clamp(4rem, 3rem + 4vw, 7rem); }
body.standalone .wrap { padding-top: var(--page-top); }
body.standalone .pagenav { margin-top: 0; }
@media (min-width: 82rem) {
  body.standalone .pagenav { top: var(--page-top); }
  body.standalone .pagenav { max-height: calc(100vh - var(--page-top) - 3rem); }
}

/* The sticky masthead (and, in strip mode, the strip) would otherwise sit on
   top of whatever a #hash jump lands on. */
[id] { scroll-margin-top: calc(var(--masthead-h) + 3.75rem); }
@media (min-width: 82rem) {
  [id] { scroll-margin-top: calc(var(--masthead-h) + 1.5rem); }
}

/* ---------- Parts of a combined page ------------------------------------ */

/* One rule per part, because the parts are real structure: each was a separate
   document before this page existed. h2 names the part, h3 names a section
   inside it, and the two are told apart by size and by face (h3 lands at prose
   size but stays on the grotesque while the prose around it is serif). */
.part { margin-top: 4.5rem; padding-top: 1.75rem; border-top: 1px solid var(--border-strong); }
.part > h2 { margin-top: 0; }
.part > h3 { margin: 2.75rem 0 0.5rem; }
.part > .group:first-of-type { margin-top: 1.5rem; }




/* An editorial aside: hanging label over a top rule. No side stripe. */
.aside {
  margin: 2rem 0;
  padding-top: 0.875rem;
  border-top: 1px solid var(--border-strong);
  max-width: none;
}
.aside > .aside-label {
  display: block;
  font-size: var(--t-micro);
  font-weight: 600;
  color: var(--accent-ink);
  margin-bottom: 0.25rem;
}
.aside p { margin: 0.375rem 0; font-size: var(--t-small); color: var(--muted); }
.aside p strong { color: var(--ink); }

/* ---------- Measurement block ------------------------------------------- */

/* Replaces the 4-up stat-tile row. One bordered block, hairline-divided,
   values in mono because they are measured. Reads as an instrument summary,
   not as four SaaS metric cards. */
.measures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  /* The container's colour shows through the 1px gap, so dividers appear in
     both axes and stay correct however the grid wraps. A border-left on each
     cell leaves a stray rule at the start of every wrapped row. */
  gap: 1px;
  background: var(--border);
  margin: 0 0 2.5rem;
  overflow: hidden;
}
.measure { padding: 0.875rem 1.125rem; background: var(--surface); }
.measure .m-val {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  display: block;
}
.measure .m-key { font-size: var(--t-micro); color: var(--muted); display: block; margin-top: 0.125rem; }

/* ---------- Figures ------------------------------------------------------ */

figure { margin: 2.25rem 0; }

.plot,
.diagram {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface);
  padding: 1.25rem 1.375rem 0.75rem;
}

/* A schematic, not a reading, so it gets the plot's box and none of its
   motion: nothing here counts up or grows from an origin. Node labels are
   labels, so they stay on the grotesque; .m marks the spans that are actually
   measured or are identifiers, which is what mono is reserved for. Sans also
   keeps the sub-labels inside their boxes, where mono would overrun them. */
/* The floor is on the box, not the svg, so a .scroll-x wrapper scrolls the
   whole bordered figure instead of letting the drawing escape its padding. */
.diagram { padding-bottom: 1.25rem; min-width: 44rem; }
.diagram svg { display: block; width: 100%; height: auto; color: var(--ink); }
.diagram svg text { font-family: var(--sans); }
.diagram svg text.m { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.plot-title { font-size: var(--t-h3); font-weight: 600; margin: 0; }
.plot-method { font-size: var(--t-micro); color: var(--muted); margin: 0.1875rem 0 0.875rem; font-family: var(--mono); }
.plot svg { display: block; width: 100%; height: auto; overflow: visible; }
.plot .band { opacity: var(--band); }
.plot svg text { font-family: var(--mono); }

figcaption { color: var(--muted); font-size: var(--t-small); line-height: 1.6; margin-top: 0.75rem; }
figcaption strong { color: var(--ink); }

/* Snapattern-style field plates. Dark stays dark in both themes: these are
   instrument faces, not inverted illustrations. */
.hero-field,
.visual-trio {
  margin: 1.25rem 0 2.5rem;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: #0A0D16;
}
.hero-field img,
.visual-trio img {
  display: block;
  width: 100%;
  height: auto;
}
.visual-trio { background: #F4F6FB; }

/* Square plates cropped to the column's 16:9 so a mid-article figure
   does not become a 928px-tall billboard. Use the -plain SVGs here so a
   baked-in label is not chopped. */
.hero-field.crop img {
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  object-fit: cover;
  object-position: center center;
}

.visual-plates {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem;
  margin: 1.5rem 0 2.5rem;
}
.visual-plates.pair {
  grid-template-columns: repeat(2, 1fr);
}
.visual-plates figure {
  margin: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: #0A0D16;
}
.visual-plates img { display: block; width: 100%; height: auto; }
@media (max-width: 40rem) {
  .visual-plates,
  .visual-plates.pair { grid-template-columns: 1fr; }
}

/* ---------- Tables ------------------------------------------------------- */

.scroll-x { overflow-x: auto; max-width: 100%; margin: 1.25rem 0; }
table { width: 100%; border-collapse: collapse; font-size: var(--t-small); }
th, td { text-align: left; padding: 0.625rem 0.875rem; border-bottom: 1px solid var(--border); vertical-align: top; }
thead th { color: var(--muted); font-weight: 500; font-size: var(--t-micro); border-bottom-color: var(--border-strong); white-space: nowrap; }
tbody tr:last-child td { border-bottom: 0; }
td.num { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }

/* ---------- Verdict chip ------------------------------------------------- */

/* The accent's real job: the tier verdict. Never decoration. */
.tier { display: inline-block; font-size: var(--t-micro); font-weight: 600; padding: 0.125rem 0.5rem; border-radius: var(--r-sm); border: 1px solid; white-space: nowrap; }
.tier-native { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, transparent); background: color-mix(in srgb, var(--green) 12%, transparent); }
.tier-invested { color: var(--yellow); border-color: color-mix(in srgb, var(--yellow) 45%, transparent); background: color-mix(in srgb, var(--yellow) 12%, transparent); }
.tier-emerging { color: var(--red); border-color: color-mix(in srgb, var(--red) 45%, transparent); background: color-mix(in srgb, var(--red) 12%, transparent); }
.tier-shipped { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, transparent); background: color-mix(in srgb, var(--green) 12%, transparent); }
.tier-planned { color: var(--muted); border-color: var(--border-strong); background: var(--surface-2); }

/* ---------- Index: an editorial list, not a card grid ------------------- */

.index-list { list-style: none; padding: 0; margin: 0.5rem 0 0; max-width: none; }
.index-list > li { margin: 0; border-top: 1px solid var(--border); }
.index-list > li:last-child { border-bottom: 1px solid var(--border); }

.index-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.5rem 1.5rem;
  align-items: baseline;
  padding: 1.125rem 0.25rem;
  text-decoration: none;
  color: inherit;
}

@media (hover: hover) and (pointer: fine) {
  .index-row:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
  .index-row:hover .index-title { color: var(--accent-ink); }
}

.index-title { font-size: 1.1875rem; font-weight: 600; letter-spacing: -0.015em; }
.index-desc { grid-column: 1; color: var(--muted); font-size: var(--t-small); line-height: 1.55; margin-top: 0.1875rem; }
.index-kind { font-size: var(--t-micro); color: var(--muted); font-family: var(--mono); white-space: nowrap; }

/* The lead entry is bigger. Varied rhythm beats a uniform grid. */
.index-list > li.lead .index-title { font-size: 1.5rem; letter-spacing: -0.02em; }
.index-list > li.lead .index-row { padding: 1.5rem 0.25rem; }

@media (max-width: 34rem) {
  .index-row { grid-template-columns: 1fr; }
  .index-kind { grid-row: 1; }
}

/* ---------- Eval catalog: grouped rows, not 19 identical cards --------- */

.group { margin: 2.5rem 0; }
.group-head { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-strong); }
.group-head h2, .group-head h3 { margin: 0; font-size: 1.25rem; }
.group-note { font-size: var(--t-small); color: var(--muted); margin: 0.625rem 0 0; }

.evals { list-style: none; padding: 0; margin: 0; max-width: none; }
.evals > li { display: grid; grid-template-columns: 2.25rem 1fr; gap: 0 0.75rem; padding: 0.875rem 0; border-bottom: 1px solid var(--border); }
.evals > li:last-child { border-bottom: 0; }
.evals .e-n { font-family: var(--mono); font-size: var(--t-small); color: var(--accent-ink); padding-top: 0.1875rem; }
.evals .e-name { font-weight: 600; font-size: 1rem; }
.evals .e-what { color: var(--muted); font-size: var(--t-small); line-height: 1.55; margin: 0.1875rem 0 0; }

/* ---------- Social posts: composed drafts ------------------------------ */

.draft { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); padding: 1.25rem 1.375rem; margin: 1.5rem 0; }
.draft-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; padding-bottom: 0.625rem; border-bottom: 1px solid var(--border); }
.draft-head h2 { margin: 0; font-size: 1.125rem; }
.draft-meta { font-size: var(--t-micro); color: var(--muted); font-family: var(--mono); }
.draft-body { white-space: pre-wrap; font-size: 0.9375rem; line-height: 1.65; }
.draft-body p { max-width: none; }

.thread { list-style: none; padding: 0; margin: 0; max-width: none; }
.thread > li { display: grid; grid-template-columns: 2rem 1fr; gap: 0 0.625rem; padding: 0.875rem 0; border-top: 1px dashed var(--border); }
.thread > li:first-child { border-top: 0; padding-top: 0; }
.thread .t-n { font-family: var(--mono); font-size: var(--t-small); color: var(--accent-ink); }
.thread .t-body { white-space: pre-wrap; font-size: 0.9375rem; line-height: 1.6; }

/* ---------- FAQ ---------------------------------------------------------- */

.qa { border-top: 1px solid var(--border); }
.qa:last-of-type { border-bottom: 1px solid var(--border); }
.qa summary {
  cursor: pointer;
  padding: 1rem 0.25rem;
  font-weight: 600;
  font-size: 1.0625rem;
  letter-spacing: -0.01em;
  list-style: none;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
}
.qa summary::-webkit-details-marker { display: none; }
.qa summary:hover { color: var(--accent-ink); }
.qa summary::after {
  content: "";
  flex: none;
  width: 0.625rem; height: 0.625rem;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: rotate(45deg) translateY(-0.125rem);
}
.qa[open] summary::after { transform: rotate(225deg) translateY(-0.125rem); }
.qa[open] summary { color: var(--ink); }
.qa .answer { padding: 0 0.25rem 1.125rem; }
.qa .answer p { margin: 0; color: var(--muted); }
.qa .answer p strong { color: var(--ink); }

/* ---------- Lines: the positioning one-liners --------------------------- */

.lines { list-style: none; padding: 0; margin: 1rem 0 0; max-width: none; }
.lines li { font-size: 1.1875rem; line-height: 1.45; letter-spacing: -0.01em; padding: 0.875rem 0; border-bottom: 1px solid var(--border); }
.lines li:last-child { border-bottom: 0; }

/* ---------- Footer ------------------------------------------------------- */

.foot { margin-top: 4rem; padding-top: 1.25rem; border-top: 1px solid var(--border); color: var(--muted); font-size: var(--t-small); }
.foot .foot-meta { font-family: var(--mono); font-size: var(--t-micro); }
.foot.repo {
  margin-top: 4.5rem;
  padding-top: 1.75rem;
  border-top-color: var(--border-strong);
}

/* ---------- Motion ------------------------------------------------------- */

/* The pages are a calibration certificate. Motion is the reading being taken,
   not decoration: values fill in, bars grow from the origin, a list arrives
   as you reach it. Hover and press are the only recurring motion, and they
   stay under 220ms. Everything below is gated on no-preference so a reduced-
   motion, print, or no-JS reader sees the already-final default. */

@media (prefers-reduced-motion: no-preference) {
  html:has(.pagenav) { scroll-behavior: smooth; }

  .wordmark {
    transition: color var(--fast) var(--ease);
  }
  .docnav a,
  .pagenav a {
    transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
  }
  .theme-toggle {
    transition:
      color var(--fast) var(--ease),
      border-color var(--fast) var(--ease),
      transform 120ms var(--ease);
  }
  .theme-toggle:active { transform: scale(0.97); }
  .cta {
    transition: background var(--fast) var(--ease), transform 120ms var(--ease);
  }
  .cta:active { transform: scale(0.97); }

  .qa summary {
    transition: color var(--fast) var(--ease);
  }
  .qa summary::after {
    transition: transform var(--med) var(--ease);
  }
  .qa .answer.is-opening {
    animation: settle var(--enter) var(--ease) both;
  }

  /* The row leans in on hover. Transform a child, never the hit area, so the
     cursor cannot leave the parent and flicker. Colour/background hover lives
     outside this query so reduced-motion still gets the state, just not the
     travel. */
  .index-row {
    transition: background var(--fast) var(--ease);
  }
  .index-title, .index-desc {
    transition: transform var(--med) var(--ease), color var(--fast) var(--ease);
  }
  @media (hover: hover) and (pointer: fine) {
    .index-row:hover .index-title,
    .index-row:hover .index-desc { transform: translateX(6px); }
  }

  /* JS marks html.js before first paint. Without it, every reading is already
     in its final state. With it, the not-yet-read state holds the from-values
     until IntersectionObserver adds is-read / is-in. */
  html.js .measures:not(.is-read) .m-val { opacity: 0; }

  html.js .plot:not(.is-read) .bar {
    transform: scaleX(0);
    transform-box: fill-box;
    transform-origin: left center;
  }
  html.js .plot:not(.is-read) .band { opacity: 0; }
  html.js .plot:not(.is-read) .plot-reading { opacity: 0; }

  .plot.is-read .bar {
    transform-box: fill-box;
    transform-origin: left center;
    will-change: transform;
    animation: bar-in 520ms var(--ease) both;
  }
  .plot.is-read .bars .bar:nth-child(1) { animation-delay: 40ms; }
  .plot.is-read .bars .bar:nth-child(2) { animation-delay: 110ms; }
  .plot.is-read .bars .bar:nth-child(3) { animation-delay: 180ms; }
  .plot.is-read .bars .bar:nth-child(4) { animation-delay: 250ms; }
  html.js .chart:not(.is-read) .mark { transform: scaleY(0); }
  .chart.is-read .mark {
    transform-box: fill-box;
    transform-origin: center bottom;
    animation: bar-up 520ms var(--ease) both;
  }
  html.js .chart:not(.is-read) .val { opacity: 0; }
  .chart.is-read .val { animation: reading-in 200ms var(--ease) 320ms both; }

  .plot.is-read .band {
    animation: band-in 400ms var(--ease) 80ms both;
  }
  .plot.is-read .plot-reading {
    animation: reading-in 200ms var(--ease) 380ms both;
  }

  html.js .index-list:not(.is-in) > li,
  html.js .lines:not(.is-in) > li,
  html.js .thread:not(.is-in) > li {
    opacity: 0;
    transform: translateX(-6px);
  }
  html.js .draft:not(.is-in) {
    opacity: 0;
    transform: translateY(8px);
  }

  .index-list.is-in > li,
  .lines.is-in > li,
  .thread.is-in > li {
    animation: row-in var(--enter) var(--ease) both;
  }
  .draft.is-in {
    animation: settle 250ms var(--ease) both;
  }

  .index-list.is-in > li:nth-child(1),
  .lines.is-in > li:nth-child(1),
  .thread.is-in > li:nth-child(1) { animation-delay: 0ms; }
  .index-list.is-in > li:nth-child(2),
  .lines.is-in > li:nth-child(2),
  .thread.is-in > li:nth-child(2) { animation-delay: 45ms; }
  .index-list.is-in > li:nth-child(3),
  .lines.is-in > li:nth-child(3),
  .thread.is-in > li:nth-child(3) { animation-delay: 90ms; }
  .index-list.is-in > li:nth-child(4),
  .lines.is-in > li:nth-child(4),
  .thread.is-in > li:nth-child(4) { animation-delay: 135ms; }
  .index-list.is-in > li:nth-child(5),
  .thread.is-in > li:nth-child(5) { animation-delay: 180ms; }
  .index-list.is-in > li:nth-child(6),
  .thread.is-in > li:nth-child(6) { animation-delay: 225ms; }
  .index-list.is-in > li:nth-child(7),
  .thread.is-in > li:nth-child(7) { animation-delay: 270ms; }

  @keyframes bar-in { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  @keyframes bar-up { from { transform: scaleY(0); } to { transform: scaleY(1); } }
  @keyframes band-in { from { opacity: 0; } to { opacity: var(--band); } }
  @keyframes reading-in { from { opacity: 0; } to { opacity: 1; } }
  @keyframes settle {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: none; }
  }
  @keyframes row-in {
    from { opacity: 0; transform: translateX(-6px); }
    to { opacity: 1; transform: none; }
  }
}

/* ---------- Print: the one-pager goes to a meeting ---------------------- */

@media print {
  :root { --bg: #fff; --surface: #fff; --surface-2: #f4f4f6; --border: #c9ccd6; --border-strong: #9aa0b0; --ink: #101218; --muted: #4a4f5e; --accent: #24439c; --accent-ink: #24439c; }
  body { background: #fff; color: var(--ink); font-size: 11pt; line-height: 1.5; }
  .masthead, .docnav, .pagenav { display: none; }
  .part { border-top: 1px solid var(--border-strong); margin-top: 24pt; padding-top: 12pt; break-before: auto; }
  .wrap { max-width: none; padding: 0; }
  /* body.standalone .wrap outranks the reset above (0,2,1 vs 0,1,0), so the
     screen page's top air has to be cleared explicitly or it prints as about
     an inch of blank. */
  body.standalone .wrap { padding-top: 0; }
  /* Back to the grotesque on paper: a pixel face loses its grid at print DPI,
     and the webfont may not be available when the print job runs. */
  h1 { font-family: var(--sans); font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; font-size: 24pt; }
  .hero-lockup { font-family: var(--sans); font-weight: 600; font-size: 13pt; }
  h2 { font-size: 14pt; margin-top: 18pt; break-after: avoid; }
  .plot, .diagram, .measures, .draft, .hero-field, .visual-trio { break-inside: avoid; }
  .chart .mark, .chart .val { animation: none; transform: none; opacity: 1; }
  .data-table > summary { display: none; }
  .data-table[open] > summary { display: block; }
  a { color: var(--ink); text-decoration: underline; }
  .cta { color: #fff; background: var(--accent); }
  .hero-actions, .foot.repo { break-inside: avoid; }
  figure, table, .measures { break-inside: avoid; }
  .plot .bar, .plot .band, .plot-reading,
  .index-list > li, .lines > li, .thread > li, .draft, .qa .answer {
    animation: none;
    transform: none;
    opacity: 1;
  }
}
