/* ═══════════════════════════════════════════════════════════════════════════
   hotato.dev — Instrument
   This file IS the design. See DESIGN-STANDARD.md. Pages ship semantic markup
   and no CSS of their own; tools/check_css.py fails the build otherwise.

   The previous system was an 83 KB corrective layer sitting on top of 3.74 MB
   of page-local CSS, loaded last so it could out-specify the pages it was
   trying to fix. Every rule here is a first statement, not a correction, so
   nothing needs `!important` and nothing needs to name `html body` to win.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Fonts ─────────────────────────────────────────────────────────────── */
@font-face{font-family:"Spline Mono";src:url(/assets/fonts/splinemono-var.woff2)format("woff2");
  font-weight:100 900;font-display:swap;font-style:normal}
@font-face{font-family:"Hanken";src:url(/assets/fonts/hanken-var.woff2)format("woff2");
  font-weight:100 900;font-display:swap;font-style:normal}

/* ─── Tokens ────────────────────────────────────────────────────────────── */
/* Light is the base and it is plotter output on paper, not an inverted dark
   theme: warm white ground, ink-black rules, and darker, more saturated trace
   colours so a 1px line survives on white. Dark is redefined twice — once for
   the system preference, once for an explicit choice — so the toggle wins in
   both directions and the default (no attribute) still follows the OS. */
:root{
  --bg:#fbfbf8; --panel:#ffffff;
  --ink:#14161a; --ink-2:#3d434c; --ink-3:#6a707a;
  --rule:#c9c9c0; --rule-2:#e4e4dc; --grid:#e8e8de;
  --caller:#0b5cd5; --caller-fill:rgba(11,92,213,.14);
  --agent:#0c7a5e;  --agent-fill:rgba(12,122,94,.14);
  --sev:#b4340c;    --sev-fill:rgba(180,52,12,.10);
  --open:#8a5200;   --open-fill:rgba(138,82,0,.12);
  --void:#6b3fc4;   --void-fill:rgba(107,63,196,.12);
  --pf-l:44%; --pf-c:.13; --pf-fill:.12; --pf-edge:.32; --pf-on:.20;
  --potato:#dcac5f; --potato-hi:#f0cf90; --potato-lo:#b07f33; --potato-dot:#7d531f;
  --potato-line:#241a10;
  --ember:#e2470f; --ember-bright:#ff5a1f; --glow:rgba(255,90,31,.22);
  --btn-ink:#fbfbf8;

  --mono:"Spline Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
  --sans:"Hanken",system-ui,-apple-system,sans-serif;

  /* Fixed, never fluid. clamp() on body-level type is what put a heading at
     32.8px on one page and 50.4px on the next. */
  --fs-display:44px; --fs-h1:30px; --fs-h2:20px;  --fs-h3:16px;
  --fs-lede:18px;    --fs-body:16px; --fs-data:13px; --fs-label:11px;

  /* Four tracking values, a function of size. The old system shipped ten. */
  --tr-display:-.03em; --tr-head:-.02em; --tr-body:0; --tr-label:.14em;

  --sp-1:4px; --sp-2:8px;  --sp-3:12px; --sp-4:16px;
  --sp-5:24px; --sp-6:32px; --sp-7:48px; --sp-8:64px;
  --band-y:clamp(72px,8vw,112px);

  --col:66ch; --col-ref:80ch; --col-wide:min(1180px,100% - var(--sp-6) * 2);
}
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]){
    --bg:#08090b; --panel:#0d0f12;
    --ink:#eef1f4; --ink-2:#b6bcc5; --ink-3:#868d97;
    --rule:#2b2f36; --rule-2:#1a1d22; --grid:#171a1f;
    --caller:#66aaff; --caller-fill:rgba(102,170,255,.16);
    --agent:#3ddba4;  --agent-fill:rgba(61,219,164,.16);
    --sev:#ff7a4d;    --sev-fill:rgba(255,122,77,.14);
    --open:#e0a33c;   --open-fill:rgba(224,163,60,.14);
    --void:#b998f5;   --void-fill:rgba(185,152,245,.14);
    --pf-l:80%; --pf-c:.12; --pf-fill:.16; --pf-edge:.34; --pf-on:.26;
    --potato:#e0b877; --potato-hi:#f0d29a; --potato-lo:#b78843; --potato-dot:#8a5f2c;
    --potato-line:#241a10;
    --ember:#ff5a1f; --ember-bright:#ff7a3c; --glow:rgba(255,90,31,.34);
    --btn-ink:#08090b;
  }
}
:root[data-theme="dark"]{
  --bg:#08090b; --panel:#0d0f12;
  --ink:#eef1f4; --ink-2:#b6bcc5; --ink-3:#868d97;
  --rule:#2b2f36; --rule-2:#1a1d22; --grid:#171a1f;
  --caller:#66aaff; --caller-fill:rgba(102,170,255,.16);
  --agent:#3ddba4;  --agent-fill:rgba(61,219,164,.16);
  --sev:#ff7a4d;    --sev-fill:rgba(255,122,77,.14);
  --open:#e0a33c;   --open-fill:rgba(224,163,60,.14);
  --void:#b998f5;   --void-fill:rgba(185,152,245,.14);
  --pf-l:80%; --pf-c:.12; --pf-fill:.16; --pf-edge:.34; --pf-on:.26;
  --potato:#e0b877; --potato-hi:#f0d29a; --potato-lo:#b78843; --potato-dot:#8a5f2c;
  --potato-line:#241a10;
  --ember:#ff5a1f; --ember-bright:#ff7a3c; --glow:rgba(255,90,31,.34);
  --btn-ink:#08090b;
}

/* ─── Reset ─────────────────────────────────────────────────────────────── */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0; background:var(--bg); color:var(--ink);
  font:400 var(--fs-body)/1.65 var(--sans);
  letter-spacing:var(--tr-body);
  -webkit-font-smoothing:antialiased;
}
/* Radius is 0 everywhere. Stated once, not per component, so it cannot drift. */
*{border-radius:0}
img,svg{max-width:100%}
img{height:auto;display:block}
/* Lists keep the user agent's `margin:1em 0` and `padding-inline-start:40px`
   unless something says otherwise, and 1em of 11px mono is an 11px margin --
   off the grid, from a rule nobody wrote. Reset both and let the components
   that need indent or rhythm ask for it in grid units. */
ul,ol,dl,dd,figure,blockquote,h1,h2,h3,h4,h5,h6,p{margin:0}
ul,ol{padding:0;list-style-position:outside}
ul:not([class]),ol:not([class]){padding-left:var(--sp-5)}
:where(ul,ol,dl,p,figure,blockquote) + :where(ul,ol,dl,p,figure,blockquote){margin-top:var(--sp-4)}
hr{border:0;border-top:1px solid var(--rule);margin:var(--sp-7) 0}

/* Every number aligns. A column of times that does not is a bug. */
[class*="mono"],code,kbd,samp,pre,th,td,time,.num,.readout-v,
h1,h2,h3,h4,h5,h6,nav,.label,.badge{font-variant-numeric:tabular-nums}

