/* Arc Sentinel — shared design system.
 *
 * Two surfaces, one product. The terminal (`/`) is read by an analyst watching
 * a feed: dense, dark, mono, tables that scroll. The console (`/console`) is
 * read by an integrator wiring an API: plans, keys, quotas, copyable requests.
 * They are different jobs and they get different layouts, but they must not
 * look like two different products, so everything visual lives here.
 *
 * Tokens are the ones the dashboard already used. They were extracted rather
 * than redesigned: the palette was fine, and the only thing wrong was that
 * adding a second page would have meant a second copy of it.
 */

/* ------------------------------------------------------------- typefaces --
 *
 * Declared before anything else, because `--mono` and `--sans` below name
 * these families and a face that has not been declared is a name the browser
 * silently drops from the stack.
 *
 * `font-weight: 400 600` is a *range*, not a weight. Both files are variable
 * fonts: one file holds every weight between 400 and 600 and the browser
 * interpolates. Declaring three separate faces would have meant three
 * downloads of the same 48 KB, which is what the old CDN link did.
 *
 * `font-display: swap` and not `block`: a table of addresses that is invisible
 * for 100 ms while a font loads is a worse outcome than the same table in
 * Consolas. The layout is built to survive the substitution — every numeric
 * column is `font-variant-numeric: tabular-nums`, so the digits stay in their
 * columns in either face.
 *
 * No `unicode-range`: these files are the `latin` subset already, which covers
 * ASCII plus Latin-1 — enough for both languages this product speaks, accents
 * and `¿`/`¡` included. The other seven subsets Google serves by default
 * (Cyrillic, Greek, Vietnamese and their extensions) were more bytes than the
 * glyphs this page actually draws.
 */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/fonts/inter.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/fonts/jetbrains-mono.woff2') format('woff2');
}

:root {
  /* The native parts — scrollbars, form controls, the search field's clear
   * button — have to be told which ground they are on or they render for the
   * wrong one. This is the dark half of the pair; the light block below is the
   * other. It has to sit here, *before* that block, because `:root` and
   * `[data-theme="light"]` have identical specificity and source order is the
   * only thing that decides between them: the same declaration placed after the
   * light block silently pins the page to dark native chrome. */
  color-scheme: dark;

  /* Surfaces, darkest to lightest.
   *
   * The app is dark because it is a monitoring surface — the data is the bright
   * thing, not the chrome. But "dark" is not the same as "black", and the first
   * version of this palette made that mistake: every surface was a neutral
   * near-black within a few points of the others, so a card on a card read as a
   * seam rather than as a layer and the whole page flattened into one grey
   * rectangle with text on it.
   *
   * Two changes fix that. The ramp now has a blue cast, because a cool black
   * separates from the amber and rose of the risk scale instead of competing
   * with it — a neutral ground makes every warning look muddy. And the steps
   * are further apart, so elevation is legible without a border doing the work.
   *
   * The second pass pushed the blue cast further and darkened the floor. The
   * page is read for hours by somebody watching numbers, and the previous
   * ground sat close enough to pure black that the panels had to carry the
   * whole job of saying "this is a layer" on their borders alone. `#0b0f19`
   * still reads as black at a glance and is measurably not one: it holds
   * enough blue that a card can be told from the page by its fill.
   */
  --bg: #0b0f19;

  /* The telemetry band's own step. The topbar used to be the page colour with
   * a hairline under it, which made the one row of live numbers on the page
   * look like a header. A surface of its own puts the numbers on a shelf
   * without a heavier divider, and gives the band something to do when the
   * page scrolls under it. */
  --ticker: #0f1524;

  /* The navigation rail, a step lighter than the page and a step darker than
   * the band. Three surfaces at the top of the ramp, all within eight points
   * of each other and all distinguishable, which is the whole point of
   * separating elevation from borders. */
  --side: #0d1420;

  --panel: #111827;
  --panel2: #16202f;
  --panel3: #1e2b3d;
  --line: #1f2a3a;
  --line2: #2c3a4e;

  /* Graphite surfaces with accessible, cool neutral text. */
  --tx: #f1f5f9;
  --tx2: #b4c1d1;
  --tx3: #8a99ad;
  --tx3-rgb: 138, 153, 173;

  /* ---------------------------------------------------------------------
   * Brand, and why it is not the same token as the signal colours.
   *
   * The chrome needs an identity: the nav, the focus rings, the hero gauge.
   * The data needs semantics: money in, money out, warning, failure. When one
   * token does both jobs, restyling the brand repaints the data and every
   * reading of the screen changes meaning. So they are separate now.
   *
   * `--brand` is ice blue and `--brand2` a pale cyan; they belong to the chrome.
   * Nothing that carries a verdict is ever painted with them.
   * --------------------------------------------------------------------- */
  --brand: #93b7ff;
  --brand2: #7ee7dc;
  --brand-rgb: 147, 183, 255;
  --brand2-rgb: 126, 231, 220;
  --brand-fill: rgba(var(--brand-rgb), .10);
  --brand-line: rgba(var(--brand-rgb), .26);
  --brand-glow: rgba(var(--brand-rgb), .16);

  /* ---------------------------------------------------------------------
   * The mark's own three colours, named.
   *
   * The logo is a green→teal→cyan gradient and it is the one thing on the
   * page the interface did not invent. Naming its stops does two jobs: the
   * nav's active row can carry the same gradient by reference instead of by
   * a copy of three hex values, and `test/brand.mjs` can fail the build when
   * the SVGs and these drift apart — which is the failure that is otherwise
   * invisible, because a logo two degrees off the chrome still looks fine.
   *
   * They are declared here as the source of truth and duplicated as literals
   * inside the three SVGs, because an SVG served as an `<img>` cannot read a
   * CSS variable. The duplication is deliberate; the drift check is what
   * makes it safe.
   * --------------------------------------------------------------------- */
  --mark-1: #4ade4a;
  --mark-2: #2fd6a8;
  --mark-3: #22d3ee;
  --mark2-rgb: 47, 214, 168;
  --mark3-rgb: 34, 211, 238;
  /* The same two, pre-mixed at the strength a filled surface wants. `rgba()`
   * cannot take a token, and the file's own convention for a tint is a named
   * token beside the solid — see `--brand-fill` above. */
  --mark-fill: rgba(var(--mark2-rgb), .17);
  --mark-fill-2: rgba(var(--mark3-rgb), .07);

  /* ---------------------------------------------------------------------
   * The four meanings, named.
   *
   * Every colour in this file that carries a judgement is one of these five
   * values and nothing else. That is the point of naming them: before, a
   * verdict was whatever hex the rule happened to hardcode, so the same
   * "this is fine" was `#1fe89b` in one place and `#2fd6a8` in another, and
   * the reader had no way to learn the vocabulary because there wasn't one.
   *
   *   --ok        🟢 validated, funded, arrived
   *   --info      🔵 structural or neutral — a fact, not a verdict
   *   --medium    🟡 a warning you can keep working through
   *   --high      🟠 a warning you should act on
   *   --critical  🔴 a warning you cannot ignore
   *
   * Five and not four because this product has two axes that both need
   * colour: the risk of a token (`low`…`critical`, four steps) and the
   * severity of an event (`info`…`critical`, four steps). A four-colour
   * scale cannot serve both — the event `info` would have to borrow a risk
   * colour and start meaning a verdict it does not have. So `--info` is the
   * fifth, and it is the one that never means "danger".
   * --------------------------------------------------------------------- */
  --ok: #34d399;
  --info: #5b9dff;
  --medium: #f5c542;
  --high: #ff9e5e;
  --critical: #ff3d68;

  /* The same five as channel triplets, for the `rgba()` tints a hex token
   * cannot write. `rgba(var(--ok-rgb), .11)` is the one form that lets a rule
   * keep its own alpha while the *colour* still answers to a token — and it is
   * what lets the light theme flip every tint by changing the channel and never
   * the rule. */
  --ok-rgb: 52, 211, 153;
  --info-rgb: 91, 157, 255;
  --medium-rgb: 245, 197, 66;
  --high-rgb: 255, 158, 94;
  --critical-rgb: 255, 61, 104;

  /* Tints for fills, so a badge never has to hardcode an rgba(). Same five,
   * at the alpha each one needs to sit on `--panel` without vibrating. */
  --ok-fill: rgba(var(--ok-rgb), .13);
  --info-fill: rgba(var(--info-rgb), .13);
  --medium-fill: rgba(var(--medium-rgb), .15);
  --high-fill: rgba(var(--high-rgb), .16);
  --critical-fill: rgba(var(--critical-rgb), .18);

  /* The older names, kept as aliases rather than deleted.
   *
   * They are read by 60-odd rules and by every page's inline script, and
   * `--accent` in particular is the name the product has used for "good" since
   * the first commit. Renaming it would have touched all six surfaces to say
   * the same thing; aliasing it says it once. `--accent2` is the blue, which
   * is why links are `--info`.
   *
   * `--bad` is `--critical` and not `--high`: everywhere it appears it means
   * "this failed" — a dead RPC, a rejected response, a count of alerts — and a
   * failure is the one thing that is never merely orange. */
  --accent: var(--ok);
  --accent2: var(--info);
  --warn: var(--medium);
  --bad: var(--critical);
  --crit: var(--critical);
  --violet: #b48cff;
  --violet-rgb: 124, 92, 255;

  /* Text that sits *on* a filled accent surface — a primary button, a filled
   * badge. It is a token and not `#04201a` because the surface it sits on is a
   * token, and the two flip together: on a light accent this is near-white and
   * on a dark one it is near-black, which no single literal can be. */
  --on-accent: #05201a;

  /* ---------------------------------------------------------------------
   * The risk scale, separated from the brand accent.
   *
   * The palette above was fine for a page of tables. It stopped being fine
   * once the same screen had to carry a risk verdict, because `--accent` was
   * doing two jobs at once: it was the brand green AND the `low` risk level.
   * A token with no flags rendered in the same colour as a button you are
   * meant to click, which reads as "this is the good one, act on it" — the
   * opposite of what a risk column is for.
   *
   * So the scale is its own ramp now. It is ordered by severity and each step
   * is a different hue, not a different lightness of one hue, because four
   * shades of the same colour are indistinguishable at 11px on a dark ground
   * and are exactly the case colour-blind readers lose first.
   *
   * `low` used to be teal, because the brand was a mint green and a healthy
   * token would otherwise have rendered in the same colour as a button you are
   * meant to click. That problem is gone at the root now: the brand is ice
   * blue, so green is free to mean the one thing the reader expects it to mean.
   * Keeping teal here would have been a local fix to a problem the palette no
   * longer has — and it would have left `low` as the only verdict in the
   * product that is not one of the five named meanings above.
   *
   * So the scale is an alias of those five, and the four steps are ordered by
   * severity with a different hue at each one rather than four lightnesses of
   * one hue, because four shades of the same colour are indistinguishable at
   * 11px on a dark ground and are exactly the case colour-blind readers lose
   * first.
   * --------------------------------------------------------------------- */
  --risk-low: var(--ok);
  --risk-medium: var(--medium);
  --risk-high: var(--high);
  --risk-critical: var(--critical);

  /* Tints for fills, so a badge never has to hardcode an rgba(). */
  --risk-low-fill: var(--ok-fill);
  --risk-medium-fill: var(--medium-fill);
  --risk-high-fill: var(--high-fill);
  --risk-critical-fill: var(--critical-fill);
  --risk-unknown-fill: rgba(var(--tx3-rgb), .18);

  /* Severity of an *event*, which is not the same axis as risk of a token.
   * An alert being `info` says nothing about any token's score — which is the
   * reason `--info` exists as its own meaning instead of borrowing `--ok`. */
  --sev-info: var(--info);
  --sev-warn: var(--medium);
  --sev-high: var(--high);
  --sev-critical: var(--critical);

  /* The chrome that only the exploration layer uses. Kept apart from the
   * surfaces above so a filter bar can be tuned without moving the feed.
   *
   * `--field` is darker than `--panel`, which is the whole trick: a control
   * that sits *in* a card should look like a well cut into it, and a control
   * the same colour as its card looks like a label with a border. */
  --field: #0d1322;
  --field-hover: #131b2b;
  --chip: #1a2334;
  --chip-on: #22304a;

  /* ---------------------------------------------------------------------
   * Two families, and the rule for which one a thing gets.
   *
   * `--mono` is for anything a machine produced and a reader may need to
   * compare character by character: addresses, hashes, counts, prices, block
   * numbers, code. Its figures are tabular, so a column of numbers lines up
   * on the decimal whether or not the glyphs are the same width.
   *
   * `--sans` is for anything the product wrote: headings, labels, prose, table
   * headers, button text. A page that sets its own sentences in a monospace is
   * a page that looks like a terminal and reads like a spreadsheet.
   *
   * Both are self-hosted, in `public/fonts/`, and both are variable files — one
   * file per family covering 400 through 600. Not a CDN link: this product is
   * zero dependencies, and a font served from somebody else's origin is a
   * dependency on that origin being up, reachable from the reader's country,
   * and still hosting the file at that URL in a year. Two files, 80 KB
   * together, cached once.
   *
   * The stacks keep the system fonts behind them. A page whose font fails to
   * load must still be a page: `ui-monospace` is SF Mono on a Mac and
   * `Consolas` on Windows, and either is a perfectly good technical face.
   * --------------------------------------------------------------------- */
  --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --sans: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;

  --radius: 12px;
  --radius-sm: 7px;

  /* Elevation. Three steps, because two is not enough to separate a card from
   * the page and a popover from a card. The first line of each is an inset
   * highlight along the top edge — on a dark ground that hairline is what
   * makes a surface read as lit from above rather than as a hole cut in the
   * page, and it costs one line instead of an image. */
  --elev1: inset 0 1px 0 rgba(var(--sheen), .035), 0 1px 2px rgba(0, 0, 0, .5);
  --elev2: inset 0 1px 0 rgba(var(--sheen), .045), 0 10px 26px -16px rgba(0, 0, 0, .95);
  --elev3: inset 0 1px 0 rgba(var(--sheen), .06), 0 22px 48px -22px rgba(0, 0, 0, 1);
  --shadow: var(--elev2);

  /* The dimming laid over the page behind a modal or a drawer. It is a token
   * rather than a literal because its weight is one of the few decisions that
   * genuinely flips with the ground: on a dark page a scrim has to be heavy to
   * read as "behind", and on a light one that same alpha turns the page black.
   * Same job, different number. */
  --scrim: rgba(0, 0, 0, .62);

  /* The colour of a subtle on-surface overlay, as channel triplets. On a dark
   * ground a sheen is a lift of white and on a light one it is a lift of ink —
   * the same job, the opposite colour — so every "rgba(255,255,255,…)" sheen in
   * this file and every page is written `rgba(var(--sheen), …)` and flips with
   * the theme instead of vanishing on the ground it was not painted for. */
  --sheen: 255, 255, 255;

  /* The last of the hardcoded colours, named so they can flip.
   *
   * These were literals in rules until the light theme made them matter: a
   * sidebar gradient, a code block, two hover steps, and the muted text a
   * callout uses so its prose does not compete with the badge beside it. Each
   * is a token now, and the dark value is the literal it replaced — so the dark
   * theme renders exactly as it did, and only the light one needed a new
   * number. The muted callout text is deliberately softer than the five
   * meanings; the light theme needs the *darker* end of that same softness. */
  --side-grad: linear-gradient(180deg, #060810, #05070d);
  --code-bg: #0d1014;
  --accent-hover: #45e3b9;
  --brand-hover: #a9c6ff;
  --warn-tx: #e8c98a;
  --warn-b: #f7d9a5;
  --danger-tx: #f0b3b3;
  --danger-b: #ffcccc;

  /* Motion. One duration and one curve for the whole product: a screen where
   * every element animates on its own schedule reads as broken rather than as
   * lively, and the fastest way to look cheap is four different easings. */
  --t-fast: 120ms;
  --t: 200ms;
  --t-slow: 420ms;
  --ease: cubic-bezier(.22, .61, .36, 1);
}

/* =====================================================================
 * LIGHT THEME — the same vocabulary, flipped.
 *
 * This is not an inversion. Three things change and the rest is shared:
 *
 *   1. The surfaces. On the dark ground lighter means "more elevated", and the
 *      page is the darkest thing on screen. A light ground runs out of headroom
 *      above, so the ramp turns around: the page is a cool light grey and the
 *      panels are the white — the *lift* is white on grey instead of a lighter
 *      grey on a darker one.
 *
 *   2. The verdicts and the brand. The dark values are light pastels tuned to
 *      read at 11px on black, and the same hexes on white read as washed-out
 *      grey-green. Each is replaced by a *darker* step of the same hue, chosen
 *      to hold its meaning and its contrast on white — so `--ok` is still green
 *      and `--critical` is still rose, and only the lightness moved. The channel
 *      triplets move with them, so every `rgba(var(--*-rgb), …)` tint flips too
 *      and no rule has to.
 *
 *   3. The light itself. A sheen is ink instead of white (`--sheen`), a shadow
 *      has no inset highlight because there is nothing above to catch it
 *      (`--elev*`), and `color-scheme` tells the browser so the native parts —
 *      scrollbars, form controls, the `<input type="search">` clear button —
 *      render for the ground they are on instead of ignoring the theme.
 * ===================================================================== */
[data-theme="light"] {
  color-scheme: light;

  --bg: #f3f5fa;
  --ticker: #ffffff;
  --side: #eceff6;
  --panel: #ffffff;
  --panel2: #eff2f8;
  --panel3: #e6ebf3;
  --line: #e0e6ef;
  --line2: #ccd5e2;

  --tx: #121826;
  --tx2: #3c4657;
  --tx3: #758093;
  --tx3-rgb: 117, 128, 147;

  --brand: #2f6fe0;
  --brand2: #0e9384;
  --brand-rgb: 47, 111, 224;
  --brand2-rgb: 14, 147, 132;

  --ok: #0c9d6b;
  --ok-rgb: 12, 157, 107;
  --info: #2163d9;
  --info-rgb: 33, 99, 217;
  --medium: #a86e07;
  --medium-rgb: 168, 110, 7;
  --high: #d4691a;
  --high-rgb: 212, 105, 26;
  --critical: #d81f4e;
  --critical-rgb: 216, 31, 78;

  --violet: #6d45e0;
  --violet-rgb: 109, 69, 224;
  --mark2-rgb: 22, 163, 120;
  --mark3-rgb: 8, 145, 178;

  --field: #eef1f7;
  --field-hover: #e4e9f2;
  --chip: #e9edf4;
  --chip-on: #dce3ee;

  --on-accent: #ffffff;
  --sheen: 15, 23, 42;

  --side-grad: linear-gradient(180deg, #eceff6, #e4e8f1);
  --code-bg: #f1f4f9;
  --accent-hover: #0fb577;
  --brand-hover: #2459c0;
  --warn-tx: #7c5a06;
  --warn-b: #5e4505;
  --danger-tx: #a01332;
  --danger-b: #7a0e26;

  --elev1: 0 1px 2px rgba(15, 23, 42, .07);
  --elev2: 0 8px 24px -14px rgba(15, 23, 42, .24);
  --elev3: 0 18px 44px -20px rgba(15, 23, 42, .30);
  --scrim: rgba(15, 23, 42, .38);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--tx);
  /* `--sans` and not `system-ui`: the whole page is built on this one line, and
   * leaving it on the system face was how the two pages ended up looking like
   * two products on two machines. The stack behind it still catches the case
   * where the file does not load. */
  font: 13.5px/1.55 var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--info); text-decoration: none; }
