/* ============================================================================
   KIRELTA DESIGN SYSTEM v3 — "INSTRUMENT"
   One file, loaded by every page. Everything below follows from five rules.

   1. A READING IS NEVER SHOWN WITHOUT ITS SCALE.
      A number alone is decoration. A number on its graduated scale, with its
      threshold drawn and labelled, is a measurement. This is why there is no
      big-number card in this system: `.reading` (value + scale + threshold) is
      the primitive, and `.count` (a bare figure) is capped at --fs-lg on
      purpose so it can never shout louder than something that has been measured.

   2. HUE IS EVIDENCE. THE BRAND GETS NONE.
      Every saturated colour in this product carries semantic meaning and
      nothing else, so when you see colour, something is being asserted. The
      primary action is a value inversion (bone on graphite / graphite on bone),
      not a brand colour. This is forced as much as chosen: the semantic set
      already occupies green/amber/red/blue, violet is the AI cliche, and a
      brand hue would either collide with a state or be arbitrary.

   3. MONO MEANS MACHINE.
      JetBrains Mono is reserved for values the engine produced (verdicts,
      rates, ids, keys, hashes). IBM Plex Sans is Kirelta speaking. You can tell
      what came from the engine at a glance -- interface truthfulness made
      visible. Inherited from v2 and promoted: it is the spine of the identity.

   4. TYPE FLOOR: nothing below 12px. --fs-xs IS the floor; there is no smaller
      token. (v2 set this after a real 9.5px console label that was unreadable
      on a phone. Unchanged.)

   5. ONLY THE READING MOVES.
      Frames, panels, borders and layout never animate. Falls out of the mark
      -- brackets hold still, the point moves -- and kills gratuitous motion by
      rule rather than by taste.

   WHY ONE FILE. There is no build step here, by choice, matching the engine's
   zero-dependency ethos. `@import` would turn one request into a serial
   waterfall (fetch, parse, then fetch four more, all render-blocking), and five
   <link> tags would be five things to keep in sync across 20-odd pages. One
   sectioned file is the faster and the more maintainable answer at this size.
   test_accessibility.py also parses this file directly for its contrast maths,
   so the token blocks must live here literally.

   SECTIONS
     1  tokens            9  forms
     2  reset & base     10  tables
     3  typography       11  navigation
     4  bidi / RTL       12  overlays
     5  layout           13  feedback
     6  surfaces         14  evidence components
     7  readings         15  utility
     8  verdict & status 16  charts · 17 responsive · 18 print
   ========================================================================== */

/* ══ 1. TOKENS ══════════════════════════════════════════════════════════════
   The only place raw values exist. Every colour pair below was verified with
   the WCAG 2.2 relative-luminance formula before it was written down, against
   EVERY surface it can sit on -- not just the page background, which is the
   easiest pair in any system and the one that hides real failures. */

:root, [data-theme="dark"]{
  /* Ground — a cool-neutral ramp, ~205deg at 4-8% saturation. Deliberately not
     dead grey: tempered steel reads as an instrument, pure grey reads as unset.
     Five steps because five are genuinely in use (page, sunken, surface,
     inset, raised); collapsing them would be a regression, not a simplification. */
  --bg:#0A0D11; --bg2:#0E1217; --surface:#12171D; --surface2:#171D24; --raise:#1E262E;

  /* Text — four tiers, each >= 4.5:1 on all five surfaces above (worst pair:
     --faint on --raise at 4.51). Verified, not estimated. */
  --text:#EDF1F5; --text2:#B6C1CC; --muted:#98A5B2; --faint:#7F8D9B;

  /* Edges. Elevation in this system is border weight + surface value, not blur:
     --hair separates rows inside one surface, --line bounds a panel, --line2
     states an edge that matters (focus-adjacent, selected, input). */
  --line:rgba(148,163,184,.16); --line2:rgba(148,163,184,.30); --hair:rgba(148,163,184,.08);

  /* Accent = inversion, not hue (rule 2). 16.51:1 on --bg, which also makes the
     focus ring the most visible element on the page -- the old teal measured
     5.52 there. An accessibility win that fell out of a brand decision. */
  --accent:#E7EDF3; --accent2:#FFFFFF; --accent-dim:rgba(231,237,243,.10); --accent-ink:#0A0D11;

  /* VERDICT COLOURS — one per engine tier, used nowhere else, ever.
     (a) None of them is the accent, and the accent has no hue at all, so a
         button can never be misread as a verdict.
     (b) TRUSTED is deliberately the quietest: 5.29:1 on --bg against
         UNTRUSTED's 6.76, and lower saturation (0.55 vs 0.57). Severity is
         carried by THREE redundant channels -- the word (TRUSTED/DEGRADED/
         UNTRUSTED), the mark (check/bang/cross), and the colour -- so no reader
         ever depends on hue alone. Hues are 154 / 39 / 354 / 212: four-way
         separated, and distinguishable under the common dichromacies because
         the word and mark carry the same information. */
  --ok:#429470;   --ok-dim:rgba(66,148,112,.14);    /* TRUSTED   — quiet on purpose */
  --warn:#D9A036; --warn-dim:rgba(217,160,54,.14);  /* DEGRADED  */
  --bad:#F76A78;  --bad-dim:rgba(247,106,120,.15);  /* UNTRUSTED — loudest */
  --info:#63A4EE; --info-dim:rgba(99,164,238,.13);

  /* Chart ink. Two tokens v2 never had, which is why its one chart looked
     unowned -- it borrowed --line for its graticule and had no way at all to
     say "this is the baseline, that is the live data" without spending colour.
     --graticule: scale ticks and grid. Present, never competing (1.44:1 on
       --surface -- above the ~1.3 where a hairline disappears, below where it
       would read as content).
     --reference: baseline / expected / prior. 4.09:1, dashed by convention, so
       reference-vs-live survives greyscale and every colour-vision deficiency. */
  --graticule:#2B353F; --reference:#6B7A88;

  /* Elevation is permitted ONLY on genuinely floating layers (drawer, modal,
     popover, toast). Panels get borders. There is no --shadow for surfaces
     because surfaces must not have one. */
  --sh-sheet:0 24px 60px -30px rgba(0,0,0,.9), 0 2px 8px -4px rgba(0,0,0,.6);

  /* Type. Floor is --fs-xs (rule 4). The top end is tuned DOWN from v2:
     instruments do not shout, and 38px display type on a monitoring screen is
     a marketing reflex. */
  --fs-xs:12px; --fs-sm:13px; --fs-base:15px; --fs-md:16px;
  --fs-lg:18px; --fs-xl:21px; --fs-2xl:26px; --fs-3xl:34px;

  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px;
  --sp-6:24px; --sp-8:32px; --sp-10:40px; --sp-12:48px; --sp-16:64px;

  /* Radius near-zero: machined edges, not soft cards. 2px on controls, 3px on
     surfaces, 0 where two surfaces meet (tables, panel headers). --r-round is
     for things that are actually circles (status dot, avatar) and nothing else
     -- there are no pills in this system. */
  --r-sm:2px; --r-md:3px; --r-round:999px;
  --bw:1px; --bw2:2px;

  /* Motion (rule 5). Two durations, one curve, and nothing else to reach for. */
  --dur-1:90ms; --dur-2:140ms; --ease:cubic-bezier(.2,0,.2,1);

  --sans:'IBM Plex Sans','IBM Plex Sans Arabic',system-ui,-apple-system,sans-serif;
  --mono:'JetBrains Mono','IBM Plex Sans Arabic',ui-monospace,SFMono-Regular,monospace;
}

