/* patala — shared design system for the landing page and the docs viewer.
   Self-contained: no @import, no remote url(), fonts vendored in ../fonts/. */

/* Archivo — the display face. SIL OFL 1.1, Omnibus-Type; the licence ships
   beside the file as LICENSE-archivo.txt.

   It REPLACES Syne, which was the wrong register for this page and is gone:
   Syne is a wide, rounded, sculptural geometric — a poster face. On a page
   whose whole thesis is "instrument panel beside a paper register" it read as
   a fashion mark, and its width forced every section heading onto three or
   four lines ("There is no GUI, and that is a security property" wrapped four
   times at 1440). Archivo is a grotesque drawn for high-performance print and
   UI out of American gothic signage — the register of a statement, a ticket,
   a control panel. It sets the same headline on ONE line where Syne needed
   two, and its figures are the point: this page's argument is carried by
   numerals (293 / 572 / 2 / 0) and Archivo's are unmannered and tabular where
   Syne's were decorative.

   Not what any sibling landing uses either (gitstate and evermesh: Bricolage
   Grotesque, cackle: Figtree, lilmail: Fraunces). One variable file across
   wght 100-900 replaces the three static Syne files it retires. */
@font-face{
  font-family:'Archivo';font-style:normal;font-weight:100 900;font-display:swap;
  src:url('../fonts/archivo-latin-wght-normal.woff2') format('woff2');
}
/* Schibsted Grotesk — the body face. A grotesque with real personality
   (flared terminals, a taller x-height than Inter) instead of the reflexive
   system-sans default. One variable file covers the whole weight range this
   page uses. */