a:hover { text-decoration: underline; }

code, kbd, samp {
  font-family: var(--mono);
  background: var(--panel2);
  padding: 1px 5px;
  border-radius: 4px;
  font-size: 11px;
  color: var(--tx);
}
kbd { border: 1px solid var(--line2); }

/* ---------------------------------------------------------------- shell --
 *
 * The shell is Nansen-shaped on purpose. A fixed sidebar owns the navigation —
 * grouped, iconed, collapsible, exactly the distribution of app.nansen.ai —
 * and a slim topbar owns the one control every page shares, the global search,
 * plus the live telemetry. Before this the nav was a row of words in a top
 * bar, which is the distribution of a website; the sidebar is the distribution
 * of a terminal, and this product is a terminal.
 *
 * The sidebar is fixed rather than sticky so the content column scrolls under
 * it and the nav never moves. The width lives in `--side-w` so the collapsed
 * rail is a one-variable change instead of a rule for every child. */
:root { --side-w: 236px; }

.side {
  position: fixed;
  inset: 0 auto 0 0;
  width: var(--side-w);
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: var(--side-grad);
  border-right: 1px solid var(--line);
  transition: width var(--t-slow) var(--ease), transform var(--t-slow) var(--ease);
}

.maincol { margin-left: var(--side-w); min-width: 0; }

.side-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 15px 16px 13px;
  font-weight: 600;
  font-size: 14px;
  letter-spacing: -.005em;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}
.side-brand a { color: inherit; display: flex; align-items: center; gap: 10px; min-width: 0; }
.side-brand a:hover { text-decoration: none; }
/* The footer's wordmark reuses the brand type without the sidebar's tile —
 * a plain flex row so the dot aligns with the text baseline. */
.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  font-size: 14px;
  letter-spacing: -.005em;
  white-space: nowrap;
}
/* ---------------------------------------------------------------- the mark --
 *
 * The mark is the real logo now, and the two things that changed are the point.
 *
 * It used to be a painted tile: a 22px rounded square with a teal gradient and
 * a white highlight in the corner, invented as a placeholder before there was a
 * logo. It was a plausible shape and it carried no information — it would have
 * looked the same for any product with a green accent. The mark is the actual
 * drawing: the concave hexagon, the network, the eye with a heartbeat through
 * it, which is what the product does.
 *
 * The second change is that the mark is an `<img>` and not a `::before`. A
 * pseudo-element cannot be given an `alt`, cannot be swapped for the compact cut
 * at small sizes, and cannot be dropped when the layout has no room — so the
 * placeholder was a mark that existed only where the CSS said. The element is
 * there in the markup and the CSS only sizes it.
 *
 * Two files, chosen by size, and the choice is not cosmetic:
 *
 *   /brand/mark-compact.svg   32px and under — the sidebar, the favicon
 *   /brand/mark.svg           above that — the landing page, the social card
 *
 * At 22px the full mark's inner network rasterises to a grey mesh and the whole
 * thing is a smudge; at 120px the compact cut is a hexagon with an eye and
 * nothing else. One file cannot do both, which is why there are two.
 */
.side-mark {
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
  display: block;
}
/* The mark's own radar arcs, animated — the same three `d` paths `mark.svg`
 * carries, cropped to the band they occupy so the stroke stays readable at
 * sidebar size (the full 200-box spends 85% of its frame on nothing and a
 * 2-unit stroke measures 0.22px at 22px; the tight cut is the same stroke at
 * 0.9px). It sits beside the mark as the brand radiating, not as a status
 * light — liveness is the dot at the far end and the pulse in the topbar,
 * both wired to the stream; this one is the mark's motion and animates
 * always, with `prefers-reduced-motion` honoured inside the SVG itself.
 * The `alt=""` is deliberate: it is decoration beside a wordmark that
 * already says the name. */
.side-sweep {
  width: 34px;
  height: 11px;
  flex: 0 0 auto;
  display: block;
}
/* The wordmark, in the app's own type. The logo's own wordmark is a wide, heavy
 * grotesque with generous tracking; Inter at 600 with .07em of it is the same
 * voice in a font the page already has, which is what keeps the mark and the
 * interface from looking like two different products stapled together. */
.side-word { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.side-name {
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--tx);
  line-height: 1.1;
}
/* The tagline, at the tracking the logo's own tagline uses. It is sized against
 * the narrowest place it appears and not against the widest: 224px of sidebar,
 * less its two 1px borders, less 20px of padding on each side, less the 22px
 * mark, the 10px gap beside it, the 10px gap before the live dot and the dot's
 * own 5px, leaves 135px. The logo's full line, "LIVE NETWORK INTELLIGENCE", is
 * 25 characters and 160px at any size worth reading, so the sidebar carries the
 * half of it that says what the product is and the pulsing dot beside it carries
 * the half that says it is live. Two elements, one sentence, and neither is
 * truncated: a wordmark that ends in an ellipsis reads as a layout bug, not as
 * a decision.
 *
 * The numbers are measured, not estimated — `scripts/brandproof.mjs` builds a
 * page carrying this row at this width and the screenshot run reports the
 * string's natural width against its box. As shipped: "Network intelligence" is
 * 127px of the 135, which is 8px of slack, and "Inteligencia de red" is 111px.
 * It is tight on purpose, and it is the reason the string is not one word
 * longer: the fallback face renders for a frame or two before Inter swaps in,
 * and 8px is what keeps the ellipsis from appearing during the swap. */
.side-tag {
  font-size: 8.5px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--tx3);
  line-height: 1.1;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The live dot goes to the far edge, so the brand row reads left to right as
 * "what this is" and then "whether it is up". */
.side-brand .dot { margin-left: auto; }

.side-nav {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 6px 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* Group labels, the way Nansen separates Market from Portfolio from Developer:
 * a micro-label in the dimmest ink, because the grouping should organise the
 * list without competing with the items in it. */
.side-group {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--tx3);
  padding: 14px 8px 5px;
  white-space: nowrap;
}
.side-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 10px;
  border-radius: 8px;
  color: var(--tx2);
  font-size: 12.5px;
  font-weight: 500;
  border: 1px solid transparent;
  white-space: nowrap;
  transition: color var(--t) var(--ease), background var(--t) var(--ease),
              border-color var(--t) var(--ease);
}
/* The icons are mask images keyed by href, exactly as the old top-bar nav had
 * them: one definition, `currentColor`, so the icon follows hover and active
 * for free and no page carries a copy of the markup. */
.side-item::before {
  content: "";
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  background: currentColor;
  opacity: .8;
  -webkit-mask: var(--ico) center / contain no-repeat;
  mask: var(--ico) center / contain no-repeat;
}
.side-item[href="/overview"]    { --ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7' height='7' rx='1.6'/%3E%3Crect x='14' y='3' width='7' height='7' rx='1.6'/%3E%3Crect x='3' y='14' width='7' height='7' rx='1.6'/%3E%3Crect x='14' y='14' width='7' height='7' rx='1.6'/%3E%3C/svg%3E"); }
.side-item[href="/smart-money"] { --ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 17.5l5.2-5.2 3.8 3.2 8-8.5'/%3E%3Cpath d='M14.5 7H20v5.5'/%3E%3C/svg%3E"); }
.side-item[href="/"]            { --ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='2.2'/%3E%3Cpath d='M7 9l3.2 3.2L7 15.4M12.6 15.6H17'/%3E%3C/svg%3E"); }
.side-item[href="/risk"]        { --ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.2l7.5 3v5.4c0 4.6-3.1 8-7.5 9.2-4.4-1.2-7.5-4.6-7.5-9.2V6.2z'/%3E%3Cpath d='M12 9v4M12 16.2v.1'/%3E%3C/svg%3E"); }
.side-item[href="/console"]     { --ico: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6h16M4 12h16M4 18h10'/%3E%3Ccircle cx='19' cy='18' r='1.6'/%3E%3C/svg%3E"); }

.side-item:hover {
  color: var(--tx);
  background: var(--panel2);
  text-decoration: none;
}
.side-item:hover::before { opacity: 1; }

/* The active item is a lit pill with a lit edge on the sidebar's own border —
 * a vertical list answers "where am I" from an edge marker faster than from a
 * glow under the row, which is what the old horizontal bar needed. */
.side-item.on {
  color: var(--tx);
  /* The active row carries the mark's own gradient, at a fifth of its strength.
   * It used to be a violet — `rgba(124, 92, 255)` — which was the palette two
   * redesigns ago and survived here because the workspace layer below overrides
   * this rule for five of the six pages, so only the console ever rendered it.
   * A colour nobody sees is a colour nobody notices is wrong.
   *
   * The workspace layer used to override it away entirely, which left this rule
   * dead on all six pages and the sidebar painted ice blue while the logo two
   * pixels to its left was green and teal. Two accents in one rail is not an
   * identity, it is a near miss, so the two rules now agree on the mark's
   * gradient and the logo is the only thing the chrome is quoting. */
  background: linear-gradient(180deg, var(--mark-fill), var(--mark-fill-2));
  border-color: var(--brand-line);
  box-shadow: inset 0 1px 0 rgba(var(--sheen), .09), 0 3px 14px -6px var(--brand-glow);
}
.side-item.on::before { opacity: 1; color: var(--mark-3); }
.side-item.on::after {
  content: "";
  position: absolute;
  left: -10px; top: 18%; bottom: 18%;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg, transparent, var(--mark-2), transparent);
}

.side-item:focus-visible,
.side-brand a:focus-visible,
.side-collapse:focus-visible,
.burger:focus-visible,
.gsearch input:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 8px;
}

.side-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border-top: 1px solid var(--line);
  flex: 0 0 auto;
}
.side-foot .lang { flex: 1; }
/* The collapse button, the ⌘B affordance Nansen's own sidebar carries. Icon
 * only — its meaning is positional, and a word for it costs width the rail
 * does not have when collapsed. */
.side-collapse {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--tx3);
  cursor: pointer;
  flex: 0 0 auto;
  padding: 0;
  transition: color var(--t) var(--ease), border-color var(--t) var(--ease),
              background var(--t) var(--ease);
}
.side-collapse::before {
  content: "";
  width: 14px;
  height: 14px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='2.2'/%3E%3Cpath d='M9.6 4v16'/%3E%3Cpath d='M6.1 8.4h.9M6.1 11.2h.9'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='2.2'/%3E%3Cpath d='M9.6 4v16'/%3E%3Cpath d='M6.1 8.4h.9M6.1 11.2h.9'/%3E%3C/svg%3E") center / contain no-repeat;
}
.side-collapse:hover { color: var(--tx); background: var(--panel2); border-color: var(--line2); }

/* The theme toggle, beside the collapse button. Same shape, and the glyph names
 * the theme it would turn *on* — a sun in the dark, a moon in the light — so it
 * is an action and not a state you have to decode. The mask is coloured with
 * `currentColor`, so it follows the button's text colour in both palettes
 * without a per-theme value of its own. */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--tx3);
  cursor: pointer;
  flex: 0 0 auto;
  padding: 0;
  transition: color var(--t) var(--ease), border-color var(--t) var(--ease),
              background var(--t) var(--ease);
}
.theme-toggle:hover { color: var(--tx); background: var(--panel2); border-color: var(--line2); }
.theme-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.theme-toggle::before {
  content: "";
  width: 15px;
  height: 15px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4L6 18M18 6l1.4-1.4'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2.5v2M12 19.5v2M4.6 4.6l1.4 1.4M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4L6 18M18 6l1.4-1.4'/%3E%3C/svg%3E") center / contain no-repeat;
}
[data-theme="light"] .theme-toggle::before {
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 13.2A8.6 8.6 0 1 1 10.8 3.4a7 7 0 0 0 9.8 9.8z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.6 13.2A8.6 8.6 0 1 1 10.8 3.4a7 7 0 0 0 9.8 9.8z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* The collapsed rail. Icons stay, words go, and everything that only makes
 * sense with words — the group labels, the language switch — leaves with them.
 * Desktop only: on a phone the sidebar is a drawer and collapsing it into a
 * narrower drawer answers a question nobody asked. */
@media (min-width: 961px) {
  body.side-collapsed { --side-w: 64px; }
  body.side-collapsed .side-brand { justify-content: center; padding-left: 0; padding-right: 0; }
  /* The wordmark goes and the mark stays. The rule used to hide the `<a>`,
   * which was right when the `<a>` held nothing but the words — the mark was a
   * `::before` on the parent and survived on its own. Now the `<a>` holds the
   * mark too, so hiding it would leave a collapsed sidebar with no logo at all
   * in the one place a logo is most useful. */
  body.side-collapsed .side-brand .side-word,
  body.side-collapsed .side-group,
  body.side-collapsed .side-item span,
  body.side-collapsed .side-foot .lang { display: none; }
  body.side-collapsed .side-item { padding-left: 12px; }
}

.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  flex: 0 0 auto;
  position: relative;
  box-shadow: 0 0 0 3px rgba(var(--ok-rgb), .14), 0 0 12px rgba(var(--ok-rgb), .5);
}
/* The halo. A scanner that is alive should look alive; a static dot reads as a
 * decoration and stops being watched, which defeats the point of it. */
.dot::after {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 50%;
  border: 1px solid currentColor;
  color: var(--accent);
  animation: pulse-ring 2.4s var(--ease) infinite;
}
.dot.off { background: var(--bad); box-shadow: 0 0 0 3px rgba(var(--critical-rgb), .15), 0 0 12px rgba(var(--critical-rgb), .45); }
.dot.off::after { color: var(--bad); animation: none; opacity: .5; }
.dot.idle { background: var(--warn); box-shadow: 0 0 0 3px rgba(var(--medium-rgb), .15), 0 0 12px rgba(var(--medium-rgb), .45); }
.dot.idle::after { color: var(--warn); }

@keyframes pulse-ring {
  0%   { transform: scale(.72); opacity: .85; }
  70%  { transform: scale(1.5);  opacity: 0; }
  100% { transform: scale(1.5);  opacity: 0; }
}

.hstats {
  display: flex;
  gap: 20px;
  flex-wrap: wrap;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--tx2);
  font-variant-numeric: tabular-nums;
}
.hstats b { color: var(--tx); font-weight: 500; }

.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 18px;
  position: sticky;
  top: 0;
  z-index: 40;

  /* The layout half of the bar: sticky, and above what it floats over. The
   * paint half — background, border, the scrolled state — is declared further
   * down with the rest of the shell's chrome, and it takes its colour from a
   * token so both themes reach it. A background here as well would be a literal
   * that no theme could flip, and the later rule would win anyway. */
}
/* The hairline under the topbar, lit from the left — kept from the old shell
 * because it is the one flourish that survives every restyle: a gradient rule
 * reads as a light source and costs one pseudo-element. */
.topbar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: linear-gradient(90deg,
    transparent, var(--brand-line) 12%, rgba(var(--mark3-rgb), .28) 38%, transparent 72%);
  pointer-events: none;
}

.burger {
  display: none;
  width: 32px;
  height: 32px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: transparent;
  color: var(--tx2);
  cursor: pointer;
  flex: 0 0 auto;
  padding: 0;
  align-items: center;
  justify-content: center;
}
.burger::before {
  content: "";
  width: 15px;
  height: 15px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7h16M4 12h16M4 17h16'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7h16M4 12h16M4 17h16'/%3E%3C/svg%3E") center / contain no-repeat;
}
.burger:hover { color: var(--tx); background: var(--panel2); border-color: var(--line2); }

/* The global search. One input on every page, the ⌘K affordance the whole
 * product hangs its navigation on: an address jumps straight to the token
 * page, anything else lands in the explorer pre-filtered. It is a label
 * wrapping the input, so the whole pill is clickable without script. */