[data-theme="light"]{
  --bg:#FFFFFF; --bg2:#FAFBFC; --surface:#F5F7F9; --surface2:#EFF2F5; --raise:#EAEEF2;
  /* Worst pair here is --faint on --raise at 4.73:1. The tiers stay monotonic
     (text darkest -> faint lightest) so raising a colour to pass AA has not
     collapsed two tiers into one, which is the usual way this gets "fixed". */
  --text:#0B0F13; --text2:#414B55; --muted:#56626D; --faint:#5E6A77;
  --line:rgba(15,23,42,.13); --line2:rgba(15,23,42,.22); --hair:rgba(15,23,42,.06);
  --accent:#151A20; --accent2:#000000; --accent-dim:rgba(21,26,32,.07); --accent-ink:#FFFFFF;
  --ok:#1C7A4F;   --ok-dim:rgba(28,122,79,.10);     /* TRUSTED  — 5.32:1, quietest */
  --warn:#8A5300; --warn-dim:rgba(138,83,0,.10);
  --bad:#C1102C;  --bad-dim:rgba(193,16,44,.10);    /* UNTRUSTED — 6.22:1, loudest */
  --info:#1D4ED8; --info-dim:rgba(29,78,216,.09);
  --graticule:#C4CED7; --reference:#7C8894;
  --sh-sheet:0 20px 50px -28px rgba(15,23,42,.34), 0 2px 8px -4px rgba(15,23,42,.14);
}

/* ══ 2. RESET & BASE ════════════════════════════════════════════════════════ */
*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{
  background:var(--bg); color:var(--text);
  font-family:var(--sans); font-size:var(--fs-base); line-height:1.6;
  -webkit-font-smoothing:antialiased;
  /* Tabular figures everywhere by default. Numbers in this product are read in
     columns and compared down the page; proportional digits make a rate that
     changed from 0.11 to 0.19 appear to change width, which reads as movement
     where there is none. */
  font-variant-numeric:tabular-nums;
}
a{color:inherit;text-decoration:none}
/* The focus ring is bone/graphite (rule 2), which is the highest-contrast thing
   on the page in either theme. Offset so it never sits on top of a border. */
:focus-visible{outline:var(--bw2) solid var(--accent);outline-offset:2px;border-radius:var(--r-sm)}
::selection{background:var(--accent-dim)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* Visible to assistive tech only. Clipped rather than display:none, because
   display:none removes it from the accessibility tree too. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* ══ 3. TYPOGRAPHY ══════════════════════════════════════════════════════════ */
/* MONO MEANS MACHINE (rule 3). This selector means one thing everywhere in this
   codebase: literal engine output, not prose. Automatic browser translation
   (Safari's Google Translate, confirmed active on this project's own test
   device) does not know that distinction -- it rewrites "TRUSTED" like any other
   English word unless told not to. The working mechanism is the `notranslate`
   class + the HTML `translate="no"` attribute, both applied by kirelta.js. */
.mono,.val,.verdict-word,.key,code,pre,.vd,.hash{
  font-family:var(--mono);font-feature-settings:"tnum" 1,"zero" 1;letter-spacing:-.01em}

h1{font-size:var(--fs-2xl);font-weight:600;letter-spacing:-.02em;line-height:1.22}
h2{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.015em;line-height:1.3}
h3{font-size:var(--fs-lg);font-weight:600;letter-spacing:-.01em}
h4{font-size:var(--fs-md);font-weight:600}
/* The eyebrow is the instrument-panel label: mono, spaced, small. It names a
   region the way a legend plate names a dial. */
.eyebrow{font-family:var(--mono);font-size:var(--fs-xs);font-weight:500;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted)}
.lead{font-size:var(--fs-md);color:var(--text2);line-height:1.65}
.sub{font-size:var(--fs-sm);color:var(--text2)}
.dim{color:var(--muted)}
.small{font-size:var(--fs-xs);color:var(--muted)}
.prose{max-width:68ch}
.prose p+p{margin-top:var(--sp-3)}

/* ══ 4. BIDI / RTL ══════════════════════════════════════════════════════════
   This system is built correct in both directions, not flipped at the end.
   Every rule below uses logical properties (inline-start/end, not left/right);
   tests/test_design_system.py fails the build on a physical direction property
   in this file. Three things that direction: rtl alone does NOT fix, handled
   here:

   (a) Machine values must never reorder. A hash, an API key, a model id or a
       number placed next to Arabic text will be re-sequenced by the bidi
       algorithm unless it is isolated. Everything mono is isolated by default.
   (b) Numerals stay Western (0-9) in both languages -- correct for technical
       and enterprise use in the Gulf and Levant, and it keeps tabular columns
       aligned with the mono values. A deliberate decision, not an omission.
   (c) Only genuinely directional icons mirror. An arrow means "forward" and
       must flip; a bell, a check or a gauge means the same thing in both
       directions and flipping it just makes it wrong. Opt in with .is-dir. */
.mono,.val,.key,code,pre,.hash,.num{unicode-bidi:isolate}
bdi,.bidi{unicode-bidi:isolate}
[dir="rtl"] .is-dir{transform:scaleX(-1)}
/* Arabic sits lower and needs more line box than Latin at the same size. */
[lang="ar"] body,[dir="rtl"] body{line-height:1.75}
[dir="rtl"] .eyebrow{letter-spacing:0}   /* letter-spacing breaks Arabic joining */
[dir="rtl"] .mono{letter-spacing:0}

/* ══ 5. LAYOUT ══════════════════════════════════════════════════════════════ */
.shell{display:grid;grid-template-columns:244px 1fr;min-height:100vh}
.main{display:flex;flex-direction:column;min-width:0}
.content{padding:var(--sp-6);max-width:1360px;width:100%}
.wrap{max-width:1140px;margin:0 auto;padding:0 var(--sp-6)}
.wrap.narrow{max-width:720px}
.wrap.mid{max-width:900px}
/* Composition helpers, so a page's own <style> never has to define layout.
   The per-page style budget is 20 lines and is enforced by test. */
.stack{display:flex;flex-direction:column;gap:var(--sp-4)}
.stack.tight{gap:var(--sp-2)} .stack.loose{gap:var(--sp-6)}
.cluster{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3)}
.cluster.tight{gap:var(--sp-2)}
.spacer{flex:1}
.grid{display:grid;gap:var(--sp-4);grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}
.grid.g2{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}
.split{display:grid;grid-template-columns:300px 1fr;gap:var(--sp-4);align-items:start}
.split.wide{grid-template-columns:1fr 340px}

/* ══ 6. SURFACES ════════════════════════════════════════════════════════════
   THREE KINDS, AND ONLY THREE. The v2 system had one (.card) and stamped a
   gradient hairline on every instance of it site-wide -- decoration applied to
   everything asserts nothing, so it was removed rather than restyled.

     .panel  a bounded region of related content. Hairline border, NO shadow,
             square. This is the default and should be the answer most times.
     .row    list/table content. No border of its own; separated by hairlines.
             A row is not a card and must never be given one.
     .sheet  floating only -- drawer, modal, popover, toast. Shadow allowed
             HERE and nowhere else, because here it means "above the page"
             rather than "please notice me".

   A metric is never a card. It is a `.reading` inside a panel (section 7). */
.panel{background:var(--surface);border:var(--bw) solid var(--line);border-radius:var(--r-md);
  position:relative}
.panel.sunken{background:var(--bg2)}
.panel.pad{padding:var(--sp-5)}
.panel-h{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-4);border-bottom:var(--bw) solid var(--line);min-height:46px}
.panel-h .t{font-weight:600;font-size:var(--fs-sm)}
.panel-h .meta{font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted)}
.panel-b{padding:var(--sp-4)}
.panel-f{padding:var(--sp-3) var(--sp-4);border-top:var(--bw) solid var(--line);
  font-size:var(--fs-xs);color:var(--muted)}
/* .card is v2's name and is still referenced widely. Kept as a padded panel so
   nothing breaks mid-migration -- but with no ::before, no hover lift, and no
   shadow. Prefer .panel in new markup. */
.card{background:var(--surface);border:var(--bw) solid var(--line);border-radius:var(--r-md);
  padding:var(--sp-5);position:relative}
.row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) var(--sp-4);
  border-bottom:var(--bw) solid var(--hair)}
.row:last-child{border-bottom:none}
.row.link{cursor:pointer;transition:background var(--dur-1) var(--ease);
  width:100%;text-align:start;background:none;border-inline:none;border-block-start:none;color:inherit}
.row.link:hover{background:var(--hair)}
.row.on{background:var(--accent-dim)}
.sheet{background:var(--surface);border:var(--bw) solid var(--line2);border-radius:var(--r-md);
  box-shadow:var(--sh-sheet)}