/* ─── Type ──────────────────────────────────────────────────────────────── */
h1,h2,h3,h4,h5,h6{font-family:var(--mono);font-weight:500;margin:0;text-wrap:balance}
h1{font-size:var(--fs-h1);line-height:1.15;letter-spacing:var(--tr-display)}
h2{font-size:var(--fs-h2);line-height:1.3;letter-spacing:var(--tr-head)}
h3{font-size:var(--fs-h3);line-height:1.35;font-weight:600;letter-spacing:var(--tr-head)}
h4{font-size:var(--fs-h3);line-height:1.35;font-weight:600;letter-spacing:var(--tr-head)}
p{margin:0}
p + p{margin-top:var(--sp-4)}
strong,b{font-weight:600}
small{font-size:var(--fs-data)}

.display{font-size:var(--fs-display);line-height:1.05;letter-spacing:var(--tr-display)}
.lede{font-family:var(--sans);font-size:var(--fs-lede);line-height:1.6;color:var(--ink-2);max-width:var(--col)}
.muted{color:var(--ink-3)}
.data{font-family:var(--mono);font-size:var(--fs-data);line-height:1.5}

/* The eyebrow: the one uppercase role. */
.eyebrow{
  display:block;font-family:var(--mono);font-size:var(--fs-label);font-weight:500;
  line-height:1.4;letter-spacing:var(--tr-label);text-transform:uppercase;color:var(--ink-3);
}

/* Links carry no hue. A link is not a signal. */
a{color:var(--ink);text-decoration:underline;text-decoration-color:var(--rule);
  text-underline-offset:.18em;text-decoration-thickness:from-font}
a:hover{text-decoration-color:var(--ink)}
:where(nav,.nav,.btn,.badge,.frame-head) a{text-decoration:none}
:focus-visible{outline:2px solid var(--ink);outline-offset:2px}

/* ─── Layout ────────────────────────────────────────────────────────────── */
.wrap{width:var(--col-wide);margin-inline:auto}
.band{padding-block:var(--band-y)}
.band + .band{border-top:1px solid var(--rule-2)}
/* The measure belongs to TEXT, not to the container. Capping `.content` at
   66ch capped everything inside it, so a two-column record nested in it got
   two 288px columns on a 1440px screen -- and collapsing that record to one
   column to fix the width made it TALLER than the atlas's own 2.3-viewport
   ceiling. Both were the same mistake: treating a prose rule as a layout rule.
   Prose children carry the measure; a grid, a table, a figure or a plot is
   free to use the room, which is what those elements are for. */
.content{max-width:var(--col-wide)}
.content :where(p,.lede,blockquote,.honest){max-width:var(--col)}
/* except where the element is a cell in something wider: a grid column, a
   table, a caption under a figure. Those already have their own width. */
.content :where(.cards,table,figure,.frame,.row,.side) :where(p,.lede,blockquote){
  max-width:none}
.content-ref{max-width:var(--col-ref)}
.content > * + *{margin-top:var(--sp-4)}

/* Rhythm belongs to the ELEMENTS, not to one wrapper. Scoping it to
   `.content >` meant a heading inside a <section> -- which is how every page
   but the home page is built -- got no margin at all and welded itself to its
   own lede. Stated at zero specificity so a component can still override. */
/* NOT `:where()`. These have to out-specify the reset three lines up, which
   sets margin:0 on h1..h4 and p at (0,0,1); a :where() rule is (0,0,0) and
   loses to it silently -- which is exactly how every heading on the site came
   to sit welded to its own lede while the rule "existed". */
h1 + *,h2 + *,h3 + *,h4 + *{margin-top:var(--sp-4)}
h1 + .lede,h2 + .lede{margin-top:var(--sp-5)}
p + h2,p + h3,ul + h2,ul + h3,ol + h2,ol + h3,dl + h2,dl + h3,
figure + h2,figure + h3,table + h2,table + h3,
.frame + h2,.frame + h3,.codeblk + h2,.codeblk + h3,
.register + h2,.register + h3,.cards + h2,.cards + h3,
.tablewrap + h2,.tablewrap + h3,.instrument + h2,.instrument + h3{margin-top:var(--sp-7)}
section + section{margin-top:var(--sp-7)}
.content > * + :is(h2,h3){margin-top:var(--sp-8)}
.content > :is(h2,h3) + *{margin-top:var(--sp-4)}
.stack{display:flex;flex-direction:column;gap:var(--sp-6)}

.section-head{margin-bottom:var(--sp-8)}
.section-head .eyebrow{margin-bottom:var(--sp-3)}
.section-head h2 + p{margin-top:var(--sp-3);color:var(--ink-2)}

/* ─── Frame: the one container ──────────────────────────────────────────── */
/* Replaces card, dcard, postcard, callout, note and viz — six names for one
   idea, which the old system shipped at fifteen different paddings. Structure
   is the hairline and the corner brackets, never a fill. */
.frame{position:relative;border:1px solid var(--rule);padding:var(--sp-4);background:none}
.frame--panel{background:var(--panel)}
.frame::before,.frame::after{
  content:"";position:absolute;width:7px;height:7px;border:1px solid var(--ink-3);pointer-events:none;
}
.frame::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
.frame::after{bottom:-1px;right:-1px;border-left:0;border-top:0}
.frame-head{
  display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-5);align-items:baseline;
  padding-bottom:var(--sp-3);margin-bottom:var(--sp-4);
  border-bottom:1px solid var(--rule-2);
  font-family:var(--mono);font-size:var(--fs-data);color:var(--ink-3);
}
.frame-head strong{color:var(--ink);font-weight:600}
/* A container whose content already draws its own box steps back, rather than
   drawing a second hairline and a second set of corner brackets around it. */
.frame:has(.frame),.frame:has(> .codeblk),.frame:has(> .tablewrap > table){
  border:0;padding:0;background:none}
.frame:has(.frame)::before,.frame:has(.frame)::after,
.frame:has(> .codeblk)::before,.frame:has(> .codeblk)::after,
.frame:has(> .tablewrap > table)::before,.frame:has(> .tablewrap > table)::after{display:none}
.frame-head > :last-child:not(:only-child){margin-left:auto}

/* ─── Instrument + readout ──────────────────────────────────────────────── */
.instrument{width:var(--col-wide);margin-inline:auto}
/* A plot wider than its column scrolls inside its own box and says so. It must
   never take the document sideways: min-width:0 on the track, because a grid
   or flex child defaults to min-content and a 660px plot will otherwise blow
   out a 390px layout from inside. */
.instrument-scroll{overflow-x:auto;min-width:0;-webkit-overflow-scrolling:touch}
.instrument svg{display:block;min-width:0}
.axis text,.tick{font-family:var(--mono);font-size:var(--fs-label);fill:var(--ink-3)}
.gridline{stroke:var(--grid);stroke-width:1}
.trace-caller{fill:var(--caller)}
.trace-agent{fill:var(--agent)}
.region-sev{fill:var(--sev-fill);stroke:var(--sev);stroke-dasharray:3 3}

