/* TriData Observatory — the whole design system, hand-authored.
 *
 * IMPLEMENTS docs/design-tokens.md. That document is precedent: if this file
 * and it disagree, this file is the bug. No colour, spacing step, radius,
 * shadow or breakpoint appears here that is not in its tables.
 *
 * Read by scripts/site_css.py at import time, hashed, and written to
 * /assets/tdo.<hash>.css. It is never minified: the hash is the cache key, the
 * bytes are the source, and a build step that rewrites CSS is a build step
 * that can silently change what ships.
 *
 * SENTINELS. Three spans are extracted verbatim by scripts/site_css.py so that
 * other code reuses these bytes instead of restating them:
 *
 *   @tokens    both themes' custom properties
 *   @critical  the resilience shell inlined in <head>
 *   @embed     what the chrome-less /embed/ page needs on a third-party site
 *
 * The sentinel comments are load-bearing. Do not renumber, reorder or delete
 * them, and do not let a span's braces straddle its end marker.
 */

/* ==========================================================================
 * CRITICAL — the resilience shell
 * --------------------------------------------------------------------------
 * Inlined into every <head>. Its ONE job is that a page whose stylesheet did
 * not arrive — a 404 in the hash window between deploy and cache expiry, a
 * blocked CDN, a dead connection mid-load — is still a readable document on a
 * correct background, not black-on-white unstyled text at full window width.
 *
 * It is a FALLBACK, not "critical CSS". Do not grow it into an above-the-fold
 * optimisation: a test asserts it stays under 1200 bytes, and that ceiling is
 * the point. Every byte here is paid 3411 times.
 *
 * It therefore DUPLICATES the handful of tokens it uses, deliberately. A
 * fallback that resolves its variables from the file it is the fallback for is
 * not a fallback — every var() would be empty in exactly the case this block
 * exists for. tests/test_site_markup.py asserts these duplicated values are
 * identical to the ones in the @tokens span below, so the copy cannot drift.
 * ======================================================================== */

/* @critical:start */
:root {
  color-scheme: light dark;
  --bg: #f7f8fa; --text: #10151f; --accent: #2a5db0;
  --sp-2: 1rem; --sp-3: 1.5rem; --sp-6: 4rem;
  --fs-200: 1rem; --fs-500: 1.728rem; --lh-body: 1.65; --lh-tight: 1.25;
  --w-prose: 720px; --w-wide: 1100px;
  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    --bg: #0e1117; --text: #e8ecf4; --accent: #7aa5e8;
  }
}
:root[data-theme="dark"] {
  --bg: #0e1117; --text: #e8ecf4; --accent: #7aa5e8;
}
*, ::before, ::after { box-sizing: border-box; }

/* Author display rules outrank the UA sheet, so the attribute needs this to
   mean anything. See test_the_hidden_attribute_actually_hides. */
[hidden] { display: none !important; }
body {
  margin: 0; background: var(--bg); color: var(--text);
  font: var(--fs-200)/var(--lh-body) var(--font-sans);
}
.wrap { max-width: var(--w-prose); margin: 0 auto;
  padding: var(--sp-3) var(--sp-2) var(--sp-6); }
body.w-wide .wrap { max-width: var(--w-wide); }
a { color: var(--accent); }
h1 { font-size: var(--fs-500); line-height: var(--lh-tight); margin: 0 0 var(--sp-2); }
/* @critical:end */

/* ==========================================================================
 * TOKENS — docs/design-tokens.md §14
 * --------------------------------------------------------------------------
 * The dark values are written TWICE, identically, and that is mandatory, not
 * duplication awaiting a refactor: the media block serves a no-JS visitor on a
 * dark OS, the [data-theme="dark"] block serves the manual toggle. DRYing them
 * into one selector list breaks the toggle. A test asserts the two blocks
 * declare an identical token set with identical values (RULE T2).
 *
 * :where(:not([data-theme="light"])) contributes ZERO specificity, so the
 * media block stays at (0,1,0) and loses to :root[data-theme="dark"] at
 * (0,1,1). A bare :not() would be (0,2,0) and would outrank the explicit
 * toggle — the manual choice would lose to the OS, on some machines only.
 *
 * RULE T1: every token has its value on bare :root. None is defined only
 * inside a media query or a [data-theme] block.
 * ======================================================================== */