/* REFERENCE MODE. Recorded engine output shown as a demonstration is real data,
   but it is not YOUR data, and nothing that renders it may be mistakable for a
   live reading. A hatched edge is not decoration here: it is the one visual
   state in the system whose entire job is to say "this did not come from your
   account". Survives greyscale and colour-vision deficiency. */
.panel.is-reference,.card.is-reference{border-color:var(--line2)}
.panel.is-reference::before,.card.is-reference::before{
  content:"";position:absolute;inset-block:0;inset-inline-start:0;width:4px;
  border-start-start-radius:var(--r-md);border-end-start-radius:var(--r-md);
  background:repeating-linear-gradient(135deg,var(--muted) 0 3px,transparent 3px 7px)}

/* ══ 7. READINGS ════════════════════════════════════════════════════════════
   RULE 1 MADE INTO A COMPONENT. `.reading` is a value WITH the scale it was
   measured on and the threshold that makes it mean something. It is the reason
   this system has no 38px hero number: a figure that large is asserting
   importance it has not earned, and the same pixels spent on a scale tell the
   reader whether the number is good.

   Anatomy:  label ......... value / limit
             [====|-------·-------]        <- track, threshold tick, graduation
             one sentence saying what it means, in words. */
.reading{display:block}
.reading .rhead{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3)}
.reading .rlab{font-size:var(--fs-xs);color:var(--text2);font-weight:500}
.reading .rval{font-family:var(--mono);font-size:var(--fs-md);font-weight:600;color:var(--text)}
.reading .rval.ok{color:var(--ok)}
.reading .rval.warn{color:var(--warn)}
.reading .rval.bad{color:var(--bad)}
.reading .rval .lim{color:var(--muted);font-weight:400}
.reading .rtrack{position:relative;height:8px;margin-top:var(--sp-2);background:var(--bg2);
  border:var(--bw) solid var(--line);border-radius:var(--r-sm);overflow:hidden}
.reading .rfill{position:absolute;inset-block:0;inset-inline-start:0;background:var(--muted);
  transition:inline-size var(--dur-2) var(--ease)}
.reading .rfill.ok{background:var(--ok)}
.reading .rfill.warn{background:var(--warn)}
.reading .rfill.bad{background:var(--bad)}
/* The threshold. Drawn, always, and labelled by the foot text -- a bar that
   fills toward an invisible limit tells you nothing about how close you are. */
.reading .rthr{position:absolute;inset-block:-1px;inline-size:var(--bw2);background:var(--text);
  opacity:.85}
/* Graduation: the ornament of this system, and it is functional -- it turns a
   proportion you have to estimate into one you can count. */
.reading .rticks{position:absolute;inset:0;pointer-events:none;
  background-image:repeating-linear-gradient(to right,var(--graticule) 0 1px,transparent 1px 25%)}
[dir="rtl"] .reading .rticks{background-image:repeating-linear-gradient(to left,var(--graticule) 0 1px,transparent 1px 25%)}
.reading .rfoot{margin-top:var(--sp-2);font-size:var(--fs-xs);color:var(--muted);line-height:1.6}
.reading .rfoot b{color:var(--text2);font-weight:600}

/* The verdict result block: a stamped mark beside the word and the action. Used
   wherever a single assessment is the whole answer on screen (try, playground,
   console). It was defined in v2, and removing it during the migration silently
   unstyled four call sites in try.js and playground.js -- caught by the class
   audit in tests/test_design_system.py, which now runs on every build for
   exactly this reason. */
.vhero{display:flex;align-items:center;gap:var(--sp-4)}
.vhero .vmark{width:40px;height:40px;flex:none;border-radius:var(--r-md);display:grid;
  place-items:center;font-family:var(--mono);font-size:20px;font-weight:700;
  border:var(--bw2) solid currentColor}
.vhero.ok{color:var(--ok)} .vhero.warn{color:var(--warn)} .vhero.bad{color:var(--bad)}
.vhero .vword{font-family:var(--mono);font-size:var(--fs-lg);font-weight:700;letter-spacing:.01em}
.vhero .vact{font-size:var(--fs-sm);color:var(--text2);margin-top:2px;font-family:var(--sans)}

/* A bare figure that genuinely has no scale (how many models, how many keys).
   Capped at --fs-lg BY DESIGN: an unmeasured count must never outweigh a
   measured reading on the same screen. This is the rule that stops every
   number on the page from being enormous. */
.count{display:block}
.count .k{font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted);
  text-transform:uppercase;letter-spacing:.1em}
.count .v{font-family:var(--mono);font-size:var(--fs-lg);font-weight:600;color:var(--text);
  margin-top:var(--sp-2);line-height:1.1;display:block}
.count .d{font-size:var(--fs-xs);color:var(--muted);margin-top:var(--sp-2);line-height:1.55}
/* `.stat` is v2's name for the same thing and is emitted by decisions.js,
   account.js and admin.js. Aliased rather than renamed across three files -- but
   it inherits the --fs-lg cap, so the 28px figures those pages drew are gone.
   It carries its own box, because those pages use it standalone. */
.stat{background:var(--surface);border:var(--bw) solid var(--line);
  border-radius:var(--r-md);padding:var(--sp-4)}
.stat .k{font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted);
  text-transform:uppercase;letter-spacing:.1em}
.stat .v{font-family:var(--mono);font-size:var(--fs-lg);font-weight:600;
  margin-top:var(--sp-2);line-height:1.1;display:block}
.stat .d{font-size:var(--fs-xs);color:var(--muted);margin-top:var(--sp-2);line-height:1.55}

/* Evidence meter -- the reading that matters most, kept as its own name because
   pages reference it. Same anatomy, sized as a block of its own. */
.meter{background:var(--bg2);border:var(--bw) solid var(--line);border-radius:var(--r-sm);
  padding:var(--sp-4);margin-bottom:var(--sp-4)}
.meter .mhead{display:flex;align-items:baseline;justify-content:space-between;gap:var(--sp-3)}
.meter .mlab{font-size:var(--fs-xs);color:var(--text2);font-weight:500}
.meter .mval{font-family:var(--mono);font-size:var(--fs-md);font-weight:600}
.meter .mval.ok{color:var(--ok)}.meter .mval.warn{color:var(--warn)}.meter .mval.bad{color:var(--bad)}
.meter .mbar{position:relative;height:10px;margin-top:var(--sp-2);background:var(--bg2);
  border:var(--bw) solid var(--line);border-radius:var(--r-sm);overflow:hidden}
.meter .mbar i{display:block;height:100%;background:var(--muted)}
.meter .mbar i.ok{background:var(--ok)}.meter .mbar i.warn{background:var(--warn)}
.meter .mbar i.bad{background:var(--bad)}
.meter .mthr{position:absolute;inset-block:-1px;inset-inline-end:0;width:var(--bw2);
  background:var(--text);opacity:.85}
.meter .mfoot{margin-top:var(--sp-2);font-size:var(--fs-xs);color:var(--muted);line-height:1.65}

/* ══ 8. VERDICT & STATUS ════════════════════════════════════════════════════
   Three channels, always: the word, the mark, the colour. Remove any one and
   the state is still readable -- that is the test each of these has to pass. */
/* THE BADGE SITS ON A REAL SURFACE, NOT A TINT.
   This was the one place the system broke its own rule that state is carried by
   border, value and mark rather than a colour wash -- and it is exactly where it
   failed. A verdict colour on a wash of ITSELF loses contrast against the very
   text it is colouring: --ok measured 4.89:1 on --surface and only 4.12:1 once a
   13% green fill went behind it; --bad fell 6.25 -> 4.30. Both below AA.

   test_accessibility.py could not see this. It checks token PAIRS from the
   stylesheet and has no way to know which pairs actually occur on screen, and
   "verdict colour over a tint of the same verdict colour" is a pair that exists
   only at render time. tools/check_ui.py measures the composited result in the
   live DOM, which is how it surfaced.

   --bg2 is a genuine surface step, so the plate reads as slightly inset on dark
   and slightly raised on light, and the text keeps its verified contrast
   regardless of what the badge is sitting inside. */