.readouts{display:flex;flex-wrap:wrap;gap:var(--sp-7);margin-top:var(--sp-4);
  padding-top:var(--sp-4);border-top:1px solid var(--rule-2)}
.readout-k{display:block;font-family:var(--mono);font-size:var(--fs-label);font-weight:500;
  letter-spacing:var(--tr-label);text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--sp-2)}
.readout-v{font-family:var(--mono);font-size:var(--fs-h3);font-weight:600;color:var(--ink)}
.readout-v .unit{font-size:var(--fs-label);color:var(--ink-3);margin-left:var(--sp-1)}
.readout--sev .readout-v{color:var(--sev)}

/* ─── Register: the replacement for a bullet list ───────────────────────── */
.register{list-style:none;margin:0;padding:0;font-family:var(--mono);font-size:var(--fs-data)}
.register > li{display:grid;grid-template-columns:2.5em 1fr;gap:var(--sp-4);
  padding-block:var(--sp-3);border-top:1px solid var(--rule-2)}
.register > li:last-child{border-bottom:1px solid var(--rule-2)}
.register > li::before{counter-increment:reg;content:counter(reg,decimal-leading-zero);
  color:var(--ink-3);grid-column:1;grid-row:1}
/* An item may be a line or it may be a heading and two paragraphs. Without
   this every child after the first auto-places into the NEXT grid cell, so on
   /evidence the tier headings landed back in the counter column and overlapped
   the text above them by up to 70px. The counter owns column one; everything
   else stacks in column two. */
.register > li > *{grid-column:2}
.register > li > * + *{margin-top:var(--sp-3)}
.register{counter-reset:reg}

/* ─── Table ─────────────────────────────────────────────────────────────── */
.tablewrap{overflow-x:auto;min-width:0}
table{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:var(--fs-data)}
caption{text-align:left;font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);padding-bottom:var(--sp-3)}
thead th{
  font-size:var(--fs-label);font-weight:500;letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);text-align:left;
  padding:var(--sp-2) var(--sp-3);border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);
}
tbody td,tbody th{padding:var(--sp-3);border-bottom:1px solid var(--rule-2);
  text-align:left;font-weight:400;color:var(--ink-2);vertical-align:top}
tbody th{color:var(--ink);font-weight:600}
.num,td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}

/* Under 640px a data table reflows to one labelled card per row: the header
   goes to the screen reader and each cell prints its column name. A table that
   scrolls sideways with nothing saying so drops its last column silently. */
@media (max-width:640px){
  table.data{display:block}
  table.data thead{position:absolute;width:1px;height:1px;overflow:hidden;
    clip:rect(0,0,0,0);white-space:nowrap}
  table.data tbody,table.data tbody tr,table.data tbody th,table.data tbody td{display:block}
  table.data tbody tr{padding:var(--sp-4) 0;border-top:1px solid var(--rule-2)}
  table.data tbody td{padding:var(--sp-2) 0 0;border:0}
  table.data tbody th{padding:0;font-size:var(--fs-h3)}
  table.data tbody td[data-label]::before{
    content:attr(data-label) " ";display:block;font-size:var(--fs-label);
    letter-spacing:var(--tr-label);text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--sp-1)}
}

/* ─── Badge ─────────────────────────────────────────────────────────────── */
/* One shape. Hue only for a failure; every other status is text plus a glyph,
   because the old system spent five colours on statuses and read as a paint
   chart. */
.badge{display:inline-flex;align-items:center;gap:var(--sp-2);
  padding:var(--sp-1) var(--sp-2);border:1px solid var(--rule);
  font-family:var(--mono);font-size:var(--fs-label);font-weight:500;
  letter-spacing:var(--tr-label);text-transform:uppercase;color:var(--ink-2);white-space:nowrap}
.badge--sev{color:var(--sev);border-color:var(--sev)}
.badge--caller{color:var(--caller)}
.badge--agent{color:var(--agent)}

/* ─── Code ──────────────────────────────────────────────────────────────── */
code,kbd,samp{font-family:var(--mono);font-size:var(--fs-data)}
:not(pre) > code{padding:0 var(--sp-1);border:1px solid var(--rule-2);color:var(--ink)}
pre{margin:0;overflow-x:auto;min-width:0}
.codeblk{background:var(--panel);border:1px solid var(--rule);padding:var(--sp-4)}
.codeblk pre{font-size:var(--fs-data);line-height:1.6;color:var(--ink-2)}
.codeblk .c-cmd{color:var(--ink)}
.codeblk .c-dim{color:var(--ink-3)}
.codeblk .c-ok{color:var(--agent)}
.codeblk .c-fail{color:var(--sev)}

/* ─── Button ────────────────────────────────────────────────────────────── */
.btn{display:inline-flex;align-items:center;gap:var(--sp-2);
  padding:var(--sp-3) var(--sp-5);border:1px solid var(--ink);background:none;
  font-family:var(--mono);font-size:var(--fs-data);font-weight:600;color:var(--ink);
  cursor:pointer;text-decoration:none}
.btn--primary{background:var(--ink);color:var(--btn-ink)}
.btn:hover{border-color:var(--ink)}
.btn--primary:hover{opacity:.88}

/* ─── Site chrome ───────────────────────────────────────────────────────── */
.skip{position:absolute;left:var(--sp-4);top:-100px;z-index:99;
  padding:var(--sp-3) var(--sp-4);background:var(--bg);border:1px solid var(--ink);
  font-family:var(--mono);font-size:var(--fs-data);color:var(--ink);text-decoration:none}
.skip:focus{top:var(--sp-4)}

.site-nav{border-bottom:1px solid var(--rule)}
.nav-in{display:flex;align-items:center;gap:var(--sp-6);
  height:56px;font-family:var(--mono);font-size:var(--fs-label);
  letter-spacing:var(--tr-label);text-transform:uppercase}
.brand{display:inline-flex;align-items:center;gap:var(--sp-2);
  font-size:var(--fs-h3);font-weight:600;letter-spacing:var(--tr-head);
  text-transform:none;color:var(--ink);text-decoration:none}
.brand .tick{width:5px;height:14px;background:var(--sev);display:inline-block}
.nav-links{display:flex;flex-wrap:wrap;gap:var(--sp-5);margin-left:auto}
.nav-links a{color:var(--ink-3)}
.nav-links a:hover,.nav-links a[aria-current]{color:var(--ink)}
.tgl{margin-left:var(--sp-4);padding:var(--sp-2) var(--sp-3);border:1px solid var(--rule);
  background:none;font-family:var(--mono);font-size:var(--fs-label);
  letter-spacing:var(--tr-label);text-transform:uppercase;color:var(--ink-2);cursor:pointer}
.tgl:hover{color:var(--ink);border-color:var(--ink)}