/* @tokens:start */
:root {
  color-scheme: light dark;

  /* surfaces — the only three grounds a token's contrast is guaranteed on */
  --bg: #f7f8fa;
  --surface: #ffffff;
  --surface-2: #eef1f6;

  /* --border is a DECORATIVE SEPARATOR ONLY (1.11:1). It may never bound an
     interactive control; --border-strong (3.21:1) is the only token that may.
     See docs/design-tokens.md §4, RULES B1/B2. */
  --border: #e2e6ed;
  --border-strong: #7d8797;

  --text: #10151f;
  --text-muted: #59637a;
  --accent: #2a5db0;   /* FROZEN: also hardcoded in FAVICON_SVG and in
                          _favicon_png's (42,93,176,255). Changing it here
                          without those two ships a mismatched favicon that no
                          test catches. docs/design-tokens.md §3. */
  --on-accent: #ffffff;  /* text ON an --accent fill. NEVER a literal #fff:
                            white on the dark accent is 2.51:1. §6.1 */
  --focus: #1f4f9c;
  --success: #12703a;
  --warning: #8a5300;
  --danger: #b42318;

  /* categorical slots — assigned IN ORDER from 1, never skipped for looks.
     The order is the colour-blind safety mechanism (§7), not a preference. */
  --cat-1: #2a78d6;
  --cat-2: #eb6834;
  --cat-3: #1baf7a;
  --cat-4: #eda100;
  --cat-5: #e87ba4;
  --cat-6: #008300;

  /* elevation — light shadows are tinted with the --text hue, never black */
  --elev-1: 0 1px 2px rgba(16,21,31,.06), 0 1px 1px rgba(16,21,31,.04);
  --elev-2: 0 4px 12px rgba(16,21,31,.10), 0 1px 3px rgba(16,21,31,.06);

  /* --- theme-independent scales: declared once, here ------------------- */

  --sp-0: 0.25rem;   /* half-step: icon/label gaps INSIDE a control only */
  --sp-1: 0.5rem;
  --sp-2: 1rem;
  --sp-3: 1.5rem;
  --sp-4: 2rem;
  --sp-5: 3rem;
  --sp-6: 4rem;

  --fs-100: 0.833rem;  /* the FLOOR. There is no step below this. */
  --fs-200: 1rem;
  --fs-300: 1.2rem;
  --fs-400: 1.44rem;
  --fs-500: 1.728rem;
  --fs-600: 2.074rem;
  --fs-700: 2.488rem;

  --lh-tight: 1.25;
  --lh-body: 1.65;

  --r-sm: 0.25rem;
  --r-md: 0.5rem;
  --r-lg: 0.75rem;
  --r-pill: 999px;

  --w-prose: 720px;
  --w-wide: 1100px;

  --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    --bg: #0e1117;
    --surface: #161b23;
    --surface-2: #1e2530;
    --border: #2b3441;
    --border-strong: #697487;
    --text: #e8ecf4;
    --text-muted: #a2adbf;
    --accent: #7aa5e8;
    --on-accent: #0e1117;
    --focus: #8fb8f0;
    --success: #4cc47c;
    --warning: #e0a03c;
    --danger: #f2807a;
    --cat-1: #3987e5;
    --cat-2: #d95926;
    --cat-3: #199e70;
    --cat-4: #c98500;
    --cat-5: #d55181;
    --cat-6: #008300;
    --elev-1: 0 1px 2px rgba(0,0,0,.55);
    --elev-2: 0 4px 14px rgba(0,0,0,.65);
  }
}

/* color-scheme must be restated here: on bare :root it says "follow the OS",
   so a forced theme left UA control internals painting for the OTHER one
   (1.11:1). See test_forced_themes_restate_color_scheme. */
:root[data-theme="light"] { color-scheme: light; }

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #0e1117;
  --surface: #161b23;
  --surface-2: #1e2530;
  --border: #2b3441;
  --border-strong: #697487;
  --text: #e8ecf4;
  --text-muted: #a2adbf;
  --accent: #7aa5e8;
  --on-accent: #0e1117;
  --focus: #8fb8f0;
  --success: #4cc47c;
  --warning: #e0a03c;
  --danger: #f2807a;
  --cat-1: #3987e5;
  --cat-2: #d95926;
  --cat-3: #199e70;
  --cat-4: #c98500;
  --cat-5: #d55181;
  --cat-6: #008300;
  --elev-1: 0 1px 2px rgba(0,0,0,.55);
  --elev-2: 0 4px 14px rgba(0,0,0,.65);
}
/* @tokens:end */