.vd{display:inline-flex;align-items:center;gap:6px;font-size:var(--fs-xs);font-weight:600;
  padding:3px 7px;border-radius:var(--r-sm);white-space:nowrap;letter-spacing:.02em;
  background:var(--bg2);border:var(--bw) solid var(--line)}
.vd::before{content:attr(data-mark);font-weight:700}
.vd.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 42%,transparent)}
.vd.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 42%,transparent)}
.vd.bad{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 48%,transparent)}
.vd.info{color:var(--info);border-color:color-mix(in srgb,var(--info) 42%,transparent)}
.vd.none{color:var(--muted);border-color:var(--line)}

/* The other context: one decision opened to be read. Heavy inline-start rule
   standing in for a stamped margin -- a shape, not a pill, so it reads as a
   mark made against a record rather than a badge on a card. */
.vd-hero{display:block;font-family:var(--mono);font-weight:700;font-size:var(--fs-2xl);
  letter-spacing:-.02em;padding-inline-start:var(--sp-4);
  border-inline-start:3px solid var(--muted);line-height:1.15;color:var(--text)}
.vd-hero.ok{color:var(--ok);border-inline-start-color:var(--ok)}
.vd-hero.warn{color:var(--warn);border-inline-start-color:var(--warn)}
.vd-hero.bad{color:var(--bad);border-inline-start-color:var(--bad)}
.vd-hero .vsub{display:block;font-family:var(--sans);font-size:var(--fs-sm);font-weight:400;
  color:var(--text2);margin-top:var(--sp-1);letter-spacing:0}

/* Status dot. Never used alone -- always beside its word. */
.dot{width:8px;height:8px;border-radius:var(--r-round);flex:none;background:var(--faint);
  display:inline-block}
.dot.ok{background:var(--ok)}.dot.warn{background:var(--warn)}
.dot.bad{background:var(--bad)}.dot.info{background:var(--info)}
.dot.acc{background:var(--accent)}

/* A labelled chip: a fact with its name, in the machine voice. Square, bordered
   -- it is a plate, not a pill. */
.pill{font-family:var(--mono);font-size:var(--fs-xs);padding:4px 8px;border-radius:var(--r-sm);
  border:var(--bw) solid var(--line);display:inline-flex;align-items:center;gap:7px;
  color:var(--text2);background:var(--bg2)}

/* ══ 9. BUTTONS & CONTROLS ══════════════════════════════════════════════════
   Primary is an inversion, not a colour (rule 2). It is also, incidentally, the
   highest-contrast control this system can produce. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-2);
  font-family:var(--sans);font-weight:600;font-size:var(--fs-sm);
  border-radius:var(--r-sm);padding:9px 14px;border:var(--bw) solid transparent;
  cursor:pointer;white-space:nowrap;min-height:36px;
  transition:background var(--dur-1) var(--ease),border-color var(--dur-1) var(--ease),
             color var(--dur-1) var(--ease)}
.btn.p{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.btn.p:hover{background:var(--accent2);border-color:var(--accent2)}
.btn.g{background:transparent;border-color:var(--line2);color:var(--text);font-weight:500}
.btn.g:hover{background:var(--hair);border-color:var(--text2)}
.btn.q{background:transparent;border-color:transparent;color:var(--text2);font-weight:500}
.btn.q:hover{background:var(--hair);color:var(--text)}
.btn.d{background:transparent;border-color:var(--bad);color:var(--bad);font-weight:500}
.btn.d:hover{background:var(--bad-dim)}
.btn.lg{padding:12px 20px;font-size:var(--fs-md);min-height:44px}
.btn.sm{padding:5px 10px;font-size:var(--fs-xs);min-height:30px}
.btn.full{width:100%}
/* A long full-width CTA has room to wrap onto a second line without looking
   broken; a compact inline button does not. The one exception to nowrap. */
.btn.full{white-space:normal;line-height:1.3}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn:active:not(:disabled){transform:translateY(1px)}
/* Loading is a state, not a spinner: the label changes and the control stops
   accepting input. A busy button that still looks pressable is a lie. */
.btn[aria-busy="true"]{cursor:progress;opacity:.75}
.btn[aria-busy="true"]::after{content:"";width:11px;height:11px;flex:none;
  border:var(--bw2) solid currentColor;border-block-start-color:transparent;
  border-radius:var(--r-round);animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.btn[aria-busy="true"]::after{animation:none;opacity:.5}}

.iconbtn{width:36px;height:36px;border-radius:var(--r-sm);border:var(--bw) solid var(--line);
  background:transparent;display:grid;place-items:center;color:var(--text2);cursor:pointer;
  position:relative;transition:background var(--dur-1) var(--ease),color var(--dur-1) var(--ease)}
.iconbtn:hover{background:var(--hair);color:var(--text)}
/* 44x44 comfortable tap target (Apple HIG / Material) without making the icon
   look bigger: the control keeps its 36px box and grows its hit area. */
.iconbtn::after{content:"";position:absolute;inset:-4px}

/* Segmented control -- one choice from a small set. Square, hairline-divided,
   the selected face raised in value rather than tinted. */
.seg{display:inline-flex;border:var(--bw) solid var(--line2);border-radius:var(--r-sm);
  overflow:hidden;background:var(--bg2)}
.seg button,.rangebar button{background:transparent;border:none;color:var(--text2);
  font-family:var(--mono);font-size:var(--fs-xs);padding:7px 12px;cursor:pointer;
  min-height:34px;transition:background var(--dur-1) var(--ease),color var(--dur-1) var(--ease)}
.seg button+button,.rangebar button+button{border-inline-start:var(--bw) solid var(--line)}
.seg button:hover,.rangebar button:hover{color:var(--text);background:var(--hair)}
.seg button.on,.rangebar button.on{background:var(--raise);color:var(--text);font-weight:600}
.rangebar{display:inline-flex;border:var(--bw) solid var(--line2);border-radius:var(--r-sm);
  overflow:hidden;background:var(--bg2)}

/* ══ 10. FORMS ══════════════════════════════════════════════════════════════ */
label{display:block;font-size:var(--fs-xs);font-weight:600;color:var(--text2);
  margin-bottom:6px;letter-spacing:.01em}
input,select,textarea{width:100%;background:var(--bg2);border:var(--bw) solid var(--line2);
  border-radius:var(--r-sm);padding:9px 11px;color:var(--text);
  font-family:var(--sans);font-size:var(--fs-sm);min-height:36px}
input.mono,textarea.mono{font-family:var(--mono)}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);
  box-shadow:0 0 0 var(--bw2) var(--accent-dim)}
input::placeholder,textarea::placeholder{color:var(--faint)}
input:disabled,select:disabled,textarea:disabled{opacity:.55;cursor:not-allowed}
/* An error must point at the field it is about, not only sit in a message
   below the form. Colour is not the only channel: the border weight changes too. */
input.err-field,select.err-field,textarea.err-field{border-color:var(--bad);border-width:var(--bw2)}
input.err-field:focus,select.err-field:focus,textarea.err-field:focus{
  border-color:var(--bad);box-shadow:0 0 0 var(--bw2) var(--bad-dim)}
.field{margin-bottom:var(--sp-4)}
.field .hint{font-size:var(--fs-xs);color:var(--muted);margin-top:5px;line-height:1.55}
.field .err{font-size:var(--fs-xs);color:var(--bad);margin-top:5px;display:none}
.field .err.show{display:block}
.toolbar{display:flex;gap:var(--sp-2);flex-wrap:wrap;align-items:center;margin-bottom:var(--sp-3)}
.toolbar input,.toolbar select{width:auto;min-width:160px}
.toolbar input[type="search"]{min-width:240px;flex:1}

/* ══ 11. TABLES ═════════════════════════════════════════════════════════════
   Rows, not cards. Header in the machine voice because a column header names a
   field the engine produced. Numeric cells are isolated and end-aligned so a
   column of figures lines up on its last digit in BOTH directions. */