.site-foot{border-top:1px solid var(--rule);padding-block:var(--sp-8) var(--sp-7);
  font-family:var(--mono);font-size:var(--fs-data);color:var(--ink-3)}
.foot-cols{display:grid;gap:var(--sp-6);
  grid-template-columns:repeat(auto-fit,minmax(min(14rem,100%),1fr))}
.foot-cols h2{font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--sp-3)}
.foot-cols ul{list-style:none;margin:0;padding:0;display:grid;gap:var(--sp-2)}
.foot-cols a{color:var(--ink-2)}
.foot-meta{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-5);
  margin-top:var(--sp-7);padding-top:var(--sp-4);border-top:1px solid var(--rule-2)}

/* ─── Narrow ────────────────────────────────────────────────────────────── */
@media (max-width:900px){
  :root{--col-wide:calc(100% - var(--sp-5) * 2);--fs-display:32px;--fs-h1:24px}
  .nav-in{height:auto;padding-block:var(--sp-3);flex-wrap:wrap;gap:var(--sp-3)}
  .nav-links{gap:var(--sp-4);margin-left:0;width:100%}
  .readouts{gap:var(--sp-5)}
}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:1ms!important;transition-duration:1ms!important}
}

/* ─── Hero rhythm ───────────────────────────────────────────────────────── */
/* The hero is not `.content`, so it does not inherit the article flow. Stated
   here rather than left to collapse: the headline, its lede, the actions and
   the note under them each need their own step or they stack flush. */
.band > .wrap > .eyebrow{margin-bottom:var(--sp-4)}
.band > .wrap > h1{margin-bottom:var(--sp-5)}
.band > .wrap > .lede{margin-bottom:var(--sp-6)}
.cta-row{display:flex;flex-wrap:wrap;justify-content:flex-start;
  gap:var(--sp-3);align-items:center}
.cta-note{margin-top:var(--sp-3);color:var(--ink-3)}
.instrument{margin-top:var(--sp-8)}
.reg-proof{margin-top:var(--sp-6)}
.reg-steps > li{padding-block:var(--sp-5)}
.reg-steps h3{margin-bottom:var(--sp-2)}
.codeblk + p{margin-top:var(--sp-4)}
.content-ref > h3{margin-top:var(--sp-7)}
.content-ref > h3:first-child{margin-top:0}
.content-ref > h3 + p{margin-top:var(--sp-3)}

/* ─── Document shell ────────────────────────────────────────────────────── */
/* The docs, the essays and the use-cases are one shape the home page never
   exercises: a persistent section index beside a single column of prose. It is
   a grid, not a float and not a sticky sidebar with its own scroll container —
   the index scrolls with the page, so it can never strand the reader in a pane
   with its own scrollbar. Under 900px the index moves above the prose and
   stops being sticky, because a sticky index on a phone eats the viewport. */
/* One column by default. The second column is claimed ONLY when the document
   actually has a section index -- the atlas index uses the same wrapper with
   no sidebar, and assuming one put its whole page in a 15rem column. A layout
   that depends on a child it cannot see is a layout that will be wrong on the
   first page that omits it; browsers without :has() get the single column,
   which is the readable half of the choice. */
/* A document is scanned, not scrolled: the reader is looking for a section,
   not being walked through an argument. So its rhythm is one step tighter than
   a marketing band's -- the same reason the atlas enforces a viewport budget on
   a record and not on the home page. */
.doc{display:grid;grid-template-columns:minmax(0,1fr);justify-content:center;
  gap:var(--sp-7);align-items:start;padding-block:var(--sp-6)}
.doc .section-head{margin-bottom:var(--sp-6)}
.doc .content > * + :is(h2,h3){margin-top:var(--sp-7)}
.doc:has(> .side){grid-template-columns:15rem minmax(0,var(--col-ref))}
.side{position:sticky;top:var(--sp-5);font-family:var(--mono);font-size:var(--fs-data)}
.sgroup + .sgroup{margin-top:var(--sp-5)}
.st{font-size:var(--fs-label);font-weight:500;letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--sp-2)}
.snav{display:grid;gap:var(--sp-1)}
.snav a{display:block;padding-block:var(--sp-1);color:var(--ink-2)}
.snav a:hover{color:var(--ink)}
.snav a[aria-current]{color:var(--ink);box-shadow:inset 2px 0 0 var(--sev);padding-left:var(--sp-2)}

/* Breadcrumb and pager: where you are, and the two steps either side. */
.crumb{font-family:var(--mono);font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);padding-block:var(--sp-4);
  border-bottom:1px solid var(--rule-2)}
.crumb a{color:var(--ink-3)}
.crumb a:hover{color:var(--ink)}
.crumb .sep{margin-inline:var(--sp-2);color:var(--ink-3)}

.prevnext{display:flex;flex-wrap:wrap;justify-content:space-between;gap:var(--sp-4);
  margin-top:var(--sp-8);padding-top:var(--sp-5);border-top:1px solid var(--rule);
  font-family:var(--mono);font-size:var(--fs-data)}
.prevnext a{display:grid;gap:var(--sp-1);color:var(--ink)}
.prevnext .nx{text-align:right;margin-left:auto}
.prevnext .tt{color:var(--ink-3);font-size:var(--fs-label);
  letter-spacing:var(--tr-label);text-transform:uppercase}

/* A definition list is the docs' main structure: term, then what it does. */
.dl{display:grid;gap:var(--sp-4);margin:var(--sp-5) 0}
.dl .dt{font-family:var(--mono);font-size:var(--fs-data);font-weight:600;color:var(--ink);
  margin-bottom:var(--sp-2)}
.dl .dd{color:var(--ink-2)}

.deskonly{display:block}

@media (max-width:900px){
  /* `.doc:has(> .side)` is (0,2,0) and out-specifies a bare `.doc` even inside
     this query, so the narrow rule has to match it or the two-column grid
     survives onto a 390px screen and takes the page sideways. */
  .doc,.doc:has(> .side){grid-template-columns:minmax(0,1fr);gap:var(--sp-6)}
  .side{position:static;top:auto;padding-bottom:var(--sp-5);border-bottom:1px solid var(--rule)}
  .deskonly{display:none}
}

/* Two more code tokens the docs use: a key in a config sample, a string, and
   the exit line a CI example ends on. */
.codeblk .c-key{color:var(--caller)}
.codeblk .c-str{color:var(--agent)}
.codeblk .c-exit{color:var(--ink);font-weight:600}

/* The small parts of the document chrome. Each of these was a page-local rule
   on all seven docs pages, which is why they are here and not there. */
.sidebody{display:contents}
.crumb .dir,.crumb .dot{color:var(--rule)}
.crumb .nm{color:var(--ink-2)}
.crumb .link{color:var(--ink-3)}
.ct{font-family:var(--mono);font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--sp-2)}

/* A figure caption states what the reader is looking at and where it came
   from. It sits under the figure, never over it. */
.viz-cap{font-family:var(--mono);font-size:var(--fs-label);color:var(--ink-3);
  margin-top:var(--sp-3);padding-top:var(--sp-3);border-top:1px solid var(--rule-2)}
