/* site.css — everything that was inline <style> on the three pages.
 *
 * WHY THIS IS A FILE AND NOT A <style> BLOCK
 *
 * A Content-Security-Policy that permits inline styles has to say
 * style-src 'unsafe-inline', which switches off the protection it exists to
 * provide. Externalising the CSS (and the clock script into site.js) lets the
 * policy be simply default-src 'self' with no exceptions at all, which is what
 * a security reviewer will actually want to see.
 *
 * The union of all three pages' rules lives here. A handful are unused on any
 * given page; that costs a few hundred bytes once, against three round trips
 * and no shared cache.
 */

:root { color-scheme: dark; }
html, body { background: #05070c; }
body {
  color: #c7d5e8;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Telemetry numerics must align vertically. */
.font-mono, code, kbd, samp, pre, .tnum {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "zero" 1;
}

/* No soft shadows anywhere — crisp borders only. */
* { box-shadow: none !important; }

/* Background void: structural grid lines + faint logo watermark. */
.void-grid {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(26,39,64,0.55) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(26,39,64,0.55) 1px, transparent 1px);
  background-size: 96px 96px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 78%);
}

/* JPEG, not PNG: the mark has no transparency, and mix-blend-mode:screen makes
 * its black ground drop out anyway. At 3.5% opacity behind a radial mask the
 * compression is not observable, and it costs 39 KB instead of the 1.3 MB
 * master in logo-bg.png.
 *
 * THE ?v= IS A CACHE KEY, NOT A FILENAME. This file's bytes were replaced on
 * 2026-09-06 while it was still being served under the previous
 * max-age=31536000, immutable header. `immutable` tells a browser not to
 * revalidate even on reload, so every visitor from before that date had the old
 * mark pinned for up to a year and no change to the header could reach them --
 * a corrected header only applies to a client that asks again, and those had
 * been told not to ask. A browser caches by full URL, so the query is what
 * actually breaks the tie. Bump it whenever these bytes change under a name
 * that stays the same. */
.void-watermark {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.04;
  background-image: url('./logo-512.jpg?v=2');
  background-repeat: no-repeat;
  background-position: center 42%;
  background-size: min(78vw, 900px) auto;
  mix-blend-mode: screen;
}
.void-watermark.soft { opacity: 0.035; background-position: center 38%; }

/* ── Page-size control ─────────────────────────────────────────────────────
 * Lives in the status rail. Styled as three bordered cells so it reads as an
 * instrument control alongside the clock rather than as browser chrome bolted
 * on. The middle cell is both the readout and the reset button — a separate
 * reset would be a fourth cell in a strip that is already competing for room on
 * a narrow viewport.
 *
 * Hidden by default and revealed by site.js. If zoom.js failed to load, or the
 * page opted out with data-tool, the control would otherwise sit there doing
 * nothing, and a dead control is worse than no control.
 */