.gsearch {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 0 1 400px;
  min-width: 150px;
  height: 34px;
  padding: 0 10px;
  background: var(--field);
  border: 1px solid var(--line);
  border-radius: 10px;
  cursor: text;
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.gsearch:focus-within {
  border-color: var(--brand-line);
  box-shadow: 0 0 0 3px rgba(var(--violet-rgb), .12), 0 4px 18px -8px var(--brand-glow);
}
.gs-ico {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  background: var(--tx3);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='M19.8 19.8L15.6 15.6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='M19.8 19.8L15.6 15.6'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: background var(--t) var(--ease);
}
.gsearch:focus-within .gs-ico { background: var(--brand2); }
.gsearch input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  outline: none;
  color: var(--tx);
  font-family: inherit;
  font-size: 12.5px;
  padding: 0;
}
.gsearch input::placeholder { color: var(--tx3); }
.gsearch input:focus-visible { outline: none; }
.gsearch kbd { flex: 0 0 auto; }

/* -------------------------------------------------------- the search panel --
 * The dropdown under the global search, and the reason the pill is a
 * positioning context: the panel is about the pill, so it has to follow it when
 * the window resizes rather than being pinned to a coordinate computed once.
 *
 * It is absolutely positioned and therefore out of the flex row, which matters —
 * the label is `display: flex` and a panel that took part in that layout would
 * push the input narrower every time a query returned.
 *
 * The panel lives inside the topbar's stacking context (z-index 40), so it is
 * above the page and below the phone drawer's scrim (55). That is the correct
 * order: an open drawer covers the search, it does not float under it.
 *
 * `width: max-content` with `min-width: 100%` is deliberate and not the same as
 * `width: 100%`. A token's symbol and name need more room than a 400px pill on a
 * wide screen, and a row truncated to the pill's width would hide the part that
 * tells two similar tokens apart. The floor is the pill, the ceiling is the
 * viewport, and the row ellipsises in between.
 */
.gsearch { position: relative; }

.gs-panel {
  position: absolute;
  top: calc(100% + 7px);
  left: 0;
  z-index: 3;
  min-width: 100%;
  width: max-content;
  max-width: min(560px, calc(100vw - 24px));
  padding: 5px;
  background: var(--panel);
  border: 1px solid var(--line2);
  border-radius: 12px;
  box-shadow: 0 24px 48px -18px rgba(0, 0, 0, .72), 0 2px 8px rgba(0, 0, 0, .35);
  cursor: default;
  overflow: hidden;
}
/* Stated rather than inherited from the user agent. The day this panel needs
 * `display: flex` for its rows, `[hidden]` stops hiding it and the panel is
 * simply always open — a one-word change away, and this is the guard. */
.gs-panel[hidden] { display: none; }

.gs-hit {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 9px;
  border-radius: 8px;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
/* The highlight is keyed off `aria-selected`, not off a class of its own: the
 * attribute is what tells a screen reader which row Enter will open, and a
 * second visual-only copy of that state is a second thing that can disagree
 * with it. Hover is a separate rule because it is a different fact — the row
 * under the pointer, which is not necessarily the row that is armed. */
.gs-hit[aria-selected="true"] {
  background: var(--brand-fill);
  box-shadow: inset 0 0 0 1px var(--brand-line);
}
.gs-hit:hover { background: var(--field-hover); }
.gs-hit[aria-selected="true"]:hover { background: var(--brand-fill); }

.gs-av {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 7px;
  overflow: hidden;
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: .02em;
  color: rgba(var(--sheen), .92);
}
/* The image is painted over the initials rather than chosen instead of them, so
 * a logo that 404s — or that points at a gateway which rate-limits a reader's
 * browser, which is most of them — degrades to the mark the row would have had
 * with no second render and no broken-image glyph. */
.gs-av img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The three non-token marks. They share one box so the rows line up whatever
 * kind of thing they are, and one mask rule so the three glyphs cannot drift
 * apart in weight or size. */
.gs-av.ic-deployer,
.gs-av.ic-wallet,
.gs-av.ic-terminal { background: var(--panel3); }
.gs-av.ic-deployer::after,
.gs-av.ic-wallet::after,
.gs-av.ic-terminal::after {
  content: "";
  width: 13px;
  height: 13px;
  background: var(--tx2);
  -webkit-mask: var(--gs-glyph) center / contain no-repeat;
  mask: var(--gs-glyph) center / contain no-repeat;
}
.gs-av.ic-deployer {
  --gs-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.7l8 4.4v9.8l-8 4.4-8-4.4V7.1z'/%3E%3Cpath d='M4 7.1l8 4.4 8-4.4M12 11.5V21'/%3E%3C/svg%3E");
}
.gs-av.ic-wallet {
  --gs-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linejoin='round'%3E%3Crect x='2.8' y='5.8' width='18.4' height='12.4' rx='2.6'/%3E%3Cpath d='M2.8 10h18.4'/%3E%3Ccircle cx='16.6' cy='14.4' r='1.15' fill='%23000' stroke='none'/%3E%3C/svg%3E");
}
.gs-av.ic-terminal {
  --gs-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5.5 8.2l4 3.8-4 3.8'/%3E%3Cpath d='M12.6 15.8h6'/%3E%3C/svg%3E");
}

.gs-txt { flex: 1; min-width: 0; display: block; }
.gs-l1 {
  display: block;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--tx);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gs-l2 {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 1px;
  font-size: 10.5px;
  color: var(--tx3);
  white-space: nowrap;
}
/* The kind chip, and the reason it is not the same colour as a risk badge: it
 * answers "what is this", the badge answers "how bad is it", and one palette for
 * both makes a deployer look like a verdict. */
.gs-kind {
  flex: 0 0 auto;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--brand-fill);
  color: var(--brand);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.gs-sub {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Pushed to the trailing edge so the badges form a column and the eye can run
 * down them, which is the only thing a risk level is for in a list this short. */
.gs-l2 .badge { flex: 0 0 auto; margin-left: auto; }

.gs-note {
  padding: 8px 10px;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--tx2);
}
.gs-foot {
  margin-top: 3px;
  padding: 5px 10px 3px;
  border-top: 1px solid var(--line);
  font-size: 10px;
  color: var(--tx3);
}

main { padding: 18px 20px 60px; max-width: 1560px; margin: 0 auto; }
main.narrow { max-width: 1080px; }

/* The telemetry only matters on a screen wide enough to watch it while
 * reading a table; below that the KPI cards on the page carry the same
 * numbers and the topbar would be a second, wrapping copy of them. */
@media (max-width: 1240px) { .topbar .hstats { display: none; } }

@media (max-width: 960px) {
  .burger { display: inline-flex; }
  .side {
    width: min(300px, 84vw);
    transform: translateX(-102%);
    box-shadow: none;
  }
  body.side-open .side { transform: none; box-shadow: 0 0 60px rgba(0, 0, 0, .55); }
  body.side-open::after {
    content: "";
    position: fixed;
    inset: 0;
    background: var(--scrim);
    z-index: 55;
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
  }
  .maincol { margin-left: 0; }
  .topbar { gap: 10px; padding: 9px 12px; }
  .gsearch kbd { display: none; }
}

footer {
  padding: 16px 20px;
  color: var(--tx3);
  font-size: 11px;
  border-top: 1px solid var(--line);
  text-align: center;
}
footer a { color: var(--tx2); }

/* ------------------------------------------------------------ structure -- */

.grid { display: grid; gap: 14px; }
.cols { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: start; }
.cols3 { grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start; }
@media (max-width: 1100px) { .cols, .cols3 { grid-template-columns: 1fr; } }

.panel {
  position: relative;
  /* Layered, not flat. The gradient runs light-to-dark down the card, which is
   * how a physical panel catches light from above; a single fill reads as a
   * rectangle of colour no matter how good the colour is. */
  background: linear-gradient(180deg, var(--panel2), var(--panel) 62%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 14px;
  box-shadow: var(--elev2);
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
/* A panel with something in it should feel slightly raised when you go to read
 * it. This is a 1px border change and a shadow, not a transform: a card that
 * moves under the cursor on a data table is a card that makes you misclick. */
.panel:hover {
  border-color: var(--line2);
  box-shadow: var(--elev3);
}

.panel h2 {
  margin: 0;
  padding: 11px 14px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--tx2);
  text-transform: uppercase;
  letter-spacing: .085em;
  border-bottom: 1px solid var(--line);
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 9px;
  background: linear-gradient(180deg, rgba(var(--sheen), .022), transparent);
}

/* ------------------------------------------------------------------ fold --
 *
 * A panel that can be closed, built on `<details>` rather than on a div with a
 * click handler.
 *
 * The reason is not brevity. `<details>`/`<summary>` is the only construction
 * that gives a disclosure the right role, the right expanded state, keyboard
 * activation on Enter and Space, and inclusion in the tab order — all of which
 * a div needs JavaScript to fake, and all of which the fake versions get subtly
 * wrong. The browser also keeps the open state in the DOM, so nothing has to
 * remember it.
 *
 * The header is the `<summary>`, and the `<h2>` inside it is stripped back to
 * plain text: the summary carries the padding, the hairline and the hover
 * target, so the click area is the full width of the panel instead of just the
 * words. A header that is only clickable over its own text is the most common
 * way this control is built wrong.
 */
details.panel.fold > summary {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 14px;
  cursor: pointer;
  /* The default disclosure triangle is replaced by the chevron below. Both
   * spellings are needed: `::-webkit-details-marker` for Blink and WebKit,
   * `::marker` for Gecko. */
  list-style: none;
  background: linear-gradient(180deg, rgba(var(--sheen), .022), transparent);
  border-bottom: 1px solid var(--line);
  user-select: none;
}
details.panel.fold > summary::-webkit-details-marker { display: none; }
details.panel.fold > summary::marker { content: ''; }
details.panel.fold > summary:hover { background: linear-gradient(180deg, rgba(var(--sheen), .04), rgba(var(--sheen), .012)); }
details.panel.fold > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* The heading inside the summary keeps its type and loses its box. */
details.panel.fold > summary > h2 {
  padding: 0;
  border-bottom: 0;
  background: none;
  flex: 1 1 auto;
  min-width: 0;
}

/* Closed, the summary IS the panel, so it must not draw a line under itself
 * with nothing beneath it. */
details.panel.fold:not([open]) > summary { border-bottom: 0; }

/* The one fact worth showing without opening the panel. Monospace and tabular
 * so the number does not shuffle sideways as it counts up on every poll. */
details.panel.fold .foldmeta {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--tx3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* Separated from the subtitle, because they land side by side and read as one
   * sentence without this. `.panel h2 .n` carries `margin-left:auto`, which
   * pushes the subtitle to the right edge of the header — which is where the
   * collapsed line begins. So "what survives a restart" and "Last block
   * 21,612,883" arrived as "…a restart Last block 21,612,883", and the number
   * looked like the end of the subtitle rather than a reading. A hairline says
   * these are two fields without spending a word on it. */
  border-left: 1px solid var(--line);
  margin-left: 10px;
  padding-left: 10px;
}
/* When the panel has something bad to say, the collapsed line is the only place
 * it can say it. A folded row that looks calm over a broken database is the
 * failure mode this rule exists to prevent. */
details.panel.fold .foldmeta.bad { color: var(--bad); }
details.panel.fold.bad > summary { border-left: 2px solid var(--bad); }

/* The chevron, drawn from two borders rather than from a glyph: a glyph needs a
 * font that has it, and at 9px the difference between a chevron and a triangle
 * is the difference between a control and a decoration. */
details.panel.fold .foldchev {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin-right: 2px;
  border-right: 1.6px solid var(--tx3);
  border-bottom: 1.6px solid var(--tx3);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--t) var(--ease), border-color var(--t) var(--ease);
}
details.panel.fold[open] .foldchev { transform: rotate(-135deg) translate(-2px, -2px); }
details.panel.fold > summary:hover .foldchev { border-color: var(--tx2); }
/* The tick. One 2px mark in the brand colour, before every panel title, is what
 * turns a page of boxes into a page of sections — it gives the eye a repeated
 * anchor to run down and makes the titles look deliberate rather than typed. */
.panel h2::before {
  content: "";
  width: 3px;
  height: 13px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--brand2), var(--brand));
  flex: 0 0 auto;
}
.panel h2 .n {
  font-family: var(--mono);
  color: var(--tx3);
  font-size: 11px;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  /* Pushed right, rather than spaced apart. `space-between` would float the
   * title into the middle of the bar as soon as the tick joined the row, and
   * the title is a bare text node in most panels so it cannot carry a margin. */
  margin-left: auto;
}
.panel .pad { padding: 14px; }
.body { max-height: 460px; overflow: auto; }
.body.tall { max-height: 620px; }

.pagehead { margin: 0 0 18px; }
.pagehead h1 {
  margin: 0 0 6px;
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -.022em;
}
.pagehead p { margin: 0; color: var(--tx2); font-size: 13px; max-width: 74ch; }

.sectionlabel {
  font-size: 11px;
  color: var(--tx3);
  text-transform: uppercase;
  letter-spacing: .085em;
  margin: 28px 0 11px;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 10px;
}
/* The rule after the label. It closes the section instead of letting the label
 * float, which is what makes a long page feel like it has chapters. */
.sectionlabel::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, var(--line), transparent 68%);
}
.sectionlabel:first-child { margin-top: 0; }

/* ------------------------------------------------------------------ kpi -- */

/* `auto-fill`, not `auto-fit`.
 *
 * They differ only when a row is not full, and that is exactly the case here:
 * the terminal has twelve counters in a nine-column grid, so the second row
 * holds three. With `auto-fit` the empty tracks collapse and those three tiles
 * stretch to a third of the page each — a row of three giant boxes under a row
 * of nine small ones, which reads as a layout bug rather than as a grid.
 * `auto-fill` keeps the empty tracks, so every tile is the same size and the
 * last row is simply short. */
/* The KPI strip. `display` is declared here and not left to `.grid`, because
 * `.kpis` already owns the columns: a class that decides how many columns a
 * row of cards has, but not that it is a row at all, is a trap that has now
 * sprung twice — the x402 panel and the durable-state panel both wrote
 * `class="kpis"` without `grid`, and both rendered eight full-width cards
 * stacked on top of each other. Nothing failed; the panels just looked wrong
 * in a way that is easy to read as "this panel has nothing in it". Declaring
 * both halves in one place means the second half cannot be forgotten.
 * `.grid.kpis` still works and still contributes the 14px gap. */
.kpis { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); margin-bottom: 16px; }
.kpis.tight { grid-template-columns: repeat(auto-fill, minmax(112px, 1fr)); }
/* Less trailing margin inside a fold, where the strip sits directly above the
 * table the panel opens onto and the full 16px reads as a gap in the middle of
 * one panel rather than as separation between two. */
details.panel.fold > .kpis { margin-bottom: 8px; }

.kpi {
  position: relative;
  background: linear-gradient(180deg, var(--panel2), var(--panel) 70%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 13px 15px 14px;
  box-shadow: var(--elev1);
  overflow: hidden;
  transition: border-color var(--t) var(--ease), box-shadow var(--t) var(--ease);
}
.kpi:hover { border-color: var(--line2); box-shadow: var(--elev2); }
/* A hairline across the top of every tile, in the brand gradient. It reads as a
 * row of instruments rather than a row of boxes, and it is the single cheapest
 * change on this page. */
.kpi::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, var(--brand), var(--brand2) 55%, transparent);
  opacity: .55;
}
.kpi .l {
  font-size: 10.5px;
  color: var(--tx3);
  text-transform: uppercase;
  letter-spacing: .075em;
  margin-bottom: 6px;
  font-weight: 500;
}
.kpi .v {
  font-family: var(--mono);
  font-size: 21px;
  font-weight: 500;
  color: var(--tx);
  line-height: 1.15;
  letter-spacing: -.02em;
  /* Tabular figures everywhere a number can change. Without this, a counter
   * ticking from 11 to 99 makes the whole tile twitch as the glyph widths
   * change, which on a live page is a permanent low-grade flicker. */
  font-variant-numeric: tabular-nums;
}
.kpi .v.sm { font-size: 15px; }
.kpi .v.xs { font-size: 13px; word-break: break-all; }
.kpi.acc .v { color: var(--accent); }
.kpi.bad .v { color: var(--bad); }
.kpi.warn .v { color: var(--warn); }
.kpi.blue .v { color: var(--accent2); }
.kpi .sub { font-size: 11px; color: var(--tx3); margin-top: 4px; font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------- table -- */

table { width: 100%; border-collapse: collapse; font-size: 12px; }

/* The export bar. It sits *above* the table rather than inside it, because a
   control in a header cell would be a column heading for a column that holds no
   data — the same reason the watch-list gutter carries a visually hidden label
   instead of a word. Right-aligned and quiet: this is a control an analyst
   reaches for once they have found the view worth keeping, not chrome that
   competes with the data while they are still looking for it. */
.tblexp {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s2);
  padding: 6px 10px 4px;
}
.tblexp-lab {
  margin-right: auto;
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--tx3);
}
.tblexp-btn { padding: 3px 9px; font-size: 11px; border-radius: 6px; }
th {
  position: sticky;
  top: 0;
  background: var(--panel2);
  text-align: left;
  padding: 7px 12px;
  font-weight: 500;
  color: var(--tx3);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .05em;
  border-bottom: 1px solid var(--line);
  z-index: 1;
  white-space: nowrap;
}
td {
  padding: 7px 12px;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  color: var(--tx2);
  vertical-align: top;
}
tr:last-child td { border-bottom: none; }
td.sym { color: var(--tx); font-weight: 500; }
td.num { text-align: right; }
td.acc { color: var(--accent); }
td.bad { color: var(--bad); }
td.warn { color: var(--warn); }
td.wrap { white-space: normal; font-family: inherit; }

table.plain td { border-bottom-color: transparent; }
table.plain tr:hover td { background: rgba(var(--sheen), .015); }