table{width:100%;border-collapse:collapse}
th{text-align:start;font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted);
  text-transform:uppercase;letter-spacing:.08em;font-weight:500;
  padding:10px var(--sp-4);border-bottom:var(--bw) solid var(--line);white-space:nowrap}
td{padding:11px var(--sp-4);border-bottom:var(--bw) solid var(--hair);font-size:var(--fs-sm);
  vertical-align:middle}
tbody tr:last-child td{border-bottom:none}
tbody tr.link{cursor:pointer}
tbody tr.link:hover{background:var(--hair)}
th.num,td.num{text-align:end;font-family:var(--mono);unicode-bidi:isolate}
td.mono{font-family:var(--mono);unicode-bidi:isolate}
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}

/* ══ 12. NAVIGATION ═════════════════════════════════════════════════════════ */
.side{border-inline-end:var(--bw) solid var(--line);background:var(--bg2);
  display:flex;flex-direction:column;position:sticky;top:0;height:100vh;overflow-y:auto}
.brand{display:flex;align-items:center;gap:9px;padding:var(--sp-4);
  border-bottom:var(--bw) solid var(--line)}
.brand .mk{width:26px;height:26px;flex:none}
.brand .wm{font-weight:600;font-size:var(--fs-md);letter-spacing:-.02em}
.nav{padding:var(--sp-3) var(--sp-2);display:flex;flex-direction:column;gap:1px}
.nav .sec{font-family:var(--mono);font-size:var(--fs-xs);letter-spacing:.14em;text-transform:uppercase;
  color:var(--faint);padding:var(--sp-4) var(--sp-2) var(--sp-2)}
.nav a,.nav .item{display:flex;align-items:center;gap:var(--sp-3);padding:8px var(--sp-3);
  border-radius:var(--r-sm);color:var(--text2);font-size:var(--fs-sm);font-weight:500;
  position:relative;cursor:pointer;min-height:36px;
  transition:background var(--dur-1) var(--ease),color var(--dur-1) var(--ease)}
.nav a:hover{background:var(--hair);color:var(--text)}
/* Selected state WITHOUT a colour tint: the accent has no hue to tint with, so
   selection is carried by a surface-value step, a weight change, and a solid
   rule at the inline start. All three survive greyscale. */
.nav a.on{background:var(--raise);color:var(--text);font-weight:600}
.nav a.on::before{content:"";position:absolute;inset-inline-start:0;inset-block:7px;width:2px;
  border-radius:1px;background:var(--accent)}
.nav .cnt{margin-inline-start:auto;font-family:var(--mono);font-size:var(--fs-xs);
  color:var(--muted)}
.nav .cnt.alert{color:var(--bad);font-weight:700}
.side-foot{margin-top:auto;padding:var(--sp-4);border-top:var(--bw) solid var(--line);
  font-size:var(--fs-xs);color:var(--muted);line-height:1.7}
.side-foot a{color:var(--text2);text-decoration:underline;text-underline-offset:2px}

.topbar{position:sticky;top:0;z-index:30;display:flex;align-items:center;gap:var(--sp-3);
  padding:0 var(--sp-6);height:54px;border-bottom:var(--bw) solid var(--line);
  background:color-mix(in srgb,var(--bg) 92%,transparent);backdrop-filter:blur(10px)}
.crumb{display:flex;align-items:center;gap:var(--sp-2);font-size:var(--fs-sm);min-width:0}
.crumb .b{color:var(--muted)}
.crumb .s{color:var(--faint)}
.crumb h1.c,.crumb .c{font-weight:600;font-size:var(--fs-sm);letter-spacing:normal;
  line-height:inherit;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* `.top` and `.topnav` are the same component under two names, and `.top` was
   NEVER DEFINED -- ten marketing and legal pages (404, 500, calibration, contact,
   faq, features, privacy, security, status, terms) have shipped a bare unstyled
   <header class="top">, inheriting nothing, for as long as the class has been
   there. Nothing errored; the header simply had no padding, no rule beneath it
   and no layout. Found by the class audit while migrating, not by looking. */
.topnav,.top{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4) var(--sp-6);
  border-bottom:var(--bw) solid var(--line);position:sticky;top:0;z-index:30;
  background:color-mix(in srgb,var(--bg) 92%,transparent);backdrop-filter:blur(10px)}
.topnav .lnk,.top .lnk{font-size:var(--fs-sm);color:var(--text2)}
.topnav .lnk:hover,.top .lnk:hover{color:var(--text)}
.top .sp{flex:1}
/* Same story for try.html's footer wrapper. */
.foot{border-top:var(--bw) solid var(--line);margin-top:var(--sp-16);padding:var(--sp-8) 0}
.fwrap{max-width:1140px;margin:0 auto;padding:0 var(--sp-6);display:flex;flex-wrap:wrap;
  gap:var(--sp-3);align-items:center;font-size:var(--fs-xs);color:var(--muted)}

.tablist{display:flex;gap:2px;border-bottom:var(--bw) solid var(--line);margin-bottom:var(--sp-5);
  overflow-x:auto;-webkit-overflow-scrolling:touch}
.tab{background:none;border:none;border-bottom:var(--bw2) solid transparent;color:var(--text2);
  font-family:var(--sans);font-size:var(--fs-sm);font-weight:600;padding:10px var(--sp-3);
  cursor:pointer;white-space:nowrap;flex:none;min-height:40px}
.tab:hover{color:var(--text)}
.tab[aria-selected="true"],.tab.on{color:var(--text);border-bottom-color:var(--accent)}
.tabpanel{display:none}
.tabpanel.active{display:block}

.pager{display:flex;align-items:center;justify-content:flex-end;gap:var(--sp-2);
  padding:var(--sp-3) var(--sp-4);border-top:var(--bw) solid var(--line)}
.pager .n{margin-inline-end:auto;font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted)}

/* Stepped walkthrough (onboarding). A step that is not yet reachable says so by
   value and by a disabled control, never by colour alone. */
.rail{display:flex;flex-direction:column;gap:var(--sp-3)}
.step{display:grid;grid-template-columns:auto 1fr;gap:var(--sp-4);padding:var(--sp-5);
  background:var(--surface);border:var(--bw) solid var(--line);border-radius:var(--r-md)}
.step .sdot{width:26px;height:26px;border-radius:var(--r-round);display:grid;place-items:center;
  font-family:var(--mono);font-size:var(--fs-xs);font-weight:700;flex:none;
  background:var(--raise);color:var(--muted);border:var(--bw) solid var(--line2)}
.step.on{border-color:var(--line2)}
.step.on .sdot{background:var(--accent);color:var(--accent-ink);border-color:var(--accent)}
.step.done .sdot{background:var(--ok-dim);color:var(--ok);border-color:var(--ok)}
.step.locked{opacity:.55}
.step h3{font-size:var(--fs-md);margin-bottom:5px}

/* ══ 13. OVERLAYS ═══════════════════════════════════════════════════════════ */
.scrim{position:fixed;inset:0;background:rgba(3,5,8,.62);z-index:90;display:none}
.scrim.open{display:block}
.drawer{position:fixed;inset-block:0;inset-inline-end:0;width:min(660px,100vw);
  background:var(--surface);border-inline-start:var(--bw) solid var(--line2);
  box-shadow:var(--sh-sheet);z-index:91;display:none;flex-direction:column}
.drawer.open{display:flex}
.drawer-h{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-4);
  border-bottom:var(--bw) solid var(--line);flex:none}
.drawer-h .t{font-weight:600;font-size:var(--fs-md);flex:1;min-width:0}
.drawer-b{flex:1;overflow-y:auto;padding:var(--sp-5)}
.drawer-f{flex:none;padding:var(--sp-4);border-top:var(--bw) solid var(--line);
  display:flex;gap:var(--sp-2);flex-wrap:wrap}

.pmodal-scrim{position:fixed;inset:0;background:rgba(3,5,8,.62);z-index:90;display:none}
.pmodal-scrim.open{display:block}
.pmodal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
  width:min(420px,calc(100vw - 40px));background:var(--surface);
  border:var(--bw) solid var(--line2);border-radius:var(--r-md);box-shadow:var(--sh-sheet);
  z-index:91;display:none;padding:var(--sp-5)}