@font-face{
  font-family:'Schibsted Grotesk';font-style:normal;font-weight:400 900;font-display:swap;
  src:url('../fonts/schibsted-grotesk-latin-wght-normal.woff2') format('woff2');
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;
  src:url('../fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2');
}
@font-face{
  font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;
  src:url('../fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2');
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE PALETTE IS A SCHEMA, NOT A MOOD.
   patala moves value over rails that differ in exactly two ways a reader must
   never have to guess about, so those two — and only those two — get hue:

     1. SETTLEMENT CLASS (--cls-*) — what you get when the money moves.
        jade   = NonCustodialFinal   (wallet-to-wallet, no reversal)
        violet = CustodialReversible (chargebacks, KYC, T+2)
        slate  = no class at all     (the offline mock)
        Class hue appears ONLY in class positions: the rail lines in the hero,
        the rail ledger's icons and class chips, the M×1 fan.

     2. EVIDENCE (--ev-*) — how much this repo has actually proved, as a
        ladder rather than a pass/fail:
        slate  --ev-none     nothing was run; no evidence either way
        blue   --ev-design   offline BY DESIGN (the mock) — correct, not a gap
        green  --ev-exec     a real process actually ran (sidecar, bindings, CI)
        amber  --ev-testnet  a real live network — TESTNET ONLY. The highest
                             rung anything in this repo has ever reached, and
                             amber says "yes, but" on purpose.
        rose   --ev-fail     a genuine rejection (Stellar's 2nd instalment)
        There is deliberately NO hue for mainnet. Nothing has reached it, so
        nothing may wear a colour that implies it has.

     3. --vermilion is reserved for patala ITSELF — the seam, the aperture,
        the primary action. It is never a class and never an evidence level,
        so "brand colour" and "data colour" can never be confused.

   Every one of these is measured with the WCAG relative-luminance formula
   against the two grounds it can actually land on (--canvas and --surface) in
   both themes; the figures are in the light-theme note below. --vermilion
   stays decoupled from --vermilion-deep in the light theme (see that note) —
   one is tuned as a FILL under --vermilion-ink, the other as TEXT.
   ═══════════════════════════════════════════════════════════════════════════ */
:root{
  color-scheme:dark;
  --canvas:#0d1220;--indigo:#151c33;--overlay:#273052;
  --overlay-2:#323d63;
  --ink:#eef1f8;--ink-dim:#a9b1cf;--ink-faint:#7c83a2;
  --hair:rgba(238,241,248,.10);--hair-strong:rgba(238,241,248,.18);
  --vermilion:#f06b3d;--vermilion-bright:#ff9068;--vermilion-deep:#f06b3d;
  --vermilion-ink:#151c33; /* label colour on a filled vermilion button */

  /* settlement class */
  --cls-final:#3ecfaf;--cls-rev:#a48cff;--cls-none:#8e97ba;
  /* evidence ladder */
  --ev-none:#8e97ba;--ev-design:#6c8ff5;--ev-exec:#3fbe7c;--ev-testnet:#e6b23c;--ev-fail:#f2506f;
  /* legacy aliases — docs.html and the shared .status component speak these */
  --settled:var(--ev-exec);--pending:var(--ev-design);--failed:var(--ev-fail);

  --rule-color:var(--vermilion);
  --surface:var(--indigo);--surface-raised:var(--overlay);
  --shadow:0 20px 60px -20px rgba(0,0,0,.55);
  --disp:'Archivo',system-ui,-apple-system,sans-serif;
  --body:'Schibsted Grotesk',system-ui,-apple-system,sans-serif;
  --mono:'IBM Plex Mono',ui-monospace,'SF Mono',Menlo,monospace;
}
/* ── MEASURED CONTRAST ───────────────────────────────────────────────────────
   Every value below is checked with the WCAG relative-luminance formula
   against BOTH grounds it can land on — --canvas and --surface — in its own
   theme. Text floor is 4.5:1; nothing that carries meaning is under it.

   DARK   ground canvas #0d1220 / surface #151c33
     --ink        #eef1f8  16.52 / 14.91      --cls-final   #3ecfaf   9.56 / 8.63
     --ink-dim    #a9b1cf   8.78 /  7.93      --cls-rev     #a48cff   6.91 / 6.24
     --ink-faint  #7c83a2   5.00 /  4.52      --cls-none    #8e97ba   6.48 / 5.85
     --vermilion  #f06b3d   6.11 /  5.52      --ev-design   #6c8ff5   6.12 / 5.53
     --verm-bright#ff9068   8.40 /  7.58      --ev-exec     #3fbe7c   7.89 / 7.12
                                              --ev-testnet  #e6b23c   9.60 / 8.67
                                              --ev-fail     #f2506f   5.47 / 4.94

   LIGHT  ground canvas #ebeef5 / surface #fbfcfe
     --ink        #141a2e  14.86 / 16.81      --cls-final   #046a5f   5.59 / 6.33
     --ink-dim    #454c6b   7.23 /  8.19      --cls-rev     #5637ce   6.38 / 7.22
     --ink-faint  #5c6382   5.08 /  5.74      --cls-none    #525977   5.92 / 6.70
     --verm-deep  #b24016   4.97 /  5.62      --ev-design   #2a4cc7   6.11 / 6.92
     (--vermilion #de5d32 is 3.16/3.58 as     --ev-exec     #0c6f42   5.37 / 6.07
      TEXT and is never used as text in       --ev-testnet  #7a5000   6.08 / 6.88
      this theme — see the note below)        --ev-fail     #b81f3e   5.47 / 6.19

   FILLS  --vermilion-ink #151c33 on --vermilion: 5.52 dark / 4.59 light.

   Why --vermilion and --vermilion-deep stay decoupled in the light theme: an
   earlier pass collapsed them, which fixed every TEXT/stroke use of
   --vermilion (already redirected to --vermilion-deep by the
   `:root[data-theme="light"] .x{color:var(--vermilion-deep)}` overrides
   throughout this file) but silently broke every FILL use carrying
   --vermilion-ink text on top (.btn.pri, .lang-tab[aria-selected],
   ::selection) down to 3.56:1. They diverge on purpose and must stay
   divergent: --vermilion is tuned as a FILL, --vermilion-deep as TEXT.

   The light ground moved from a warm ivory (#f6f2e9) to this pale indigo
   paper. That is deliberate and not a neutral swap: patala's dark theme is a
   deep indigo instrument, and the light theme is now the SAME instrument
   under a different light rather than a separate warm world — the light and
   dark grounds are the same hue family, four stops apart. Every token above
   was re-measured against the new ground; --ink-faint (#686e87 -> #5c6382),
   --vermilion-deep (#be471f -> #b24016) and the whole evidence/class set were
   re-tuned for it, since a cooler, slightly lighter paper raises the bar for
   every mid-tone.
   ──────────────────────────────────────────────────────────────────────── */
:root[data-theme="light"]{
  color-scheme:light;
  --canvas:#ebeef5;--indigo:#fbfcfe;--overlay:#e1e6f2;
  --overlay-2:#d3daec;
  --ink:#141a2e;--ink-dim:#454c6b;--ink-faint:#5c6382;
  --hair:rgba(20,26,46,.12);--hair-strong:rgba(20,26,46,.22);
  --vermilion:#de5d32;--vermilion-bright:#c74a20;--vermilion-deep:#b24016;
  --vermilion-ink:#151c33;

  --cls-final:#046a5f;--cls-rev:#5637ce;--cls-none:#525977;
  --ev-none:#525977;--ev-design:#2a4cc7;--ev-exec:#0c6f42;--ev-testnet:#7a5000;--ev-fail:#b81f3e;

  --rule-color:var(--vermilion-deep);
  --surface:#fbfcfe;--surface-raised:#e1e6f2;
  --shadow:0 20px 50px -24px rgba(20,26,46,.22);
}

/* ── The instrument band ────────────────────────────────────────────────────
   Two sections on the landing are not paper — they are the machine itself,
   and they stay a deep indigo panel in BOTH page themes. This is the same
   argument the code surface already makes (see the --code-* note further
   down): a terminal and an instrument panel do not repaint for a paper mode,
   and alternating paper/instrument is what stops nine sections in a row from
   sharing one shape.

   The band works by re-declaring the theme tokens on itself rather than by
   overriding each child, so every component inside — .status, .ledger, .kick,
   .btn, .tag — is the same component it is anywhere else and simply resolves
   against a darker ground. --vermilion-deep is pointed at --vermilion here so
   the `[data-theme="light"] ... {color:var(--vermilion-deep)}` rules elsewhere
   in this file, which fire because the PAGE is still light, land on a value
   that is legible on this dark ground instead of the paper-tuned one.
   Ground is #0a0f1c, marginally darker than --canvas, so every dark-theme
   ratio above is met or exceeded here. */
.band{
  --canvas:#0a0f1c;--indigo:#121a2e;--overlay:#212a47;--overlay-2:#2b365a;
  --ink:#eef1f8;--ink-dim:#a9b1cf;--ink-faint:#7c83a2;
  --hair:rgba(238,241,248,.10);--hair-strong:rgba(238,241,248,.18);
  --vermilion:#f06b3d;--vermilion-bright:#ff9068;--vermilion-deep:#f06b3d;
  --cls-final:#3ecfaf;--cls-rev:#a48cff;--cls-none:#8e97ba;
  --ev-none:#8e97ba;--ev-design:#6c8ff5;--ev-exec:#3fbe7c;--ev-testnet:#e6b23c;--ev-fail:#f2506f;
  --surface:#121a2e;--surface-raised:#212a47;--rule-color:#f06b3d;
  background:var(--canvas);color:var(--ink);
}

*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
@media(prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}
}
body{
  background:var(--canvas);color:var(--ink);font-family:var(--body);
  font-size:16px;line-height:1.6;-webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;overflow-x:hidden;
}
::selection{background:var(--vermilion);color:var(--vermilion-ink)}
a{color:inherit;text-decoration:none}
img{max-width:100%;display:block}
:focus-visible{outline:2px solid var(--vermilion-deep);outline-offset:3px;border-radius:2px}
:root:not([data-theme="light"]) :focus-visible{outline-color:var(--vermilion)}
/* Designed focus, not just an outline: interactive controls that already
   carry a border pick up the vermilion as a BORDER + a soft ring, so focus
   reads at a glance even for someone scanning quickly rather than reading
   pixel-perfect outline placement. */
.btn:focus-visible,.iconbtn:focus-visible,.lang-tab:focus-visible,.docs-nav a:focus-visible{
  outline:none;border-color:var(--vermilion-deep);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--vermilion-deep) 30%, transparent);
}
:root:not([data-theme="light"]) .btn:focus-visible,
:root:not([data-theme="light"]) .iconbtn:focus-visible,
:root:not([data-theme="light"]) .lang-tab:focus-visible,
:root:not([data-theme="light"]) .docs-nav a:focus-visible{
  border-color:var(--vermilion);box-shadow:0 0 0 3px color-mix(in srgb, var(--vermilion) 30%, transparent);
}