.empty { padding: 22px 14px; color: var(--tx3); font-size: 12px; text-align: center; }

/* -------------------------------------------------------------- sparklines --
 *
 * The depth and price chart on `/token`. Two panels side by side on a wide
 * screen and stacked on a narrow one, drawn as inline SVG scaled by `viewBox`
 * rather than measured in pixels, so there is no resize listener and no
 * redraw on the first paint after a tab becomes visible.
 *
 * The line's colour is the only place this panel makes a claim, and it is a
 * claim about direction, so it is the *data* colour and not the brand. Green
 * for a rising line and red for a falling one — the Western convention, chosen
 * explicitly by the product owner on 2026-10-03 over the Chinese one this
 * comment first asserted by mistake.
 *
 * The mistake is worth recording, because the code and this comment disagreed
 * for one revision: the comment said "red up, green down, as the Chinese
 * convention" and the rules below painted `--ok` for `up`. Nothing failed. The
 * suite asserted that the rule paints from a token and not from a literal, the
 * screenshot looked correct in both readings, and the only thing that caught it
 * was reading the comment against the code. When a comment states a decision,
 * it is a claim about the code and it can be wrong in exactly this way.
 *
 * `--ok` and `--critical` rather than literal green and red: both flip with the
 * theme, and a hardcoded `#dc2626` drawn on the light background is the one
 * colour pair that survives every review because it looks fine in the theme the
 * reviewer happens to be using.
 */
.sp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 12px;
}

.sp-panel { padding-bottom: 12px; }
.sp-panel > h2 { border-bottom: none; padding-bottom: 4px; }

.sp-wrap {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 14px 0;
}

.sp {
  flex: 1 1 auto;
  min-width: 0;
  height: 64px;
  overflow: visible;
}

.sp path {
  fill: none;
  stroke-width: 1.8;
  stroke-linejoin: round;
  stroke-linecap: round;
  /* `non-scaling-stroke` on the element keeps the line one pixel wide however
   * wide the box gets. Without it, `preserveAspectRatio="none"` stretches the
   * stroke horizontally and a chart in a wide column draws in a visibly
   * thicker line than the same chart in a narrow one. */
}

.sp.up   path { stroke: var(--ok); }
.sp.down path { stroke: var(--critical); }
.sp.flat path { stroke: var(--tx3); }

.sp-dot { fill: var(--tx3); stroke: none; }

.sp-now { text-align: right; flex: 0 0 auto; min-width: 62px; }
.sp-val {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 14px;
  color: var(--tx);
  line-height: 1.2;
}
.sp-range {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 10.5px;
  color: var(--tx3);
  margin-top: 2px;
  white-space: nowrap;
}

.sp-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  padding: 8px 14px 0;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 10.5px;
  color: var(--tx3);
}

/* ---------------------------------------------------------- drain forecast
 *
 * Two states, and they are deliberately not the same shape. An estimate is a
 * number the reader came for; a refusal is a sentence explaining why there is
 * not one. Painting them identically would let a page that has nothing to say
 * look like a page that said something.
 *
 * The colour is the only signal that differs between "there is an estimate" and
 * "there is not": `--warn` on the refusal, the neutral text colour on the
 * number, so neither reads as good or bad news. This is a measurement, not a
 * verdict.
 */
.drain-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 10px 14px 0;
  flex-wrap: wrap;
}
.drain-lbl {
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--tx3);
}
.drain-val {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 22px;
  line-height: 1.1;
  color: var(--tx);
}
.drain-head.none .drain-lbl { color: var(--medium); }
.drain-why {
  font-size: 12.5px;
  color: var(--tx2);
  flex: 1 1 240px;
  min-width: 0;
}

/* The caveat line, printed only when it is true of this series. Coloured as a
 * caveat and not as an error: a wide gap in the samples is a fact about how
 * the pool was watched, not a failure. */
.sp-why {
  padding: 6px 14px 0;
  font-size: 11px;
  color: var(--tx3);
  line-height: 1.5;
}


/* ---------------------------------------------------------------- chips -- */

.badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .03em;
  white-space: nowrap;
}
.b-low { background: var(--risk-low-fill); color: var(--risk-low); }
.b-medium { background: var(--risk-medium-fill); color: var(--risk-medium); }
.b-high { background: var(--risk-high-fill); color: var(--risk-high); }
.b-critical { background: var(--risk-critical-fill); color: var(--risk-critical); }
.b-unknown { background: var(--risk-unknown-fill); color: var(--tx3); }
/* The rest of the badge family, on the named meanings rather than on its own
 * hardcoded rgba()s. These five rules used to carry three different greens and
 * two different ambers between them — close enough to look like a mistake and
 * far enough apart to look like a different status. */
.b-info { background: var(--info-fill); color: var(--info); }
.b-off { background: var(--risk-unknown-fill); color: var(--tx3); }
.b-ok { background: var(--ok-fill); color: var(--ok); }
.b-warn { background: var(--medium-fill); color: var(--medium); }
.b-bad { background: var(--critical-fill); color: var(--critical); }

.tag {
  display: inline-block;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 10px;
  background: var(--info-fill);
  color: var(--info);
  margin-left: 5px;
}

/* The HTTP verb. Colour-coded because every route in this API is a GET and the
 * one that is not should stand out. */
.method {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--info-fill);
  color: var(--info);
}

/* A monospaced identifier in a table cell — a route path, an event signature.
 * Shared, because the console lists routes and the terminal lists signatures,
 * and they are the same kind of thing: an exact string that must be read
 * character by character. The `<em>` marks the variable part: a `:param` in a
 * route, a parameter type in a signature. */
.mono-id {
  font-family: var(--mono);
  color: var(--tx);
  font-weight: 500;
  word-break: break-all;
}
.mono-id em {
  color: var(--tx3);
  font-style: normal;
}

.flag { font-size: 10.5px; color: var(--tx3); font-family: var(--mono); }

/* ----------------------------------------------------------------- bars -- */

.bar { height: 4px; background: var(--line); border-radius: 2px; overflow: hidden; margin-top: 4px; }
.bar > i { display: block; height: 100%; background: var(--accent); transition: width .3s ease; }
.bar > i.warn { background: var(--warn); }
.bar > i.bad { background: var(--bad); }
.bar.lg { height: 6px; margin-top: 6px; }

.qbar { margin: 0; }
.qbar .top {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--tx2);
  margin-bottom: 1px;
}
.qbar .top b { color: var(--tx); font-weight: 500; }

/* --------------------------------------------------------------- blocks -- */

.codeblock {
  position: relative;
  background: var(--code-bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  margin: 0 0 10px;
  overflow: hidden;
}
.codeblock .cap {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
  background: var(--panel2);
  font-size: 10.5px;
  color: var(--tx3);
  text-transform: uppercase;
  letter-spacing: .06em;
}
.codeblock pre {
  margin: 0;
  padding: 11px 12px;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.65;
  color: var(--tx);
  white-space: pre;
}
.codeblock pre .c { color: var(--tx3); }         /* comment */
.codeblock pre .s { color: var(--accent); }      /* string  */
.codeblock pre .k { color: var(--accent2); }     /* key     */
.codeblock pre .n { color: var(--violet); }      /* number  */
.copy {
  font: inherit;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .05em;
  background: var(--panel3);
  color: var(--tx2);
  border: 1px solid var(--line2);
  border-radius: 4px;
  padding: 2px 8px;
  cursor: pointer;
}
.copy:hover { color: var(--tx); border-color: var(--tx3); }
.copy.done { color: var(--accent); border-color: rgba(var(--ok-rgb), .4); }

.callout {
  padding: 11px 14px;
  border: 1px solid var(--line2);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  color: var(--tx2);
  background: var(--panel2);
  margin: 0 0 12px;
}
.callout b { color: var(--tx); }
.callout.info { border-left-color: var(--accent2); background: rgba(var(--info-rgb), .06); }
.callout.warn { border-left-color: var(--warn); background: rgba(var(--medium-rgb), .07); color: var(--warn-tx); }
.callout.warn b { color: var(--warn-b); }
.callout.danger { border-left-color: var(--bad); background: rgba(var(--critical-rgb), .07); color: var(--danger-tx); }
.callout.danger b { color: var(--danger-b); }
.callout.ok { border-left-color: var(--accent); background: rgba(var(--ok-rgb), .06); }
.callout.ok b { color: var(--tx); }

.warnbox {
  margin: 0 0 14px;
  padding: 10px 14px;
  border: 1px solid rgba(var(--medium-rgb), .3);
  background: rgba(var(--medium-rgb), .07);
  border-radius: 9px;
  font-size: 11.5px;
  color: var(--warn-tx);
}

/* ------------------------------------------------------------ key/value -- */

.kv { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); gap: 7px 16px; margin: 0; }
.kv dt { color: var(--tx3); font-size: 11.5px; text-transform: uppercase; letter-spacing: .05em; padding-top: 2px; }
.kv dd { margin: 0; font-family: var(--mono); font-size: 12px; color: var(--tx); word-break: break-all; }
@media (max-width: 640px) { .kv { grid-template-columns: 1fr; gap: 2px 0; } .kv dd { margin-bottom: 8px; } }

/* ----------------------------------------------------------------- plans -- */

.plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; }
.plan {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel2);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.plan.hot { border-color: rgba(var(--ok-rgb), .35); background: rgba(var(--ok-rgb), .035); }
.plan h3 { margin: 0; font-size: 14px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.plan .price { font-family: var(--mono); font-size: 12px; color: var(--accent); }
.plan .note { font-size: 11.5px; color: var(--tx2); flex: 1; }
.plan ul { margin: 0; padding: 0; list-style: none; font-family: var(--mono); font-size: 11.5px; color: var(--tx2); }
.plan ul li { display: flex; justify-content: space-between; gap: 10px; padding: 2px 0; border-top: 1px solid var(--line); }
.plan ul li:first-child { border-top: none; }
.plan ul li b { color: var(--tx); font-weight: 500; }

/* ---------------------------------------------------------------- steps -- */

.steps { counter-reset: step; margin: 0; padding: 0; list-style: none; }
.steps > li { position: relative; padding: 0 0 16px 34px; counter-increment: step; }
.steps > li::before {
  content: counter(step);
  position: absolute;
  left: 0; top: 0;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--panel3);
  border: 1px solid var(--line2);
  color: var(--tx2);
  font-family: var(--mono);
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.steps > li::after {
  content: "";
  position: absolute;
  left: 11px; top: 26px; bottom: 4px;
  width: 1px;
  background: var(--line);
}
.steps > li:last-child { padding-bottom: 0; }
.steps > li:last-child::after { display: none; }
.steps h4 { margin: 0 0 4px; font-size: 13px; font-weight: 600; }
.steps p { margin: 0 0 8px; color: var(--tx2); font-size: 12.5px; }

.hr { height: 1px; background: var(--line); margin: 18px 0; border: 0; }

.muted { color: var(--tx3); }
.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }
.right { text-align: right; }

/* Loading and error states. A page that silently shows zeros while its fetch
 * is failing is the same class of lie as a metric that reports zero — so both
 * surfaces have to be able to say "I do not know yet" and "I could not ask". */
.loading { color: var(--tx3); font-size: 12px; padding: 18px 14px; text-align: center; }
.fetchfail {
  margin: 0 0 14px;
  padding: 10px 14px;
  border: 1px solid rgba(var(--critical-rgb), .35);
  background: rgba(var(--critical-rgb), .08);
  border-radius: 9px;
  font-size: 12px;
  color: var(--danger-tx);
}

/* ============================================================================
 * Marketing surface — the product page.
 *
 * The two data surfaces are read at 13px with everything on one screen, because
 * an analyst scanning a feed wants density. The product page is read once, by
 * someone deciding whether to care, so it gets a different rhythm: bigger type,
 * a 68-character measure, and real vertical space between sections. Same tokens,
 * same palette, same components underneath — it has to read as the same product
 * described, not a different product with a nicer landing page.
 * ========================================================================= */

:root {
  /* A spacing scale, so vertical rhythm is a decision rather than a number
   * invented per rule. Everything below uses these, which is why the sections
   * line up without anyone measuring. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px; --s9: 96px;

  --radius-lg: 14px;
  --measure: 68ch;
  --page: 1200px;
}

::selection { background: rgba(var(--ok-rgb), .26); }

/* A visible focus ring on everything focusable. Not decoration: this is a
 * keyboard-navigable product, and the default ring disappears on dark. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---------------------------------------------------------- language ----- */

/* A segmented control, not a link. The choice is a setting, and a link would
 * look like navigation — which is exactly the wrong affordance for something
 * that must not lose the reader's place. */
.lang {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line2);
  border-radius: 7px;
  overflow: hidden;
  flex: 0 0 auto;
}
.lang button {
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  background: transparent;
  color: var(--tx3);
  border: 0;
  padding: 4px 9px;
  cursor: pointer;
}
.lang button + button { border-left: 1px solid var(--line2); }
.lang button:hover { color: var(--tx2); background: var(--panel2); }
.lang button.on { color: var(--tx); background: var(--panel3); }

.spacer { margin-left: auto; }

/* ------------------------------------------------------------- buttons --- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  font: inherit;
  font-size: 13.5px;
  font-weight: 550;
  letter-spacing: -.005em;
  padding: 10px 18px;
  border-radius: 9px;
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  transition: background .14s ease, border-color .14s ease, color .14s ease;
}
.btn:hover { text-decoration: none; }
.btn.primary { background: var(--accent); color: var(--on-accent); }
.btn.primary:hover { background: var(--accent-hover); }
.btn.ghost { background: var(--panel); border-color: var(--line2); color: var(--tx); }
.btn.ghost:hover { border-color: var(--tx3); background: var(--panel2); }
.btn.sm { padding: 6px 12px; font-size: 12px; border-radius: 7px; }
.btn:disabled { opacity: .45; cursor: default; }

/* ---------------------------------------------------------------- hero --- */

.hero {
  position: relative;
  padding: var(--s9) 0 var(--s8);
  overflow: hidden;
}
/* One soft light source behind the headline. Flat dark backgrounds read as
 * "unfinished"; a single gradient reads as lit. It is behind everything and
 * costs one paint. */
.hero::before {
  content: "";
  position: absolute;
  top: -280px; left: -160px;
  width: 900px; height: 620px;
  background: radial-gradient(ellipse at center, rgba(var(--ok-rgb), .10), rgba(var(--ok-rgb), 0) 62%);
  pointer-events: none;
}
.hero::after {
  content: "";
  position: absolute;
  top: -180px; right: -220px;
  width: 760px; height: 560px;
  background: radial-gradient(ellipse at center, rgba(var(--info-rgb), .09), rgba(var(--info-rgb), 0) 62%);
  pointer-events: none;
}
.hero > * { position: relative; }
.hero .grid2 { align-items: center; gap: var(--s8); }

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: 11.5px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--accent);
  margin-bottom: var(--s5);
}
.eyebrow::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 4px rgba(var(--ok-rgb), .13);
}
.eyebrow.neutral { color: var(--tx3); }
.eyebrow.neutral::before { background: var(--tx3); box-shadow: none; }

.hero h1 {
  margin: 0;
  font-size: clamp(30px, 4.6vw, 52px);
  line-height: 1.05;
  letter-spacing: -.028em;
  font-weight: 640;
  max-width: 19ch;
  text-wrap: balance;
}
.lede {
  margin: var(--s5) 0 0;
  font-size: 16px;
  line-height: 1.68;
  color: var(--tx2);
  max-width: var(--measure);
}
.hero .actions { display: flex; gap: var(--s3); margin-top: var(--s6); flex-wrap: wrap; align-items: center; }
.hero .hint { font-size: 12px; color: var(--tx3); margin: var(--s4) 0 0; }

/* The panel beside the headline is not a screenshot — it is the live numbers,
 * fetched from the same endpoints the dashboard uses. A product page that shows
 * real current values is making a claim it can be held to. */
.proof {
  background: linear-gradient(180deg, var(--panel2), var(--panel));
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  box-shadow: var(--shadow);
}
.proof .cap {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--tx3);
  margin-bottom: var(--s4);
}
.proof .rows { display: grid; gap: 1px; background: var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.proof .row { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s4); background: var(--panel); padding: 10px var(--s3); }
.proof .row .k { font-size: 11.5px; color: var(--tx2); }
.proof .row .v { font-family: var(--mono); font-size: 14px; font-weight: 500; color: var(--tx); font-variant-numeric: tabular-nums; }
.proof .row .v.acc { color: var(--accent); }

/* ------------------------------------------------------------ sections --- */

.section { padding: var(--s8) 0; border-top: 1px solid var(--line); }
.section h2 {
  margin: 0;
  font-size: clamp(22px, 2.6vw, 31px);
  line-height: 1.16;
  letter-spacing: -.02em;
  font-weight: 620;
  max-width: 26ch;
}
.section .sub {
  margin: var(--s4) 0 0;
  font-size: 15px;
  line-height: 1.68;
  color: var(--tx2);
  max-width: var(--measure);
}
.section .head { margin-bottom: var(--s7); }

/* The four-stage pipeline. A hairline rule across the top with a short accent
 * tick over each node — a process line, drawn rather than described. */