.diagram{width:100%;overflow-x:auto;min-width:0;margin-block:var(--sp-5)}
.diagram svg{display:block;min-width:0;max-width:100%;height:auto}

/* `honest` marks the sentence that states a limit of the measurement. It is
   set apart by a rule and the label, not by a colour: a scope boundary is not
   a warning. */
.honest{border-left:2px solid var(--rule);padding-left:var(--sp-4);
  color:var(--ink-2);margin-block:var(--sp-5)}

.cards{display:grid;gap:var(--sp-5);
  grid-template-columns:repeat(auto-fit,minmax(min(22rem,100%),1fr))}

/* The rest of the code palette: a literal, a boolean, and the two exit lines.
   Pass is not green-as-decoration — it is the agent hue, already spent on
   "this side behaved", and a bad exit is the one failure hue. */
.codeblk .c-num,.codeblk .c-true{color:var(--caller)}
.codeblk .c-false{color:var(--sev)}
.codeblk .c-pass{color:var(--agent);font-weight:600}
.codeblk .c-exitbad{color:var(--sev);font-weight:600}

/* A frame that reports a passing result, and a figure the reader can open.
   `pass` tints the rule, not a fill — the agent hue already means "this side
   behaved", so it is being reused, not introduced. */
.frame.pass{border-color:var(--agent)}
.frame.pass .ct{color:var(--agent)}
.reveal > summary{cursor:pointer;font-family:var(--mono);font-size:var(--fs-data);
  color:var(--ink-2);padding-block:var(--sp-2)}
.reveal > summary:hover{color:var(--ink)}
.reveal[open] > summary{margin-bottom:var(--sp-3);
  border-bottom:1px solid var(--rule-2)}
.tl-svg{display:block;width:100%;height:auto;min-width:0;
  font-family:var(--mono);font-size:var(--fs-label)}
.tl-svg text{fill:var(--ink-3)}

/* ─── Inline meaning ────────────────────────────────────────────────────── */
/* The prose marks four things inline. Only one of them is a hue: a failure.
   Yes and no are a glyph plus a word, so they survive being read aloud and
   they survive being printed in grey. The hue is a second channel on top of
   that, never the only one. */
.em{color:var(--ink);font-weight:600}
.dim,.ck{color:var(--ink-3)}
.yes,.good,.pass{color:var(--ink)}
.no,.fail,.warn,.refuse{color:var(--sev)}
.yes::before{content:"+ ";color:var(--agent)}
.no::before{content:"- ";color:var(--sev)}

/* ─── Sub-navigation ────────────────────────────────────────────────────── */
/* A page-level index inside one section (the integrations pages). It is the
   nav row's type at the body's colour, not a second navigation system. */
.subnav{display:flex;flex-wrap:wrap;gap:var(--sp-2) var(--sp-5);list-style:none;
  margin:0 0 var(--sp-6);padding:0 0 var(--sp-3);border-bottom:1px solid var(--rule-2);
  font-family:var(--mono);font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase}
.subnav a{color:var(--ink-3)}
.subnav a:hover,.subnav a[aria-current]{color:var(--ink)}

/* ─── Post list ─────────────────────────────────────────────────────────── */
.postmeta{font-family:var(--mono);font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);margin-bottom:var(--sp-5)}
.more,.go{font-family:var(--mono);font-size:var(--fs-data);color:var(--ink-3)}
a:hover .more,a:hover .go{color:var(--ink)}

/* ─── Window chrome ─────────────────────────────────────────────────────── */
/* A screenshot needs a frame that says "this is a captured window" without
   drawing a fake title bar with fake traffic lights. One rule, one label. */
.wbar{display:flex;align-items:center;gap:var(--sp-2);padding-bottom:var(--sp-3);
  margin-bottom:var(--sp-4);border-bottom:1px solid var(--rule-2);
  font-family:var(--mono);font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3)}
.wdot{width:5px;height:5px;background:var(--ink-3);flex:none}
.wbody{min-width:0;overflow-x:auto}
.wshot,.winframe{display:block;max-width:100%;height:auto;border:1px solid var(--rule)}

/* ─── Two-column band ───────────────────────────────────────────────────── */
/* The marketing pages set a figure beside its explanation. Below 900px the
   figure goes under the prose, never beside it at half width. */
/* Both columns start at the top. Centring a two-paragraph column against a
   three-card one puts a 150px void above the prose and another below it, which
   reads as a layout accident rather than a choice. */
.viz-left{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);
  gap:var(--sp-7);align-items:start}
.centerviz,.s-viz,.s-visual{min-width:0;overflow-x:auto}
@media (max-width:900px){
  .viz-left{grid-template-columns:minmax(0,1fr);gap:var(--sp-5)}
}

/* ─── Command row ───────────────────────────────────────────────────────── */
/* A command and the button that copies it, on one line that wraps rather than
   scrolls. The button is the smallest the button rule allows, not a new one. */
.cmdrow{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);min-width:0}
.cmdrow pre,.cmdrow code{min-width:0;overflow-x:auto}
.cmdrow .btn{padding:var(--sp-1) var(--sp-3);font-size:var(--fs-label);
  letter-spacing:var(--tr-label);text-transform:uppercase}

/* ─── Closing call to action ────────────────────────────────────────────── */
.footcta{border-top:1px solid var(--rule);padding-block:var(--band-y)}
.footcta-card{max-width:var(--col-ref);margin-inline:auto}
.foot-cmd{margin-top:var(--sp-5)}

/* ─── Row ───────────────────────────────────────────────────────────────── */
/* The scorecard, the refuse ledger and the incident report were three page-
   local names for one thing: a labelled line inside a frame, with the value
   pushed to the right and wrapping under the label rather than off the edge. */
.badgerow{display:flex;flex-wrap:wrap;gap:var(--sp-2);list-style:none;margin:0;padding:0}
.row{display:grid;grid-template-columns:minmax(0,14rem) minmax(0,1fr);
  gap:var(--sp-2) var(--sp-5);align-items:baseline;padding-block:var(--sp-3);
  border-top:1px solid var(--rule-2);font-family:var(--mono);font-size:var(--fs-data)}
@media (max-width:640px){.row{grid-template-columns:minmax(0,1fr)}}
.row:first-of-type{border-top:0}
/* A flex child defaults to min-content width, so a row holding an id like
   pat_d4645a0f0620c222f17fdbc4 refuses to wrap and takes the page sideways at
   390px -- which is exactly how every atlas record overflowed. The value is
   allowed to break anywhere, and a badge carrying one stops being a single
   unbreakable line. */
.row > *{min-width:0;max-width:100%}
.row code,.row .badge,.row a{overflow-wrap:anywhere}
.row .badge{white-space:normal}

/* ─── Confusion matrix ──────────────────────────────────────────────────── */
/* What the detector called against what the label says. Correct cells are
   plain, errors carry the failure hue — the only place a cell gets a colour,
   because an error IS the information the figure exists to show. */