.tabular,.num,td.num,.tests,.stat-num{font-variant-numeric:tabular-nums;font-feature-settings:'tnum' 1}

/* ---- motion: explains, never decorates ----
   Two mechanisms, both gated by the SAME reduced-motion block above (which
   collapses every animation/transition to near-zero duration), so neither
   needs its own reduced-motion query:

   1. `.rv`/`.in` — a plain fade-up reveal, toggled by one shared
      IntersectionObserver each page wires up once (see the inline script at
      the bottom of index.html/docs.html). Used sparingly, on section HEADS
      and the three big diagrams only, not on every element — motion on
      everything reads as noise, not "alive". Gated on `html.js`; see below.
   2. `.rl-pulse` (defined in index.html, since only the landing's seam
      diagram has one) — a small bright dash riding each rail track, PAUSED
      until the diagram gains `.in-view` from the same observer, so "value
      moving along the rail" happens once the diagram is actually on screen
      rather than the instant the page loads off-screen.

   The former `.rail-pulse`/`.mx1-pulse` rules lived here because both
   diagrams were single SVGs animated with stroke-dashoffset. The seam is now
   built from DOM text and CSS (so its rail names stay legible at 390px) and
   the M×1 tree was replaced by the M×N/M×1 comparison, which is a static
   argument and wants no motion at all — so both rule sets are gone rather
   than left behind as dead selectors that describe elements no longer on
   either page. */
/* Gated on `html.js`, which the inline head script sets synchronously before
   first paint. Without it, `.rv{opacity:0}` is a content-hiding bug rather
   than an animation: the IntersectionObserver that would reveal these elements
   never runs, so with JS off the seam diagram, the evidence ladder and the
   M×N comparison were all rendering as blank space. */
html.js .rv{opacity:0;transform:translateY(14px);transition:opacity .6s cubic-bezier(.16,1,.3,1),transform .6s cubic-bezier(.16,1,.3,1)}
html.js .rv.in{opacity:1;transform:none}

/* Reduced motion: show the content outright rather than animating it in.
   The global @media block near the top of this file collapses every
   transition to ~0, which makes the reveal instant but does NOT make it
   unconditional — the elements still start at opacity:0 and still wait for
   the IntersectionObserver to mark them `in`. A reader with reduced motion
   set therefore still depends on scroll observation to see the seam diagram
   and the evidence ladder, which is exactly what that setting is asking us
   not to do. openrate and llmux both already do this; patala did not.

   This block sits HERE, after the two rules it overrides, and not up in the
   global reduced-motion block — a media query adds no specificity, so
   `html.js .rv` inside one placed earlier in the file loses the cascade to
   the identical selector below it and silently does nothing. That mistake
   has been made in five Vulos repos now. */
@media (prefers-reduced-motion: reduce){
  html.js .rv,
  html.js .rv.in{opacity:1;transform:none;transition:none}
}

/* ---- the hero's load sequence ----
   The hero is above the fold, so the observer marks it `in` on the first frame
   and it has never had an entrance of its own. This gives it one, in reading
   order: the status badge, the headline, the lede and its actions, then the
   honesty card — so the claim arrives just before the caveat that qualifies it.

   An animation, not a second .rv state. A reveal resting at `opacity:0` loses
   its content whenever the class that clears it does not arrive; this rests on
   the finished frame and `backwards` borrows the opening one only while it
   runs, so nothing here depends on script. The reduced-motion query above
   already collapses every animation to .001ms and takes this with it. */
@keyframes patalaHeroIn{from{opacity:0;transform:translateY(13px)}to{opacity:1;transform:none}}
html.js .hero > .wrap > .badge,
html.js .hero > .wrap > h1,
html.js .hero .hero-top > div:first-child,
html.js .hero .hero-top > .honest{animation:patalaHeroIn .64s cubic-bezier(.16,1,.3,1) backwards}
html.js .hero > .wrap > h1{animation-delay:.07s}
html.js .hero .hero-top > div:first-child{animation-delay:.15s}
html.js .hero .hero-top > .honest{animation-delay:.23s}
/* The reduced-motion query above collapses animation DURATION but not DELAY,
   and `backwards` holds the opening frame for the whole delay — which left the
   honesty card blank for 230ms for precisely the reader who asked for less
   motion. The shorthand resets the delay along with everything else. */
@media(prefers-reduced-motion:reduce){
  html.js .hero > .wrap > .badge,
  html.js .hero > .wrap > h1,
  html.js .hero .hero-top > div:first-child,
  html.js .hero .hero-top > .honest{animation:none!important}
}

/* ---- type scale + rhythm ----
   A single ratio (~1.25, "major third") drives every display size on both
   pages, expressed with clamp() so it is fluid rather than breakpoint-
   stepped. --sp-* is an 8px baseline grid: every section padding, gap and
   margin on both pages is one of these seven values, so vertical rhythm is
   a deliberate choice, not whatever a component happened to need. */
:root{
  --fs-body:16px; --fs-lead:clamp(15px,1.6vw,17.5px); --fs-small:13.5px; --fs-micro:12px;
  --fs-h3:clamp(17px,1.7vw,19px); --fs-h2:clamp(26px,3.6vw,38px); --fs-h1:clamp(36px,5.6vw,64px);
  --sp-1:4px; --sp-2:8px; --sp-3:16px; --sp-4:24px; --sp-5:32px; --sp-6:48px; --sp-7:64px;
}

.wrap{max-width:1160px;margin:0 auto;padding:0 24px;position:relative;z-index:1}
/* There was a `.wrap.wide{max-width:1320px}` here, written for "the two
   sections that carry a real table — the rail ledger and the evidence ladder".
   Only ONE of the two ever took it. The rail ledger stayed at 1160 and the
   evidence ladder went to 1320, so the largest block on the landing began 80px
   to the LEFT of every heading above and below it: one page, two left edges,
   for a distinction the page had stopped making. The ladder is back on the
   measure and the class is deleted rather than left defined and unused, so
   that re-adding `class="wide"` to a section is a change somebody has to make
   on purpose instead of a class that silently does nothing.

   A wider measure for data is still a good idea. It has to be applied to every
   data block or to none, and today it is none. The docs page keeps its own
   measure (--doc-max) and never used this. */