/* ==========================================================================
 * RESET + BASE TYPE
 * ======================================================================== */

img, svg, video { max-width: 100%; }
h1, h2, h3, h4, p, ul, ol, figure, table { margin: 0; }

/* German, Swedish and Dutch compounds are single unbreakable words wider
   than a 320px viewport: 81 localized pages scrolled sideways, 0 English.
   See test_long_words_cannot_push_a_page_sideways. */
h1, h2, h3, h4, th, td, a, caption, legend, label {
  overflow-wrap: break-word; hyphens: auto;
}
ul, ol { padding-left: var(--sp-3); }

h1 { font-size: var(--fs-500); line-height: var(--lh-tight);
  letter-spacing: -0.01em; margin: 0 0 var(--sp-2); }
h2 { font-size: var(--fs-400); line-height: var(--lh-tight);
  margin: var(--sp-4) 0 var(--sp-1); }
h3 { font-size: var(--fs-300); line-height: var(--lh-tight);
  margin: 0 0 var(--sp-0); }
p { margin: 0 0 var(--sp-2); }
p:last-child { margin-bottom: 0; }
strong { font-weight: 650; }
code, kbd, samp { font-family: var(--font-mono); font-size: var(--fs-100); }

a:hover { text-decoration: none; }

/* One focus treatment, everywhere, no exceptions (§13). Never removed, never
   replaced by a box-shadow, never narrowed to :focus. The offset is what keeps
   the ring visible on a control that already has a --border-strong edge. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The site has no animation to disable today. This block exists so that the
   first person to add one inherits the honest default instead of shipping
   motion to a visitor who asked for none. */
@media (prefers-reduced-motion: reduce) {
  *, ::before, ::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==========================================================================
 * LAYOUT + PAGE CHROME
 * ======================================================================== */

/* Width comes from a class on <body>, set per page by head(width=...). It is
   NOT state on the Site object: two pages rendered in one build must never be
   able to leak a width into each other. */
body.w-prose .wrap { max-width: var(--w-prose); }

main { display: block; }

/* Skip link: off-screen until focused, then a real, visible control. */
.skip {
  position: absolute; left: var(--sp-1); top: -100%;
  z-index: 10;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  padding: var(--sp-1) var(--sp-2);
  text-decoration: none; font-weight: 600;
}
.skip:focus { top: var(--sp-1); }

/* Visually hidden but still announced. NOT display:none — the theme toggle's
   accessible name lives in one of these, and display:none would delete it. */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

header.site {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--sp-1); margin-bottom: var(--sp-4);
}
header.site .brand { flex: 1; min-width: 0; }
header.site a.home {
  color: inherit; text-decoration: none;
  font-weight: 700; font-size: var(--fs-300);
}
header.site .tag { color: var(--text-muted); font-size: var(--fs-100); }

/* Theme toggle. Hidden without [data-js] so a visitor whose JS never ran is
   never shown a control that cannot work. The button is rendered by the
   server on every page; this is what keeps it honest. */
.theme-toggle { display: none; }
:root[data-js] .theme-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: 2.75rem; height: 2.75rem; padding: 0;  /* WCAG 2.5.8: 44x44 */
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  cursor: pointer;
}
.theme-toggle:hover { background: var(--surface-2); }
.theme-toggle svg { width: 1.125rem; height: 1.125rem;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round; }
/* Two icons in the markup, one shown: the swap is CSS, so it costs no JS and
   cannot desynchronise from the theme the page is actually painted in. */
.theme-toggle .i-moon { display: none; }
:root[data-theme="dark"] .theme-toggle .i-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .i-moon { display: inline; }

.site-foot {
  margin-top: var(--sp-5);
  border-top: 1px solid var(--border);
  padding-top: var(--sp-2);
  color: var(--text-muted); font-size: var(--fs-100);
}