.cmx{display:grid;grid-template-columns:auto repeat(2,minmax(72px,1fr));
  gap:0;font-family:var(--mono);font-size:var(--fs-data);min-width:0}
.cmx-corner{background:none}
.cmx-colhead,.cmx-rowhead{font-size:var(--fs-label);letter-spacing:var(--tr-label);
  text-transform:uppercase;color:var(--ink-3);padding:var(--sp-2);align-self:center}
.cmx-colhead{text-align:center}
.cmx-rowhead{text-align:right}
.cmx-cell{padding:var(--sp-4) var(--sp-2);text-align:center;border:1px solid var(--rule-2)}
.cmx-cell.is-error{border-color:var(--sev);color:var(--sev)}
.cmx-n{display:block;font-size:var(--fs-h3);font-weight:600;color:inherit}
.cmx-lbl,.cmx-g{display:block;font-size:var(--fs-label);color:var(--ink-3)}
.cmx-fig{min-width:0;overflow-x:auto}

/* ─── Diagram parts ─────────────────────────────────────────────────────── */
/* Inline SVG in the essays: the stages of the loop, and the label a stage
   carries. Stated here so the figure inherits the theme instead of freezing
   one set of colours into the markup. */
.s1{fill:var(--caller-fill);stroke:var(--caller)}
.s2{fill:var(--agent-fill);stroke:var(--agent)}
.s3{fill:var(--sev-fill);stroke:var(--sev)}
.steam{stroke:var(--ink-3);fill:none}
.tc-col{min-width:0}
.elevel{color:var(--ink);text-decoration-color:var(--rule)}
.vpcdiagram{display:block;width:100%;height:auto;min-width:0}
.vpcdiagram text{fill:var(--ink-3);font-family:var(--mono);font-size:11px}
.vpcdiagram rect,.vpcdiagram path,.vpcdiagram line{stroke:var(--rule);fill:none}

/* ─── Incident timeline SVG ─────────────────────────────────────────────── */
/* The incident report's plot names its own parts. Same rule as the hero: the
   axis and its ticks are structure, the onset marker and the failure spans
   are the finding, and only the finding carries the hue. */
.ax,.tk{fill:var(--ink-3);font-family:var(--mono);font-size:11px}
.ls-n{fill:var(--ink);font-family:var(--mono);font-size:11px;font-weight:600}
.onset{stroke:var(--sev);stroke-width:1;stroke-dasharray:3 3}
.sp-fail{fill:var(--sev-fill);stroke:var(--sev)}
.sp-warn{fill:none;stroke:var(--ink-3);stroke-dasharray:2 2}
.lb-fail{fill:var(--sev)}
.lb-onset,.lb-warn{fill:var(--ink-3)}

/* ─── Small remainders ──────────────────────────────────────────────────── */
.id{font-family:var(--mono);font-size:var(--fs-data);color:var(--ink-3)}
.term-note,.tiernote{font-family:var(--mono);font-size:var(--fs-label);
  color:var(--ink-3);margin-top:var(--sp-3)}
.cmx-cell.is-correct{color:var(--ink)}

/* The 404 is the one page the mascot survives on, so it is also the one page
   that gets a display-sized figure with nothing to measure. */
.e404{font-family:var(--mono);font-size:var(--fs-display);font-weight:600;
  color:var(--ink);letter-spacing:var(--tr-display)}
.e-mark{display:block;width:64px;height:auto;margin-bottom:var(--sp-5)}

/* ─── Toolbar ───────────────────────────────────────────────────────────── */
/* The atlas index filters and sorts itself. The controls sit on one line that
   wraps, above the rule that separates them from the results they act on. */
.toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--sp-3);
  padding-bottom:var(--sp-4);margin-bottom:var(--sp-5);
  border-bottom:1px solid var(--rule);
  font-family:var(--mono);font-size:var(--fs-data)}
.toolbar input,.toolbar select{
  padding:var(--sp-2) var(--sp-3);border:1px solid var(--rule);background:var(--bg);
  font-family:var(--mono);font-size:var(--fs-data);color:var(--ink)}
.toolbar input:focus-visible,.toolbar select:focus-visible{outline:2px solid var(--ink);outline-offset:1px}
/* The filter bar's selected control. The script sets aria-current on it, so
   the state is announced and drawn from one attribute rather than a class
   the markup would have to carry. */
.toolbar a[aria-current]{color:var(--ink);border-color:var(--ink);font-weight:600}

/* ─── Quoted evidence ───────────────────────────────────────────────────── */
/* A record quotes a report in the reporter's own words. It is set apart by a
   rule and attributed underneath, never styled as a pull quote — it is
   evidence, not emphasis. */
blockquote{margin:0;padding-left:var(--sp-4);border-left:2px solid var(--rule);
  color:var(--ink-2)}
.ev-quote{font-family:var(--mono);font-size:var(--fs-data)}

/* A long identifier truncates with an ellipsis rather than widening its
   column; the full value stays in the title attribute and in the DOM. */
.trunc{display:inline-block;max-width:100%;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap;vertical-align:bottom}
.nw{white-space:nowrap}

/* An evidence row is a disclosure whose summary is the row itself: who, when,
   which stack, which source, and the link. They are separate facts and have to
   read as separate facts, so the summary is a wrapping row with real gaps
   rather than a run of adjacent spans. */
.reveal > summary{display:flex;flex-wrap:wrap;align-items:baseline;
  gap:var(--sp-2) var(--sp-4)}
.reveal > summary .ct{margin-bottom:0}
.ev-list > .reveal{border-top:1px solid var(--rule-2);padding-block:var(--sp-2)}
/* An evidence list takes real columns, and the label strip above it heads
   them. The strip and every row share ONE template, so a column's width is a
   property of the list rather than of whichever quote happens to be longest --
   five rows of five facts read down as well as across. Values that would
   widen a column clip with an ellipsis and keep the full text in `title`; a
   stack chip is never clipped, because a half-chip reads as a different
   stack. */
.ev-list > .frame-head,
.ev-list .reveal > summary{
  display:grid;
  grid-template-columns:minmax(0,1fr) 6.5rem minmax(0,.8fr) minmax(8rem,.9fr) minmax(0,1.1fr);
  gap:var(--sp-2) var(--sp-4);align-items:baseline}