.pmodal.open{display:block}
.pmodal h4{font-size:var(--fs-md);margin-bottom:var(--sp-2)}
.pmodal p{font-size:var(--fs-sm);color:var(--text2);line-height:1.6;margin-bottom:var(--sp-4);
  white-space:pre-line}
.pmodal input,.pmodal select{margin-bottom:var(--sp-4)}
.pmodal .row{display:flex;gap:var(--sp-2);justify-content:flex-end;padding:0;border:none}
.pmodal.danger h4{color:var(--bad)}
.pmodal .code{margin-bottom:var(--sp-4);display:flex;align-items:center;
  justify-content:space-between;gap:var(--sp-2);word-break:break-all;white-space:normal}

.notifbtn{position:relative}
.notifbtn .ndot{position:absolute;top:3px;inset-inline-end:3px;min-width:15px;height:15px;
  border-radius:var(--r-sm);background:var(--bad);color:#fff;font-family:var(--mono);
  font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center;
  padding:0 3px;box-shadow:0 0 0 2px var(--bg)}
.notifbtn .ndot.show{display:flex}
.notifpanel{position:absolute;top:calc(100% + 8px);inset-inline-end:0;
  width:min(340px,calc(100vw - 24px));max-height:420px;overflow-y:auto;background:var(--surface);
  border:var(--bw) solid var(--line2);border-radius:var(--r-md);box-shadow:var(--sh-sheet);
  z-index:50;display:none}
.notifpanel.open{display:block}
.notifpanel .nph{padding:var(--sp-3) var(--sp-4);border-bottom:var(--bw) solid var(--hair);
  font-size:var(--fs-xs);font-weight:600;color:var(--text2);display:flex;
  justify-content:space-between;align-items:center}
.notifpanel .npempty{padding:var(--sp-8) var(--sp-4);text-align:center;color:var(--muted);
  font-size:var(--fs-sm)}
.notifrow{display:block;padding:10px var(--sp-4);border-bottom:var(--bw) solid var(--hair);
  color:inherit;transition:background var(--dur-1) var(--ease)}
.notifrow:hover{background:var(--hair)}
.notifrow .nv{font-family:var(--mono);font-size:var(--fs-xs);font-weight:600}
.notifrow .nv.bad{color:var(--bad)}.notifrow .nv.warn{color:var(--warn)}.notifrow .nv.ok{color:var(--ok)}
.notifrow .nm{font-size:var(--fs-sm);color:var(--text);margin-top:3px}
.notifrow .nt{font-size:var(--fs-xs);color:var(--faint);margin-top:2px}
.notifpanel .npfoot{padding:10px var(--sp-4);text-align:center;border-top:var(--bw) solid var(--hair)}
.notifpanel .npfoot a{font-size:var(--fs-xs);color:var(--text2);text-decoration:underline}

#toasts{position:fixed;bottom:18px;inset-inline-end:18px;display:flex;flex-direction:column;
  gap:var(--sp-2);z-index:60}
.toast{display:flex;align-items:center;gap:9px;background:var(--surface);
  border:var(--bw) solid var(--line2);border-radius:var(--r-sm);padding:10px var(--sp-4);
  font-size:var(--fs-sm);box-shadow:var(--sh-sheet);
  transition:opacity var(--dur-2) var(--ease),transform var(--dur-2) var(--ease)}
.toast.err{border-color:var(--bad);color:var(--bad)}

.totop{position:fixed;bottom:22px;inset-inline-end:22px;width:40px;height:40px;
  border-radius:var(--r-sm);background:var(--surface);border:var(--bw) solid var(--line2);
  color:var(--text2);cursor:pointer;display:grid;place-items:center;box-shadow:var(--sh-sheet);
  opacity:0;pointer-events:none;transform:translateY(8px);
  transition:opacity var(--dur-2) var(--ease),transform var(--dur-2) var(--ease);z-index:40}
.totop.show{opacity:1;pointer-events:auto;transform:translateY(0)}
.totop:hover{color:var(--text);border-color:var(--text2)}

.skip-link{position:absolute;top:0;inset-inline-start:0;transform:translateY(-120%);
  background:var(--accent);color:var(--accent-ink);font-family:var(--sans);font-size:var(--fs-sm);
  font-weight:600;padding:10px var(--sp-4);border-radius:0 0 var(--r-sm) 0;
  z-index:200;transition:transform var(--dur-1) var(--ease)}
.skip-link:focus{transform:translateY(0)}

/* ══ 14. FEEDBACK ═══════════════════════════════════════════════════════════ */
.notice{display:flex;gap:10px;align-items:flex-start;font-size:var(--fs-sm);color:var(--text2);
  background:var(--surface);border:var(--bw) solid var(--line);border-radius:var(--r-md);
  padding:12px var(--sp-4);line-height:1.65;
  border-inline-start:3px solid var(--line2)}
.notice b{color:var(--text)}
.notice .ic{flex:none;margin-top:2px}
.notice.info{border-inline-start-color:var(--info)}
.notice.info .ic{color:var(--info)}
.notice.warn{border-inline-start-color:var(--warn);background:var(--warn-dim)}
.notice.warn .ic{color:var(--warn)}
.notice.bad{border-inline-start-color:var(--bad);background:var(--bad-dim)}
.notice.bad .ic{color:var(--bad)}
.notice.ok{border-inline-start-color:var(--ok)}
.notice.ok .ic{color:var(--ok)}
.msg{display:none;border-radius:var(--r-sm);padding:10px 12px;font-size:var(--fs-sm);
  margin-top:var(--sp-3);border:var(--bw) solid transparent}
.msg.show{display:block}
/* Same rule as .vd, and the same reason: a status colour over a wash of itself
   loses the contrast it was chosen for. These are hidden until .show, so the
   live check never rendered them -- a latent instance of the defect it found in
   the badge, fixed here rather than left to surface on a customer's error. */
.msg.err{background:var(--bg2);color:var(--bad);border-color:color-mix(in srgb,var(--bad) 45%,transparent)}
.msg.ok{background:var(--bg2);color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent)}

/* Empty state: says what would be here, and what to do to make it appear.
   Carries the mark with its point off-centre -- the instrument, seeking. */
.empty{padding:var(--sp-10) var(--sp-6);text-align:center;color:var(--muted);
  font-size:var(--fs-sm);line-height:1.75}
.empty h4{color:var(--text);font-size:var(--fs-md);font-weight:600;margin-bottom:6px}
.empty .mk{width:32px;height:32px;margin:0 auto var(--sp-4);display:block;opacity:.5}
.empty .act{margin-top:var(--sp-4)}

/* Loading is a shape, not a spinner: a skeleton that matches the layout it is
   standing in for, so nothing jumps when the data lands. */
.skel{background:var(--raise);border-radius:var(--r-sm);position:relative;overflow:hidden}
.skel::after{content:"";position:absolute;inset:0;transform:translateX(-100%);
  background:linear-gradient(90deg,transparent,var(--hair),transparent);
  animation:skel 1.3s infinite}
@keyframes skel{100%{transform:translateX(100%)}}
@media(prefers-reduced-motion:reduce){.skel::after{animation:none}}
.skel.line{height:12px;margin-bottom:8px}
.skel.line.w60{width:60%} .skel.line.w40{width:40%} .skel.line.w80{width:80%}
.skel.block{height:120px}

/* ══ 15. EVIDENCE COMPONENTS ════════════════════════════════════════════════
   The reasoning chain, made spatial. Every decision surface walks the same
   order and it never gets reshuffled per page:
     decision -> evidence -> confidence & its limits -> risk -> status ->
     action -> audit trail
   A hairline thread physically links the nodes so the eye walks the argument in
   the order a reviewer reasons, rather than in whatever order the API returned. */
.chain{position:relative;padding-inline-start:var(--sp-6)}
.chain::before{content:"";position:absolute;inset-inline-start:9px;top:22px;bottom:22px;
  width:var(--bw);background:var(--line)}
.node{position:relative;background:var(--surface);border:var(--bw) solid var(--line);
  border-radius:var(--r-md);padding:var(--sp-5);margin-bottom:var(--sp-3)}