/* Language switcher. The inner <a> markup is pinned byte-for-byte by a frozen
   test, so this styles the wrapper only. */
.langbar { margin-top: var(--sp-3); }
.langbar .note { margin: 0; }
.langbar [aria-current] { color: var(--text); }

/* ==========================================================================
 * COMPONENTS
 * ======================================================================== */

.meta { color: var(--text-muted); font-size: var(--fs-100);
  margin: 0 0 var(--sp-3); }
.note { color: var(--text-muted); font-size: var(--fs-100); }

.card {
  background: var(--surface);
  border: 1px solid var(--border);   /* not clickable as a whole -> RULE B1 */
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
  padding: var(--sp-2) var(--sp-3);
  margin: var(--sp-2) 0;
}
.card h3 { margin: 0 0 var(--sp-0); font-size: var(--fs-300); }
.card h3 a { text-decoration: none; }
/* The ratified card-heading grammar (Phase 3 wave B consolidation): card
   titles are <h2> — the outline under a page h1 is h1 -> h2, no skip — drawn
   at the same weight .card h3 has always had. line-height and font-size are
   explicit so the 1024px h2 upscale never applies inside a card. .card h3
   stays styled for the transition and for JS-rendered legacy markup. */
.card h2 { margin: 0 0 var(--sp-0); font-size: var(--fs-300);
  line-height: var(--lh-tight); }
.card h2 a { text-decoration: none; }
.card p { margin: var(--sp-1) 0 0; color: var(--text-muted); }
.card p:last-child { margin-bottom: 0; }

/* Responsive card grid for listing/home card lists. Opt-in wrapper: the cards
   keep their own look; the grid only takes over their vertical margins (gap
   owns the spacing, or every row is sp-2 + sp-2 tall). minmax(0,1fr) lets a
   long unbroken title shrink instead of forcing a page scroll at 320px. */
.card-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-2);
  margin: var(--sp-2) 0; }
.card-grid > .card { margin: 0; }

.pill {
  display: inline-block;
  padding: 0 var(--sp-1);
  border-radius: var(--r-pill);
  font-size: var(--fs-100);
  border: 1px solid var(--border);
  color: var(--text-muted);
  margin: 0 var(--sp-0) var(--sp-0) 0;
}
.pill-accent { border-color: var(--accent); color: var(--accent); }

table { width: 100%; border-collapse: collapse; font-size: var(--fs-200); }
th, td {
  text-align: left; padding: var(--sp-1);
  border-bottom: 1px solid var(--border); vertical-align: top;
}
th {
  color: var(--text-muted); font-size: var(--fs-100);
  text-transform: uppercase; letter-spacing: .04em;
}

/* Table-to-card (promoted from the W-TOOLS page-scoped block, byte-identical
   values): a `.tw.tcard` wrapper renders card-per-row below 768px and a plain
   table from 768px up. The hidden header row's text reappears per cell as a
   ::before label. Everything about that label EXCEPT its text lives here; the
   text is translated content, so each page ships a three-rule content-only
   style block generated from the SAME translated th_ and dh_ header strings
   its own thead uses (zero i18n keys, and the two can never drift — and note
   for editors: never write those two key globs joined by a slash in this
   file, the star-slash pair terminates a CSS comment and silently truncates
   the whole stylesheet from this point on; a browser check caught exactly
   that). ::before renders nothing until a
   page rule supplies content, so these rules are inert elsewhere. Column 1
   (the row's own title) gets no label. NOTE for page rules: scope them
   @media (max-width: 767.98px) — their :nth-of-type selectors outweigh any
   content:none reset this file could stage against them. */
.tcard table, .tcard tbody { display: block; }
.tcard thead { display: none; }
.tcard tr {
  display: block; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
  padding: var(--sp-1) var(--sp-2); margin: 0 0 var(--sp-2);
}
.tcard td { display: block; border: 0; padding: var(--sp-0) 0; }
.tcard td::before {
  display: block;
  color: var(--text-muted); font-size: var(--fs-100);
  text-transform: uppercase; letter-spacing: .04em;
}

/* 44px floor: .cta renders for the first time the night a thesis publishes, so
   it has never been seen on the live site. It measured 227x42. */