@media(max-width:480px){.wrap{padding:0 18px}}

.disp{font-family:var(--disp);font-weight:700}
.kick{
  font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ink-faint);
}

/* ---- toothed rule: the cowrie aperture, stretched into a divider ----
   Used sparingly. An earlier pass put one between every pair of sections —
   seven identical full-width saw-blades down one page, which made the loudest
   element on the page also the most repeated one. It now appears three times
   on the landing, at half the old opacity and a finer pitch, and only where it
   is announcing a change of register. */
.tooth-rule{
  height:11px;width:100%;background-color:var(--rule-color);
  -webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;
  -webkit-mask-size:28px 11px;mask-size:28px 11px;
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 14'%3E%3Crect x='0' y='11' width='34' height='2.4' rx='1.2'/%3E%3Cpath d='M5 11L11 11L8 1Z'/%3E%3Cpath d='M20 11L26 11L23 1Z'/%3E%3C/svg%3E");
  mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 34 14'%3E%3Crect x='0' y='11' width='34' height='2.4' rx='1.2'/%3E%3Cpath d='M5 11L11 11L8 1Z'/%3E%3Cpath d='M20 11L26 11L23 1Z'/%3E%3C/svg%3E");
  opacity:.5;
}
.tooth-rule.dim{opacity:.3}
/* The rule is the one place the divider is allowed to announce what follows:
   it takes the hue of the section beneath it, so the page's colour schema is
   legible even in the gaps between sections. */
.tooth-rule.final{--rule-color:var(--cls-final)}
.tooth-rule.rev{--rule-color:var(--cls-rev)}
.tooth-rule.evidence{--rule-color:var(--ev-testnet)}
.tooth-rule.exec{--rule-color:var(--ev-exec)}

/* ---- settlement-class chip: the schema, spelled out ----
   A word plus a hue plus a distinct border weight. Used in the rail ledger and
   beside the hero's rail names, and nowhere an evidence level is meant. */
.cls{
  display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:12px;
  font-weight:500;letter-spacing:.03em;text-transform:uppercase;padding:3px 8px;border-radius:3px;
  white-space:nowrap;border:1px solid currentColor;
}
.cls::before{content:'';width:6px;height:6px;flex:none;background:currentColor}
.cls.final{color:var(--cls-final)}
.cls.final::before{border-radius:50%}                   /* ● final       */
.cls.rev{color:var(--cls-rev)}
.cls.rev::before{border-radius:1px;transform:rotate(45deg)}  /* ◆ reversible */
.cls.none{color:var(--cls-none)}
.cls.none::before{background:transparent;box-shadow:inset 0 0 0 1.4px currentColor;border-radius:50%} /* ○ none */

/* Full-bleed helper: a band spans the viewport while its .wrap keeps the
   measure. 100vw would include the scrollbar and cause a horizontal scroll,
   so the width comes from the element's own containing block instead. */
.full{width:100%;margin-left:0;margin-right:0}

/* cowrie mark */
.cowrie{display:block}

/* texture: repeated cowrie glyph, very low opacity, decorative only */
.shell-texture{
  position:absolute;inset:0;pointer-events:none;z-index:0;
  background-color:var(--ink);
  opacity:.05;
  -webkit-mask-repeat:repeat;mask-repeat:repeat;
  -webkit-mask-size:64px 64px;mask-size:64px 64px;
  -webkit-mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cpath fill-rule='evenodd' d='M64 12C83 12 100 33 100 62C100 93 83 116 64 116C45 116 28 93 28 62C28 33 45 12 64 12Z M64 26L70 32L65.5 38L70 45L65.5 51L70 58L65.5 64L70 71L65.5 77L70 84L65.5 90L70 97L64 104L58 97L62.5 90L58 84L62.5 77L58 71L62.5 64L58 58L62.5 51L58 45L62.5 38L58 32Z'/%3E%3C/svg%3E");
  mask-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cpath fill-rule='evenodd' d='M64 12C83 12 100 33 100 62C100 93 83 116 64 116C45 116 28 93 28 62C28 33 45 12 64 12Z M64 26L70 32L65.5 38L70 45L65.5 51L70 58L65.5 64L70 71L65.5 77L70 84L65.5 90L70 97L64 104L58 97L62.5 90L58 84L62.5 77L58 71L62.5 64L58 58L62.5 51L58 45L62.5 38L58 32Z'/%3E%3C/svg%3E");
}

/* ---- nav ---- */
nav.top{
  position:sticky;top:0;z-index:30;background:color-mix(in srgb, var(--canvas) 86%, transparent);
  backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
  border-bottom:1px solid var(--hair);
}
nav.top .wrap{display:flex;align-items:center;justify-content:space-between;height:64px;gap:16px}
.brand{display:flex;align-items:center;gap:10px;min-height:44px}
.brand .cowrie{width:24px;height:24px}
.brand b{font-family:var(--disp);font-weight:700;font-size:19px;letter-spacing:-.03em;color:var(--ink)}
.nlinks{display:flex;gap:8px;align-items:center}
.nlinks a.nl{
  display:inline-flex;align-items:center;min-height:44px;
  font-family:var(--body);font-size:13.5px;color:var(--ink-dim);padding:8px 12px;border-radius:6px;
  transition:color .15s;
}
.nlinks a.nl:hover{color:var(--ink)}
@media(max-width:760px){.nlinks a.nl.hidem{display:none}}
/* Below 560px the header no longer fits its own row: measured at 334px of
   content inside a 320px viewport. Drop by replaceability, never function —
   the Vulos credit is repeated in the footer, the GitHub button is repeated
   in the closing CTA and the footer, and "Docs" and the theme toggle are the
   only route to the documentation and the only theme control, so they stay. */
@media(max-width:560px){.nlinks .vulos-btn{display:none}}
@media(max-width:430px){nav.top .nlinks .btn.sm{display:none}nav.top .wrap{gap:10px}}