.flow {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 1px solid var(--line);
}
.flow > .fnode { position: relative; padding: var(--s5) var(--s5) 0 0; }
.flow > .fnode::before {
  content: "";
  position: absolute;
  top: -1px; left: 0;
  width: 40px; height: 2px;
  background: var(--accent);
}
.flow .fnum {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--tx3);
  margin-bottom: var(--s3);
}
.flow h3 { margin: 0 0 var(--s2); font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.flow p { margin: 0; font-size: 13px; line-height: 1.66; color: var(--tx2); }

@media (max-width: 900px) {
  .flow { grid-template-columns: 1fr; border-top: 0; }
  .flow > .fnode { padding: var(--s4) 0 var(--s4) var(--s5); border-left: 1px solid var(--line); }
  .flow > .fnode::before { top: 0; left: -1px; width: 2px; height: 26px; }
}

/* A hairline grid instead of cards-with-gaps: the 1px gaps ARE the dividers, so
 * there is no doubled border anywhere and the whole block reads as one object. */
.features {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
@media (max-width: 980px) { .features { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { .features { grid-template-columns: 1fr; } }

.feat { background: var(--panel); padding: var(--s6) var(--s5); }
.feat .ic {
  width: 30px; height: 30px;
  border-radius: 8px;
  background: var(--panel3);
  border: 1px solid var(--line2);
  display: flex; align-items: center; justify-content: center;
  color: var(--accent);
  margin-bottom: var(--s4);
}
.feat .ic svg { width: 16px; height: 16px; display: block; }
.feat h3 { margin: 0 0 var(--s2); font-size: 14.5px; font-weight: 600; letter-spacing: -.01em; }
.feat p { margin: 0; font-size: 13px; line-height: 1.68; color: var(--tx2); }

/* Use cases: fewer, larger, each with a monospace tag naming the route or the
 * concept it maps to. The tag is the honest part — it says where the claim
 * lives in the product. */
.uses { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s5); }
@media (max-width: 800px) { .uses { grid-template-columns: 1fr; } }
.use {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--s5);
  background: var(--panel);
  transition: border-color .16s ease;
}
.use:hover { border-color: var(--line2); }
.use h3 { margin: 0 0 var(--s3); font-size: 16px; font-weight: 600; letter-spacing: -.012em; }
.use p { margin: 0 0 var(--s4); font-size: 13.5px; line-height: 1.68; color: var(--tx2); }
.use .tags { display: flex; gap: var(--s2); flex-wrap: wrap; }

/* ------------------------------------------------------- architecture --- */

/* Four columns, each a stack of boxes, with the data flowing left to right. */
.arch {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s4);
  align-items: start;
}
@media (max-width: 900px) { .arch { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.arch .col { display: grid; gap: var(--s2); }
.arch .colhead {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--tx3);
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
.arch .node {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel2);
  padding: 9px var(--s3);
  font-size: 12.5px;
  color: var(--tx);
}
.arch .node.dim { color: var(--tx2); background: var(--panel); }
.arch .node.hi { border-color: rgba(var(--ok-rgb), .38); background: rgba(var(--ok-rgb), .05); }

/* --------------------------------------------------------- playground --- */

.pg { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: var(--s5); align-items: start; }
@media (max-width: 1080px) { .pg { grid-template-columns: 1fr; } }

.field { display: grid; gap: 5px; margin-bottom: var(--s3); }
.field > label {
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--tx3);
}
.field input, .field select {
  font: inherit;
  font-size: 12.5px;
  font-family: var(--mono);
  background: var(--panel2);
  color: var(--tx);
  border: 1px solid var(--line2);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  width: 100%;
}
.field input:focus, .field select:focus { border-color: var(--accent2); outline: none; }
.field .help { font-size: 11px; color: var(--tx3); }
.field.row2 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s3); }
.field.row2 > .field { margin-bottom: 0; }

.paramgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--s3); margin-bottom: var(--s3); }

/* Tabs, for the snippet languages. Underline rather than pill: pills at this
 * size read as buttons and compete with the actual button below them. */
.tabs { display: flex; gap: var(--s1); border-bottom: 1px solid var(--line); margin-bottom: var(--s3); }
.tabs button {
  font: inherit;
  font-size: 11.5px;
  font-weight: 500;
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--tx3);
  padding: 6px 11px;
  cursor: pointer;
  margin-bottom: -1px;
}
.tabs button:hover { color: var(--tx2); }
.tabs button.on { color: var(--tx); border-bottom-color: var(--accent); }

.respmeta {
  display: flex;
  gap: var(--s4);
  align-items: baseline;
  flex-wrap: wrap;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--tx2);
  margin-bottom: var(--s3);
}
.respmeta b { font-weight: 600; }
.respmeta .code.ok { color: var(--accent); }
.respmeta .code.bad { color: var(--bad); }
.respmeta .code.warn { color: var(--warn); }

/* --------------------------------------------------------------- misc --- */

.bar-strip { display: flex; height: 3px; border-radius: 2px; overflow: hidden; margin-top: var(--s4); }
.bar-strip i { display: block; height: 100%; }
.bar-legend { display: flex; gap: var(--s4); flex-wrap: wrap; margin-top: var(--s3); font-size: 11.5px; color: var(--tx2); }
.bar-legend span { display: inline-flex; align-items: center; gap: 6px; }
.bar-legend i { width: 8px; height: 8px; border-radius: 2px; display: block; }

.final {
  padding: var(--s9) 0;
  border-top: 1px solid var(--line);
  text-align: center;
}
.final h2 { margin: 0 auto; font-size: clamp(24px, 3vw, 34px); letter-spacing: -.024em; font-weight: 630; max-width: 22ch; }
.final p { margin: var(--s4) auto 0; font-size: 15px; line-height: 1.68; color: var(--tx2); max-width: 58ch; }
.final .actions { display: flex; gap: var(--s3); justify-content: center; margin-top: var(--s6); flex-wrap: wrap; }

/* Footer: quiet, multi-column, and the only place the honest limits of the
 * product are stated in one line. */
.foot { border-top: 1px solid var(--line); padding: var(--s7) 0 var(--s6); }
.foot .cols { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: var(--s6); }
@media (max-width: 800px) { .foot .cols { grid-template-columns: 1fr 1fr; } }
.foot h4 {
  margin: 0 0 var(--s3);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--tx3);
  font-weight: 500;
}
.foot ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 7px; }
.foot li a { font-size: 12.5px; color: var(--tx2); }
.foot li a:hover { color: var(--tx); text-decoration: none; }
.foot .about { font-size: 12.5px; line-height: 1.7; color: var(--tx2); max-width: 42ch; }
.foot .bottom {
  margin-top: var(--s7);
  padding-top: var(--s5);
  border-top: 1px solid var(--line);
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  flex-wrap: wrap;
  font-size: 11.5px;
  color: var(--tx3);
}

/* A wide container for the marketing page, narrower than the terminal's 1560px
 * because prose has a comfortable width and a table does not. */
main.wide { max-width: var(--page); padding: 0 24px 0; }
main.wide > .section:first-of-type { border-top: 0; }

/* =========================================================================
 * Exploration layer.
 *
 * The terminal used to answer one question — what is happening right now —
 * with a feed of the newest things. That is a good answer and it stops being
 * enough the moment there are more tokens than fit on a screen: "what is
 * happening" is a stream, and "show me the dangerous ones" is a query. The
 * stream cannot answer a query, so the exploration layer exists.
 *
 * Everything below is used by that layer and nothing else. It shares the
 * tokens above so the two read as one product, which is the whole point of
 * one stylesheet.
 * ====================================================================== */

/* ------------------------------------------------------------- toolbar -- */

.toolbar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 10px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  border-bottom: 0;
  flex-wrap: wrap;
}

.search {
  position: relative;
  flex: 1 1 260px;
  min-width: 200px;
  display: flex;
  align-items: center;
}
.search svg { position: absolute; left: 10px; width: 13px; height: 13px; color: var(--tx3); pointer-events: none; }
.search input {
  width: 100%;
  padding: 7px 30px 7px 30px;
  background: var(--field);
  border: 1px solid var(--line2);
  border-radius: var(--radius-sm);
  color: var(--tx);
  font-family: var(--mono);
  font-size: 12px;
  outline: none;
}
.search input::placeholder { color: var(--tx3); }
.search input:focus { border-color: var(--accent2); background: var(--panel2); }
/* The clear affordance only exists once there is something to clear, so it
 * does not sit there as a dead control on an empty field. */
.search .clear {
  position: absolute;
  right: 6px;
  display: none;
  padding: 2px 5px;
  background: none;
  border: 0;
  border-radius: 4px;
  color: var(--tx3);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.search .clear:hover { color: var(--tx); background: var(--panel3); }
/* The class this keys off is `has-value` and not `has`, and the difference is
 * not cosmetic: the script applied `has` for months while this rule waited for
 * `has-value`, and since `.search .clear` above is `display: none`, the result
 * was a clear button that existed in the markup, had a click handler, had an
 * accessible name, and could never be seen or pressed. A class name is a
 * contract between two files, and this one was signed under a different name on
 * each side — the same shape of defect as the `blocks` columns and the v2 pool
 * address, and it goes quiet in exactly the same way. */
.search.has-value .clear { display: block; }

/* `busy` is applied for as long as a query is in flight and was styled by
 * nothing at all, so the explorer's only in-flight signal was the count in the
 * corner of the panel. A search that takes a second and shows nothing reads as
 * a keystroke that did not register. The cue is static on purpose — the border
 * flattens and the text steps back — because it reports a state and not a
 * duration, and a spinner here would claim the page is waiting for something
 * when what it is doing is showing you the previous answer. */
.search.busy input { border-color: var(--line); color: var(--tx3); }

.tb-right { display: flex; align-items: center; gap: var(--s2); margin-left: auto; }

.count { font-size: 11.5px; color: var(--tx3); font-family: var(--mono); white-space: nowrap; }
.count b { color: var(--tx2); font-weight: 500; }

/* --------------------------------------------------------------- chips -- */

.chiprow {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-top: 0;
  border-bottom: 0;
  flex-wrap: wrap;
}
.chiprow + .chiprow { border-top: 1px solid var(--line); }

/* The range row is a band of its own, not a fourth row of chips.
 *
 * It sits below the three facet rows and it is a different kind of control —
 * chips are facets you toggle, and this is a set of questions with fixed
 * answers. So it is recessed rather than raised: the darker `--field` ground
 * pushes it back behind the chips instead of competing with them, and the wider
 * gaps give each label-and-select pair room to read as one control rather than
 * as six loose items in a line.
 *
 * The labels after the first get a left margin so a pair never drifts towards
 * the pair before it. The first one does not, because it is flush with the
 * panel's own padding like every other row's label. */
.chiprow.rangrow {
  background: var(--field);
  gap: 8px 10px;
  padding: 10px 14px;
}
.chiprow.rangrow .lbl { margin-right: 4px; }
.chiprow.rangrow .lbl ~ .lbl { margin-left: 10px; }
.chiprow .lbl {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--tx3);
  font-weight: 500;
  margin-right: 2px;
  white-space: nowrap;
}

/* The range selects, and why they do not look like the chips above them.
 *
 * A native `<select>` left alone renders as an OS control: a grey box with a
 * system arrow, which next to a row of chips reads as a form someone dropped
 * into a dashboard. Styling it to match a chip would be the other mistake —
 * a chip is a facet you toggle and a select is a question with a fixed set of
 * answers, and the two looking different is how a reader knows which one they
 * are looking at before they click it.
 *
 * So it is a field, not a pill. `appearance:none` removes the system arrow and
 * a data-URI chevron replaces it, drawn as a mask-free inline SVG in the
 * stylesheet so it inherits nothing from the document and needs no extra
 * request. The colour is baked into the data URI, which is why it is the one
 * place in this file where a hex is repeated: an SVG background cannot read a
 * custom property.
 */
.sel {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  font-size: 11px;
  font-family: var(--mono);
  color: var(--tx2);
  background-color: var(--field);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 4px 26px 4px 9px;
  cursor: pointer;
  max-width: 176px;
  text-overflow: ellipsis;
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.75 6 8.25l3.5-3.5' fill='none' stroke='%238b93ad' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 11px 11px;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.sel:hover { border-color: var(--line2); color: var(--tx); }
.sel:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; border-color: var(--brand-line); }
/* The chosen range is the one piece of state on this row, so it is the one
 * thing allowed to be bright. Without it a reader cannot tell a filter that is
 * doing something from one sitting at "Any" — the two look identical and the
 * table above them does not say which.
 *
 * The class is applied by script rather than derived in CSS, because
 * `:has(option:checked:not([value=""]))` is not supported everywhere and a
 * selector that silently fails leaves the filter invisible again. */
.sel.on {
  color: var(--accent);
  border-color: rgba(var(--ok-rgb), .42);
  background-color: rgba(var(--ok-rgb), .07);
}
/* The dropdown itself is painted by the OS, so the only part that can be
 * themed is the option list — and it has to be, or a dark page opens a white
 * menu. */
.sel option { background: var(--panel2); color: var(--tx); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  background: var(--chip);
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--tx2);
  font-size: 11px;
  font-family: var(--mono);
  cursor: pointer;
  white-space: nowrap;
  transition: none;
}
.chip:hover { background: var(--chip-on); color: var(--tx); border-color: var(--line2); }
.chip .n { color: var(--tx3); font-size: 10px; }
.chip[aria-pressed="true"] { background: var(--chip-on); color: var(--tx); border-color: var(--line2); }
.chip[aria-pressed="true"] .n { color: var(--tx2); }

/* A chip whose count is zero is still shown, because hiding it would make the
 * vocabulary of the tool appear and disappear with the data — but it is
 * visibly inert, so clicking it is not the way to learn that it is empty. */
.chip.zero { opacity: .42; }
.chip.zero:hover { opacity: .7; }

/* Each risk chip carries its level's colour on the dot only. Colouring the
 * whole chip would turn a filter row into a wall of colour and the level would
 * stop being readable as a signal. */
.chip .dot { width: 6px; height: 6px; border-radius: 50%; flex: 0 0 auto; }
.chip.r-low .dot { background: var(--risk-low); }
.chip.r-medium .dot { background: var(--risk-medium); }
.chip.r-high .dot { background: var(--risk-high); }
.chip.r-critical .dot { background: var(--risk-critical); }
.chip.r-unknown .dot { background: var(--tx3); }
.chip[aria-pressed="true"].r-low { border-color: var(--risk-low); }
.chip[aria-pressed="true"].r-medium { border-color: var(--risk-medium); }
.chip[aria-pressed="true"].r-high { border-color: var(--risk-high); }
.chip[aria-pressed="true"].r-critical { border-color: var(--risk-critical); }

/* Flag chips show their weight, because the weight is the reason to care:
 * `mintable` is worth 20 and `noSupply` is worth 8, and a row that shows only
 * names makes those two look equally interesting. */
.chip .w { color: var(--tx3); font-size: 10px; }
.chip[aria-pressed="true"] .w { color: var(--risk-medium); }

.tb-btn {
  padding: 6px 11px;
  background: var(--field);
  border: 1px solid var(--line2);
  border-radius: var(--radius-sm);
  color: var(--tx2);
  font-family: var(--mono);
  font-size: 11.5px;
  cursor: pointer;
  white-space: nowrap;
}
.tb-btn:hover { background: var(--panel2); color: var(--tx); }
.tb-btn.on { background: var(--chip-on); color: var(--tx); border-color: var(--accent2); }

/* ---------------------------------------------------------- token table -- */

table.tokens { font-size: 12px; }
table.tokens thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--panel2);
  white-space: nowrap;
  user-select: none;
}
table.tokens thead th.sortable { cursor: pointer; }
table.tokens thead th.sortable:hover { color: var(--tx); background: var(--panel3); }
table.tokens thead th .arw { color: var(--tx3); margin-left: 3px; font-size: 9px; }
table.tokens thead th.sorted { color: var(--tx); }
table.tokens thead th.sorted .arw { color: var(--accent2); }

table.tokens tbody tr { cursor: pointer; }
table.tokens tbody tr:hover td { background: rgba(var(--sheen), .022); }

/* The identity cell is the one place a row is allowed two lines: a symbol
 * alone does not tell you whether you are looking at the token you think you
 * are, and there are duplicate symbols on this chain by design. */
.tok { display: flex; align-items: center; gap: 8px; min-width: 0; }
.av {
  flex: 0 0 auto;
  /* 39px, up from 26 — a 50% increase, asked for by name after seeing 26.
   *
   * The history matters because the first version of this comment argued the
   * other way, and it was not wrong: at 22 the mark was small enough that a
   * logo which is a dark shape on a transparent background read as an empty
   * circle at a glance, and 26 fixed that. But the row is the *first* thing a
   * reader scans, and the mark is the only part of it that can be recognised
   * without reading — so "how small can it be and still work" turned out to be
   * the wrong question. The right one is "how large can it be before the table
   * stops being a table", and the answer is well past 26.
   *
   * The cost is real and worth stating plainly: the identity cell is two lines
   * of text (a 12px symbol over a 10.5px name, ~28px together), so a 39px
   * circle is taller than both and sets the row height. Rows go from ~43px to
   * ~54px, which is about four fewer rows on a 1080p screen. That is the trade
   * — fewer rows, each one identifiable at a glance — and it is the intended
   * one, not an accident of the number. */
  width: 39px;
  height: 39px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  /* Scaled with the box rather than left behind. The initials are the fallback
   * for every token whose contract publishes no logo — measured on this chain,
   * most of them — so they are the common case and not an edge case. */
  font-size: 15.5px;
  font-weight: 500;
  letter-spacing: -.02em;
  color: var(--on-accent);
  background: var(--tx3);
}
/* Avatar tint is derived from the risk level, so a row's identity and its
 * verdict agree at a glance instead of being two unrelated colour systems. */
.av.r-low { background: var(--risk-low); }
.av.r-medium { background: var(--risk-medium); }
.av.r-high { background: var(--risk-high); }
.av.r-critical { background: var(--risk-critical); color: var(--on-accent); }
.av.r-unknown { background: var(--tx3); }