.node::before{content:"";position:absolute;inset-inline-start:calc(-1 * var(--sp-6) + 5px);
  top:24px;width:9px;height:9px;border-radius:var(--r-round);
  background:var(--ndot,var(--muted));border:2px solid var(--bg)}
.node .eyebrow{margin-bottom:var(--sp-3)}

/* Evidence layers: every INDEPENDENT signal is listed, INCLUDING the ones that
   are dark -- and a dark layer says why it is dark. A report that quietly omits
   a layer looks like a report that never had it. */
.layers{display:flex;flex-direction:column;gap:1px;background:var(--line);
  border:var(--bw) solid var(--line);border-radius:var(--r-sm);overflow:hidden}
.layer{display:grid;grid-template-columns:auto 1fr auto;gap:var(--sp-3);align-items:start;
  background:var(--surface);padding:var(--sp-3) var(--sp-4)}
.layer.off{background:var(--bg2)}
.layer .lname{font-size:var(--fs-sm);font-weight:600;color:var(--text)}
.layer.off .lname{color:var(--muted);font-weight:500}
.layer .lwhy{font-size:var(--fs-xs);color:var(--muted);margin-top:3px;line-height:1.6}
.layer .lstate{font-family:var(--mono);font-size:var(--fs-xs);font-weight:600;white-space:nowrap}

/* Key/value evidence rows. The key is Kirelta naming a thing; the value is what
   the engine returned -- so the key is sans and the value is mono, every time. */
.kv{display:flex;justify-content:space-between;align-items:baseline;gap:var(--sp-4);
  padding:8px 0;border-bottom:var(--bw) solid var(--hair)}
.kv:last-child{border-bottom:none}
.kv .k{font-family:var(--sans);font-size:var(--fs-xs);color:var(--text2)}
.kv .v{font-family:var(--mono);font-size:var(--fs-sm);font-weight:600;color:var(--text);
  text-align:end;unicode-bidi:isolate}
.kv .v.ok{color:var(--ok)}.kv .v.warn{color:var(--warn)}.kv .v.bad{color:var(--bad)}
.kvs{display:flex;flex-direction:column;gap:1px;border-radius:var(--r-sm);overflow:hidden}
.kvbox{display:flex;justify-content:space-between;align-items:center;gap:var(--sp-3);
  padding:10px 12px;background:var(--surface)}
.kvbox .k{font-size:var(--fs-sm);color:var(--text2)}
.kvbox .v{font-size:var(--fs-sm);color:var(--text);font-weight:500}
.kvbox .v.bad{color:var(--bad)}

/* Contribution bars. Achromatic on purpose: "which feature moved" is a
   magnitude, not a verdict, and spending a semantic colour on it would make
   share-of-deviation look like severity. */
.impwrap{margin-top:var(--sp-4);padding-top:var(--sp-3);border-top:var(--bw) solid var(--line)}
.imp{display:grid;grid-template-columns:minmax(70px,auto) 1fr 52px;align-items:center;
  gap:10px;margin-bottom:6px}
.imp .ifeat{font-size:var(--fs-xs);color:var(--text2);font-family:var(--mono);
  unicode-bidi:isolate;overflow:hidden;text-overflow:ellipsis}
.imp .ibar{height:8px;background:var(--bg2);border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);overflow:hidden}
.imp .ibar i{display:block;height:100%;background:var(--text2)}
.imp .ipct{font-size:var(--fs-xs);color:var(--text);text-align:end;font-weight:600;
  font-family:var(--mono)}

.enotes{margin-top:var(--sp-4);padding-top:var(--sp-3);border-top:var(--bw) solid var(--line);
  display:flex;flex-direction:column;gap:var(--sp-2)}
.enote{display:flex;gap:9px;align-items:flex-start;font-size:var(--fs-xs);color:var(--warn);
  line-height:1.6;background:var(--warn-dim);border-radius:var(--r-sm);padding:9px 11px;
  border-inline-start:2px solid var(--warn)}
.enote .ei,.enote .ic{flex:none}   /* console.js emits .ei, other callers .ic */

/* Audit timeline. Ledger treatment: rules, not boxes. */
.tl{position:relative;padding-inline-start:var(--sp-5)}
.tl::before{content:"";position:absolute;inset-inline-start:4px;inset-block:6px;width:var(--bw);
  background:var(--line)}
.tl .ev{position:relative;padding-bottom:var(--sp-4)}
.tl .ev::before{content:"";position:absolute;inset-inline-start:calc(-1 * var(--sp-5) + 1px);
  top:6px;width:7px;height:7px;border-radius:var(--r-round);background:var(--muted);
  border:2px solid var(--bg)}
.tl .ev.bad::before{background:var(--bad)}
.tl .ev.warn::before{background:var(--warn)}
.tl .ev.ok::before{background:var(--ok)}
.tl .ev .w{font-size:var(--fs-sm);color:var(--text)}
.tl .ev .m{font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted);margin-top:2px;
  unicode-bidi:isolate}
.tl .ev .note{font-size:var(--fs-xs);color:var(--text2);margin-top:3px}

/* Integrity / seal. The evidence snapshot is content-hashed and the audit log is
   signed; both need somewhere honest to render, including when they FAIL. */
.integrity{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);
  font-size:var(--fs-xs)}
.integrity.ok{color:var(--ok)} .integrity.bad{color:var(--bad)} .integrity.none{color:var(--muted)}
.hash{font-size:var(--fs-xs);color:var(--muted);word-break:break-all;unicode-bidi:isolate}

/* ══ 16. UTILITY ════════════════════════════════════════════════════════════ */
.code{background:var(--bg2);border:var(--bw) solid var(--line);border-radius:var(--r-sm);
  padding:12px var(--sp-4);font-family:var(--mono);font-size:var(--fs-xs);line-height:1.75;
  color:var(--text2);overflow-x:auto;white-space:pre;position:relative;unicode-bidi:isolate;
  direction:ltr;text-align:left}
.code b{color:var(--text);font-weight:600}
.copy{position:absolute;top:6px;inset-inline-end:6px;background:var(--surface);
  border:var(--bw) solid var(--line2);color:var(--text2);border-radius:var(--r-sm);
  padding:6px 10px;font-size:var(--fs-xs);font-family:var(--sans);cursor:pointer;min-height:32px}
.copy:hover{color:var(--text);background:var(--raise)}

.collapsible{margin-top:var(--sp-3);border:var(--bw) solid var(--line);
  border-radius:var(--r-sm);overflow:hidden}
.collapsible summary{padding:10px var(--sp-4);cursor:pointer;font-size:var(--fs-sm);
  color:var(--text2);list-style:none;display:flex;align-items:center;gap:var(--sp-2);
  user-select:none;min-height:40px}
.collapsible summary::-webkit-details-marker{display:none}
.collapsible summary::before{content:"";width:0;height:0;
  border-inline-start:5px solid var(--faint);
  border-block-start:4px solid transparent;border-block-end:4px solid transparent;
  transition:transform var(--dur-1) var(--ease)}
.collapsible[open] summary::before{transform:rotate(90deg)}
[dir="rtl"] .collapsible[open] summary::before{transform:rotate(-90deg)}
.collapsible summary:hover{color:var(--text)}
.collapsible table{border-top:var(--bw) solid var(--hair)}

.usercard{display:flex;align-items:center;gap:10px;padding:var(--sp-3) var(--sp-4);
  border-top:var(--bw) solid var(--line)}
.usercard .av{width:30px;height:30px;border-radius:var(--r-round);background:var(--raise);
  color:var(--text2);display:grid;place-items:center;flex:none;font-family:var(--mono);
  font-size:11px;font-weight:700;border:var(--bw) solid var(--line2)}