/* Tap targets are >=44px on every interactive control, including the
   ostensibly "small"/icon-only ones — a hard requirement (390px is a real
   breakpoint here, not just a layout check), so .iconbtn/.btn.sm grow their
   HIT AREA to 44px via min-width/min-height even where the visible chip
   stays compact. */
.iconbtn{
  display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;
  border:1px solid var(--hair-strong);border-radius:8px;background:transparent;color:var(--ink-dim);
  cursor:pointer;transition:.15s;
}
.iconbtn:hover{color:var(--ink);border-color:var(--ink-faint)}
.iconbtn svg{width:17px;height:17px}
/* The theme button shows the theme ONE CLICK AWAY — a sun while you are on
   ink, a moon while you are on paper. Both glyphs ship in the markup and CSS
   picks between them off the same data-theme the head script settles before
   first paint. It used to be an inline style:display:none plus a script at the
   END of the body: on a machine asking for paper the page painted paper and
   the button showed the sun — "switch to light", while already light — until
   that script caught up. The default here (no attribute, script disabled) is
   the sun, matching the dark palette a reader without JavaScript gets. */
#themeToggle .icon-moon{display:none}
:root[data-theme="light"] #themeToggle .icon-sun{display:none}
:root[data-theme="light"] #themeToggle .icon-moon{display:block}

/* The one place Vulos appears beyond the footer's single line: a compact,
   logo-only button in the header, nothing else — no band, no tagline, no
   badge sprawl. */
.vulos-btn img{width:18px;height:18px;border-radius:4px;display:block;opacity:.92}
.vulos-btn:hover img{opacity:1}
/* The source mark is dark ink on transparent — correct on the light theme's
   near-white button, unreadable on the dark theme's near-black one. Flipped
   with a filter rather than shipping a second bitmap. */
:root:not([data-theme="light"]) .vulos-btn img{filter:invert(1) brightness(1.15)}

.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:8px;font-family:var(--body);font-weight:500;
  font-size:13.5px;padding:10px 18px;min-height:44px;border-radius:8px;border:1px solid var(--hair-strong);
  color:var(--ink);transition:.15s;white-space:nowrap;
}
.btn:hover{border-color:var(--ink-faint);background:var(--surface-raised)}
.btn.pri{background:var(--vermilion);border-color:var(--vermilion);color:var(--vermilion-ink);font-weight:500}
.btn.pri:hover{background:var(--vermilion-bright);border-color:var(--vermilion-bright)}
.btn.sm{padding:7px 13px;font-size:12.5px;min-height:44px}
/* The header's source link is the GitHub MARK, no word — the same treatment
   the Vulos mark beside it already gets. It keeps the .btn chip it always had
   rather than becoming a bare glyph, so it squares off to the .iconbtn's 44px
   and its Octicon fills with currentColor, which is what makes it legible on
   both the paper and the ink palette off one path. The word is gone, so the
   accessible name comes from aria-label on the anchor. */
.btn.sm.gh-mark{width:44px;padding:0;gap:0}
.btn.sm.gh-mark svg{width:18px;height:18px;display:block}

footer.site{border-top:1px solid var(--hair);padding:32px 0;color:var(--ink-faint);font-size:13px}
footer.site .fwrap{display:flex;flex-wrap:wrap;gap:12px 28px;justify-content:space-between;align-items:center}
/* .flinks are standalone nav links (not inline prose, unlike the "vulos"
   credit link to their left), so — same 44px hit-area rule as the header's
   .nlinks a.nl — each gets a real tap target via inline-flex + min-height,
   not just its 13px visible text line. */
footer.site .suite{display:block;margin-top:6px;color:var(--ink-faint)}
footer.site .flinks{display:flex;gap:8px;flex-wrap:wrap}
/* Real 44×44 hit areas, not just 44px tall: "Docs" is only 31px of glyphs, so
   the target is grown with padding (and the visual rhythm restored by shrinking
   the flex gap by the same amount) rather than left short on one axis. */
footer.site .flinks a{display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:44px;padding:0 8px}
footer.site a:hover{color:var(--ink-dim)}
footer.site b{color:var(--ink-dim);font-family:var(--disp);font-weight:700}

/* ---- generic status word ----------------------------------------------------
   Colour is never the only carrier: every status is a WORD first, with the hue
   and the pip as reinforcement, so the evidence ladder survives greyscale,
   colour-blindness and a bad projector. `.status.none` is deliberately the
   dullest thing on the page — "nothing was run" should look like nothing. */
.status{
  display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:12px;
  font-weight:500;letter-spacing:.02em;white-space:nowrap;
}
.status::before{content:'';width:7px;height:7px;border-radius:50%;flex:none;background:currentColor}
.status.none{color:var(--ev-none)}
/* An unreached rung is drawn hollow — an outline, not a filled pip. */
.status.none::before{background:transparent;box-shadow:inset 0 0 0 1.5px currentColor}
.status.design{color:var(--ev-design)}
.status.exec{color:var(--ev-exec)}
.status.testnet{color:var(--ev-testnet)}
.status.fail{color:var(--ev-fail)}
/* legacy names kept so docs.html and older markup keep resolving */
.status.settled{color:var(--ev-exec)}
.status.pending{color:var(--ev-design)}
.status.failed{color:var(--ev-fail)}

/* ---- ledger table (rule 5: hairline rows, toothed rule only between sections; rule 6 tabular nums; rule 3 no vermilion in data) ---- */
.ledger{width:100%;border-collapse:collapse;font-size:14px;line-height:1.55}
.ledger th,.ledger td{text-align:left;padding:10px 13px;border-bottom:1px solid var(--hair);vertical-align:top}
.ledger thead th{
  font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-faint);font-weight:500;border-bottom:1px solid var(--hair-strong);
}
.ledger tbody tr:last-child td{border-bottom:none}
.ledger td.mono,.ledger th.mono{font-family:var(--mono)}
.ledger-wrap{overflow-x:auto;overscroll-behavior-x:contain;border:1px solid var(--hair);border-radius:2px}

/* ---- terminal / output pane: real captured command output, styled.
   Used both by the live pages (as static screenshots, see
   scripts/render-shots.mjs) and available for any future inline use. ---- */