/* The token's own logo, when the contract publishes one.
 *
 * The initials block is not replaced, it is covered. Both live in the same
 * element and the image is painted over the letters, which means the fallback
 * is already rendered at the moment the image fails — there is no flash of an
 * empty circle, no second render, and no branch in the table that has to know
 * whether the logo loaded. The error listener in index.html only has to take
 * the image away and the mark is back.
 *
 * `overflow:hidden` is what makes a square PNG sit inside the circle instead of
 * spilling over the row; `object-fit:cover` is what stops it being squashed
 * into an oval. A logo with a transparent background gets the panel colour
 * behind it rather than the gradient, because a green letterform over a violet
 * gradient reads as a rendering fault rather than as a logo. */
.av { position: relative; overflow: hidden; }
.av.has-img { color: transparent; }
.av .avi {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--panel2);
  /* The one place a border is right: a white logo on a white-ish logo needs an
   * edge to sit inside, and this is cheaper than a second image. */
  box-shadow: inset 0 0 0 1px rgba(var(--sheen), .06);
}

/* The market cell: capitalisation on top, the price it came from underneath.
 *
 * Two lines in a numeric column is a compromise and it is the right one here.
 * A capitalisation alone cannot be checked by a reader — it is a supply
 * multiplied by a price, and only one of those two is on screen. A price alone
 * does not say how large the token is. They are read together or not at all, so
 * they are one cell.
 *
 * The second line is dimmer and smaller so the column still scans as a column
 * of numbers rather than as a stack of paragraphs, and both lines are tabular
 * so the digits line up down the column. */