.ev-list > .frame-head > :last-child:not(:only-child){margin-left:0}
.ev-list .reveal > summary > *{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ev-list .reveal > summary > *:has(> .tag-stack){overflow:visible;white-space:normal}
/* Narrow, the columns collapse to the wrapping row the disclosure uses
   everywhere else -- a date and a stack chip are self-describing, and five
   one-word columns are not columns. The label strip goes with them: a label
   over a column that no longer exists is worse than no label. */
@media (max-width:900px){
  .ev-list > .frame-head{display:none}
  .ev-list .reveal > summary{display:flex;flex-wrap:wrap;align-items:baseline;
    gap:var(--sp-2) var(--sp-4)}
  .ev-list .reveal > summary > *{overflow:visible;white-space:normal}
}

/* ─── Text inside a figure ──────────────────────────────────────────────── */
/* The inline SVGs set their own font-size as a presentation attribute, and
   across the site they had TEN of them: 8.5, 9.5, 10, 10.5, 11, 11.5, 12,
   12.5, 13, 16. That is the same drift the rest of this overhaul removes,
   hiding inside SVG where no rule reached it -- a figure's tick label was a
   different size on every page that drew one.
   A CSS declaration beats a presentation attribute, so one rule here retires
   all ten without touching a single figure's markup. A label is a label; a
   value the reader is meant to read off the plot is data. */
/* `g` carries it too: the figures set font-size on the group and let the text
   inherit, so a rule that names only `text` leaves the group's own value in
   place and the gate still sees 12px and 10.5px. */
svg text,svg g{font-size:var(--fs-label);font-family:var(--mono)}
svg .num,svg .ls-n,svg .val{font-size:var(--fs-data);font-weight:600}

/* ─── Status and stack colour ───────────────────────────────────────────── */
/* Reinstates the two coded dimensions the single-stylesheet migration dropped.
   The earlier system carried the stack hue on a `style="--pf-h:N"` attribute,
   which check-css now forbids outright, so the hue is keyed off the filter
   href instead: same sha256-walked wheel, no inline style, and the generator
   does not have to emit anything it does not already emit.
   Status keeps the one badge shape. Only the hue and the leading dot change,
   so a status still reads aloud and still prints in grey. */

/* Status. Open is unresolved and gets its own amber rather than borrowing the
   failure red, which means a failure, not the state of one. Acknowledged and
   fixed reuse the caller/agent pair a reader has already learned; stale is
   metadata and stays neutral. */
.badge--open{color:var(--open);border-color:var(--open)}
.badge--acknowledged{color:var(--caller);border-color:var(--caller)}
.badge--fixed{color:var(--agent);border-color:var(--agent)}
.badge--stale,.badge--withdrawn-report{color:var(--ink-3);border-color:var(--rule)}
.badge--disputed{color:var(--sev);border-color:var(--sev)}
.badge--merged,.badge--withdrawn{color:var(--void);border-color:var(--void)}

/* The atlas index already carries the status on the row, so the whole table
   colours itself with no change to the generated markup. */
tr[data-status="open"] td.nw .badge{color:var(--open);border-color:var(--open)}
tr[data-status="acknowledged"] td.nw .badge{color:var(--caller);border-color:var(--caller)}
tr[data-status="fixed"] td.nw .badge{color:var(--agent);border-color:var(--agent)}
tr[data-status="stale"] td.nw .badge{color:var(--ink-3)}

/* A 6px leading disc, so the state survives being read at a glance down a
   column of 32 rows and does not rely on hue alone. */
tr[data-status] td.nw .badge::before,.badge--open::before,.badge--acknowledged::before,
.badge--fixed::before,.badge--stale::before,.badge--disputed::before,
.badge--merged::before,.badge--withdrawn::before{
  content:"";width:6px;height:6px;background:currentColor;flex:none}

/* Stack tags. One lightness and one chroma for every hue is what keeps
   seventeen tags reading as one palette instead of seventeen decisions, and
   what keeps them all above 4.5:1. The flat declarations come first so a
   browser without oklch() lands on the neutral chip rather than nothing. */
a[href*="/atlas/?stack="],.tag-stack{
  color:var(--ink-2);border-color:var(--rule);
  color:oklch(var(--pf-l) var(--pf-c) var(--pf-h,0));
  background:oklch(var(--pf-l) var(--pf-c) var(--pf-h,0) / var(--pf-fill));
  border-color:oklch(var(--pf-l) var(--pf-c) var(--pf-h,0) / var(--pf-edge))}
a[href*="/atlas/?stack="]:hover,a.tag-stack:hover{
  background:oklch(var(--pf-l) var(--pf-c) var(--pf-h,0) / var(--pf-on))}
/* 17 stacks, hue = sha256(name) walked clear of its neighbours by 18deg. */
a[href$="/atlas/?stack=assemblyai"]{--pf-h:128}
a[href$="/atlas/?stack=asterisk"]{--pf-h:222}
a[href$="/atlas/?stack=azure-speech"]{--pf-h:238}
a[href$="/atlas/?stack=cartesia"]{--pf-h:145}
a[href$="/atlas/?stack=deepgram"]{--pf-h:206}
a[href$="/atlas/?stack=elevenlabs"]{--pf-h:345}
a[href$="/atlas/?stack=gemini-live"]{--pf-h:256}
a[href$="/atlas/?stack=home-assistant"]{--pf-h:273}
a[href$="/atlas/?stack=kokoro"]{--pf-h:92}
a[href$="/atlas/?stack=livekit"]{--pf-h:3}
a[href$="/atlas/?stack=nemo"]{--pf-h:112}
a[href$="/atlas/?stack=openai-agents"]{--pf-h:291}
a[href$="/atlas/?stack=openai-realtime"]{--pf-h:53}
a[href$="/atlas/?stack=pipecat"]{--pf-h:310}
a[href$="/atlas/?stack=piper"]{--pf-h:164}
a[href$="/atlas/?stack=silero"]{--pf-h:181}
a[href$="/atlas/?stack=telnyx"]{--pf-h:37}
a[href$="/atlas/?stack=twilio"]{--pf-h:328}
a[href$="/atlas/?stack=ultravox"]{--pf-h:72}
a[href$="/atlas/?stack=whisper"]{--pf-h:21}

/* ─── Spending the region washes ────────────────────────────────────────── */
/* `--caller-fill` and `--agent-fill` were defined in both themes and used on
   one diagram class each. They are the two region colours the reader already
   associates with the two parties, so a card that belongs to one side is
   washed with that side's colour rather than left on the panel grey. */
.frame.pass .frame-head{background:var(--agent-fill)}
.frame.fail{border-color:var(--sev)}
.frame.fail .frame-head{background:var(--sev-fill)}

/* Success was the one verdict the page printed in grey: `.no/.fail` was red
   while `.yes/.good/.pass` was ink, so the only coloured verdict on the site
   was the bad one. The agent hue already means "this side behaved", so it is
   being reused here, not introduced. */
.yes,.good,.pass{color:var(--agent)}

/* The mode name is the row's subject, not a chip. It arrives from the atlas
   generator wearing `.badge` like everything else in the table, which boxed
   every title in a hairline and made the one thing a reader scans for look
   like metadata. Undo the chip and let it read as the link it is. */
.a-index td:first-child>a.badge,tr[data-status] td:first-child>a.badge{
  display:inline;border:0;padding:0;font-family:var(--sans);
  font-size:var(--fs-body);text-transform:none;letter-spacing:normal;
  font-weight:600;color:var(--ink)}
.a-index td:first-child>a.badge:hover,tr[data-status] td:first-child>a.badge:hover{
  color:var(--caller);text-decoration:underline}

/* ─── Mascot, motion and warmth ─────────────────────────────────────────── */
/* The overhaul removed every keyframe, every transition and the whole warm
   half of the palette, which is why the pages read as a spreadsheet. The
   measurement surfaces stay clinical on purpose — a plot is not decorated —
   but the brand surfaces around them get the character back.
   Everything here is decorative and aria-hidden, and everything here stops
   dead under prefers-reduced-motion. */

.steam path{opacity:0;transform-origin:center bottom}
.steam .sw1{animation:steam 3.6s ease-in-out infinite}
.steam .sw2{animation:steam 3.6s ease-in-out .8s infinite}
.steam .sw3{animation:steam 3.6s ease-in-out 1.7s infinite}
@keyframes steam{
  0%{opacity:0;transform:translateY(6px) scaleY(.7)}
  25%{opacity:.85}
  70%{opacity:.35}
  100%{opacity:0;transform:translateY(-20px) scaleY(1.15)}}
.floaty{animation:float 6s ease-in-out infinite}
@keyframes float{0%,100%{transform:translateY(0) rotate(-2deg)}50%{transform:translateY(-12px) rotate(2deg)}}
.turn-orb{animation:toss 3.2s cubic-bezier(.42,0,.58,1) infinite}
@keyframes toss{0%,100%{transform:translate(0,0)}47%,53%{transform:translate(-7px,-150px)}}
.orb-glow{animation:pulse 3.4s ease-in-out infinite}
@keyframes pulse{0%,100%{opacity:.55}50%{opacity:1}}
.blink{animation:blink 5.5s steps(1,end) infinite}
@keyframes blink{0%,94%,100%{transform:scaleY(1)}96%,98%{transform:scaleY(.12)}}
.mascot .eye-grp,.fmascot .eye-grp{transform-origin:center;transform-box:fill-box}

/* Stage: the hero mascot sits beside the hero plot, never on top of it. */
.stage{position:relative;justify-self:center;width:100%;max-width:320px;aspect-ratio:1/1}
.stage .mascot{width:100%;height:100%;overflow:visible}
.brandmark{width:28px;height:28px;overflow:visible;flex:none}
.foot-mark{width:44px;height:26px;overflow:visible}
.fmascot{width:96px;height:96px;overflow:visible}

/* The ember wash. Two corners, never a full field, so the type still sits on
   flat ground and the contrast gate has something predictable to measure. */
.band--warm{position:relative;overflow:hidden}
.band--warm>*{position:relative;z-index:1}
.band--warm::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:
    radial-gradient(58% 52% at 82% 8%,var(--glow),transparent 62%),
    radial-gradient(42% 40% at 6% 88%,color-mix(in srgb,var(--ember) 12%,transparent),transparent 60%);
  animation:emberdrift 15s ease-in-out infinite}