.term{border:1px solid var(--hair-strong);background:var(--surface);border-radius:2px;overflow:hidden}
.term-head{
  display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;
  border-bottom:1px solid var(--hair);font-family:var(--mono);font-size:12px;color:var(--ink-faint);
  letter-spacing:.05em;text-transform:uppercase;
}
.term-head .dots{display:flex;gap:6px}
.term-head .dots span{width:7px;height:7px;border-radius:50%;background:var(--hair-strong)}
.term pre{
  margin:0;padding:18px 20px;font-family:var(--mono);font-size:12.5px;line-height:1.64;
  color:var(--ink-dim);white-space:pre-wrap;word-break:break-word;
}
.term pre .prompt{color:var(--vermilion)}
:root[data-theme="light"] .term pre .prompt{color:var(--vermilion-deep)}
.term pre .cmd{color:var(--ink)}
.term pre .ok{color:var(--settled)}

/* ---- syntax highlighting (highlight.js core, vendored — see ../vendor/) ----

   ONE PALETTE, NOT TWO, AND IT IS THE SUITE'S. Every Vulos project renders
   code on the same fixed carbon panel with the same ten token colours, so a
   Rust block in patala, a JSON block in llmux and a shell block in openrate
   are the same object wearing the same clothes. That is why the surface no
   longer follows the page theme: a shared palette can only be measured once
   if the ground it is measured against is constant. The previous pass shipped
   two palettes (a dark set and a paper set) which were internally fine and
   family-wise wrong — patala was the only project in the suite whose code
   turned to paper in light mode.

   The rule the page keeps is unchanged in spirit and stricter in practice:
   captured terminal output (§06's .webp panes) and source you would read are
   BOTH instrument, and neither repaints for paper mode.

   MEASURED, on --hl-bg #0C1116, with the WCAG relative-luminance formula.
   Floor is 4.5:1; nothing is under it:

     fg       #C8D4E3  12.63:1     property  #93C5FD  10.52:1
     string   #5EEAD4  12.82:1     variable  #FDA4AF  10.03:1
     number   #FBBF24  11.36:1     keyword   #C4B5FD  10.27:1
     function #7DD3FC  11.37:1     punct     #8195AC   6.17:1
     meta     #FDBA74  11.24:1     comment   #6C7D94   4.51:1

   Comment is #6C7D94 rather than the nominal #6B7C93 for the same reason the
   sibling projects use that value: #6B7C93 measures 4.45:1, a hair under AA,
   and one LSB per channel is invisible but clears the floor. The header bar
   (--hl-bar #111820) carries UI text, not tokens, so it is measured on its
   own ground: --code-chrome #93A6BC is 7.16:1 there and --code-chip #B0BECE
   is 9.45:1.

   CLASS INVENTORY. Every hljs-* scope the six vendored grammars (rust, go,
   python, ini/toml, bash, json) actually emit over the code this site ships
   was dumped from the live pages and is mapped by hand below — measured, not
   assumed. What Rust alone contributes, and what a generic theme misses:
     hljs-meta                     #[async_trait], #[derive(...)]  -> meta
     hljs-symbol                   'a / 'static lifetimes          -> variable
     hljs-title function_ invoke__ a call site, `foo()`            -> function
     hljs-title class_             a declared type name            -> function
     hljs-type                     bool, Vec, String, RailClass    -> property
     hljs-built_in                 Some/Ok/Vec! and friends        -> meta
   The one deliberate divergence from the suite sheet is hljs-type: the shared
   sheet folds it into `keyword`, which is right for JSON/YAML-shaped content
   and wrong here, where a Rust page is half type names and folding them into
   the keyword violet erases the distinction between what a line DECLARES and
   what it is MADE OF. It takes `property` — a suite colour, in a suite slot,
   for the same category of thing (a declared name).

   NOTE ON HUE AND THE SCHEMA. Everywhere else on this page hue carries exactly
   two variables (settlement class, evidence level) and vermilion is reserved
   for patala itself. Inside a code block hue means something different, so the
   block is fenced off from the schema in two ways: it is the only object on
   the page with its own ground, and the LANGUAGE CHIPS in its header are
   deliberately ACHROMATIC — a language is not a settlement class and not an
   evidence level, so it is not allowed a colour. Language identity is carried
   by an original drawn mark plus the language's name, never by hue.
   ---------------------------------------------------------------------- */
:root{
  --hl-bg:#0C1116; --hl-bar:#111820; --hl-line:#22303C; --hl-line-2:#2C3D4C;
  --hl-fg:#C8D4E3; --hl-comment:#6C7D94; --hl-keyword:#C4B5FD; --hl-string:#5EEAD4;
  --hl-number:#FBBF24; --hl-function:#7DD3FC; --hl-property:#93C5FD;
  --hl-variable:#FDA4AF; --hl-meta:#FDBA74; --hl-punct:#8195AC;
  --hl-add:#86EFAC; --hl-del:#FCA5A5;
  /* The component's own names, so .codeblock reads as a component rather than
     as a highlighter. They are aliases, never a second palette — and they are
     declared once, on :root, because the surface is theme-independent. */
  --code-surface:var(--hl-bg); --code-edge:var(--hl-line); --code-bar:var(--hl-bar);
  --code-text:var(--hl-fg);
  /* The block's own chrome (file path, chip, controls) is UI text, not a code
     token, so it never borrows --hl-comment. Measured on --hl-bar. */
  --code-chrome:#93A6BC; --code-chip:#B0BECE;
}
.hljs{display:block;color:var(--hl-fg);background:transparent}
.hljs-comment,.hljs-quote{color:var(--hl-comment);font-style:italic}
/* A TODO/SAFETY tag inside a comment has to stand out from the prose round it,
   so it stays upright and bold rather than taking the string colour. */