.usercard .id{min-width:0;flex:1}
.usercard .nm{font-size:var(--fs-sm);font-weight:600;color:var(--text);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.usercard .em{font-size:var(--fs-xs);color:var(--faint);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;unicode-bidi:isolate}
.usercard .out{flex:none;background:none;border:none;color:var(--faint);cursor:pointer;
  padding:6px;display:grid;place-items:center;border-radius:var(--r-sm)}
.usercard .out:hover{color:var(--bad);background:var(--bad-dim)}

.gate{max-width:420px;margin:var(--sp-16) auto;text-align:center}
footer{border-top:var(--bw) solid var(--line);margin-top:var(--sp-16);padding:var(--sp-8) 0}
.foot-bar{display:flex;flex-wrap:wrap;gap:var(--sp-3);align-items:center;
  font-size:var(--fs-xs);color:var(--muted)}
.blk-t2{font-family:var(--mono);font-size:var(--fs-xs);color:var(--muted);
  text-transform:uppercase;letter-spacing:.12em;margin-bottom:10px}

/* ══ 17. CHARTS ═════════════════════════════════════════════════════════════
   The dataviz language. Rules, all enforced by charts.js and tested:

   * Every chart states its question in words above it, and draws the threshold
     that makes the answer meaningful.
   * Never smooth. Never interpolate across a gap -- a gap in readings is a gap
     in knowledge and is drawn as one.
   * Rate axes start at 0. A truncated axis makes a 2% change look like a
     collapse, which is exactly the misreading this product exists to prevent.
   * The martingale axis is logarithmic (the quantity spans 0.5 to 1e12) and is
     labelled as logarithmic, every time.
   * Uncertainty is a band, never a point.
   * Reference/baseline is DASHED and drawn in --reference; live data is solid
     ink. Distinguishable with no colour vision at all.
   * The time axis does NOT mirror in RTL. Labels, legend, tooltip and reading
     order do. Mirroring a time series would invert the meaning of "before" and
     "after" -- a correctness bug wearing a localisation costume. */
.chart{display:block;width:100%;height:auto;overflow:visible}
.chart-head{margin-bottom:var(--sp-3)}
.chart-q{font-size:var(--fs-sm);font-weight:600;color:var(--text);line-height:1.45}
.chart-sub{font-size:var(--fs-xs);color:var(--muted);margin-top:3px;line-height:1.6}
.chart .grid-line{stroke:var(--graticule);stroke-width:1}
.chart .axis-label{font-family:var(--mono);font-size:10px;fill:var(--muted)}
.chart .axis-unit{font-family:var(--mono);font-size:10px;fill:var(--faint)}
/* Data ink is achromatic. Colour enters a chart only where a THRESHOLD has been
   crossed or a STATE is being named -- consistent with rule 2. */
.chart .series{fill:none;stroke:var(--text2);stroke-width:1.75;stroke-linejoin:round;
  stroke-linecap:butt}
.chart .series-area{fill:var(--text2);opacity:.07}
.chart .reference{fill:none;stroke:var(--reference);stroke-width:1.5;stroke-dasharray:4 3}
.chart .band{fill:var(--reference);opacity:.13}
.chart .pt{fill:var(--text2)}
.chart .pt.warn{fill:var(--warn)} .chart .pt.bad{fill:var(--bad)} .chart .pt.ok{fill:var(--ok)}
/* The threshold: a hard, labelled rule. Never dotted-and-forgotten. */
.chart .threshold{stroke:var(--bad);stroke-width:1.5;stroke-dasharray:6 3}
.chart .threshold-label{font-family:var(--mono);font-size:10px;fill:var(--bad);font-weight:600}
.chart .ev-line{stroke-width:1.25;stroke-dasharray:3 3;opacity:.8}
.chart .ev-line.info{stroke:var(--info)} .chart .ev-line.warning{stroke:var(--warn)}
.chart .ev-line.critical{stroke:var(--bad)}
/* A gap in the data is drawn, and named. */
.chart .gap{fill:var(--graticule);opacity:.5}
.chart .gap-label{font-family:var(--mono);font-size:9px;fill:var(--faint)}
.chart-legend{display:flex;gap:var(--sp-4);flex-wrap:wrap;margin-top:var(--sp-3);
  font-size:var(--fs-xs);color:var(--muted)}
.chart-legend .sw{display:inline-block;width:14px;height:0;border-top:2px solid currentColor;
  margin-inline-end:6px;vertical-align:middle}
.chart-legend .sw.dash{border-top-style:dashed}
.chart-legend .sw.box{height:9px;border:none;border-radius:1px;background:currentColor;
  width:9px;opacity:.5}
.chart-legend .ref{color:var(--reference)} .chart-legend .live{color:var(--text2)}
.chart-legend .thr{color:var(--bad)}
/* n is always stated. A trend drawn through four points is not a trend, and the
   reader has to be able to see that without counting the dots. */
.chart-n{font-family:var(--mono);font-size:var(--fs-xs);color:var(--faint);margin-top:var(--sp-2)}
.chart-n.thin{color:var(--warn)}
.chartbox{padding:var(--sp-5)}

/* ══ 18. RESPONSIVE ═════════════════════════════════════════════════════════ */
@media(max-width:1100px){
  .split,.split.wide{grid-template-columns:1fr}
}
@media(max-width:920px){
  .shell{grid-template-columns:1fr}
  .side{position:static;height:auto;border-inline-end:none;border-bottom:var(--bw) solid var(--line)}
  .side .nav{flex-direction:row;flex-wrap:wrap;gap:4px;padding:var(--sp-2)}
  .side .nav .sec{display:none}
  .side .nav a.on::before{inset-inline:0;inset-block:auto;bottom:0;top:auto;width:auto;height:2px}
  .side-foot{display:none}
  .content{padding:var(--sp-4) var(--sp-3) var(--sp-10)}
  .topbar{padding:0 var(--sp-3)}
  .drawer{width:100vw}
}
@media(max-width:620px){
  /* Both header names, not just .topnav. `.top` is the one ten marketing and legal pages
     actually use, and it was overflowing a 360px viewport by up to 104px -- the header was one
     non-wrapping row, so the page had to grow wider than the screen to fit it. */
  .topnav,.top{padding:var(--sp-3) var(--sp-4);flex-wrap:wrap}
  .topnav .wrap,.top .wrap{flex-wrap:wrap;gap:var(--sp-2) var(--sp-3)}
  .topnav .spacer,.top .sp{display:none}
  .topnav .lnk,.top .lnk{font-size:var(--fs-xs)}
  .top .btn{font-size:var(--fs-xs);padding:6px 10px}
  .wrap{padding:0 var(--sp-4)}
  .toolbar input[type="search"]{min-width:100%}
  .toolbar input,.toolbar select{min-width:0;flex:1}
  .step{grid-template-columns:1fr}
  .step .sdot{display:none}
}

/* Legal / prose pages. */
.wrap.narrow{padding-top:var(--sp-8);padding-bottom:var(--sp-16)}
.wrap.narrow h1{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.02em;margin-bottom:var(--sp-2)}
.wrap.narrow .upd{color:var(--muted);font-size:var(--fs-xs);margin-bottom:var(--sp-6)}
.wrap.narrow h2{font-size:var(--fs-md);font-weight:600;margin:var(--sp-6) 0 var(--sp-2)}
.wrap.narrow p,.wrap.narrow li{color:var(--text2);font-size:var(--fs-sm);line-height:1.75}
.wrap.narrow ul{margin:var(--sp-2) 0 var(--sp-2);padding-inline-start:20px}

/* ══ 19. PRINT ══════════════════════════════════════════════════════════════
   An audit trail that cannot be handed to an auditor on paper is half an audit
   trail. Chrome only, one column, no navigation, and every hash printed in full
   rather than truncated with an ellipsis. */
@media print{
  .side,.topbar,.toolbar,.pager,.totop,#toasts,.skip-link,.notifpanel,.drawer-f{display:none!important}
  .shell{grid-template-columns:1fr}
  body{background:#fff;color:#000;font-size:11pt}
  .panel,.card,.node{border:1px solid #999;break-inside:avoid;box-shadow:none}
  .hash{word-break:break-all;overflow:visible;white-space:normal}
  .drawer{position:static;width:auto;box-shadow:none;display:block!important}
  .scrim{display:none!important}
  a[href^="http"]::after{content:" (" attr(href) ")";font-size:9pt;color:#555}
}