@keyframes emberdrift{
  0%,100%{opacity:.85;transform:translate3d(0,0,0)}
  50%{opacity:1;transform:translate3d(-1.6%,1.1%,0)}}

/* An eyebrow gets its ember rule back. */
.eyebrow::before{content:"";display:inline-block;width:32px;height:2px;
  margin-right:var(--sp-2);vertical-align:middle;background:var(--ember)}

/* Scroll-reveal. `.reveal` is a details/summary widget on this stylesheet now,
   so the scroll behaviour gets its own name rather than fighting it. */
.js .rise{opacity:0;transform:translateY(12px);
  transition:opacity .6s cubic-bezier(.22,.61,.36,1),transform .6s cubic-bezier(.22,.61,.36,1)}
.js .rise.in{opacity:1;transform:none}

/* Hover life. The overhaul left the stylesheet with no transition at all. */
.btn,.site-nav a,.site-foot a{transition:color .18s,border-color .18s,background-color .18s}
.btn:hover{border-color:var(--ember)}

@media (prefers-reduced-motion:reduce){
  .steam path,.floaty,.turn-orb,.orb-glow,.blink,.band--warm::before{animation:none!important}
  .turn-orb{transform:translate(-7px,-150px)}
  .steam path{opacity:.5}
  .js .rise{opacity:1;transform:none;transition:none}
  .btn,.site-nav a,.site-foot a{transition:none}}

/* Hero: copy left, mascot right, and the mascot is the first thing to go when
   the column gets narrow — it is character, not content. */
.hero-grid{display:grid;grid-template-columns:minmax(0,1fr) 320px;
  gap:var(--sp-8);align-items:center}
.hero-copy{min-width:0}
@media (max-width:900px){
  .hero-grid{grid-template-columns:1fr;gap:var(--sp-6)}
  .hero-grid .stage{grid-row:1;max-width:220px;justify-self:start}}

/* The same hues for the tags on a record page, which are spans, not filter links. */
.tag-stack[data-stack="assemblyai"]{--pf-h:128}
.tag-stack[data-stack="asterisk"]{--pf-h:222}
.tag-stack[data-stack="azure-speech"]{--pf-h:238}
.tag-stack[data-stack="cartesia"]{--pf-h:145}
.tag-stack[data-stack="deepgram"]{--pf-h:206}
.tag-stack[data-stack="elevenlabs"]{--pf-h:345}
.tag-stack[data-stack="gemini-live"]{--pf-h:256}
.tag-stack[data-stack="home-assistant"]{--pf-h:273}
.tag-stack[data-stack="kokoro"]{--pf-h:92}
.tag-stack[data-stack="livekit"]{--pf-h:3}
.tag-stack[data-stack="nemo"]{--pf-h:112}
.tag-stack[data-stack="openai-agents"]{--pf-h:291}
.tag-stack[data-stack="openai-realtime"]{--pf-h:53}
.tag-stack[data-stack="pipecat"]{--pf-h:310}
.tag-stack[data-stack="piper"]{--pf-h:164}
.tag-stack[data-stack="silero"]{--pf-h:181}
.tag-stack[data-stack="telnyx"]{--pf-h:37}
.tag-stack[data-stack="twilio"]{--pf-h:328}
.tag-stack[data-stack="ultravox"]{--pf-h:72}
.tag-stack[data-stack="whisper"]{--pf-h:21}

/* A chip that is a direct child of `.row` is a grid item, so it inherits the
   1fr column's full width and reads as a filled bar rather than a tag. Pin it
   to the value column and let it size to its own text. */
.row>.badge,.row>.tag-stack{grid-column:2;justify-self:start}
@media (max-width:640px){.row>.badge,.row>.tag-stack{grid-column:1}}

/* The mode cell is a title and a sentence, so it gets two lines. Inline they
   collide, and `.ct` sets its caption in uppercase mono -- fine for a four-word
   label, unreadable for 32 rows of full sentences. Here it reads as prose. */
tr[data-status] td:first-child>a.badge{display:block;margin-bottom:var(--sp-1)}
tr[data-status] td:first-child>.ct{display:block;text-transform:none;
  letter-spacing:normal;font-family:var(--sans);color:var(--ink-3)}