.hljs-doctag{color:var(--hl-comment);font-style:normal;font-weight:600}
.hljs-keyword,.hljs-selector-tag,.hljs-variable.language_{color:var(--hl-keyword)}
.hljs-string,.hljs-regexp{color:var(--hl-string)}
.hljs-number,.hljs-literal,.hljs-variable.constant_{color:var(--hl-number)}
.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,
.hljs-title.function_,.hljs-title.function_.invoke__,.hljs-section{color:var(--hl-function)}
.hljs-section{font-weight:600}
.hljs-type,.hljs-attr,.hljs-attribute,.hljs-params,.hljs-property{color:var(--hl-property)}
.hljs-variable,.hljs-template-variable,.hljs-name,.hljs-symbol,
.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-link{color:var(--hl-variable)}
.hljs-meta,.hljs-built_in,.hljs-tag{color:var(--hl-meta)}
/* The string inside a #[path = "..."] attribute is still a string. */
.hljs-meta .hljs-string{color:var(--hl-string)}
.hljs-punctuation,.hljs-operator,.hljs-bullet,.hljs-selector-id{color:var(--hl-punct)}
/* An interpolated expression inside a string is code, not string content —
   this is what stops an f-string swallowing the variable inside it. */
.hljs-subst{color:var(--hl-fg)}
.hljs-addition{color:var(--hl-add)}
.hljs-deletion{color:var(--hl-del)}
.hljs-emphasis{font-style:italic}
.hljs-strong{font-weight:700}

/* ---- the code block: one component, both pages ---------------------------
   A block is presented as an ARTIFACT, not a paragraph: it has a header that
   names the language with a drawn mark, states where the code lives, and
   offers controls that actually work. The header is a real flex row that
   wraps rather than truncating, so at 320px the path drops to its own line
   instead of being clipped or pushing the block wide.

   HOW A BLOCK BEHAVES WHEN IT IS WIDER THAN ITS COLUMN — the one decision
   this component exists to make, and it is made per block, not per page.

   The failure being fixed: a 629px-wide Rust block inside a 352px phone
   column, and an 887px one inside a 722px docs column at 1440. Horizontal
   scroll alone lets a reader REACH the rest of a line; it does not let them
   READ it, because every line has to be panned separately and the eye loses
   the line it was on. So the default for an overflowing block is SOFT WRAP
   with a hanging indent: assets/js/codeblock.js rebuilds the highlighted
   output as one block element per source line (.cl), which is what makes a
   per-line hanging indent possible at all — text-indent in a <pre> only ever
   affects the first line of the box. A continuation is then unmistakably a
   continuation and never mistaken for the next statement.

   Where the COLUMNS are the content — `ls` output, an aligned table — wrapping
   destroys the information, so that block declares data-wrap="never" and keeps
   exact horizontal scroll. Either way the reader can flip the block with the
   header's Wrap control, and either way an overflowing block gets a visible
   affordance: a fade at the clipped edge that appears only while there is
   genuinely more to the right, plus a real tab stop so the scroller is
   reachable from the keyboard rather than by pointer alone. */
.codeblock{
  position:relative;
  border:1px solid var(--code-edge);background:var(--code-surface);border-radius:6px;
  overflow:hidden;min-width:0;
}
.codeblock .cb-head{
  display:flex;align-items:center;gap:10px 14px;flex-wrap:wrap;padding:9px 12px 9px 13px;
  border-bottom:1px solid var(--code-edge);background:var(--code-bar);
  font-family:var(--mono);font-size:12px;color:var(--code-chrome);min-width:0;
}
/* The language chip. ACHROMATIC on purpose — see the schema note above. The
   glyph is an original drawing in the page's own 24-box icon family; it is
   never a reproduction of a language's trademarked logo. */