.mkt {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.3;
}
.mkt b {
  font-family: var(--mono);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.mkt .sub {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--tx3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.tok .id { min-width: 0; }

/* The symbol is a link to the token's own page. It is not styled as one — no
 * underline, no blue — because a column of forty blue links turns the table
 * into a wall of calls to action, and this table is read, not clicked through.
 * The token's own colour arrives on hover, which is enough to say it is
 * interactive without competing with the score column, which is what the eye is
 * here for. */
.tok .id .sym { color: inherit; text-decoration: none; }
.tok .id .sym:hover b { color: var(--accent); text-decoration: none; }
.tok .id .sym:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; border-radius: 3px; }

.tok .id .s { color: var(--tx); font-weight: 500; font-family: var(--mono); }
.tok .id .n {
  color: var(--tx3);
  font-size: 10.5px;
  max-width: 20ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mono { font-family: var(--mono); }
.dim { color: var(--tx3); }
.copy {
  padding: 1px 4px;
  background: none;
  border: 0;
  border-radius: 3px;
  color: var(--tx3);
  font-size: 11px;
  cursor: pointer;
  vertical-align: middle;
}
.copy:hover { color: var(--tx); background: var(--panel3); }
.copy.done { color: var(--accent); }

/* Score bar: the number beside it is the truth, the bar is the glance. */
.score { display: flex; align-items: center; gap: 7px; justify-content: flex-end; }
.score .track {
  width: 42px;
  height: 4px;
  border-radius: 2px;
  background: var(--panel3);
  overflow: hidden;
  flex: 0 0 auto;
}
/* `display: block` is load-bearing, not tidying. The fill is an `<i>`, which is
 * inline by default, and an inline box ignores `width` and `height` — so
 * without it the bar renders at zero size and the column reads as an empty
 * track with a number floating next to it. The markup, the inline width and the
 * colour class are all correct in that state, which is why this survived: there
 * is nothing to see in the DOM that looks wrong. */
.score .fill { display: block; height: 100%; width: 0; border-radius: 2px; background: var(--tx3); }
.score .fill.r-low { background: var(--risk-low); }
.score .fill.r-medium { background: var(--risk-medium); }
.score .fill.r-high { background: var(--risk-high); }
.score .fill.r-critical { background: var(--risk-critical); }
.score .v { font-family: var(--mono); min-width: 2ch; text-align: right; }

/* Flag pills in a table cell. Capped visible count — a token with seven flags
 * would otherwise set the row height for the whole table. */
.fl {
  display: inline-block;
  padding: 1px 5px;
  margin-right: 3px;
  border-radius: 3px;
  background: var(--panel3);
  color: var(--tx2);
  font-size: 10px;
  font-family: var(--mono);
  white-space: nowrap;
}
.fl.hot { background: var(--risk-high-fill); color: var(--risk-high); }
.fl.cold { background: var(--chip); color: var(--tx3); }
.fl.more { background: none; color: var(--tx3); }
/* The middle weight band. Without it, a flag worth 10-17 points lands on
 * `cold` and reads as an aside, next to a 20-point flag that reads as a
 * warning — the two are one point apart in the model's own arithmetic. */
.fl.warm { background: var(--risk-medium-fill); color: var(--risk-medium); }

/* The flag cell holds a variable number of pills and must not push the row
 * wider than the table. Wrapping is what keeps a token with four flags from
 * setting a horizontal scrollbar on the whole grid. */
.fls { display: flex; flex-wrap: wrap; gap: 3px; }
.fls .fl { margin-right: 0; }

/* The identity cell's two lines. The symbol is the first name a reader looks
 * for and the contract name is the disambiguator — duplicate symbols exist on
 * this chain on purpose, so the second line is not decoration, it is the thing
 * that tells two rows apart. */
.tok .id b {
  display: block;
  color: var(--tx);
  font-weight: 500;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.25;
}
.tok .id span {
  display: block;
  color: var(--tx3);
  font-size: 10.5px;
  line-height: 1.25;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The address is a button because it is copied, not read. A truncated address
 * is not something anyone retypes, and the tooltip carries the full value for
 * the times it has to be read. */
.addr {
  padding: 1px 4px;
  margin-left: -4px;
  background: none;
  border: 0;
  border-radius: 3px;
  color: var(--tx2);
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
}
.addr:hover { color: var(--tx); background: var(--panel3); }
.addr.copied { color: var(--accent); }

/* The row is clickable and the keyboard has to be able to say so. Without a
 * focus ring the table is mouse-only, which for a screen of monospace digits
 * is the difference between usable and not. */
.trow:focus-visible { outline: 2px solid var(--accent2); outline-offset: -2px; }
.trow:focus { outline: none; }

/* ------------------------------------------------------------- watchlist ---
 *
 * The star that marks a token as watched, and the cell it lives in.
 *
 * The cell is fixed and narrow on purpose. The table's other columns are sized
 * by their content and this one is sized by the widest glyph it can hold, so it
 * reads as a gutter rather than as a column — which is what it is: it says
 * whether the row is in a list, and it is not a fact about the token like the
 * columns beside it.
 *
 * Colour is the whole state and it is the only thing that changes. An unfilled
 * star is `--tx3`, the same grey as the secondary text, because an unstarred row
 * is the default and a column of solid glyphs competing with the symbols next to
 * it would read as though all twenty-five were starred. Filled is the accent.
 *
 * The glyph itself is an SVG rather than `★`/`☆`. The characters exist and are
 * shorter to write, but their weight and their baseline differ between the two
 * states in most fonts — so a row would shift by a pixel when it was starred,
 * and the pair does not sit on the same optical line. One path with a `fill`
 * that toggles between `none` and the accent keeps both states identical in
 * size and position, which is what makes the change read as a state change
 * rather than as a redraw.
 */
.wcol { width: 30px; padding-right: 0; }

/* Visually hidden, still announced.
 *
 * Not `display: none` and not `visibility: hidden` — both of those take the
 * text out of the accessibility tree as well as off the screen, which is the
 * opposite of the point: this is the label for a column of controls, and the
 * only reader who needs it is the one who cannot see the column.
 *
 * The `1px` box with `clip-path` is the technique that survives every
 * combination of zoom, RTL and screen reader. The older `clip: rect(0,0,0,0)`
 * form is deprecated and `width: 0` collapses the line box in some engines,
 * which drops the text out of the tree in exactly the browsers this is for. */
.vh {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.star {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 2px;
  margin: 0;
  line-height: 0;
  cursor: pointer;
  color: var(--tx3);
  border-radius: 4px;
  /* The transition is on the colour only, and it is short. A star that fades in
   * over a third of a second on a table of twenty-five rows reads as the page
   * being slow; the point of the motion is to confirm the click landed, and
   * 120ms does that. */
  transition: color 120ms ease;
}
.star svg { display: block; width: 14px; height: 14px; }
.star svg path { fill: none; stroke: currentColor; stroke-width: 1.7; }
.star:hover { color: var(--tx2); }
.star[aria-pressed="true"] { color: var(--accent); }
/* Filled, and filled with `currentColor` so the two rules above cannot fight:
 * the colour is decided in one place and the fill follows it. */
.star[aria-pressed="true"] svg path { fill: currentColor; }
.star:focus-visible { outline: 2px solid var(--accent2); outline-offset: 1px; }

/* The refusal that has nowhere else to go. A full list is the one failure a
 * reader can cause and cannot see: the click lands, nothing appears to happen,
 * and without a sentence the honest reading is that the star is broken. */
.star-note {
  padding: 6px 10px;
  margin: 0 0 8px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
  color: var(--tx2);
  font-size: 12px;
}
.star-note[hidden] { display: none; }

/* The watched panel. Rows are the same two-part shape the rest of the product
 * uses for a labelled fact, so a reader who has met the depth drill-down has
 * already met this. */
.wl-empty { color: var(--tx3); font-size: 12px; padding: 4px 0; }
.wl-list { display: flex; flex-direction: column; gap: 6px; }
.wl-row {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 8px;
  border: 1px solid var(--line);
  background: var(--panel2);
}
.wl-row .sym { font-weight: 600; color: var(--tx); }
.wl-row .nm { color: var(--tx3); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wl-facts { display: flex; gap: 10px; color: var(--tx2); font-size: 11px; white-space: nowrap; }
/* An address the API does not know is kept in the panel rather than dropped.
 * Dropping it would silently edit the reader's list on their behalf, and the
 * usual cause is a token the scanner has not indexed yet rather than a typo. */
.wl-row.missing { opacity: 0.72; }
.wl-row.missing .sym { color: var(--tx3); }

/* --------------------------------------------------------------- alerts ---
 *
 * An alert row opens onto its evidence.
 *
 * The panel was a list of four columns that dropped the substance on the floor
 * before it reached the page — the wallet, the block, the record that qualified
 * it. A signal you cannot open is a notification: it asks to be trusted and
 * gives the reader nothing to check.
 *
 * The subject is a `<button>`, which means the first job here is to stop it
 * looking like a button and start it looking like what it is: the row's handle.
 */
.subj {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  margin: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.subj:hover { color: var(--accent2); }
.subj:focus-visible { outline: 2px solid var(--accent2); outline-offset: 2px; border-radius: 3px; }

/* The chevron rotates rather than swapping glyph. A triangle that becomes a
 * different triangle is a second thing to learn; a triangle that turns is the
 * same thing saying where it is. */
.chev {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-left: 4px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  opacity: .5;
  transition: transform .12s ease;
}
.arow.on .chev { transform: rotate(90deg); opacity: .9; }
.arow.on td { background: rgba(var(--violet-rgb), .055); }

/* The size of the buy and the lead, under the subject. Quiet: it is context for
 * the symbol above it, not a second heading. These are the two numbers that
 * make a signal worth opening, so they are readable without opening it. */
td.sym .sub {
  margin-top: 2px;
  font-size: 10.5px;
  font-weight: 400;
  color: var(--tx3);
}

/* The detail is a second <tr> and not a nested table, so that the column widths
 * stay the ones the header set — a nested table would size its own columns and
 * the detail would no longer line up with the row it belongs to. */
tr.adet > td {
  padding: 0;
  background: var(--panel2);
  border-bottom: 1px solid var(--line);
}
.adet-in { padding: 10px 12px 12px; }

.drow {
  display: grid;
  grid-template-columns: 128px minmax(0, 1fr);
  gap: 3px 10px;
  padding: 3px 0;
  font-size: 11.5px;
  line-height: 1.5;
}
.dl {
  color: var(--tx3);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .05em;
  padding-top: 2px;
}
.dv { color: var(--tx2); min-width: 0; overflow-wrap: anywhere; }
.dv .dim, .dim { color: var(--tx3); }

/* Direction of a large transfer. Money in and money out are the two things this
 * section exists to tell apart, so they get the palette's own two colours
 * rather than a plus and a minus. */
.dir {
  display: inline-block;
  min-width: 34px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .04em;
}
.dir.in { color: var(--accent); }
.dir.out { color: var(--warn); }

.dact { margin-top: 10px; }

/* The detail is a grid, and on a narrow screen a 128px label column leaves the
 * value too little room to be worth reading. Below that width the pair stacks,
 * which costs vertical space the panel already has. */
@media (max-width: 640px) {
  .drow { grid-template-columns: minmax(0, 1fr); gap: 1px; }
  .dl { padding-top: 6px; }
}

.tag {
  display: inline-block;
  margin-left: 5px;
  padding: 0 4px;
  border-radius: 3px;
  background: var(--panel3);
  color: var(--tx3);
  font-size: 9px;
  font-family: var(--mono);
  vertical-align: 1px;
}

/* ------------------------------------------------------- states / footer -- */

.emptystate { padding: 34px 20px; text-align: center; }
.emptystate .h { color: var(--tx2); font-size: 13px; margin-bottom: 5px; }
.emptystate .p { color: var(--tx3); font-size: 11.5px; line-height: 1.6; max-width: 46ch; margin: 0 auto; }
.emptystate button {
  margin-top: 12px;
  padding: 6px 13px;
  background: var(--field);
  border: 1px solid var(--line2);
  border-radius: var(--radius-sm);
  color: var(--tx2);
  font-family: var(--mono);
  font-size: 11.5px;
  cursor: pointer;
}
.emptystate button:hover { color: var(--tx); background: var(--panel2); }

.tfoot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: 10px 14px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--radius) var(--radius);
  font-size: 11.5px;
  color: var(--tx3);
  flex-wrap: wrap;
}
.tfoot .pages { display: flex; align-items: center; gap: 6px; }
.tfoot button {
  padding: 4px 9px;
  background: var(--field);
  border: 1px solid var(--line2);
  border-radius: var(--radius-sm);
  color: var(--tx2);
  font-family: var(--mono);
  font-size: 11px;
  cursor: pointer;
}
.tfoot button:hover:not(:disabled) { background: var(--panel2); color: var(--tx); }
.tfoot button:disabled { opacity: .35; cursor: default; }

/* ------------------------------------------------------------- drawer --- */

/* The token detail opens as a side drawer rather than a modal: the analyst is
 * comparing it against the table behind, and a modal would cover the thing
 * being compared. */
.drawer-scrim {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  z-index: 40;
}
.drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(560px, 100vw);
  z-index: 41;
  background: var(--panel);
  border-left: 1px solid var(--line2);
  overflow-y: auto;
  box-shadow: -20px 0 50px rgba(0, 0, 0, .45);
}
.drawer .dh {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 14px 16px;
  background: var(--panel2);
  border-bottom: 1px solid var(--line);
}
.drawer .dh .tt { min-width: 0; flex: 1; }
.drawer .dh .tt .s { font-family: var(--mono); font-size: 14px; color: var(--tx); }
.drawer .dh .tt .a { font-size: 10.5px; color: var(--tx3); font-family: var(--mono); }
.drawer .dx {
  padding: 4px 9px;
  background: none;
  border: 1px solid var(--line2);
  border-radius: var(--radius-sm);
  color: var(--tx2);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
}
.drawer .dx:hover { color: var(--tx); background: var(--panel3); }
.drawer .db { padding: 16px; }

.verdict {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px;
  background: var(--panel2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: 16px;
}
.verdict .big { font-family: var(--mono); font-size: 30px; line-height: 1; }
.verdict .big.r-low { color: var(--risk-low); }
.verdict .big.r-medium { color: var(--risk-medium); }
.verdict .big.r-high { color: var(--risk-high); }
.verdict .big.r-critical { color: var(--risk-critical); }
.verdict .meta { flex: 1; min-width: 0; }
.verdict .meta .lvl { font-size: 12px; font-weight: 500; }
.verdict .meta .sub { font-size: 11px; color: var(--tx3); margin-top: 2px; }

/* One row per flag, with its weight and what it proves. The weight column is
 * the point: the score is additive and a total the reader cannot decompose is
 * a number they have to take on faith. */
.flagrow {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
}
.flagrow:last-child { border-bottom: 0; }
.flagrow .c { font-family: var(--mono); font-size: 12px; color: var(--tx); }
.flagrow .w {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--risk-medium);
  text-align: right;
  white-space: nowrap;
}
.flagrow .d { grid-column: 1 / -1; font-size: 11.5px; color: var(--tx2); line-height: 1.55; }
.flagrow.zero .c, .flagrow.zero .w { color: var(--tx3); }

.kv { display: grid; grid-template-columns: 1fr auto; gap: 7px 12px; font-size: 12px; }
.kv .k { color: var(--tx3); }
.kv .v { font-family: var(--mono); color: var(--tx); text-align: right; }

.dsec { margin-top: 20px; }
.dsec > h3 {
  margin: 0 0 10px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--tx3);
  font-weight: 500;
}

/* Keyboard shortcut legend, shown only once the user has used one. */
.kbd {
  display: inline-block;
  padding: 0 4px;
  border: 1px solid var(--line2);
  border-radius: 3px;
  font-family: var(--mono);
  font-size: 9.5px;
  color: var(--tx3);
}

@media (max-width: 900px) {
  .toolbar { border-radius: 0; }
  .tb-right { margin-left: 0; width: 100%; }
  .drawer { width: 100vw; border-left: 0; }
  table.tokens .hide-sm { display: none; }
}

/* ======================================================================== */
/* Motion and finish.
 *
 * The rule this section follows: **animate the container, never the contents.**
 *
 * The terminal re-renders its panels from `innerHTML` every two seconds. An
 * entry animation on `.panel` would therefore restart every two seconds, and a
 * card that fades in forever is worse than a card that never does — it reads as
 * a page that cannot settle. `<main>` and `<header>` are written once by the
 * document and never replaced, so they are the only two things here that are
 * safe to animate on load.
 *
 * Everything else in this section is state-driven (hover, focus, a class the
 * script toggles) because a transition only fires when the state changes, which
 * is exactly the frequency a re-render cannot manufacture.
 * ======================================================================== */

@keyframes rise {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
@keyframes drop {
  from { opacity: 0; transform: translateY(-100%); }
  to   { opacity: 1; transform: none; }
}

main { animation: rise var(--t-slow) var(--ease) both; }
header { animation: drop 340ms var(--ease) both; }

/* The live indicator, when a panel is explicitly streaming. A gradient that
 * sweeps across the dot rather than a colour change, because a colour change
 * would collide with the status colours the dot already uses. */
@keyframes shimmer {
  from { background-position: -160% 0; }
  to   { background-position: 260% 0; }
}
.live-sweep {
  background-image: linear-gradient(100deg, transparent 38%, rgba(var(--sheen), .55) 50%, transparent 62%);
  background-size: 220% 100%;
  animation: shimmer 2.8s linear infinite;
}

/* The risk meter gets a slow sheen so it reads as a measurement rather than a
 * filled bar. `background-blend-mode` keeps the level colour underneath. */
.meter i,
.verdict .meter i {
  background-image: linear-gradient(100deg, transparent 42%, rgba(var(--sheen), .3) 50%, transparent 58%);
  background-size: 260% 100%;
  background-blend-mode: overlay;
  animation: shimmer 3.4s linear infinite;
}

/* ------------------------------------------------------------- finish -- */

::selection { background: rgba(var(--violet-rgb), .32); }

/* Focus. One ring, in the brand colour, on everything focusable. The default
 * UA ring is a thin blue that is nearly invisible on this ground, and the
 * usual "remove the outline" fix is how a keyboard user loses the page. */
:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Scrollbars. A data-dense dark app is mostly scrollbar, and the default one
 * is a bright grey slab that is the single most out-of-place element on the
 * page. Firefox uses the two standard properties; WebKit needs the pseudo
 * elements, and neither is a substitute for the other. */
* { scrollbar-width: thin; scrollbar-color: var(--line2) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--line2);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover { background: var(--tx3); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* Motion is decoration here, never information. Nothing in this file is the
 * only way to convey a state, so switching it off loses nothing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  main, header { animation: none; }
}

@media (max-width: 760px) {
  header { gap: 10px; padding: 10px 14px; }
  .nav { order: 3; width: 100%; overflow-x: auto; padding-bottom: 2px; }
  .nav a { padding: 6px 10px; }
  main { padding: 14px 14px 48px; }
}

/* Workspace redesign — one visual language, six functional surfaces.
 * Prefixed components own their layout. Data colours and scoring are unchanged.
 * Tables scroll inside labelled keyboard-focusable regions, never by clipping.
 */
:root { --side-w: 224px; --page: 1440px; }
body { font-size: 14px; background: var(--bg); }
[hidden] { display: none !important; }
.side { height: 100dvh; background: var(--side); border-color: var(--line); }
.side-brand { height: 76px; padding: 20px; gap: 10px; font-size: 16px; letter-spacing: -.04em; border-bottom: none; }
.side-brand .dot { width: 5px; height: 5px; box-shadow: none; }
.side-nav { padding: 4px 12px; gap: 6px; }
.side-group { padding: 22px 12px 9px; font-size: 10px; color: var(--tx3); letter-spacing: .14em; }
.side-item { min-height: 44px; padding: 10px 12px; font-size: 13px; gap: 12px; border-radius: 9px; }
.side-item.on { background: linear-gradient(180deg, var(--mark-fill), var(--mark-fill-2)); border-color: var(--brand-line); box-shadow: none; }
.side-item:hover { background-color: var(--panel2); }
.side-foot { padding: 18px; border-top: 1px solid var(--line); }
/* The band is opaque, not glass.
 *
 * It used to be `rgba(11,17,24,.95)` with a backdrop blur, which is a nice
 * effect and the wrong one here: the whole page scrolls under it, and a
 * translucent bar turns the table rows passing behind it into a moving
 * texture. On a page whose job is comparing numbers in columns, that reads as
 * the numbers being blurred rather than as the header being elevated. The
 * solid `--ticker` step separates the band from the content without borrowing
 * anything from what is underneath it. */
.topbar { min-height: 56px; padding: 8px 26px; background: var(--ticker); border-bottom: 1px solid var(--line); gap: 18px; }
.topbar::after { display: none; }
.topbar.scrolled { background: var(--ticker); box-shadow: 0 8px 22px -18px rgba(0, 0, 0, .9); }
.hstats { font-family: var(--mono); font-size: 11.5px; gap: 20px; font-variant-numeric: tabular-nums; }
.hstats b { font-weight: 500; }
.gsearch { width: min(440px, 50vw); flex: 0 1 440px; max-width: none; min-width: 160px; height: 36px; background: var(--field); border: 1px solid var(--line); border-radius: 8px; }
.gsearch input { font-family: inherit; font-size: 12px; }
.gsearch kbd { font: 10px var(--mono); color: var(--tx3); background: transparent; border-color: var(--line); }
.ws-network { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; color: var(--tx2); font-size: 11px; letter-spacing: .01em; }
.ws-network::before { content: ""; width: 16px; height: 16px; border: 1px solid var(--brand2); border-radius: 50%; box-shadow: inset 0 0 0 4px var(--bg); background: var(--brand2); }
.maincol > main, .maincol > main.wide, .maincol > main.narrow { width: 100%; max-width: 1480px; padding: 32px 30px 64px; }
.maincol > main.narrow { max-width: 1340px; }
.pagehead { margin-bottom: 28px; }
.pagehead h1 { font-size: clamp(26px, 2.5vw, 36px); font-weight: 650; line-height: 1.2; letter-spacing: -.045em; margin-bottom: 10px; }
.pagehead p { font-size: 13px; line-height: 1.7; color: var(--tx3); max-width: 82ch; }
.ws-pagehead { position: relative; display: grid; grid-template-columns: minmax(0,1fr) auto; column-gap: 30px; align-items: center; padding: 0; animation: none; }
.ws-pagehead h1, .ws-pagehead p { grid-column: 1; }
.ws-eyebrow { grid-column: 1; margin: 0 0 12px; color: var(--brand2); font-size: 10px; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; }
.ws-actions { grid-column: 2; grid-row: 1 / span 3; display: flex; flex-wrap: wrap; gap: 10px; }
.ws-actions .btn { font-size: 12px; padding: 10px 16px; }
.grid { gap: 20px; }
.panel { background: var(--panel); border-color: var(--line); border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,.08); margin-bottom: 20px; }
.panel:hover { box-shadow: 0 4px 20px rgba(0,0,0,.08); border-color: var(--line); }
.panel h2 { background: transparent; font-size: 14px; color: var(--tx); padding: 18px 20px; letter-spacing: -.015em; text-transform: none; gap: 10px; flex-wrap: wrap; }
.panel h2::before { display: none; }
.panel h2 .n { font-family: inherit; font-size: 11px; color: var(--tx3); }
.panel .pad { padding: 20px; }
.kpis { gap: 16px; grid-template-columns: repeat(auto-fit, minmax(155px, 1fr)); margin-bottom: 22px; }
.kpi { padding: 20px; background: var(--panel); box-shadow: none; border: 1px solid var(--line); border-radius: 12px; min-width: 0; }
.kpi:hover { box-shadow: none; border-color: var(--line2); }
.kpi::before { display: none; }
.kpi .l { text-transform: none; font: 12px/1.5 var(--sans); color: var(--tx3); letter-spacing: 0; }
/* The number is mono, and that is a deliberate change from the system face.
 * A KPI here is read against the same KPI on the previous look, and the way
 * that comparison happens is by the digits staying put while the value
 * changes. Proportional digits shuffle sideways on every tick; tabular mono
 * figures do not, so the reader sees the value change instead of the number
 * moving. */
.kpi .v { margin-top: 10px; font-family: var(--mono); font-size: 26px; font-weight: 600; letter-spacing: -.03em; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.kpi .v.sm { font-size: 18px; }
.kpi .sub { font-family: inherit; font-size: 11px; margin-top: 7px; }
.ws-metrics { grid-template-columns: repeat(4, minmax(0,1fr)); }
.ws-metrics .kpi { min-height: 108px; position: relative; }
.ws-metrics .kpi:first-child { background: linear-gradient(120deg, rgba(var(--brand-rgb), .16), var(--panel)); border-color: var(--brand-line); }
.ws-metrics .v { font-size: clamp(23px, 2.1vw, 31px); color: var(--tx); }
.ws-metrics .v.sm { font-size: clamp(18px, 1.7vw, 25px); }
.ws-metrics .kpi.bad .v { color: var(--bad); }
.warnbox { background: rgba(var(--medium-rgb),.05); border: 1px solid rgba(var(--medium-rgb),.17); border-radius: 8px; color: var(--tx3); font-size: 11px; padding: 10px 14px; line-height: 1.6; margin-bottom: 24px; }
.warnbox b { color: var(--warn); }
.ws-tabs { display: flex; gap: 24px; border-bottom: 1px solid var(--line); margin-bottom: 22px; overflow-x: auto; }
.ws-tab { color: var(--tx3); font: 500 13px/1.5 var(--sans); background: transparent; border: 0; border-bottom: 2px solid transparent; padding: 10px 3px 14px; cursor: pointer; white-space: nowrap; }
.ws-tab:hover { color: var(--tx); }
.ws-tab[aria-selected="true"] { color: var(--brand2); border-bottom-color: var(--brand2); }
[data-workspace-pane] { min-width: 0; }
.toolbar { padding: 14px 20px; gap: 12px; background: rgba(0,0,0,.10); border-bottom: 1px solid var(--line); }
.search { max-width: 460px; background: var(--field); border-radius: 8px; }
.search input { font-family: var(--mono); font-size: 12px; }
.tb-btn, .sel, .pages button, .copy { border-radius: 6px; }
.sel { min-height: 32px; font-size: 12px; }
.chiprow { padding: 8px 20px; gap: 6px; }
.chiprow .lbl { font-family: inherit; color: var(--tx3); min-width: 65px; font-size: 11px; }
.chip { font-size: 11px; padding: 5px 9px; background: var(--field); border-radius: 999px; }
.chip.on { background: var(--brand-fill); border-color: var(--brand-line); color: var(--brand); }
.rangrow { border-bottom: 1px solid var(--line); }
.rangrow .lbl { min-width: auto; }
table { font-size: 12px; }
th { background: var(--panel2); color: var(--tx3); font-size: 10px; letter-spacing: .05em; padding: 10px 16px; }
td { padding: 12px 16px; border-bottom-color: var(--line); }
table.tokens td { padding: 13px 16px; }
table.tokens .sym { font-family: inherit; font-size: 14px; }
tbody tr:hover td { background-color: rgba(var(--brand-rgb),.045); }
.badge { font-size: 10px; padding: 4px 7px; border-radius: 5px; }
.tfoot { padding: 14px 20px; background: rgba(0,0,0,.10); }
.body.tall { max-height: 560px; }
.btn { border-radius: 8px; font-weight: 550; }
/* The primary button is brand blue, not green.
 *
 * It was green, back when the brand *was* green. Now that green means one
 * thing only — a verdict of `low` — a filled green button sitting above a
 * table whose rows carry green badges is the reader's eye being pulled to the
 * wrong thing, and worse, a green badge starts to look clickable. The brand
 * owns the buttons; the semantics own the data. */
.btn.primary { background: var(--brand); color: var(--on-accent); box-shadow: none; }
.btn.primary:hover { background: var(--brand-hover); }
details.panel.fold > summary { padding: 17px 20px; background: transparent; }
.ws-smart { min-width: 0; }
.ws-smart #fm-root, .ws-smart #fm-side { min-width: 0; }
.ws-smart .tabs { gap: 24px; margin-bottom: 24px; }
.ws-smart .tab { padding: 10px 3px 14px; font-size: 13px; }
.ws-smart .tab[aria-selected="true"] { color: var(--brand2); }
.ws-smart .kpis { grid-template-columns: repeat(auto-fit,minmax(150px,1fr)); }
.ws-smart .cov { padding: 12px 16px; border: 1px solid rgba(var(--medium-rgb),.18); background: rgba(var(--medium-rgb),.025); border-radius: 8px; margin: 0 0 20px; gap: 6px; }
.ws-smart .cov .row { font-size: 11px; }
.ws-smart .cov .row .k { font-family: inherit; font-size: 10px; min-width: 84px; }
.ws-smart .toolbar { flex-wrap: wrap; }
.ws-smart .tgl { font-size: 12px; padding: 6px 0; margin-right: 12px; }
.ws-table-scroll { max-height: 540px; overflow: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
.ws-table-scroll:focus-visible { outline-offset: -2px; }
.ws-smart table.smflow, .ws-smart table.smtok { min-width: 1020px; }
.ws-smart table.smtrd { min-width: 950px; }
.ws-smart table th { background: var(--panel2); color: var(--tx3); padding: 12px 16px; font-family: inherit; font-size: 10px; position: sticky; top: 0; z-index: 1; }
.ws-smart table td { padding: 12px 16px; border-color: var(--line); }
.ws-smart table .tk a { font-family: inherit; font-size: 14px; }
.ws-smart table td.net { font-size: 13px; }
.ws-smart .pager { padding: 14px 20px; }
.ws-smart .pager button { font: inherit; font-size: 12px; color: var(--tx); background: var(--panel2); padding: 7px 13px; border: 1px solid var(--line); border-radius: 6px; cursor: pointer; }
.ws-smart .pager button:disabled { opacity: .4; cursor: default; }
.ws-method > summary { cursor: pointer; padding: 15px 20px; font-weight: 500; color: var(--tx2); }
.ws-method > summary .n { margin-left: 18px; color: var(--tx3); font-size: 11px; font-weight: 400; }
.ws-method[open] > summary { border-bottom: 1px solid var(--line); }
#fm-side { display: grid; grid-template-columns: minmax(0,1.1fr) minmax(0,1fr); gap: 20px; align-items: start; }
#fm-side > .panel { min-width: 0; }
.ws-wallet-scroll { max-height: 320px; }
.ws-smart table.wl { min-width: 610px; }
.ws-smart .sig { max-height: 320px; overflow: auto; }
.ws-smart .sig .it { display: grid; grid-template-columns: auto 1fr auto; gap: 5px 10px; padding: 13px 18px; }
.ws-smart .sig .it .sym { color: var(--brand2); }
.ws-smart .sig .it .w { color: var(--tx3); font-size: 11px; }
/* Both addresses in a signal are links. They keep the colour of the field they
 * live in — a link that repainted its own field would make this row read
 * differently from the ones around it, and the symbol's colour is what tells a
 * reader at a glance which word is the token — and they gain an underline on
 * hover, which is the only affordance they need because they are already the
 * only coloured words on the line.
 *
 * `color: inherit` rather than a rule per field: the two links want the two
 * different colours their fields already carry, and the field is where that
 * decision belongs. */
.ws-smart .sig .it .sym a,
.ws-smart .sig .it .w a { color: inherit; text-decoration: none; }
.ws-smart .sig .it .sym a:hover { text-decoration: underline; }
.ws-smart .sig .it .w a:hover { color: var(--tx2); text-decoration: underline; }
.ws-smart .sig .it .say { grid-column: 1 / -1; grid-row: 2; white-space: normal; }
.ws-smart .sig .it .when { font-size: 10px; }
/* The signal row is a control, and it carries the same three states the
   terminal's feed rows carry — hover, keyboard focus, open. Two surfaces that
   both open a payload under the pointer have to agree about what that looks
   like, or the second one reads as a different affordance. */
.ws-smart .sig .it { cursor: pointer; }
.ws-smart .sig .it:hover { background: rgba(var(--brand-rgb),.045); }
.ws-smart .sig .it:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ws-smart .sig .it.open { background: rgba(var(--brand-rgb),.06); }
/* `1 / -1` is the load-bearing line, for the reason the terminal's row states
   about `flex-wrap`: the detail has to live *inside* the row and not beside it.
   The feed caps itself at forty rows and drops the oldest, so a detail inserted
   as a sibling would outlive the row it describes and leave a panel open about
   a signal that is no longer listed. */
.ws-smart .sig .it .sig-detail { grid-column: 1 / -1; padding: 3px 0 7px; }

/* ---------------------------------------------------------- field list --
 *
 * The payload a row opens, as a two-column list. This is deliberately shared
 * between the terminal's event feed (`/`) and the signals feed on
 * `/smart-money`: a payload opened on one surface and a payload opened on the
 * other are the same question, and a component that renders differently on the
 * two is how one of them quietly stops being the same thing.
 *
 * `minmax(88px,auto)` is the label column — wide enough for `amountFormatted`,
 * the longest key either feed emits, and `auto` rather than a fixed width so a
 * longer one arriving later widens the column instead of being clipped into a
 * lie. `word-break:break-all` on the value is what lets a 42-character address
 * sit in the second column without pushing the grid wider than its panel.
 */
.ev-fields { display: grid; grid-template-columns: minmax(88px,auto) 1fr; gap: 2px 12px; margin: 0;
             font-family: var(--mono); font-size: 11px; }
.ev-fields dt { color: var(--tx3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ev-fields dd { margin: 0; color: var(--tx2); word-break: break-all; }
.ev-null { color: var(--tx3); }

/* The pause badge, shared by both feeds for the same reason as the field list.
 * A feed that has stopped moving looks exactly like a feed with nothing to say,
 * and this project already has a rule about that: a frozen number is not a zero.
 * So a pause carries its own count, and it carries it in one place. */
.feed-held { font-size: 10px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
             color: var(--warn); border: 1px solid rgba(var(--medium-rgb),.35); border-radius: 5px;
             padding: 2px 6px; margin-left: 8px; vertical-align: 1px; }
.feed-held[hidden] { display: none; }

/* Whether the stream is actually open. It is a separate element from the held
 * count because the two answer different questions — "is anything arriving" and
 * "how many are waiting for me" — and a panel that shows the second without the
 * first is how a stream that died an hour ago renders as a quiet chain.
 *
 * `EventSource` reconnects on its own and says nothing while it does, so the
 * open state is not decoration: it is the only thing on the page that
 * distinguishes "connected and quiet" from "not connected". */
.sig-state { font-size: 10px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
             color: var(--tx3); margin-left: 8px; }
.sig-state[data-state="open"] { color: var(--ok); }
.sig-state[data-state="down"] { color: var(--critical); }
.hero { padding: 40px 0 64px; border-bottom: 1px solid var(--line); }
.hero::before, .hero::after { display: none; }
.hero h1 { font-size: clamp(40px,4.5vw,68px); line-height: 1.07; letter-spacing: -.055em; }
.hero .grid2 { gap: 52px; }
.hero .proof { background: var(--panel); border-color: var(--line); border-radius: 16px; box-shadow: none; }
.flow { gap: 12px; border: none; }
.fnode { border: 1px solid var(--line); border-radius: 12px; padding: 24px; background: var(--panel); }
.fnode + .fnode { border-left: 1px solid var(--line); }
.features { gap: 18px; }
.feat { border-radius: 12px; background: var(--panel); }
.section .head h2 { letter-spacing: -.035em; }
footer.foot { text-align: left; }
footer { color: var(--tx3); }
@media (min-width:961px) {
  body.side-collapsed .side-brand { padding: 20px 17px; }
}
/* The telemetry is the last thing to go, not the first.
 *
 * It used to be hidden below 1600px, which on a 1440 or a 1512 laptop — that is
 * to say on most of the machines this page is opened on — meant the band's one
 * row of live numbers was never on screen at all. The reader was told the page
 * was live by a small dot and nothing else. The threshold is 1200px now, which
 * is where the search box and the six stats genuinely stop fitting on one line
 * together; above it, both are on screen. */
@media (max-width:1200px) { .topbar .hstats { display: none; } }
@media (max-width:1100px) {
  .ws-actions { grid-column: 1; grid-row: auto; margin-top: 16px; }
  .ws-pagehead { grid-template-columns: minmax(0,1fr); }
  #fm-side { grid-template-columns: 1fr; }
  .hero .grid2 { gap: 28px; }
}
@media (max-width:960px) {
  .side { width: 260px; }
  .topbar { padding: 12px 18px; gap: 12px; }
  .maincol > main, .maincol > main.wide, .maincol > main.narrow { padding: 24px 18px 48px; }
  .ws-metrics { grid-template-columns: repeat(2,minmax(0,1fr)); }
  .ws-smart .kpis { grid-template-columns: repeat(2,minmax(0,1fr)); }
  body.side-open { overflow: hidden; }
}
@media (max-width:600px) {
  .topbar { min-height: 60px; }
  .gsearch { flex: 1; width: auto; min-width: 0; }
  .gsearch kbd, .topbar > .spacer, .ws-network { display: none; }
  .maincol > main, .maincol > main.wide, .maincol > main.narrow { padding: 22px 14px 40px; }
  .ws-pagehead { margin-bottom: 22px; }
  .ws-actions .btn { padding: 8px 12px; }
  .ws-metrics { gap: 10px; }
  .kpi { padding: 16px; }
  .ws-metrics .kpi { min-height: 102px; }
  .ws-metrics .v { font-size: 25px; }
  .ws-metrics .v.sm { font-size: 20px; }
  .ws-tabs { gap: 22px; }
  .panel h2 { padding: 15px 16px; }
  .panel h2 .n { margin-left: 0; flex-basis: 100%; }
  .toolbar { padding: 12px 14px; flex-wrap: wrap; }
  .chiprow { padding: 8px 14px; }
  .ws-smart .cov .row { flex-direction: column; gap: 0; }
  .ws-smart .cov .row .k { min-width: 0; }
  .ws-method > summary .n { display: block; margin: 6px 0 0; }
  .hero { padding: 24px 0 40px; }
}

/* ============================================================================
 * Primitives — the parts the workspace is assembled from.
 *
 * Five things, each of which existed as a one-off somewhere and is now a rule:
 * the loading skeleton, the flash on a value that just changed, the bento
 * grid, the filter drawer, and the copy affordance next to an address.
 *
 * They are here and not in a page's `<style>` because more than one surface
 * needs each of them, and because a page that invents its own skeleton is a
 * page whose skeleton stops matching the product the moment this one changes.
 * ========================================================================== */

/* ------------------------------------------------------------- skeleton --
 *
 * What these replace, and why the replacement is not cosmetic.
 *
 * Every panel used to open on a sentence: "Loading…", "Waiting for the first
 * pool…", "Connecting…". Those sentences are honest and they are the wrong
 * shape. They are one line of centred prose where a table is about to be, so
 * the page jumps the moment data arrives — the header, the row height and the
 * column widths all land at once — and a reader who was already looking at the
 * panel has to find it again. Worse, a sentence in the middle of an empty
 * panel is indistinguishable from a panel that will never fill: a slow API, a
 * 404 and a thrown exception all render the same three words.
 *
 * A skeleton fixes the first problem by occupying the space the data will
 * occupy. It does not fix the second — a skeleton that never resolves is still
 * a page that is quietly broken — which is why the row shape is driven by the
 * real column spec and why the panels also carry `aria-busy`.
 *
 * The animation is a sweep and not a fade. A pulse changes the whole row's
 * opacity, which on a dark ground reads as the row being disabled; a gradient
 * travelling left to right reads as work happening, which is what it is.
 * `prefers-reduced-motion` turns it into a static block — the shape still
 * tells the reader what is coming, which is the part that matters.
 */
.skl {
  display: inline-block;
  vertical-align: middle;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--panel2) 25%, var(--panel3) 50%, var(--panel2) 75%);
  background-size: 220% 100%;
  animation: skl-pulse 1.15s ease-in-out infinite;
}
@keyframes skl-pulse {
  from { background-position: 130% 0; }
  to { background-position: -130% 0; }
}

/* The skeleton row keeps the real row's vertical rhythm, so the table does not
 * resize when the data lands. Without this the whole page below the table
 * shifts up by a row height at the exact moment the reader starts reading. */
.skl-row td { padding-top: 13px; padding-bottom: 13px; }
/* Not data, so it does not respond like data. A row that highlights under the
 * cursor is a row the reader expects to be able to click. */
.skl-row:hover td { background: none; }

/* Two shapes worth naming: a KPI's label and a KPI's value. Inline styles
 * would do it, but these two recur on every card of every metrics row. */
.skl.v { height: 24px; width: 62%; }
.skl.l { height: 10px; width: 38%; }

/* ------------------------------------------------------- value changed --
 *
 * The microinteraction: a row that just arrived flashes green for 300 ms.
 *
 * 300 ms is the whole design. Under about 150 ms the eye reads it as a render
 * artifact rather than as a signal; over about 500 ms the page starts to feel
 * like it is blinking at the reader, and on a feed that prepends several rows a
 * second the flashes overlap into a permanently green column. At 300 ms with
 * the feed's own cadence the reader catches the change without being told to
 * look for it.
 *
 * It fades out rather than cutting, because a hard cut on a dark ground reads
 * as a repaint and a fade reads as a highlight.
 */
@keyframes flash-ok {
  0% { background: var(--ok-fill); box-shadow: inset 3px 0 0 var(--ok); }
  100% { background: transparent; box-shadow: none; }
}
.flash-ok { animation: flash-ok .3s ease-out; }

/* The same idea for a number in the telemetry band: the block height changes
 * roughly once a second, and without a mark the reader has to remember what it
 * said a second ago to know whether it moved. */
@keyframes tick-ok {
  0% { background: var(--ok-fill); color: var(--ok); }
  100% { background: transparent; }
}
.tick { animation: tick-ok .3s ease-out; border-radius: 4px; }

/* ------------------------------------------------------ polling cadence --
 *
 * A two-pixel line that fills once per poll, for the three tables that are
 * fetched on a timer rather than streamed.
 *
 * The problem it solves is not "the user does not know when the table will
 * update" — they do not care — it is that these three tables are the only
 * panels on the page whose contents change with no visible cause. A feed that
 * prepends a row shows you why it changed; a table that silently replaces six
 * numbers shows you nothing, and the reader who has been watching a depth
 * figure for four seconds cannot tell whether it is stale, static, or broken.
 * The line answers that in the least space anything could: it is either moving
 * or it is not.
 *
 * Two pixels and not a spinner, because a spinner in a table header is a claim
 * that something is loading. Nothing is loading — the previous answer is on
 * screen and correct. This is a metronome, not a progress bar, and it is drawn
 * at the weight of a rule so it reads as one until you notice it moving.
 *
 * `transform: scaleX()` and not `width`, because width is a layout property and
 * would relayout the table above it sixty times a second. The transform is
 * composited, so the whole animation costs no layout at all.
 *
 * The duration is set from JS as `--cadence-ms`, because the period is the
 * poll interval and the interval lives in the page's script. A hardcoded
 * duration here would be a second copy of a number that is already declared,
 * and the two would drift the first time an interval was retuned.
 */
.cadence {
  display: block;
  height: 2px;
  margin: 0;
  padding: 0;
  border: 0;
  background: linear-gradient(90deg, var(--brand) 0%, var(--brand2) 100%);
  opacity: .55;
  transform: scaleX(0);
  transform-origin: left center;
  /* Linear, because the line is claiming to measure elapsed time and an eased
   * one would be claiming to measure progress towards something. */
  transition: transform var(--cadence-ms, 5000ms) linear;
}
.cadence.run { transform: scaleX(1); }

@media (prefers-reduced-motion: reduce) {
  .skl { animation: none; }
  .flash-ok, .tick { animation: none; }
  /* The line is a metronome, so with motion switched off it has nothing left to
   * say. It stays in the layout as a faint rule rather than disappearing, so
   * the panel header keeps its edge and nothing shifts by two pixels.
   *
   * `!important` because the blanket rule above sets `transition-duration` with
   * it, and a rule that only looks correct because of which sheet loaded last
   * is a rule that breaks the next time something is reordered. */
  .cadence, .cadence.run { transition: none !important; transform: scaleX(1); opacity: .14; }
}

/* ---------------------------------------------------------------- bento --
 *
 * Twelve columns, and cards that span them. The point of the grid over the
 * two-column layout it replaces: on this page the panels are not equal, and
 * forcing the event feed and the signature census into the same 50% column
 * meant the feed — the thing with the most text per row — got the same width as
 * a table of four short numbers. A span lets each panel be as wide as its
 * content needs and no wider.
 *
 * Spans are named for their width in columns and not for what sits in them, so
 * moving a panel is a class change rather than a redesign.
 */
.bento { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 16px; align-items: start; }
.bento > .panel { margin-bottom: 0; min-width: 0; }
.b-3 { grid-column: span 3; }
.b-4 { grid-column: span 4; }
.b-5 { grid-column: span 5; }
.b-6 { grid-column: span 6; }
.b-7 { grid-column: span 7; }
.b-8 { grid-column: span 8; }
.b-9 { grid-column: span 9; }
.b-12 { grid-column: span 12; }
.b-tall { grid-row: span 2; }

/* ------------------------------------------------------- filter drawer --
 *
 * The three facet rows and the three range selects used to sit permanently
 * above the table: six rows of chrome, roughly 200px, in front of the data on
 * every visit — including the majority of visits where the reader changes no
 * filter at all. That is 200px of the first screen spent on the controls rather
 * than on the answer.
 *
 * They fold into a drawer instead. Not a `<details>`: a disclosure pushes the
 * table down when it opens, which is the layout jump this whole pass is trying
 * to remove. The drawer slides over the panel it belongs to, so the table stays
 * where it was and the reader's place in it is not lost.
 *
 * It is absolutely positioned inside a `position: relative` panel, and the
 * panel needs that class added by whichever page hosts it — the drawer does not
 * reach outside itself to find a containing block.
 */
.fdrawer {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  z-index: 5;
  width: min(340px, 92%);
  display: flex;
  flex-direction: column;
  background: var(--panel2);
  border-left: 1px solid var(--line2);
  box-shadow: -18px 0 44px rgba(0, 0, 0, .42);
  transform: translateX(103%);
  transition: transform var(--t) var(--ease);
  visibility: hidden;
}
.fdrawer.open { transform: none; visibility: visible; }
.fdrawer .fh {
  display: flex; align-items: center; justify-content: space-between;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line);
  font-size: 12px; font-weight: 600; color: var(--tx);
}
.fdrawer .fh button {
  background: none; border: 1px solid var(--line2); border-radius: 6px;
  color: var(--tx2); padding: 3px 9px; cursor: pointer; font: inherit; font-size: 11px;
}
.fdrawer .fh button:hover { color: var(--tx); border-color: var(--tx3); }
.fdrawer .fb { padding: 14px 16px; overflow-y: auto; display: grid; gap: 16px; align-content: start; }
.fgroup .lbl {
  font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--tx3); margin-bottom: 7px; font-weight: 500;
}
/* The chips wrap inside their group. A row of eight flags in a 340px drawer
 * needs two or three lines, and a row that does not wrap silently hides the
 * ones that did not fit — which is the failure mode of a filter panel: the
 * reader cannot see that there is a filter they have not been offered. */
.chipset { display: flex; flex-wrap: wrap; gap: 6px; }

/* How many filters are on, on the button that opens them.
 *
 * Without it the drawer is a closed box that may or may not be doing something:
 * a reader who filtered ten minutes ago and came back to a table with four rows
 * in it has no way to tell whether the market is quiet or they left a chip on.
 * Hidden at zero, because a badge that always says 0 is a badge nobody reads. */
.fcount {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--brand-fill);
  color: var(--brand);
  font-size: 10px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------- copy button --
 *
 * The address is already a button that copies on click, and that is the wrong
 * affordance on its own: nothing about a monospaced string says "press me", so
 * the feature existed and nobody used it. The icon is the tell — and it is
 * hidden until the row is hovered, because a copy icon on all twenty-five rows
 * at once is twenty-five pieces of chrome competing with the addresses they sit
 * next to. On focus it appears too, so the keyboard path is not a hidden one.
 */
.addrwrap { display: inline-flex; align-items: center; gap: 2px; }
.copyico {
  flex: 0 0 auto;
  padding: 2px;
  border: 0; border-radius: 4px;
  background: none;
  color: var(--tx3);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.addrwrap:hover .copyico,
.copyico:focus-visible { opacity: 1; }
.copyico:hover { color: var(--tx); background: var(--panel3); }
.copyico.done { color: var(--ok); opacity: 1; }

@media (max-width: 1100px) {
  .bento { grid-template-columns: minmax(0, 1fr); }
  .bento > * { grid-column: span 1; }
  .b-tall { grid-row: auto; }
}

/* ----------------------------------------------------------- the pulse --
 *
 * The mark's heartbeat, used as a connection indicator.
 *
 * The dot in the sidebar already says whether the scanner is up, and it says it
 * with a halo that expands every 2.4 seconds — which is a claim about a process,
 * not about the wire. The question a reader of the terminal actually has is
 * whether *this page* is still receiving, and the honest answer to that is the
 * stream: it is the one thing on the page that arrives continuously, so a line
 * that moves is the only indicator that cannot be faked by a stale render.
 *
 * So this is the ECG from the mark, and it is wired to the stream rather than to
 * the poll: it animates while `/api/stream` is open and it flattens — with the
 * line going red, not stopping — when it is not. A stopped line and a flat line
 * look the same in a screenshot and mean opposite things, which is why the
 * flatline is drawn and not merely left un-animated.
 *
 * The travelling highlight is a dash offset and not a moving element: a shape
 * that moves has to be repositioned every frame in JS or keyed on `transform`,
 * and both are more machinery than a stroke that is already a line.
 */
.pulse { flex: 0 0 auto; width: 62px; height: 18px; color: var(--ok); }
.pulse svg { width: 100%; height: 100%; display: block; overflow: visible; }
.pulse .flat { fill: none; stroke: currentColor; stroke-width: 1.4; opacity: .45; }
.pulse .beat {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* One dash, longer than the path, so the gap is what travels. */
  stroke-dasharray: 34 200;
  animation: pulse-beat 2.1s linear infinite;
}
@keyframes pulse-beat {
  from { stroke-dashoffset: 234; }
  to   { stroke-dashoffset: 0; }
}
.pulse.off { color: var(--critical); }
.pulse.off .beat { animation: none; stroke-dasharray: none; opacity: .55; }
.pulse.off .flat { opacity: .55; }
/* Before the stream has opened. Grey and still, which is neither of the two
 * answers and is the true one: the page does not know yet. Starting in `.off`
 * would flash a red failure on every load at the moment the connection is
 * being made, and starting live would be a claim the page cannot support until
 * the first byte arrives. */
.pulse.wait { color: var(--tx3); }
.pulse.wait .beat { animation: none; stroke-dasharray: none; }

/* The heartbeat is a signal, and a signal that is always moving stops being
 * read. Where the reader has asked for less motion it is drawn once, flat, and
 * the colour carries the state — which is the part that was ever load-bearing. */
@media (prefers-reduced-motion: reduce) {
  .pulse .beat { animation: none; stroke-dasharray: none; }
  .pulse .flat { opacity: .45; }
}