.zoomctl { display: none; align-items: stretch; border: 1px solid #1a2740; }
.zoomctl.ready { display: inline-flex; }
.zoomctl button {
  background: transparent; border: none; border-right: 1px solid #1a2740;
  cursor: pointer; color: #5a7090;
  font-family: "JetBrains Mono", monospace; font-size: 10px; line-height: 1;
  padding: 3px 6px;
  transition: color 120ms linear, background-color 120ms linear;
}
.zoomctl button:last-child { border-right: none; }
.zoomctl button:hover:not(:disabled) { color: #c7d5e8; background: #0d1420; }
.zoomctl button:disabled { color: #24365a; cursor: default; }
.zoomctl .val {
  min-width: 5ch; text-align: center; color: #c7d5e8;
  font-variant-numeric: tabular-nums; letter-spacing: 0.04em;
}
/* A non-default size is a state the reader should be able to see at a glance,
 * so that "why is this page bigger than the last one" has a visible answer. */
.zoomctl .val.on { color: #38e1c9; }

.rule-t { border-top: 1px solid #1a2740; }
.rule-b { border-bottom: 1px solid #1a2740; }

/* Hairline grids: cell backgrounds separated by 1px of grid colour. */
.hairline-grid { background-color: #1a2740; gap: 1px; }

.tick-label { letter-spacing: 0.18em; text-transform: uppercase; }

.scan-bar {
  background-image: linear-gradient(90deg, #38e1c9 0%, #38e1c9 18%, #1a2740 18%, #1a2740 100%);
}

::selection { background: #38e1c9; color: #05070c; }

/* Compact, precise scrollbar */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: #05070c; }
::-webkit-scrollbar-thumb { background: #1a2740; border: 2px solid #05070c; }
::-webkit-scrollbar-thumb:hover { background: #24365a; }

:focus-visible { outline: 2px solid #38e1c9; outline-offset: 2px; }

/* Code blocks scroll inside themselves; the page body never scrolls sideways. */
pre.code {
  overflow-x: auto;
  border: 1px solid #1a2740;
  background: #0a0f18;
  padding: 12px 14px;
  font-size: 11.5px;
  line-height: 1.65;
  color: #c7d5e8;
}
pre.code .k { color: #38e1c9; }   /* key     */
pre.code .s { color: #9db4d4; }   /* string  */
pre.code .n { color: #ffb547; }   /* number  */
pre.code .c { color: #5a7090; }   /* comment */

.tbl-wrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: 12px; }
table.data th, table.data td {
  border: 1px solid #1a2740; padding: 7px 10px; text-align: left; vertical-align: top;
}
table.data thead th {
  background: #0d1420; font-family: "JetBrains Mono", monospace;
  font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; color: #5a7090;
  font-weight: 500; white-space: nowrap;
}
table.data tbody tr { background: #0a0f18; }

a.lnk {
  color: #38e1c9; text-decoration: underline;
  text-decoration-color: rgba(56,225,201,0.4); text-underline-offset: 2px;
}
a.lnk:hover { text-decoration-color: #38e1c9; }

.verb {
  font-family: "JetBrains Mono", monospace; font-size: 9.5px; letter-spacing: 0.12em;
  border: 1px solid #1a2740; padding: 1px 6px; color: #38e1c9; white-space: nowrap;
}

/* ── Stress-test disclosure panels ─────────────────────────────────────────
 * Native <details>/<summary>: no JavaScript, keyboard-operable and
 * screen-reader-announced for free, and it still works with scripting off. */
details.st { background: #0a0f18; border: 1px solid #1a2740; }
details.st + details.st { border-top: none; }
details.st > summary {
  list-style: none; cursor: pointer; padding: 9px 12px;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
details.st > summary::-webkit-details-marker { display: none; }
details.st > summary:hover { background: #0d1420; }
details.st > summary::before {
  content: "+"; font-family: "JetBrains Mono", monospace; font-size: 12px;
  color: #38e1c9; width: 10px; flex: none;
}
details.st[open] > summary::before { content: "\2212"; }
details.st[open] > summary { background: #0d1420; border-bottom: 1px solid #1a2740; }

.st-id { font-family: "JetBrains Mono", monospace; font-size: 10px;
         letter-spacing: 0.12em; color: #38e1c9; flex: none; }
.st-name { font-size: 12.5px; color: #c7d5e8; font-weight: 600; }
.st-sym { font-family: "JetBrains Mono", monospace; font-size: 10px; color: #5a7090; }
.st-res { margin-left: auto; font-family: "JetBrains Mono", monospace; font-size: 10px;
          letter-spacing: 0.1em; color: #3fe08b; flex: none; }
.st-res.pend { color: #ffb547; }
/* A MEASURED RESULT THAT IS NOT A PASS. Needed because `.st-res` and tailwind's
   `.text-dim` are BOTH single-class selectors, so specificity ties and source
   order decides - site.css loads after tailwind.css, so .st-res's green won and
   proving-ground.js's tone distinction had no visual effect at all. PG-06 and
   PG-07 both carried `text-dim` and both rendered in the PASS colour, which for
   PG-07 is the opposite of what it says. `.st-res.muted` is 0,2,0 and wins. */
.st-res.muted { color: #5a7090; }

.st-body { padding: 12px; display: grid; gap: 12px; }
@media (min-width: 900px) { .st-body { grid-template-columns: 1fr 1fr; } }
/* A grid item's default min-width is auto, not zero, which means a track
   cannot shrink below the widest thing inside it. These blocks contain .math
   and .kv, whose content does not wrap, so on a phone the single column
   resolved to 368px inside a 354px box and pushed the whole page sideways --
   measured 224px of overflow on the Proving Ground.
 *
 * The blocks already contain their own scrolling (.math is overflow-x: auto).
 * That scrolling was simply never reached: the track grew to fit the content
 * instead of the content scrolling inside the track. min-width: 0 lets the
 * track shrink, which is what hands the overflow back to the element built to
 * handle it. */
.st-body > * { min-width: 0; }
.st-block h4 {
  font-family: "JetBrains Mono", monospace; font-size: 9.5px; letter-spacing: 0.16em;
  text-transform: uppercase; color: #5a7090; margin-bottom: 5px;
}
.st-block p, .st-block li { font-size: 12px; line-height: 1.65; color: #5a7090; }
.st-block ul { display: grid; gap: 5px; }

/* Spacing between the sub-blocks of a stress-test panel.
 *
 * These were 153 inline `style="margin-top:9px|11px"` attributes, every one of
 * which the page's own `style-src 'self'` silently blocked — CSP makes no
 * distinction between a style ATTRIBUTE and a <style> block. The panels have
 * therefore been rendering with all of this spacing collapsed to zero.
 *
 * Sibling rules rather than 153 classes, because the structure turned out to be
 * exactly regular: a .math block is always followed by the list that discusses
 * it, and a .kv table is always followed by either a heading or a paragraph.
 * Counted before relying on it — .math+ul, .kv+h4 and .kv+p each occur exactly
 * 51 times, matching the 51 attributes of each kind one for one, so these rules
 * reach precisely the elements that carried them and nothing else. (For scale:
 * the document has 153 <h4> and 82 <p> in total; the other 102 and 31 are
 * correctly left alone.)
 *
 * The payoff over classes is that the next panel added to this page gets the
 * right spacing without anyone remembering to write it.
 */
.st-block .math + ul { margin-top: 9px; }
.st-block .kv + h4 { margin-top: 11px; }
.st-block .kv + p { margin-top: 9px; }

/* Fixed column widths for the reference tables.
 *
 * Also formerly inline, and also blocked, so these columns have been sizing
 * themselves by content. Named for the width they set: there are eight of them
 * across two files serving unrelated tables, and a semantic name per column
 * ("method", "tier", "status", "field", ...) would be eight names describing
 * one property. `table.data` is width:100% with automatic layout, so these act
 * as preferred widths and the remaining columns take the slack.
 */
.col-3ch { width: 3ch; }
.col-7ch { width: 7ch; }
.col-9ch { width: 9ch; }
.col-10ch { width: 10ch; }
.col-12ch { width: 12ch; }
.col-16ch { width: 16ch; }
.col-18ch { width: 18ch; }
.col-20ch { width: 20ch; }
.col-22ch { width: 22ch; }
.col-24ch { width: 24ch; }
.col-26ch { width: 26ch; }
.col-34ch { width: 34ch; }

.math {
  border: 1px solid #1a2740; background: #05070c; padding: 9px 11px;
  font-family: "JetBrains Mono", monospace; font-size: 11.5px; line-height: 1.8;
  color: #c7d5e8; overflow-x: auto;
}
.math .op { color: #38e1c9; }
.math .cm { color: #5a7090; }

.kv { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px;
      font-family: "JetBrains Mono", monospace; font-size: 11px; }
.kv dt { color: #5a7090; white-space: nowrap; }
/* The key column is `auto` and its keys are nowrap, so on a phone the column
   sized itself to the longest key and the pair ran 53px past the panel -- the
   last box overflow left on the Proving Ground once the grid tracks above were
   allowed to shrink. Below sm the keys are allowed to fold instead. The nowrap
   is kept everywhere else, because that is where it earns its place: a key
   breaking mid-term in a two-column readout is worse than a slightly wider
   column.

   Letting the key column merely SHRINK was tried first and was not enough: the
   auto track still resolved to 284px of the 315 available and squeezed the
   value column to 19px, so the value overflowed instead of the key. Two
   columns do not fit on a phone at this font size whatever the tracks are
   told, so below sm the pair stacks -- key over value, which is how a
   definition list is normally read on a narrow screen anyway. */
@media (max-width: 639.98px) {
  .kv { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .kv dt { white-space: normal; overflow-wrap: anywhere; margin-top: 7px; }
  .kv dt:first-child { margin-top: 0; }
  .kv dd { overflow-wrap: anywhere; }
}
.kv dd { color: #c7d5e8; }
.kv dd.go { color: #3fe08b; }
.kv dd.warn { color: #ffb547; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════════
 * SITE NAVIGATION, EXPANDED FOOTER, AND PLACEHOLDER PAGES
 *
 * WHY THESE ARE COMPONENT CLASSES AND NOT TAILWIND UTILITIES
 *
 * tailwind.css is a PRE-BUILT artefact: the generator scans the HTML and emits
 * only the utilities it finds, which is why it is 14 KB instead of several
 * hundred. A class written into the markup that was not present at build time
 * therefore resolves to nothing — and it fails SILENTLY, dropping the style
 * with no error anywhere. Regenerating needs the Tailwind CLI, so every rule
 * this navigation depends on lives here instead, where it is unconditional.
 * ═══════════════════════════════════════════════════════════════════════════ */

/* ── Primary navigation ─────────────────────────────────────────────────── */
.navbar { border-top: 1px solid #1a2740; background: rgba(10,15,24,0.72); }
.navbar-inner {
  margin: 0 auto; max-width: 1400px; padding: 0 12px;
  display: flex; align-items: stretch; justify-content: space-between;
  flex-wrap: wrap; gap: 0;
}
@media (min-width: 768px) { .navbar-inner { padding: 0 16px; } }

.navlist { display: flex; flex-wrap: wrap; align-items: stretch; gap: 0; }
/* 44px is the touch-target floor both Apple and Google publish. The label is
   10px type in a 9px padding box, which lands at 33px — comfortable with a
   mouse, fiddly with a thumb. min-height raises the HIT AREA without touching
   the padding or the type, so the type and spacing are unchanged and only the
   target grows; align-items keeps the label centred in the taller box.

   APPLIED AT EVERY WIDTH, deliberately. The first attempt relaxed it back to
   33px above 768px on the assumption that a wide viewport means a mouse. It
   does not: a touch laptop is wide AND finger-driven, so that version gave the
   worst targets to a device that needs the best ones. Viewport width is not a
   proxy for input type, and the 11px this costs a desktop nav bar is not worth
   the exception. */
.navlink {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 9px 12px;
  min-height: 44px;
  font-family: "JetBrains Mono", monospace;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  color: #5a7090; text-decoration: none;
  border-right: 1px solid #1a2740;
  transition: color 120ms linear, background-color 120ms linear;
}
.navlist > .navlink:first-child { border-left: 1px solid #1a2740; }
.navlink:hover { color: #c7d5e8; background: #0d1420; }
.navlink.is-active { color: #38e1c9; background: rgba(56,225,201,0.06); }
.navlink.is-active::before {
  content: ""; width: 5px; height: 5px; background: #38e1c9; flex: none;
}

/* Utility cluster on the right: account actions read as actions, not links. */
.navutil { display: flex; align-items: stretch; gap: 0; margin-left: auto; }
.nav-cta {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 9px 14px;
  min-height: 44px;
  font-family: "JetBrains Mono", monospace;
  font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
  text-decoration: none;
  border-left: 1px solid #1a2740;
  color: #5a7090;
}
.nav-cta:hover { color: #c7d5e8; background: #0d1420; }
.nav-cta.primary {
  color: #38e1c9; background: rgba(56,225,201,0.06);
  border-left: 1px solid rgba(56,225,201,0.4);
}
.nav-cta.primary:hover { background: rgba(56,225,201,0.15); }

/* ── The navigation on a phone ─────────────────────────────────────────────
 *
 * Nine items in a wrapping flex row, each sized to its own label, produced
 * four rows of unequal length. Measured at 385px: the rows ended 126px, 10px
 * and 197px short of the right edge, and then .navutil's margin-left:auto --
 * which is what holds Feedback and TOOLS to the right on a desktop -- pushed
 * the fourth row 197px in from the left, so it floated away from the three
 * above it.
 *
 * Two equal columns instead. Every row is then flush on both sides whatever
 * the labels say, and the reading order runs down the page the way a phone
 * menu is expected to.
 *
 * THE SPAN RULE IS CONDITIONAL ON THE COUNT BEING ODD. Seven links in two
 * columns leaves the last one alone in its row with a hole beside it, so it
 * takes the full width; but :last-child:nth-child(odd) only matches when the
 * number of links actually is odd, so adding or removing one does not leave a
 * full-width item stranded mid-row for someone to find later.
 *
 * The borders are reset and re-declared rather than adjusted because the
 * desktop rules are right-edge-only, which is what made the wrapped rows
 * ragged: a row's last item carried a border with nothing after it. Each cell
 * now owns its right and bottom edge, and the last column drops the right one
 * so the lattice stops at the container. */
@media (max-width: 767.98px) {
  .navbar-inner { display: block; padding: 0; }
  .navlist, .navutil {
    display: grid;
    grid-template-columns: 1fr 1fr;
    margin-left: 0;
  }
  .navlink, .nav-cta {
    border: none;
    border-right: 1px solid #1a2740;
    border-bottom: 1px solid #1a2740;
  }
  /* Beats .navlist > .navlink:first-child (0,3,0) on source order. */
  .navlist > .navlink:first-child { border-left: none; }
  .navutil > .nav-cta,
  .navutil > .nav-cta.primary,
  .navutil > .nav-cta.yellow { border-left: none; }
  .navlist > .navlink:nth-child(2n),
  .navutil > .nav-cta:nth-child(2n) { border-right: none; }
  .navlist > .navlink:last-child:nth-child(odd),
  .navutil > .nav-cta:last-child:nth-child(odd) {
    grid-column: 1 / -1;
    border-right: none;
  }
}

/* ── The status rail on a phone ────────────────────────────────────────────
 *
 * The rail is two groups in a wrapping flex row with justify-content:
 * space-between: status on the left, instruments on the right. Below the sm
 * breakpoint most of its contents are hidden, leaving ENGINE STATUS on one
 * side and the page-size control plus the UTC clock on the other -- together
 * about 364px of content.
 *
 * That is almost exactly a phone's content width, which is the problem. At
 * 390px it fits on one line; at 375px it does not, and the second group drops
 * onto a line of its own where space-between has nothing to space and leaves
 * it hanging at the left. The rail therefore changed shape between two common
 * phone sizes for the sake of a dozen pixels.
 *
 * Made deterministic instead: below sm it is always two full-width rows, and
 * each row spreads its own contents. The clock lands on the right edge where
 * it sits on a desktop rather than wherever the wrap happened to leave it. */
@media (max-width: 767.98px) {
  .status-rail { flex-direction: column; align-items: stretch; gap: 3px; }
  .status-rail > * { justify-content: space-between; }
}

/* ── MISSION CUSTODY and CLASS on a phone ──────────────────────────────────
 *
 * Operator, 2026-09-19: both belong on a phone. They had been hidden below sm
 * and md to keep the rail small, and showing them turns the status group into
 * three items that do not fit one 320px line (about 410px with separators).
 *
 * Below sm the status group wraps, with a fixed break before MISSION CUSTODY:
 * ENGINE STATUS and CLASS share the first line, MISSION CUSTODY takes the
 * second, on every phone width - the same shape at 320, 375 and 414px. That
 * pairing is chosen for width, not reading order: ENGINE STATUS + CLASS is
 * about 245px, ENGINE STATUS + MISSION CUSTODY about 280px. It is a wrapping
 * row rather than a grid so that at the page-size control's larger steps,
 * where even 245px does not fit, CLASS drops to a line of its own instead of
 * running past the edge. The separators stay hidden below sm, as before.
 *
 * From sm to md the group is one line with separators, and the rail above is
 * two full-width rows up to md, not only up to sm: with all three showing, the
 * 640-767px range wrapped the same way 375px once did. */
@media (max-width: 639.98px) {
  .rail-status { flex-wrap: wrap; justify-content: space-between; column-gap: 12px; row-gap: 3px; }
  .rail-status .rail-class   { order: 1; }
  .rail-status .rail-mission { order: 2; flex-basis: 100%; }
}

/* From md up the rail is one wrapping row again, and on the pages that carry
 * MISSION CUSTODY it still wraps between about 768 and 832px. When it does,
 * the instruments group now keeps to the right edge - where the clock sits on
 * a desktop - instead of hanging at the left of a line of its own. On one line
 * this is the same position space-between already gave it. */
@media (min-width: 768px) {
  .status-rail > :last-child { margin-left: auto; }
}

/* The three chips in the masthead sub-navigation are sized by their labels and
   the row was not allowed to wrap, so on a phone it ran 101px past the page.
   Nothing about the row needs it to stay on one line. */
.subnav { flex-wrap: wrap; }

/* The section eyebrow -- SECTION NAME // QUALIFIER -- is a flex row of two
   labels and a separator at 0.18em tracking. On a 320px screen the pair came
   to 4px more than the 286px available and sat outside its column. Same remedy
   and same reason as .subnav: nothing here needs one line. */
.eyebrow { flex-wrap: wrap; }

/* A long mono string in running prose -- an address, an endpoint -- has no
   space to break at, so it overflows the paragraph rather than wrapping. At
   320px the contact address was 255px inside a 256px box and hung 3px out.
   Scoped to links inside a paragraph so code samples, which are meant to be
   copied whole and already scroll, are not touched. */
p > a.font-mono { overflow-wrap: anywhere; }

/* ── Expanded footer ────────────────────────────────────────────────────── */
.foot-grid {
  display: grid; grid-template-columns: 1fr; gap: 1px;
  background: #1a2740; border: 1px solid #1a2740; margin-bottom: 20px;
}
@media (min-width: 640px) { .foot-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .foot-grid { grid-template-columns: repeat(4, 1fr); } }
.foot-col { background: #0a0f18; padding: 14px 14px 16px; }
.foot-h {
  font-family: "JetBrains Mono", monospace; font-size: 9.5px;
  letter-spacing: 0.18em; text-transform: uppercase; color: #38e1c9;
  margin-bottom: 9px;
}
.foot-links { display: grid; gap: 6px; }
.foot-link {
  font-size: 12px; line-height: 1.45; color: #5a7090; text-decoration: none;
}
.foot-link:hover { color: #c7d5e8; text-decoration: underline;
                   text-underline-offset: 2px; }
.foot-note {
  margin-top: 9px; font-family: "JetBrains Mono", monospace;
  font-size: 9px; line-height: 1.6; color: #3c4d68;
}

/* ── Placeholder pages ──────────────────────────────────────────────────── */
.stub-wrap { margin: 0 auto; max-width: 1400px; padding: 40px 12px 64px; }
@media (min-width: 768px) { .stub-wrap { padding: 56px 16px 88px; } }

.stub-eyebrow {
  display: flex; align-items: center; gap: 8px;
  font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: 0.18em; text-transform: uppercase; color: #5a7090;
  margin-bottom: 12px;
}
.stub-eyebrow .bar { height: 1px; width: 24px; background: #38e1c9; }
.stub-h1 {
  font-size: 30px; font-weight: 800; letter-spacing: -0.02em;
  line-height: 1.08; color: #c7d5e8;
}
@media (min-width: 768px) { .stub-h1 { font-size: 42px; } }
.stub-lede {
  margin-top: 14px; max-width: 62ch; font-size: 14px; line-height: 1.75;
  color: #5a7090;
}

.stub-card {
  margin-top: 28px; border: 1px solid #1a2740; background: #0a0f18;
  max-width: 900px;
}
.stub-card-head {
  border-bottom: 1px solid #1a2740; background: #0d1420;
  padding: 9px 13px; display: flex; align-items: center;
  justify-content: space-between; gap: 12px; flex-wrap: wrap;
}
.stub-card-head .lbl {
  font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase; color: #5a7090;
}
.stub-status {
  font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: 0.14em; color: #ffb547;
  border: 1px solid rgba(255,181,71,0.4); padding: 2px 8px;
}
/* A card whose subject actually exists. The default .stub-status is amber
 * PENDING because most of these pages are placeholders; this is the variant for
 * the ones that are not, and it has to be a separate class rather than a
 * utility in the markup for the reason at the top of this section. */
.stub-status.live {
  color: #3fe08b; border-color: rgba(63,224,139,0.45);
}
.stub-card.live .stub-card-head .lbl { color: #ffe14d; }

/* Tool entries: a title line that is a link, then the description. */
.tool-open {
  margin-top: 14px; display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid rgba(255,225,77,0.45); background: rgba(255,225,77,0.08);
  padding: 8px 13px; font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: #ffe14d; text-decoration: none;
}
.tool-open:hover { background: rgba(255,225,77,0.17); }

.stub-card-body { padding: 16px 14px; }
.stub-card-body p { font-size: 12.5px; line-height: 1.75; color: #5a7090; }
.stub-card-body p + p { margin-top: 10px; }

.stub-planned { margin-top: 14px; display: grid; gap: 1px; background: #1a2740;
                border: 1px solid #1a2740; }
.stub-planned li {
  list-style: none; background: #0d1420; padding: 9px 12px;
  font-size: 12px; line-height: 1.6; color: #5a7090;
  display: flex; align-items: baseline; gap: 10px;
}
.stub-planned li::before {
  content: "--"; font-family: "JetBrains Mono", monospace; font-size: 10px;
  color: #3c4d68; flex: none;
}
.stub-planned li b { color: #c7d5e8; font-weight: 600; }

.stub-back {
  margin-top: 26px; display: inline-flex; align-items: center; gap: 8px;
  border: 1px solid #1a2740; background: #0a0f18; padding: 9px 14px;
  font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: #c7d5e8; text-decoration: none;
}
.stub-back:hover { border-color: rgba(56,225,201,0.6); color: #38e1c9; }

/* Test cases on the Summit page: numbered because they run in order (later
 * cases read files the earlier ones write). Each ends in a "passes when" line. */
.tc-case { margin-top: 18px; padding-top: 14px; border-top: 1px solid #1a2740; }
.tc-label {
  font-family: "JetBrains Mono", monospace; font-size: 10px;
  letter-spacing: 0.16em; text-transform: uppercase; color: #38e1c9;
}
.tc-case .tc-label + p { margin-top: 6px; }
.tc-case pre.code { margin-top: 8px; }
.tc-case pre.code + p { margin-top: 8px; }
.stub-card-body .tc-pass b { color: #c7d5e8; font-weight: 600; }
.stub-card-body .tc-note { margin-top: 18px; }

/* Blocks of test cases (2026-09-22): the standards, then one block per source of
 * tests. A heavier rule than .tc-case's so a block reads as a section of the card,
 * and "Check it without us" styled like "Passes when" - the pair every case ends in. */
.tc-block { margin-top: 28px; padding-top: 16px; border-top: 1px solid #26395a; }
.tc-block-h {
  font-family: "JetBrains Mono", monospace; font-size: 11px;
  letter-spacing: 0.16em; text-transform: uppercase; color: #ffe14d;
}
.tc-block .tc-block-h + p { margin-top: 8px; }
.stub-card-body .tc-check b { color: #c7d5e8; font-weight: 600; }
.tc-url { overflow-wrap: anywhere; }

.nav-cta.is-active { color: #38e1c9; background: rgba(56,225,201,0.06); }
.nav-cta.primary.is-active { background: rgba(56,225,201,0.15); }

/* Brand ensign — the small US flag beside the wordmark in the header.
 *
 * A component class rather than Tailwind utilities, for the reason in BUILD.md:
 * tailwind.css is pre-built, so a utility written into the markup afterwards
 * silently resolves to nothing.
 *
 * Drawn as inline SVG, not the flag emoji. Windows renders the regional-indicator
 * pair as the letters "US" rather than a flag, so the emoji would be wrong on the
 * platform this site is authored on, and would vary by viewer OS besides. The SVG
 * also stays crisp at 20px, which a raster of this size would not.
 */
.brand-flag {
  flex: none; display: block; width: 20px; height: 11px;
  margin-left: 7px; border: 1px solid #1a2740;
}

/* The same ensign repeated beside the legal name in the footer.
 *
 * The footer wordmark is a <div> of 10px mono, so it needs to become a flex row
 * before anything can sit beside the text — hence .foot-mark rather than a bare
 * margin. A component class again, for the reason at the top of this section:
 * `flex` and `items-center` do exist in the pre-built tailwind.css today, but
 * only because other markup happened to use them, and this rule should not
 * depend on that continuing to be true.
 *
 * 17x9 rather than the masthead's 20x11: the viewBox is 19x10, so 17x9 holds
 * the 1.9 aspect ratio almost exactly and sits correctly against a 10px cap
 * height. The stars are sub-pixel at both sizes and read as a lighter canton,
 * which is what the masthead has always done.
 */
.foot-mark { display: flex; align-items: center; }
.brand-flag.sm { width: 17px; height: 9px; margin-left: 6px; }

/* ---------------------------------------------------------------------------
 * .hash-wrap — long hexadecimal identifiers that must fold rather than overflow.
 *
 * A PCR0 measurement is 96 hex characters with no spaces, so a container that
 * does not break inside a word will simply run off the side of the card on any
 * narrow viewport.
 *
 * NOT Tailwind's `break-all`. That utility is absent from tailwind.css — the
 * stylesheet is pre-built and only contains classes that existed in the markup
 * when it was generated, so writing `break-all` into a page silently resolves
 * to nothing and the overflow appears anyway. Defining it here keeps it working
 * whether or not the Tailwind CLI is ever run again, which is the same reason
 * the nav and footer live in this file.
 * ------------------------------------------------------------------------- */
.hash-wrap {
  overflow-wrap: anywhere;
  word-break: break-all;
}

/* ── .nav-cta.yellow ─────────────────────────────────────────────────────────
 * The bright-yellow call-to-action, used for TOOLS.
 *
 * NOT the `warn` token. This started life as #ffb547 to match CARD 04 on the
 * Operational Charter, and was brightened to #ffe14d on request. The class is
 * named for what it is rather than for that origin, because a selector called
 * `.warn` that does not use the warn colour is a trap for whoever reads it next
 * — they would change #ffb547 somewhere else and wonder why this did not move.
 *
 * A MODIFIER HERE RATHER THAN A UTILITY IN THE MARKUP. `.nav-cta.primary`
 * already sets `color` and carries two class selectors, so a colour utility
 * written alongside it loses on specificity (0,1,0 against 0,2,0) and the link
 * silently stays teal. The `.is-active` variant is spelled out for the same
 * reason: `.nav-cta.is-active` also sets a colour, and relying on source order
 * to settle a tie is how this breaks the next time the file is reorganised.
 * ------------------------------------------------------------------------- */
.nav-cta.yellow {
  color: #ffe14d; background: rgba(255,225,77,0.07);
  border-left: 1px solid rgba(255,225,77,0.45);
}
.nav-cta.yellow:hover { color: #ffe14d; background: rgba(255,225,77,0.16); }
.nav-cta.yellow.is-active { color: #ffe14d; background: rgba(255,225,77,0.16); }

/* The UTC readout in the status rail is a link to the time-standards page.
 * A real <a> rather than a scripted dialog: it works with JS disabled, is
 * keyboard-reachable and right-clickable for free, and needs no exception to
 * the page CSP, which is script-src self.
 *
 * IT HAS TO LOOK CLICKABLE. As plain rail text it read as a label like the ones
 * either side of it and nobody would think to press it. This borrows the
 * affordance the TOOLS button uses - tinted ground, coloured border, accent
 * text - but CANNOT reuse .nav-cta, which carries min-height 44px and 9px
 * padding sized for the masthead and would be several times the height of a
 * 10px status rail. Same language, rail scale.
 *
 * Accent teal rather than the TOOLS yellow on purpose: this rail already spends
 * amber on CLASS ADVISORY, and a second warm chip beside it would read as a
 * second warning rather than as a control. */
.time-link {
  display: inline-flex; align-items: center;
  padding: 2px 7px;
  text-decoration: none;
  color: #38e1c9;
  border: 1px solid rgba(56,225,201,0.45);
  background: rgba(56,225,201,0.07);
  transition: background-color 120ms linear, border-color 120ms linear;
}
/* The digits stay neutral so the chip still reads as label + value, the same
 * shape as every other entry in the rail. */
.time-link .text-txt { color: #c7d5e8; }
.time-link::after {
  content: "\203A"; margin-left: 6px; color: #38e1c9; opacity: 0.75;
}
.time-link:hover {
  background: rgba(56,225,201,0.17); border-color: rgba(56,225,201,0.75);
}
.time-link:focus-visible { outline: 1px solid #38e1c9; outline-offset: 2px; }
/* A 21px chip is a fine mouse target and a poor thumb one - .nav-cta carries
 * min-height 44px for exactly this reason. Below the sm breakpoint the JD and
 * FRAME slots are hidden, so the rail has the room to spend and this costs
 * nothing where it matters. */
@media (max-width: 639px) {
  .time-link { padding: 8px 10px; }
}

/* ── Time-standards page (time.html) ─────────────────────────────────────── */
.ts-now {
  border: 1px solid #1a2740; background: #0a0f18;
  padding: 10px 14px; margin-bottom: 18px;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
}
.ts-now-k {
  font-family: "JetBrains Mono", monospace; font-size: 9px;
  letter-spacing: 0.18em; text-transform: uppercase; color: #5a7090;
}
.ts-now-v {
  font-family: "JetBrains Mono", monospace; font-size: 15px; color: #38e1c9;
  font-variant-numeric: tabular-nums;
}
.ts-pre { font-size: 12px; line-height: 1.6; color: #5a7090; margin-bottom: 12px; }
.ts-pre b { color: #c7d5e8; font-weight: 400; }
/* Wide table, narrow phone. The table scrolls inside its own box so the page
 * body never scrolls sideways. */
.ts-scroll { overflow-x: auto; }
table.ts { width: 100%; border-collapse: collapse; min-width: 620px; }
table.ts th {
  font-family: "JetBrains Mono", monospace; font-size: 9px;
  letter-spacing: 0.18em; text-transform: uppercase; color: #5a7090;
  text-align: left; font-weight: 400;
  padding: 7px 10px 7px 0; border-bottom: 1px solid #1a2740; white-space: nowrap;
}
table.ts td {
  font-size: 11.5px; line-height: 1.45; color: #c7d5e8;
  padding: 7px 10px 7px 0; vertical-align: top; border-bottom: 1px solid #101a2b;
}
table.ts tr:last-child td { border-bottom: none; }
table.ts .ts-name { font-family: "JetBrains Mono", monospace; white-space: nowrap; }
/* The values are why anyone opened the page: monospaced and tabular so digits
 * align down the column and a ticking low-order digit does not shift the row. */
table.ts .ts-val {
  font-family: "JetBrains Mono", monospace; color: #38e1c9;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* A standard we decline to compute reads as a caveat, never as a value. */
table.ts .ts-unknown { color: #ffb547; font-style: normal; }
table.ts .ts-def { color: #5a7090; }
table.ts .ts-use { color: #5a7090; }

/* ══════════════════════════════════════════════════════════════════════════
 * MODULE PLATES — the empty cells in the Section B card lattice (index.html)
 *
 * WHAT THE PROBLEM ACTUALLY WAS
 *
 * The six module families hold 4, 2, 3, 4, 5 and 2 cards in a grid that is
 * three columns wide from 1024px up. None of those counts is a multiple of
 * three, so five of the six families end on a short row. `.hairline-grid` sets
 * the CONTAINER background to #1a2740 and separates the cells with a 1px gap —
 * which is the whole trick that draws the lattice — and that means the unused
 * remainder of a short row is not empty at all. It is painted #1a2740, the
 * hairline colour, across the full width of the missing cells.
 *
 * Measured on the live page at 1440px: the remainder beside card 04 is 911x160
 * of solid #1a2740. The cards beside it are #0a0f18 and the page behind is
 * #05070c, so the one region carrying no content is also the LIGHTEST large
 * area in the section. It reads as a fault in the lattice, which is the
 * opposite of what an unfilled cell should read as.
 *
 * Seven such cells exist at >=1024px (2+1+2+1+1) and two more at 768-1023px,
 * where the grid is two columns wide and a different pair of families lands
 * short. Below 768px the grid is a single column and there is no remainder at
 * all. Each plate therefore declares the widths at which its cell exists, and
 * is display:none everywhere else — a plate left in the flow at a width where
 * the row is already full would push a card onto a row of its own.
 *
 * WHAT GOES IN THEM
 *
 * A gravity well in a grid: a plane in one-point perspective, pulled down by a
 * mass sitting in it. The vertices are projected through a pinhole rather than
 * drawn by hand —
 *
 *     screen.x = 228 + 300 * X / Y
 *     screen.y =  46 + 300 * (1 + dip) / Y
 *
 * with Y the depth into the frame and dip a Lorentzian well. Everything follows
 * from that: the mesh converges because the second term vanishes as Y grows, so
 * y=46 is the horizon, and the dimple is IN the surface rather than drawn over
 * it because the dip is applied before the projection, not after.
 *
 * The well moves and changes depth from plate to plate, and picks up a
 * companion on two of them, so that no two read as the same picture. The pairs
 * that sit side by side — families A and D — differ most, and their starfields
 * are mirrored as well.
 *
 * WHY FILLER ELEMENTS RATHER THAN A BACKGROUND ON THE CONTAINER
 *
 * Putting the artwork on the grid container itself would have shown it through
 * the 1px gaps as well, since that is the same surface. The hairlines would
 * have picked up whatever colour happened to sit behind them and stopped being
 * hairlines. Real cells keep the lattice exactly as it is and fill only the
 * hole.
 *
 * WHY THESE ARE COMPONENT CLASSES
 *
 * The same reason as the navigation block above: tailwind.css is PRE-BUILT, so
 * a utility written into the markup after generation resolves to nothing and
 * fails silently. Beyond that, the page CSP is style-src 'self' with no
 * 'unsafe-inline', so there is no style attribute available either — every
 * value here has to live in this file. SVG presentation attributes (fill,
 * stroke, d) are markup rather than style and are unaffected, but the colours
 * are set from CSS anyway so the palette stays in one place.
 * ═══════════════════════════════════════════════════════════════════════════ */

.mod-plate {
  display: none;                  /* see the breakpoint block below */
  position: relative;
  overflow: hidden;
  /* Keeps the z-index order below inside this plate. Without a stacking context
     here those values would escape into the nearest ancestor that has one, and
     a plate's starfield or mesh could paint over the card beside it. */
  isolation: isolate;

  /* HOW FAR THE ARTWORK IS FADED BACK, in one place.
   *
   * The plates are filling holes in a lattice of cards that carry the actual
   * content, and at full strength the mesh competed with them: the eye landed
   * on the empty cell first, which is precisely backwards. These are the two
   * numbers to change if the balance wants adjusting -- everything else in
   * this block is geometry and has no say in how loud it reads.
   *
   * Applied as opacity rather than by darkening the stroke colours, because
   * the tiers encode DISTANCE. Re-darkening them by hand would flatten the
   * near mesh into the far mesh and the sheet would stop reading as a surface
   * receding to a horizon, which is the only thing holding the illusion up. */
  --plate-ink: 0.42;              /* the mesh, the orbit and the mass */
  --plate-stars: 0.66;            /* the starfield behind them */

  /* NO GROUND AT ALL. A plate is a hole in the instrument panel, and it is
     meant to be one literally: what shows through it is the page -- including
     the single .void-watermark logo fixed behind the whole document, wherever
     that happens to fall. Painting a ground here, even one the same colour as
     the page, punched a hole in that mark instead of revealing it.

     This only works because .mod-lattice below stops the grid container
     painting #1a2740 behind the cells. Restore a background here and the
     see-through goes with it. */
  background-color: transparent;
}

/* ── Why the module grids draw their lattice differently ───────────────────
 *
 * .hairline-grid draws the card lattice by filling the CONTAINER with #1a2740
 * and separating the cells with a 1px gap, so the gap is not empty -- it is
 * container showing through. That is a good trick and it is why every other
 * grid on the site uses it.
 *
 * It also means the surface behind a plate is that fill, not the page. A
 * transparent plate over it reveals #1a2740: the pale slab these plates exist
 * to cover. So for these grids only, the same 1px lattice is drawn by giving
 * each CELL an outline instead. An outline sits outside the border box and
 * takes no space, so two neighbours each draw 1px into the same 1px gap and
 * land on each other; at the container edge the cell's outline falls on the
 * container's own border, which is already this colour.
 *
 * The container is then transparent, the plate can be transparent, and the
 * page -- watermark included -- is what shows through the gap in the cards.
 *
 * A CLASS IN THE MARKUP, NOT :has(.mod-plate). The selector would have been
 * tidier and needed no HTML edit, but its failure mode is the wrong one: on a
 * browser without :has support the container keeps its fill while the plates
 * stay transparent, and the slab comes back. An explicit class cannot
 * half-apply. */
.mod-lattice { background-color: transparent; }
.mod-lattice > * { outline: 1px solid #1a2740; }

/* Starfield. A separate layer from the lattice above purely so each rule can
   carry one background-size: these are two dozen gradients and enumerating a
   matching size for every one of them would be two dozen more values that must
   stay in step by hand. All of them are percentage-positioned, which sizes to
   the box on its own. */
.mod-plate::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: var(--plate-stars);
  background-image:
    radial-gradient(circle 1.9px at  6% 14%, rgba(199,213,232,0.60) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at 13% 62%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 19% 31%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at 24% 84%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 29%  9%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.9px at 33% 47%, rgba( 56,225,201,0.30) 0 34%, rgba( 56,225,201,0) 100%),
    radial-gradient(circle 1.2px at 38% 73%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 42% 22%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.9px at 47% 57%, rgba(199,213,232,0.60) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at 51% 88%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 56% 17%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at 60% 41%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 64% 79%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.9px at 68% 28%, rgba(199,213,232,0.60) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at 72% 63%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 76% 11%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at 80% 49%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 84% 86%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.9px at 88% 24%, rgba( 56,225,201,0.30) 0 34%, rgba( 56,225,201,0) 100%),
    radial-gradient(circle 1.2px at 91% 68%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 94% 38%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.9px at 97% 79%, rgba(199,213,232,0.60) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.2px at  9% 93%, rgba(199,213,232,0.20) 0 34%, rgba(199,213,232,0) 100%),
    radial-gradient(circle 1.5px at 44% 95%, rgba(199,213,232,0.36) 0 34%, rgba(199,213,232,0) 100%);
}

/* Families A and D each get TWO plates side by side, and an identical starfield
   repeated across a hairline is immediately legible as one image printed twice.
   Mirroring the field costs nothing and breaks the match; the lattice is offset
   as well so its intersections do not line up across the gap either. */
.mod-plate.mirrored::before { transform: scale(-1, 1); }
.mod-plate.mirrored { background-position: 17px 9px; }

/* ── Which plates exist at which width ─────────────────────────────────────
 * md = 768px and lg = 1024px, matching the grid-cols utilities in the markup.
 * `plate-md` is bounded on BOTH sides: its cell exists only while the grid is
 * two columns wide, and at 1024px that family's row fills up again.
 * ------------------------------------------------------------------------- */
@media (min-width: 768px) and (max-width: 1023.98px) {
  .mod-plate.plate-md { display: block; }
}
@media (min-width: 768px) {
  .mod-plate.plate-mdlg { display: block; }
}
@media (min-width: 1024px) {
  .mod-plate.plate-lg { display: block; }
}

/* The figure fills the plate and contributes NO height to it. Both children are
   out of flow, so the plate measures zero tall and takes its height from the
   card sharing the row — which is what stops a plate from ever making a row
   taller than its content needs. */
.mod-fig {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 2;
  opacity: var(--plate-ink);
}

/* Every mesh is drawn in one 456x200 frame and cropped to the cell rather than
   letterboxed, so no plate shows a margin of bare ground where the aspect ratio
   does not match. The horizon sits at y=46 and the sheet runs off the bottom of
   the frame, which means the crop only ever takes sky or foreground — there is
   no composition to protect at the edges. */
.mod-fig path, .mod-fig polyline, .mod-fig line,
.mod-fig circle, .mod-fig ellipse, .mod-fig rect, .mod-fig polygon {
  fill: none;
  stroke-width: 1;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* The frame is scaled to the cell. Without this the strokes scale with it and
     a 1px hairline lands on 0.8px, which is the one thing this page's line work
     cannot afford to lose. */
  vector-effect: non-scaling-stroke;
}

/* Depth tiers for the mesh. Four flat steps rather than a gradient: distance is
   what makes the sheet read as a surface instead of a pattern, and this page
   has no gradients anywhere else in its line work.

   Prefixed with .mod-fig so they outrank the element selectors above — `.f-dot`
   alone is 0,1,0 against 0,1,1 and would silently lose its fill, the same
   specificity trap documented at .nav-cta.yellow. */
.mod-fig .f-m1 { stroke: #2f4570; }   /* foreground */
.mod-fig .f-m2 { stroke: #25375c; }
.mod-fig .f-m3 { stroke: #1c2a47; }
.mod-fig .f-m4 { stroke: #152034; }   /* at the horizon */

/* The mass in the well, and the orbit lying on the sheet around it. */
.mod-fig .f-body { stroke: #5a7090; fill: #05070c; }
.mod-fig .f-key  { stroke: #38e1c9; stroke-width: 1.1; }
.mod-fig .f-dot  { fill: #38e1c9; stroke: none; }

/* ── PROVING GROUND ──────────────────────────────────────────────────────────
 *
 * The nav entry pulses because the page behind it changes on its own — it
 * carries measurements the poller republishes, not prose. Everything else in
 * this nav is static, so a slow brightening is the one honest way to say "this
 * one moves" without a badge or a count that would rot.
 *
 * BRIGHTNESS AND NOT MOTION, deliberately. Nothing translates, scales or
 * reflows, so the pulse cannot pull the eye away from a table someone is
 * reading. It is the same colour transition the status rail already uses on
 * hover, run on a timer instead of a cursor.
 *
 * The reduce-motion block near the top of this file already sets
 * `animation: none !important` on everything, so this is disabled for anyone
 * who has asked for that — without a second rule here to fall out of step.
 */
@keyframes pg-pulse {
  0%, 100% { color: #4d6180; }
  50%      { color: #38e1c9; }
}
.navlink.pg-live { animation: pg-pulse 3.2s ease-in-out infinite; }
.navlink.pg-live:hover { animation: none; color: #38e1c9; }
.navlink.pg-live.is-active { animation: none; }

/* The two entry panels — index hero rail and the docs masthead. Same pulse on
 * the accent rule so the route reads as one thing from either page. */
@keyframes pg-edge {
  0%, 100% { border-color: #1a2740; }
  50%      { border-color: rgba(56,225,201,0.55); }
}
.pg-card { animation: pg-edge 3.2s ease-in-out infinite; }
.pg-card:hover { animation: none; border-color: rgba(56,225,201,0.75); background: #0d1420; }
.pg-card .pg-card-k { animation: pg-pulse 3.2s ease-in-out infinite; }
.pg-card:hover .pg-card-k { animation: none; color: #38e1c9; }

/* Links from the Proving Ground to the standing record - the verification ledger
 * and the stress tests. Quiet on purpose: the one pulsing route is the way INTO
 * the Proving Ground; these sit inside its hairline grid and answer on hover. */
.pg-rec { display: block; transition: background-color .15s ease; }
.pg-rec:hover, .pg-rec:focus-visible { background: #0d1420; }
.pg-rec:focus-visible { outline: 1px solid rgba(56,225,201,0.75); outline-offset: -1px; }
.pg-rec .pg-rec-go { color: #5a7090; transition: color .15s ease; }
.pg-rec:hover .pg-rec-go, .pg-rec:focus-visible .pg-rec-go { color: #38e1c9; }
@media (prefers-reduced-motion: reduce) {
  .pg-rec, .pg-rec .pg-rec-go { transition: none; }
}

/* Benchmark cards. Reuses the .st details idiom's visual weight without
 * inheriting its summary grid, because these carry a date and a limits block
 * that the stress-test entries do not. */
.pg-num { font-variant-numeric: tabular-nums; }
.pg-lim { border-left: 2px solid #3a2a1a; }