.cta {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 2.75rem;
  background: var(--accent);
  color: var(--on-accent);      /* NEVER #fff — see §6.1 */
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-md);
  text-decoration: none; font-weight: 600;
  margin-top: var(--sp-1);
}
.cta:hover { opacity: .92; }

/* ==========================================================================
 * EMBED SPAN
 * --------------------------------------------------------------------------
 * Everything from here to @embed:end is ALSO shipped inside the chrome-less
 * /tools/log-volume-calculator/embed/ page, as a self-contained <style>. That
 * page renders on somebody else's site, where a hash-window 404 on our
 * stylesheet would be an unstyled calculator inside their iframe with no
 * telemetry and no way for them to roll it back. So the embed never links the
 * hashed asset; it carries these bytes.
 *
 * Keep the span contiguous and keep it to what the calculator itself needs.
 * Anything added here is paid on every third-party page that embeds us.
 * ======================================================================== */

/* @embed:start */
/* --- buttons, inputs, textareas ---------------------------------------- */

.btn {
  font-family: inherit;
  font-size: var(--fs-200);
  min-height: 2.75rem;
  display: inline-flex; align-items: center; justify-content: center;
  padding: var(--sp-1) var(--sp-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface); color: var(--text);
  cursor: pointer;
}
.btn:hover { background: var(--surface-2); }
.btn-primary {
  background: var(--accent); color: var(--on-accent);
  border-color: var(--accent); font-weight: 600;
  padding: var(--sp-1) var(--sp-4);
}
.btn-primary:hover { opacity: .92; background: var(--accent); }

.input, .ta {
  font-family: inherit;
  font-size: var(--fs-200);
  min-height: 2.75rem;
  padding: var(--sp-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--bg); color: var(--text);
  width: 100%;
}
.ta-mono { font-family: var(--font-mono); }
.ta-embed { font-family: var(--font-mono); font-size: var(--fs-100); }
.input-search { font-size: var(--fs-300); padding: var(--sp-1) var(--sp-2); }

/* --- the calculator panel ---------------------------------------------- */

.calc {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--elev-1);
  padding: var(--sp-2) var(--sp-3);
  margin: var(--sp-3) 0;
}
.calc .row { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-bottom: var(--sp-1); }
.calc .row-sep { margin-top: var(--sp-2); }

/* ELEMENT DEFAULTS AT ZERO SPECIFICITY. `:where(.calc) label` weighs (0,0,0),
   so every utility class below beats it on specificity alone rather than on
   source order. Written as a plain `.calc label` this rule would weigh (0,1,1)
   and would silently outrank `.field-wide` and `.ta-mono` — the utility would
   be present in the markup, correct in the stylesheet, and have no effect. */
:where(.calc) :where(label) {
  display: flex; flex-direction: column; gap: var(--sp-0);
  font-size: var(--fs-100); color: var(--text-muted);
  min-width: 130px; flex: 1;
}
/* font-size is set EXPLICITLY, not inherited: the label above is --fs-100
   (13.33px) and `font: inherit` pulled controls down with it. Anything under
   16px makes iOS Safari zoom the viewport on focus and never zoom back, which
   every numeric field on the site did. min-height is the WCAG 2.5.8 target. */
:where(.calc) :where(input, select, textarea) {
  font-family: inherit;
  font-size: var(--fs-200);
  line-height: var(--lh-tight);
  min-height: 2.75rem;
  padding: var(--sp-1);
  border-radius: var(--r-md);
  border: 1px solid var(--border-strong);
  background: var(--bg); color: var(--text);
  width: 100%;
}
:where(.calc) :where(textarea) { min-height: 5.5rem; }
/* A checkbox stretched to 100% is a 700px-wide tick box. */
:where(.calc) input[type="checkbox"], :where(.calc) input[type="radio"] { width: auto; }

/* --- label / control utilities ------------------------------------------
   RULE B2: every one of these bounds an interactive control, so every one of
   them uses --border-strong. There is no exception for "it looks heavy". */

.field { display: flex; flex-direction: column; gap: var(--sp-0);
  font-size: var(--fs-100); color: var(--text-muted); min-width: 130px; flex: 1; }
.field-wide { flex: 2; }
.field-inline { flex-direction: row; align-items: center; gap: var(--sp-1); }