.codeblock .lang{
  display:inline-flex;align-items:center;gap:7px;flex:none;
  color:var(--code-chip);font-size:12px;font-weight:500;letter-spacing:.06em;
  padding:3px 9px 3px 7px;border:1px solid var(--code-edge);border-radius:20px;
}
.codeblock .lang svg{width:15px;height:15px;flex:none;display:block}
.codeblock .cb-path{
  flex:1 1 120px;min-width:0;text-align:right;letter-spacing:.02em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* 40px, not the 32 these were. These are the only standalone controls inside a
   code block, they sit in a dense mono bar where a mis-tap costs the reader
   their scroll position, and 32 is below every touch-target floor worth
   holding. The bar's own height follows from the control rather than the
   other way round. */
.codeblock .cb-copy,.codeblock .cb-wrap{
  flex:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;
  min-height:40px;padding:0 12px;border-radius:6px;cursor:pointer;
  border:1px solid var(--code-edge);background:transparent;color:var(--code-chrome);
  font-family:var(--mono);font-size:12px;letter-spacing:.04em;transition:.15s;
}
.codeblock .cb-copy svg,.codeblock .cb-wrap svg{width:14px;height:14px;flex:none}
.codeblock .cb-copy:hover,.codeblock .cb-wrap:hover{color:var(--code-text);border-color:var(--code-chrome)}
.codeblock .cb-copy[data-state="done"]{color:var(--hl-string);border-color:var(--hl-string)}
.codeblock .cb-copy .done{display:none}
.codeblock .cb-copy[data-state="done"] .idle{display:none}
.codeblock .cb-copy[data-state="done"] .done{display:inline-flex}
/* The Wrap control is a real toggle and says which state it is in — pressed
   means the block is currently wrapped. Colour alone never carries it: the
   word changes too (Wrap / Wrapped), so the state survives greyscale. */
.codeblock .cb-wrap[aria-pressed="true"]{color:var(--hl-property);border-color:var(--hl-property)}
.codeblock .cb-wrap .on{display:none}
.codeblock .cb-wrap[aria-pressed="true"] .off{display:none}
.codeblock .cb-wrap[aria-pressed="true"] .on{display:inline-flex;align-items:center;gap:6px}
.codeblock .cb-wrap .off{display:inline-flex;align-items:center;gap:6px}
/* Tap target: the visible chip stays 32px so it does not dominate a 9px-padded
   bar, but the hit area is grown to the 44px floor with a pseudo-element. */
.codeblock .cb-copy,.codeblock .cb-wrap{position:relative}
.codeblock .cb-copy::after,.codeblock .cb-wrap::after{content:"";position:absolute;left:0;right:0;top:50%;height:44px;transform:translateY(-50%)}
/* The scroll frame. `pre` is the scroller; .cb-body exists only to hold the
   edge fade in register with it, since .codeblock also contains the header. */
.codeblock .cb-body{position:relative;min-width:0}
.codeblock .cb-fade{
  position:absolute;top:0;bottom:0;right:0;width:34px;pointer-events:none;
  opacity:0;transition:opacity .18s ease;
  background:linear-gradient(90deg,transparent,var(--code-surface));
}
.codeblock[data-x="more"] .cb-fade{opacity:1}
/* The same affordance for the things that are NOT code and can never wrap:
   §06's captured terminal panes and the docs' wide tables. Both declare
   [data-scroller]; assets/js/codeblock.js wraps them in .sc-frame and measures
   them. The fade takes --canvas rather than the code surface because these sit
   on the page (or, in §06, on the band, which redeclares --canvas on itself),
   so one rule is correct on every ground it lands on. */
.sc-frame{position:relative;min-width:0}
.sc-fade{
  position:absolute;top:0;bottom:0;right:0;width:30px;z-index:2;pointer-events:none;
  opacity:0;transition:opacity .18s ease;border-radius:0 2px 2px 0;
  background:linear-gradient(90deg,transparent,var(--canvas));
}
[data-x="more"]+.sc-fade{opacity:1}
[data-scroller]:focus-visible{outline:2px solid var(--vermilion-deep);outline-offset:2px}
:root:not([data-theme="light"]) [data-scroller]:focus-visible{outline-color:var(--vermilion)}
.codeblock pre{
  margin:0;padding:15px 18px;overflow-x:auto;overscroll-behavior-x:contain;
  font-family:var(--mono);font-size:13px;line-height:1.62;color:var(--code-text);
}
.codeblock code{background:none;padding:0;font-size:1em;color:inherit;font-family:inherit}
/* One block element per source line (built by assets/js/codeblock.js). In the
   default, exact state they behave exactly like the lines they replaced. */
.codeblock .cl{display:block}
.codeblock .cl:empty::before{content:" "}
/* WRAPPED. 2.6ch of hang is two mono characters plus the gap — deep enough
   that a continuation cannot be read as a new statement, shallow enough that
   it does not eat a phone's measure. It is measured from THIS line's own
   indent (--lead, set per line by codeblock.js) rather than from the block's
   left edge, so the continuation of an indented line still sits inside that
   line's block. `anywhere` rather than `break-word` because the things that
   overflow here are unbreakable tokens (a git URL, a base58 address), and
   break-word leaves those sticking out of the box. */
.codeblock.wrapped pre{overflow-x:hidden}
.codeblock.wrapped pre code{white-space:pre-wrap;overflow-wrap:anywhere}
.codeblock.wrapped .cl{
  padding-left:calc((var(--lead,0) + 2.6) * 1ch);
  text-indent:calc((var(--lead,0) + 2.6) * -1ch);
}
@media(max-width:600px){
  .codeblock pre{padding:13px 14px;font-size:12.5px}
  .codeblock .cb-head{padding:8px 10px}
  /* Below 600px the path is the first thing to go: it is context, the chip and
     the controls are function. It reappears on its own full-width line. */
  .codeblock .cb-path{flex-basis:100%;text-align:left;order:3}
}

/* A ledger that has to be squinted at defeats the point of the aesthetic —
   and so does one that scrolls its content out of sight. Two different fixes
   for two different widths, because the failure is different at each.

   760-620px the table is merely cramped, so it keeps its columns and its type
   and the wrapper genuinely scrolls. The fix for a cramped table is more
   width, never smaller text.

   Below 620px scrolling stops working as a fix and becomes the defect: at
   360px the rail ledger showed two columns and a stack of 150px-tall rows
   that were empty, because the row height was being set by a prose column
   entirely off screen. So the table STOPS BEING A TABLE. Each row becomes its
   own bordered block and every cell is titled from the data-label the markup
   carries, which is the same string the hidden <thead> holds — one source, so
   the two views cannot drift. Nothing is dropped and nothing shrinks; the
   table is simply transposed. */
@media(max-width:760px){
  .ledger{min-width:600px}
  .ledger.ledger--wide{min-width:680px}
  .ledger th,.ledger td{padding:10px 11px}
  /* Scroll affordance: without it a clipped table reads as a complete one.
     Pinned to the wrapper, so it sits at the clipped edge. */
  .ledger-wrap{position:relative}
  .ledger-wrap::after{
    content:"";position:sticky;top:0;right:0;float:right;
    width:26px;height:100%;pointer-events:none;
    background:linear-gradient(90deg,transparent,var(--surface))
  }
}
@media(max-width:620px){
  .ledger-wrap{overflow-x:visible;border:0;border-radius:0}
  .ledger-wrap::after{display:none}
  .ledger,.ledger.ledger--wide{min-width:0;display:block;font-size:14px}
  /* display:none, not a visually-hidden clip: an absolutely positioned thead
     still lays its <th> out at full content width and measured 350px inside
     a 320px viewport. Nothing is lost — every cell carries the same string
     as its own data-label heading in this view. */
  .ledger thead{display:none}
  .ledger tbody,.ledger tr,.ledger td{display:block;width:auto}
  .ledger tr{
    border:1px solid var(--hair);border-radius:4px;margin-bottom:10px;padding:2px 14px 6px;
    background:var(--surface);
  }
  .ledger tbody tr:last-child{margin-bottom:0}
  .ledger td,.ledger tbody tr:last-child td{padding:9px 0;border-bottom:0}
  .ledger td+td{border-top:1px solid var(--hair)}
  /* The row's own title needs no label — it IS the label. */
  .ledger td:first-child{padding-top:12px;font-size:15px}
  .ledger td:not(:first-child)::before{
    content:attr(data-label);display:block;margin-bottom:4px;
    font-family:var(--mono);font-size:12px;font-weight:500;letter-spacing:.1em;
    text-transform:uppercase;color:var(--ink-faint);
  }
  .ledger td:last-child{max-width:none}
  /* The longest status word — EXECUTED - RAIL REGISTRY IS MOCK-ONLY — is
     nowrap everywhere else and measured 350px here. In the stacked view it
     is a block of text, not a chip in a column, so it is allowed to wrap. */
  .ledger td .status{white-space:normal;align-items:baseline}
  .ledger td .status::before{position:relative;top:-1px}
}