/* The results region. overflow-x keeps a wide result table inside its own
   scroller so the PAGE never scrolls sideways at the 320px floor (§12). */
.calc .out {
  border-top: 1px solid var(--border);
  margin-top: var(--sp-1); padding-top: var(--sp-2);
  overflow-x: auto;
}
.calc .big {
  font-size: var(--fs-600); line-height: var(--lh-tight); font-weight: 650;
  font-variant-numeric: tabular-nums;   /* digits must not reflow per keystroke */
}
.calc .sub { color: var(--text-muted); font-size: var(--fs-100); }
.calc table td, .calc table th { padding: var(--sp-0) var(--sp-1); }
.calc .h2-sm { font-size: var(--fs-300); }

/* Verdict colours. These were literals (#1a7f37 / #b42318) that did not theme
   and sat at 3.36:1 and 2.60:1 on the dark card — on the single most
   load-bearing string of the brake, USP, gas and vibration calculators.
   docs/design-tokens.md §6.2. */
.ok { color: var(--success); font-weight: 600; }
.bad { color: var(--danger); font-weight: 600; }

.embed-foot { font-size: var(--fs-100); margin: var(--sp-1) var(--sp-2); }
/* @embed:end */

/* ==========================================================================
 * CALCULATOR ADDITIONS — deliberately OUTSIDE the @embed span
 * --------------------------------------------------------------------------
 * The embed renders exactly one calculator (timber), which has no fieldsets
 * and lives in a fixed-height 620px iframe where a sticky panel would fight
 * the host page. Everything here is paid only by our own pages.
 * ======================================================================== */

/* Form groups (fuel ships two; the approved vib/gdd legends will follow).
   The UA-default fieldset is a grooved 2px border with padding that ignores
   every token; worse, its default min-inline-size:min-content lets a wide row
   push the page sideways at the 320px floor — min-width:0 is load-bearing.
   --border (not -strong) bounds it: a group box is a decorative separator,
   not an interactive control (RULE B1/B2). */
.calc fieldset {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--sp-1) var(--sp-2) var(--sp-2);
  margin: 0 0 var(--sp-2);
  min-width: 0;
}
.calc legend {
  color: var(--text-muted); font-size: var(--fs-100); font-weight: 600;
  padding: 0 var(--sp-0);
}

/* No sticky mobile result panel: it made 21 of 34 controls untappable at
   320px. See test_no_sticky_panel_over_the_calculator_controls. */

/* ==========================================================================
 * DATA-SOURCE DIRECTORY INDEX — promoted from the W-TOOLS page-scoped block
 * (byte-identical values). Compact entries, a section jump nav sized to the
 * 44px WCAG target, and a filter box that appears only once JS has stamped
 * data-js (the theme-toggle pattern: no-JS visitors never see a dead
 * control). The multi-column .dsg flow joins the shared breakpoints below.
 * ======================================================================== */

.dsq { display: none; margin: 0 0 var(--sp-2); }
:root[data-js] .dsq { display: block; }
.jmp { display: flex; flex-wrap: wrap; gap: var(--sp-0); margin: 0 0 var(--sp-3); }
.jmp a {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.75rem; min-height: 2.75rem;
  border: 1px solid var(--border-strong); border-radius: var(--r-md);
  text-decoration: none;
}
.dss { scroll-margin-top: var(--sp-2); }
.dss h2 { margin: var(--sp-3) 0 var(--sp-1); font-size: var(--fs-300); }
.dsg p { margin: 0 0 var(--sp-1); break-inside: avoid; line-height: var(--lh-tight); }

/* ==========================================================================
 * CHARTS — the TD.svg contract, docs/chart-spec.md §4
 * --------------------------------------------------------------------------
 * Published here, before any chart exists, because the chart workstreams must
 * style against this file rather than each inventing their own block. Paint is
 * by CLASS ONLY: the JS never reads a custom property, so a theme switch
 * repaints every chart with no redraw (chart-spec rule 0.2).
 *
 * Deliberately outside the @embed span: the timber calculator gets no chart,
 * ever (chart-spec §6.11), and it is the only thing the embed renders.
 * ======================================================================== */

.td { width: 100%; height: auto; display: block; margin-top: var(--sp-2); }
/* Mark GEOMETRY lives here, not as per-mark attributes: TD.svg (scripts/
   calc_rt.py) emits class names and coordinates, nothing else. `.td polyline`
   is (0,1,1) — deliberately above the (0,1,0) slot classes below — so a data
   line's fill stays off no matter which slot paints its stroke. `.td path`
   likewise strips the stroke from area fills (the paired polyline carries it). */
.td polyline { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.td path { stroke: none; }
/* The runtime's table wrapper (TD.tbl): its own scroller, so a wide result
   table scrolls inside the panel and the PAGE never scrolls at the 320px
   floor (docs/design-tokens.md §12). */
.tw { overflow-x: auto; }
.td-c1 { stroke: var(--cat-1); fill: var(--cat-1); }
.td-c2 { stroke: var(--cat-2); fill: var(--cat-2); }
.td-c3 { stroke: var(--cat-3); fill: var(--cat-3); }
.td-c4 { stroke: var(--cat-4); fill: var(--cat-4); }
.td-c5 { stroke: var(--cat-5); fill: var(--cat-5); }
.td-c6 { stroke: var(--cat-6); fill: var(--cat-6); }
.td-fill { fill-opacity: .18; }
.td-ok { stroke: var(--success); fill: var(--success); }
.td-bad { stroke: var(--danger); fill: var(--danger); }
.td-axis { stroke: var(--border-strong); fill: none; }
/* the one correct in-chart use of --border: gridlines carry no value */
.td-grid { stroke: var(--border); fill: none; }
/* font-size is in USER UNITS, not --fs-100: a 640-unit viewBox at the 320px
   floor scales by 0.5, so token-sized tick text would render at ~6.7px
   (chart-spec §3.9). This is also why the table is mandatory. */
.td-tick { fill: var(--text-muted); font-size: 16px; }
.td-label { fill: var(--text); font-size: 16px; }
/* Annotated rule lines are 1.5-unit dashed strokes (chart-spec §3.4); the
   dash pattern is part of the kind's identity, so it lives with the paint. */
.td-rule-limit { stroke: var(--danger); fill: none; stroke-width: 1.5; stroke-dasharray: 5 4; }
.td-rule-ref { stroke: var(--text-muted); fill: none; stroke-width: 1.5; stroke-dasharray: 5 4; }
.td-rule-ceiling { stroke: var(--border-strong); fill: none; stroke-width: 1.5; stroke-dasharray: 5 4; }
.td-band-accept { fill: var(--success); fill-opacity: .12; stroke: var(--success); }

/* ADR's scalar-against-a-ceiling meter: a native progressbar, not an SVG. */
.td-meter {
  display: block; width: 100%; height: var(--sp-2);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  overflow: hidden; margin-top: var(--sp-1);
}
.td-meter > span { display: block; height: 100%; }
.td-meter-ok > span { background: var(--success); }
.td-meter-bad > span { background: var(--danger); }

/* ==========================================================================
 * BREAKPOINTS — mobile-first, docs/design-tokens.md §12
 * --------------------------------------------------------------------------
 * 320px is the MINIMUM SUPPORTED VIEWPORT and is NOT a breakpoint: everything
 * above must already work there, unqualified. There is no @media (min-width:
 * 320px) and there never will be.
 * ======================================================================== */

@media (min-width: 768px) {
  .wrap { padding: var(--sp-4) var(--sp-3) var(--sp-6); }
  h1 { font-size: var(--fs-600); }
  .calc { padding: var(--sp-3) var(--sp-4); }
  .card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* .tcard leaves card mode and becomes the table it marks up as */
  .tcard table { display: table; }
  .tcard thead { display: table-header-group; }
  .tcard tbody { display: table-row-group; }
  .tcard tr { display: table-row; background: transparent; border: 0;
    box-shadow: none; }
  .tcard td { display: table-cell; border-bottom: 1px solid var(--border);
    padding: var(--sp-1); }
  .dsg { columns: 2; column-gap: var(--sp-3); }
}

@media (min-width: 1024px) {
  h1 { font-size: var(--fs-700); }
  h2 { font-size: var(--fs-500); }
  .card { padding: var(--sp-3) var(--sp-4); }
  .dsg { columns: 3; }
}

@media (min-width: 1440px) {
  .wrap { padding-top: var(--sp-5); }
  .card-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
