/* FINDEUS — instrument panel.
 *
 * The public site is about space; this is about density. Both follow the same
 * material rules so they feel like one product:
 *
 *   - Surfaces are translucent white over black, not flat grey fills. A panel
 *     on a panel is genuinely lighter, so depth comes from the material rather
 *     than from a border doing the work of a shadow.
 *   - Apple's system colours, and only where they carry meaning. Green is
 *     profit, red is loss, amber is a warning, blue is interactive. Nothing is
 *     coloured because it looked nice.
 *   - Radii are large enough to read as physical (10–18px) and consistent.
 *   - One easing curve, always decelerating.
 *
 * Numbers are tabular everywhere. A P&L column whose digits shift width as it
 * ticks is genuinely harder to read at a glance, and this is a screen someone
 * glances at.
 */

:root {
  color-scheme: dark;

  --bg: #000;
  --bg-1: rgba(255, 255, 255, 0.038);
  --bg-2: rgba(255, 255, 255, 0.062);
  --bg-3: rgba(255, 255, 255, 0.095);
  --border: rgba(255, 255, 255, 0.09);
  --border-hi: rgba(255, 255, 255, 0.17);

  --fg: #f5f5f7;
  --fg-2: #a1a1a6;
  /* MEASURED, not chosen by eye. #6e6e73 was Apple's tertiary grey and came out
   * at 3.90:1 against the panel it is drawn on -- below WCAG AA (4.5:1) for the
   * size it is used at. --fg-3 carries `.label`, `.muted`, `.hint` and `.dim`,
   * which between them are most of the explanatory text in this product, so it
   * was the single most-read colour in the system and the least legible. */
  --fg-3: #7a7a7f;              /* 4.64:1 */

  --blue: #0a84ff;
  --green: #30d158;
  --red: #ff453a;
  --amber: #ff9f0a;
  --purple: #bf5af0;
  --cyan: #64d2ff;

  /* White text on a filled control needs a DARKER blue than text-on-ground
   * does; they are two different measurements and were one token, which is how
   * the Confirm-trade button ended up below AA.
   *
   *   MEASURED 2026-08-22 (WCAG 2.1 relative luminance, sRGB):
   *     #fff on --blue #0a84ff .............. 3.65:1   FAIL
   *     #fff on --blue-ink #0a6fd8 .......... 4.91:1   pass
   *   The light theme's --blue #006ede already carried #fff at 4.90:1, so
   *   --blue-ink there is the same value and the token exists for symmetry. */
  --blue-ink: #0a6fd8;

  --grid: rgba(255, 255, 255, 0.06);
  --radius: 14px;
  --radius-sm: 10px;
  --radius-lg: 18px;
  /* The pill radius was the literal 980px in twelve places and 999px in a
   * thirteenth, which is how the venue switcher ended up a different shape
   * from every other pill in the product. */
  --pill: 980px;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display",
    "Helvetica Neue", "Inter", system-ui, sans-serif;
  --header-h: 54px;
  --shadow: 0 0 0 1px var(--border), 0 20px 50px -12px rgba(0, 0, 0, 0.75);

  /* ---------------------------------------------------------------- type
   *
   * Ten steps. There were EIGHTEEN, five of them half-pixel values (9.5, 10.5,
   * 11.5 and 12.5 twice over) that no scale would ever produce -- they were
   * arrived at one component at a time, which is why the notes on Research and
   * the notes on Crypto had drifted apart. The collapse is:
   *
   *     9.5 -> 10    10.5 -> 11    11.5 -> 12    12.5 -> 12
   *      15 -> 16      17 -> 16      21 -> 19      24 -> 22
   *
   * Three of those four half-pixel merges round UP, so nothing on the densest
   * surfaces got smaller. 12.5 -> 12 is the one that rounds down: it is the
   * table cell, and 12px system-font tabular is still Apple's own secondary
   * table size, so the half-pixel bought nothing and cost a scale step.
   *
   * Deliberately px and not rem. The box metrics -- 36px controls, 54px
   * header, 25px pills -- are all still literal px, and a type scale that
   * grows inside boxes that do not is worse than one that ignores the
   * browser's font-size setting, because it clips. Honouring that setting is a
   * real gap and it stays a gap until the box metrics move too. */
  --t-9:  9px;    /* chart axis micro-labels, heatmap column heads */
  --t-10: 10px;   /* .label, thead th, the uppercase caption pattern */
  --t-11: 11px;   /* fine print: .hint, .metric .s, chips, sub-captions */
  --t-12: 12px;   /* dense data and secondary prose: tables, notes, terms */
  --t-13: 13px;   /* interface controls: .btn, tabs, panel h2 */
  --t-14: 14px;   /* body */
  --t-16: 16px;   /* inputs (below this iOS zooms on focus), modal titles */
  --t-19: 19px;   /* the second-rank figure: donut centre, invite code */
  --t-22: 22px;   /* the first-rank figure: .metric .v, venue equity */
  --t-26: 26px;   /* the one figure per view that is allowed to be larger */

  /* ------------------------------------------------------------ density
   *
   * The one layer at which this product's spacing can be changed. There was
   * none: 764 raw px literals, and `gap:` alone took every integer from 1 to
   * 12, so "tighten the blotter" meant editing several hundred numbers and
   * finding out afterwards which ones had been missed.
   *
   * These eight are the measurements that actually set the rhythm of a view --
   * the inset of a panel header, of a panel body, of a KPI tile, of a table
   * cell, and the two gaps between cards. Everything else in the file is still
   * a literal, deliberately: a scale applied to a third of a stylesheet is a
   * scale nobody can trust, and eight tokens that are true beat forty that are
   * aspirational. Adding to this list is fine; it is meant to grow by use.
   *
   * --pad-x is SHARED by the panel header, the panel body and the metric tile,
   * and that is the point of it. They stack in one grid column and their
   * contents have to line up down the left edge; compact mode gave the tile its
   * own 12px against the panel's 13px, so the KPI row sat a pixel out of
   * register with the panel under it.
   *
   * body.compact now overrides these seven values and nothing else -- see the
   * density section further down, which used to be ten per-component rules. */
  --pad-x: 15px;
  --pad-head-y: 13px;
  --pad-body-y: 15px;
  --pad-tile-y: 14px;
  --cell-x: 13px;
  --cell-y: 10px;
  --gap-grid: 12px;
  --gap-stack: 12px;

  /* ------------------------------------------------------------- motion
   *
   * Two curves, and only two. `--ease-out` decelerates into place and is the
   * default for anything that moves position or size. `--ease-press`
   * overshoots very slightly and is reserved for things that respond to a
   * direct press -- a tab, a toggle -- where the tiny bounce reads as physical
   * feedback rather than as decoration.
   *
   * Five durations, and only five. There were ELEVEN as literals (0.06 0.16
   * 0.18 0.2 0.25 0.28 0.3 0.32 0.35 0.4 0.42 0.5 0.7) while both curves were
   * already tokenised -- so the one axis of motion that a reader actually
   * perceives as speed was the one with no layer to change.
   *
   * This is a trading console. The ceiling is 240ms for anything a person is
   * waiting on; past that the interface is something you wait for rather than
   * something that responds. Only --dur-data is longer, and it is the one case
   * where the duration is the point: the eye is FOLLOWING a bar or an arc to
   * its new value, and at 200ms it teleports instead. */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-press: cubic-bezier(0.34, 1.4, 0.64, 1);
  --dur-press: 60ms;   /* :active scale. Must feel simultaneous with the tap */
  --dur-fast: 140ms;   /* colour and opacity: hover, a crosshair appearing */
  --dur-base: 200ms;   /* the default: material, borders, small transforms */
  --dur-slow: 240ms;   /* a whole surface arriving: panel, modal, toast */
  --dur-data: 420ms;   /* a bar or an arc re-measuring to a new value */

  /* The stagger step for an entrance. Six children at 26ms puts the last one
   * 130ms behind the first, which reads as the page assembling and not as a
   * queue. It was 30ms over seven steps (180ms of tail) against a 500ms
   * animation, so the last panel finished two thirds of a second after the tab
   * was pressed. */
  --stagger: 26ms;

  /* Ambient rhythms are deliberately NOT on the duration ladder above. A
   * heartbeat is not a UI response time: `beat` at 1.8s, `breathe` at 2.6s and
   * `nowpulse` at 2.8s are chosen to sit below the rate at which a repeating
   * motion becomes something you look at, and tokenising them alongside
   * transition speeds would invite someone to "tidy" them into the same
   * number. Every one of them is switched off under prefers-reduced-motion.
   *
   * They are also the only things in the file that do not use --ease-out, and
   * that is not an oversight: a loop is symmetric, and easing OUT of every
   * cycle makes a pulse snap on and drift off, which reads as a fault rather
   * than as a rhythm. They use ease-in-out; `shimmer` uses linear, because a
   * sweep that eases is a sweep that stalls in the middle of the bar. */

  /* ---------------------------------------------------------- the flash
   *
   * The colour a figure is tinted when it CHANGED since the last render, and
   * how long it holds. The full argument is at the .flash-able rules; two
   * things belong up here with the rest of the palette.
   *
   * NOT --green and --red themselves. Half the figures that flash are already
   * one of those two: `.metric .v.pos` is green, and a green number tinted
   * green announces nothing -- the figure most likely to move is the one the
   * flash would have said nothing about. Mixing 20% --fg in pushes the tint
   * AWAY from the ground in whichever theme is on (lighter on black, darker on
   * white), so it reads as a change on a neutral figure and as a brightening on
   * one that was already coloured. MEASURED 2026-08-22 on the --bg-1 card:
   * dark #4ed878 is 11.0:1, light #196d37 is 5.4:1 -- both clear AA, and both
   * higher contrast than the token they are mixed from.
   *
   * One declaration, not two: color-mix over two tokens that each have a light
   * value re-resolves per theme on its own. That is the documented idiom here.
   *
   * --flash-hold is deliberately NOT on the --dur-* ladder. Every duration on
   * that ladder answers "how long does the interface make me wait"; this one
   * answers "how long does an announcement stay up", which is the same family
   * as the 1.8s heartbeat and the 2.6s breathe -- and putting it on the ladder
   * is an invitation to tidy it down to 240ms, at which point nobody whose eye
   * was elsewhere ever sees it. 720ms is the whole animation: ~290ms holding
   * the tint, then ~430ms decaying out of it. */
  --flash-up: color-mix(in srgb, var(--green) 80%, var(--fg));
  --flash-dn: color-mix(in srgb, var(--red) 80%, var(--fg));
  --flash-hold: 720ms;

  /* An elevation ladder rather than one shadow used everywhere. Each step is a
   * tight contact shadow plus a wide ambient one, which is what stops a raised
   * surface looking like a rectangle with a grey blur under it. */
  --e1: 0 1px 2px rgba(0, 0, 0, 0.28), 0 4px 12px -4px rgba(0, 0, 0, 0.34);
  --e2: 0 2px 5px rgba(0, 0, 0, 0.32), 0 12px 28px -8px rgba(0, 0, 0, 0.46);
  --e3: 0 4px 12px rgba(0, 0, 0, 0.4), 0 28px 60px -14px rgba(0, 0, 0, 0.62);

  /* WHICH STEP TO REACH FOR. The ladder and --shadow overlapped in purpose with
   * no rule, and --e3 was defined here and used nowhere for that reason.
   *
   *   --e1  a surface at rest that is part of the page: panel, metric, vcard.
   *   --e2  the same surface under the pointer, or a small readout floating
   *         over a chart. One step, never two.
   *   --e3  a surface stacked on another raised surface -- a menu over a modal.
   *   --shadow  a DETACHED surface: palette, modal, toast, tooltip. It is the
   *         only one carrying a 1px ring, because a detached surface has to
   *         hold its own edge against whatever is behind it.
   *
   * Two things that were dark-tuned literals and therefore had no light value
   * at all now live here. The scrim was a flat rgba(0,0,0,.55) in both themes;
   * the scrolled header's shadow needed a whole extra structural override at
   * :root[data-theme="light"] to undo itself, which is what a missing token
   * looks like from the outside. */
  --scrim: rgba(0, 0, 0, 0.58);
  --shadow-bar: 0 1px 20px -8px rgba(0, 0, 0, 0.5);

  /* The material recipe, in one place so every blurred surface in the product
   * is demonstrably the same glass. */
  --glass: saturate(180%) blur(24px);
  --glass-heavy: saturate(190%) blur(36px);

  /* Specular edge: the one-pixel light catch along the top of a raised
   * surface. Its strength differs between themes, so it is a variable rather
   * than a literal in eight places. */
  --sheen: color-mix(in srgb, var(--fg) 13%, transparent);
}

:root[data-theme="light"] {
  color-scheme: light;

  --bg: #fff;
  --bg-1: rgba(0, 0, 0, 0.028);
  --bg-2: rgba(0, 0, 0, 0.05);
  --bg-3: rgba(0, 0, 0, 0.08);
  --border: rgba(0, 0, 0, 0.1);
  --border-hi: rgba(0, 0, 0, 0.19);
  --fg: #1d1d1f;
  --fg-2: #515154;
  --fg-3: #707075;              /* was #86868b at 3.41:1 */
  --grid: rgba(0, 0, 0, 0.06);

  /* Every one of these was measured against a --bg-1 card (~#f8f8f8) rather
   * than against pure white, because a card is where they are actually drawn.
   *
   *   --green was #1d9e4b at 3.27:1. That is the PROFIT colour: the single most
   *   looked-at number in the product was the least legible thing on the page,
   *   and on a phone in daylight it was close to unreadable.
   *
   *   --blue was #0071e3 at 4.42:1, marginally under. It is every link and
   *   every interactive affordance.
   *
   * Hue is preserved in each case -- only lightness moved -- so nothing changes
   * meaning, and red, amber and purple already cleared AA and are untouched. */
  --blue: #006ede;              /* 4.61:1 */
  --green: #18813d;             /* 4.66:1 */
  --red: #d70015;               /* 5.07:1, already clear */
  --amber: #b25000;
  --purple: #8944ab;
  /* --cyan had NO light value. It is the Stocks venue identity, the EMA-21
   * chart line and a categorical series colour, and the dark #64d2ff measures
   * 1.62:1 on the --bg-1 card in the theme every page actually ships with --
   * effectively invisible. MEASURED 2026-08-22: #0e7490 is 5.04:1 on that card
   * (the dark value is 11.51:1 on its own). Hue moved 197.4deg -> 192.9deg,
   * i.e. only lightness changed, so nothing it identifies changes meaning. */
  --cyan: #0e7490;              /* 5.04:1 */
  --blue-ink: #006ede;          /* #fff on it: 4.90:1 */
  --shadow: 0 0 0 1px var(--border), 0 14px 34px -12px rgba(0, 0, 0, 0.18);
  /* Still black, and still substantial. A scrim's job is to say "the thing
   * behind this is out of play", and a pale scrim on a white page says the
   * opposite -- but it does not need to be as heavy as the one that has to
   * darken a page that is already black. */
  --scrim: rgba(0, 0, 0, 0.4);
  --shadow-bar: 0 1px 16px -10px rgba(0, 0, 0, 0.35);

  /* Light mode gets a far gentler ladder. Reusing the dark shadows here makes
   * every card look like it is floating over a hole. */
  --e1: 0 1px 2px rgba(0, 0, 0, 0.05), 0 3px 8px -3px rgba(0, 0, 0, 0.08);
  --e2: 0 2px 4px rgba(0, 0, 0, 0.06), 0 10px 22px -8px rgba(0, 0, 0, 0.12);
  --e3: 0 4px 10px rgba(0, 0, 0, 0.07), 0 24px 48px -14px rgba(0, 0, 0, 0.16);
  --sheen: rgba(255, 255, 255, 0.9);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  font-size: var(--t-14);
  line-height: 1.5;
  letter-spacing: -0.008em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overscroll-behavior-y: none;
}

body { padding-top: var(--header-h); }

/* An ambient field behind everything: two very large, very faint washes fixed
 * to the viewport. It is below the noticing threshold as an effect, but a page
 * whose background is one flat value is the single clearest tell that
 * something was assembled rather than designed.
 *
 * ONE HUE, not blue->purple. Blue-into-purple appeared four times in this
 * stylesheet -- this wash, the hero glow, the tab indicator and the avatar --
 * and carried meaning in none of them, while purple carries a real meaning
 * everywhere else in the product: it is the crypto venue's identity colour and
 * the owner role. Spending it on decoration in four places is what made a
 * purple dot on the portfolio strip ambiguous. The second radial stays,
 * because one light source makes the page look lit from a corner; it is now
 * the same blue at a lower strength, which is depth rather than a palette. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  background:
    radial-gradient(60rem 40rem at 12% -10%,
      color-mix(in srgb, var(--blue) 11%, transparent), transparent 60%),
    radial-gradient(48rem 34rem at 92% 4%,
      color-mix(in srgb, var(--blue) 5%, transparent), transparent 62%);
}
:root[data-theme="light"] body::before { opacity: 0.62; }

a { color: var(--blue); text-decoration: none; }
button { font-family: inherit; font-size: inherit; cursor: pointer; }
/* 16px is not a design choice: Safari auto-zooms the viewport on focus for any
 * control below it, and this product's header is `position: fixed`, so the
 * zoom leaves the whole chrome mis-laid-out until the operator pinches back
 * out. It hits the trade-ticket risk field, the weekly goal and the command
 * palette -- every input in the product. */
input, select, textarea { font-family: inherit; font-size: var(--t-16); }

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.pos { color: var(--green); }
.neg { color: var(--red); }
.amber { color: var(--amber); }
.dim { color: var(--fg-3); }
.muted { color: var(--fg-2); }
.right { text-align: right; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.hidden { display: none !important; }

.label {
  font-size: var(--t-10);
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-3);
}

/* ------------------------------------------------------------------ header */

/* At rest the header is glass with no edge; once the page has moved it gains a
 * hairline and thickens. Nothing changes size, so the condense costs no
 * reflow -- it is purely the material reacting to the content passing under
 * it, which is how a native title bar behaves. */
header.bar {
  position: fixed;
  inset: 0 0 auto 0;
  height: var(--header-h);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 calc(16px + env(safe-area-inset-right)) 0 calc(16px + env(safe-area-inset-left));
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  border-bottom: 1px solid transparent;
  transition: background var(--dur-slow) var(--ease-out), border-color var(--dur-slow) var(--ease-out),
    box-shadow var(--dur-slow) var(--ease-out);
  z-index: 60;
}
body.scrolled header.bar {
  background: color-mix(in srgb, var(--bg) 76%, transparent);
  backdrop-filter: var(--glass-heavy);
  -webkit-backdrop-filter: var(--glass-heavy);
  border-bottom-color: var(--border);
  box-shadow: var(--shadow-bar);
}

.brand { display: flex; align-items: baseline; gap: 9px; margin-right: 2px; }
.brand:hover { text-decoration: none; }
.brand .mark {
  font-weight: 600;
  font-size: var(--t-16);
  letter-spacing: 0.15em;
  color: var(--fg);
}
.brand .sub {
  font-size: var(--t-10);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--fg-3);
}
@media (max-width: 860px) { .brand .sub { display: none; } }

.spacer { flex: 1; }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 25px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  background: var(--bg-1);
  font-size: var(--t-11);
  font-weight: 500;
  color: var(--fg-2);
  white-space: nowrap;
}
.pill.ok { border-color: color-mix(in srgb, var(--green) 40%, transparent); color: var(--green); }
.pill.warn { border-color: color-mix(in srgb, var(--amber) 42%, transparent); color: var(--amber); }
.pill.bad { border-color: color-mix(in srgb, var(--red) 45%, transparent); color: var(--red); }
.pill.live { border-color: color-mix(in srgb, var(--red) 50%, transparent); color: var(--red); font-weight: 600; letter-spacing: 0.06em; }

/* The final minute before the FX week opens or closes. Loud on purpose: this
   is the one pill that is only ever on screen for sixty seconds, so it has to
   win attention against a header that is otherwise deliberately quiet. */
.pill.hot {
  border-color: color-mix(in srgb, var(--amber) 70%, transparent);
  background: color-mix(in srgb, var(--amber) 14%, transparent);
  color: var(--amber);
  font-weight: 700;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  animation: pill-hot 1s ease-in-out infinite;
}

@keyframes pill-hot {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--amber) 42%, transparent); }
  50%      { box-shadow: 0 0 0 5px color-mix(in srgb, var(--amber) 0%, transparent); }
}

/* A pulsing box-shadow is decoration, and a ring that never stops throbbing is
   exactly the kind of motion that makes some people ill. The colour and weight
   still carry the whole message without it. */
@media (prefers-reduced-motion: reduce) {
  .pill.hot { animation: none; }
}

.dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.dot.beat { animation: beat 2.4s ease-in-out infinite; }
@keyframes beat { 0%, 100% { opacity: 1; } 50% { opacity: 0.28; } }

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 30px;
  min-width: 30px;
  padding: 0 9px;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  background: var(--bg-1);
  color: var(--fg-2);
  font-size: var(--t-12);
  transition: background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out),
    color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.icon-btn:hover { background: var(--bg-3); border-color: var(--border-hi); color: var(--fg); }
.icon-btn:active { transform: scale(0.94); }
kbd {
  font-family: var(--mono);
  font-size: var(--t-10);
  padding: 1px 4px;
  border: 1px solid var(--border-hi);
  border-radius: 4px;
  color: var(--fg-3);
}
@media (max-width: 700px) { #paletteBtn span, #paletteBtn kbd { display: none; } }

/* ----------------------------------------------------------- account menu */

.acct {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 30px;
  padding: 0 11px 0 3px;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  background: var(--bg-1);
  color: var(--fg-2);
  font-size: var(--t-12);
  transition: background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.acct:hover { background: var(--bg-3); border-color: var(--border-hi); color: var(--fg); }
.avatar {
  width: 24px;
  height: 24px;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  /* --blue-ink, not --blue: this is white text on a filled 24px circle, and
   * #fff on --blue measures 3.65:1. Solid rather than blue->purple because an
   * avatar in venue-purple is a person wearing the crypto book's colour. */
  background: var(--blue-ink);
  color: #fff;
  font-size: var(--t-11);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: uppercase;
}
.acct-name { max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 700px) { .acct-name { display: none; } .acct { padding-right: 3px; } }

/* On a phone the bar ran out of room and pushed the account button and the
 * theme toggle off the right edge, where they could not be reached at all --
 * a fixed bar cannot wrap and flex items do not shrink below their text.
 *
 * So two things go, chosen because they are the two whose information is
 * available elsewhere: the next-bar countdown is on the Overview, and the
 * command palette is a keyboard affordance on a device with no keyboard.
 * Everything that reports STATE -- staleness, live-money, halted -- stays. */
/* The order things give up space on a narrow bar, from least to most costly.
 *
 * The old ladder let the BRAND truncate first, which at 390px rendered the
 * product name as "FI…" while a countdown to the FX open kept its full width.
 * Identity is not the cheapest thing on the bar; a timer that repeats what the
 * Overview tab says in full is. */
@media (max-width: 560px) {
  header.bar {
    gap: 7px;
    padding: 0 calc(10px + env(safe-area-inset-right)) 0 calc(10px + env(safe-area-inset-left));
  }
  #barPill, #paletteBtn, #mktPill { display: none; }
  /* Never truncates: it is seven characters and it is the product's name. */
  .brand { min-width: 0; margin-right: 0; flex: none; }
  .brand .mark { overflow: visible; }
  /* Last resort if a halt pill appears too: the status pills give up characters
   * before any control gives up its place on the bar. */
  .pill { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
}
@media (max-width: 400px) {
  .brand .mark { font-size: var(--t-13); letter-spacing: 0.1em; }
}

/* ------------------------------------------------------------------ density
 *
 * Comfortable by default. Compact exists because the Portfolio blotter and the
 * combined position table run to hundreds of rows, and at that length the
 * padding that makes a four-row panel readable is what forces the scrolling.
 * It changes spacing only -- never a font size below 11px, never a colour --
 * so nothing becomes less legible, only closer together. */

/* Seven values, and they are the SAME seven the comfortable mode sets -- see
 * the density block at the top of the file. This was ten per-component rules
 * that each had to remember what its component was padded with, which is how
 * the metric tile ended up at 12px against every other surface's 13px and how
 * the sparkline bleed was left pointing at the comfortable padding. Anything
 * routed through these tokens now compacts for free; anything not routed
 * through them does not compact at all, which is at least legible from here. */
body.compact {
  --pad-x: 13px;
  --pad-head-y: 9px;
  --pad-body-y: 11px;
  --pad-tile-y: 9px;
  --cell-y: 5px;
  --gap-grid: 8px;
  --gap-stack: 10px;
}
body.compact main { padding-top: 12px; }
/* The venue card is not on the token layer: its padding is square and its
   equity figure changes SIZE, which is a type decision rather than a spacing
   one and does not belong to a density token. */
body.compact .vcard { padding: 11px; }
body.compact .vc-eq { font-size: var(--t-19); margin: 8px 0 2px; }

/* ------------------------------------------------------------ boot screen */

.boot {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 12px;
  min-height: calc(100vh - var(--header-h));
  min-height: calc(100dvh - var(--header-h));
  padding: 24px;
}
.boot-mark {
  font-size: var(--t-22);
  font-weight: 600;
  letter-spacing: 0.22em;
  color: var(--fg);
  animation: breathe 2.6s ease-in-out infinite;
}
@keyframes breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.32; } }
.boot-note { font-size: var(--t-13); color: var(--fg-3); text-align: center; max-width: 42ch; }

/* -------------------------------------------------------------------- tabs */

nav.tabs {
  position: sticky;
  top: var(--header-h);
  display: flex;
  gap: 2px;
  padding: 0 12px;
  background: color-mix(in srgb, var(--bg) 74%, transparent);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
  z-index: 50;
}
nav.tabs::-webkit-scrollbar { display: none; }
/* KNOWN GAP, and it is written here because this is where someone will look
 * for it. There are sixteen tabs and about four of them fit on a 390px phone,
 * the scrollbar is hidden, and nothing on screen says the other twelve exist.
 * The two CSS-only fixes both fail on this element specifically:
 *
 *   - a `mask-image` edge fade masks the element's BACKDROP-FILTER result too,
 *     so the last 24px of the strip would show unblurred page content sliding
 *     under it and the hairline would dissolve into nothing;
 *   - the local/scroll background-attachment scroll-shadow needs an OPAQUE
 *     cover at each edge to hide the shadow when there is nothing more to
 *     scroll to, and this strip's background is translucent glass by design.
 *
 * It needs an element the CSS does not own -- a wrapper around the scroller,
 * with the fade painted on the wrapper and the glass left on the strip. Until
 * that exists this is undiscoverable rather than subtly signposted, and saying
 * so is better than an edge fade that lies on a strip that is not scrolled. */

/* One indicator that travels, rather than a rule that appears under whichever
 * button is selected. The difference is the whole feel of the control: a strip
 * of fifteen independent buttons becomes one object with a position. Driven by
 * --ti-x / --ti-w, which paintTabs() measures off the live element, because the
 * labels are all different widths and the strip scrolls on a phone. */
nav.tabs::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: var(--ti-w, 0);
  height: 2px;
  border-radius: 2px 2px 0 0;
  /* Solid --blue. The gradient ran blue at the left edge to purple at the
   * right, so the same indicator was a different colour depending on which tab
   * you were on -- a difference that looked like it meant something. */
  background: var(--blue);
  transform: translateX(var(--ti-x, 0));
  opacity: 0;
  transition: transform var(--dur-data) var(--ease-press), width var(--dur-data) var(--ease-press),
    opacity var(--dur-base) var(--ease-out);
}
nav.tabs.lit::after { opacity: 1; }

nav.tabs button {
  position: relative;
  padding: 13px 12px 12px;
  border: 0;
  border-radius: 8px 8px 0 0;
  background: none;
  color: var(--fg-3);
  font-size: var(--t-13);
  font-weight: 500;
  letter-spacing: -0.01em;
  white-space: nowrap;
  transition: color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
@media (hover: hover) {
  nav.tabs button:hover { color: var(--fg); background: var(--bg-1); }
}
nav.tabs button:active { transform: scale(0.97); }
nav.tabs button[aria-selected="true"] { color: var(--fg); }
nav.tabs .badge {
  margin-left: 6px;
  padding: 0 5px;
  border-radius: var(--pill);
  background: color-mix(in srgb, var(--blue) 20%, transparent);
  border: 1px solid color-mix(in srgb, var(--blue) 34%, transparent);
  font-size: var(--t-10);
  font-family: var(--mono);
  color: var(--blue);
}

/* ------------------------------------------------------------------ layout */

main { max-width: 1500px; margin: 0 auto; padding: 20px 18px 96px; }
@media (max-width: 640px) { main { padding: 14px 12px 96px; } }

.panel {
  position: relative;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--e1);
  transition: border-color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
/* One-pixel light catch along the top edge. Reads as a physical bevel at a
 * glance and is the cheapest way to stop a flat panel looking like a div. */
.panel::before,
.metric::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(
    90deg, transparent, var(--sheen) 22%, var(--sheen) 78%, transparent);
  pointer-events: none;
  z-index: 1;
}
:root[data-theme="light"] .panel::before,
:root[data-theme="light"] .metric::before { opacity: 0.5; }
/* A panel is NOT interactive, so it does not change elevation under the
 * pointer -- a static card that rises when you point at it is claiming to be
 * pressable. The border brightening is the whole of the feedback: it says the
 * pointer is inside this card's bounds, which is useful on a screen of twelve
 * of them, and it says nothing else.
 *
 * Guarded on `hover: hover`. Without it iOS latches the state after a tap and
 * leaves whichever panel was last touched permanently highlighted, which on a
 * dashboard reads as "this one is selected". */
@media (hover: hover) {
  .panel:hover { border-color: var(--border-hi); }
}
/* Wraps rather than forcing its panel wider. A position card's header carries
 * a pair, two chips and a P&L figure, and on a phone those together are wider
 * than the screen -- which made the panel, and therefore the page, scroll
 * sideways. */
/* One material step above its own body, not a second border. A view is
 * routinely twelve panels deep and the hairline alone made every title read at
 * the same level as the rows under it; a header that is genuinely a lighter
 * surface (darker, in the light theme -- it is the same 2.5% of --fg either
 * way) is the difference between a stack of divs and a stack of objects. It is
 * one --bg step, deliberately: two steps and the header starts looking like a
 * control you can press. */
.panel > header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: var(--pad-head-y) var(--pad-x);
  background: color-mix(in srgb, var(--fg) 2.5%, transparent);
  border-bottom: 1px solid var(--border);
}
.panel > header h2 {
  margin: 0;
  min-width: 0;
  font-size: var(--t-13);
  font-weight: 600;
  letter-spacing: -0.015em;
}
.panel > header .hint { font-size: var(--t-11); color: var(--fg-3); }
.panel .body { padding: var(--pad-body-y) var(--pad-x); }
.panel .body.flush { padding: 0; }

/* start, not stretch: a short panel beside a tall one should stay short.
 * Stretching leaves a calculator or a session strip with a lake of dead space
 * under its content, which reads as a panel that failed to load. */
.grid { display: grid; gap: var(--gap-grid); align-items: start; }
.g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.g4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* min(Npx, 100%) rather than a bare Npx. minmax()'s minimum is a hard floor,
 * so `minmax(420px, 1fr)` keeps a 420px column on a 390px phone and pushes the
 * whole page into a sideways scroll -- which is exactly what the position
 * cards were doing. The min() clamps the floor to the container. */
.g-auto { grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); }
.g-wide { grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr)); }
/* Wide-left/narrow-right. A class, not an inline style, so the breakpoint
 * below can actually collapse it -- an inline grid-template wins over any
 * media query and pinned the sidebar into a 140px column on a phone. */
.g-main { grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); }
@media (max-width: 940px) { .g-main { grid-template-columns: 1fr; } }
.stack { display: flex; flex-direction: column; gap: var(--gap-stack); }
.row { display: flex; align-items: center; gap: 10px; }
.wrap { flex-wrap: wrap; }

@media (max-width: 1000px) { .g4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 860px) { .g3, .g2 { grid-template-columns: 1fr; } }
@media (max-width: 560px) { .g4 { grid-template-columns: 1fr; } }

/* ----------------------------------------------------------------- metrics */

.metric {
  position: relative;
  padding: var(--pad-tile-y) var(--pad-x);
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 0;
  overflow: hidden;
  box-shadow: var(--e1);
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out),
    transform var(--dur-base) var(--ease-out);
}
/* The metric tile keeps its lift: a KPI tile IS a target -- clicking one is
   how you get to the view behind it -- so here the elevation change is
   reporting something true. */
@media (hover: hover) {
  .metric:hover {
    border-color: var(--border-hi);
    box-shadow: var(--e2);
    transform: translateY(-1px);
  }
}
.metric .k { display: flex; align-items: center; gap: 6px; }
.metric .v {
  margin-top: 6px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-22);
  font-weight: 500;
  letter-spacing: -0.04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.metric .s { margin-top: 3px; font-size: var(--t-11); color: var(--fg-3); }
/* A metric that carries a sparkline lets it bleed to the card's edges: the
 * shape is context for the number above it, not a separate exhibit. */
/* Derived from the tile's own padding rather than repeating it. It was
 * `-15px / -14px / +30px` against a tile that compact mode re-padded to 12px,
 * so in compact the strip was 3px wider than the card on each side and the
 * card's own `overflow: hidden` quietly clipped both ends of the sparkline --
 * the two ends being the oldest and the most recent point on it. */
.metric .cw {
  margin: 9px calc(var(--pad-x) * -1) calc(var(--pad-tile-y) * -1);
  width: calc(100% + var(--pad-x) * 2);
  opacity: 0.85;
}
/* No axis chip on a 34px strip: it would be most of the graphic. */
.metric .cw .cw-ax { display: none; }

.hero {
  position: relative;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding: 20px;
}
/* A single soft light source behind the headline number. Kept very low
 * contrast: it should register as depth, never as a coloured panel. */
.hero::after {
  content: "";
  position: absolute;
  inset: -70% 20% auto -14%;
  height: 320px;
  background:
    radial-gradient(ellipse at 26% 62%,
      color-mix(in srgb, var(--blue) 20%, transparent), transparent 66%),
    radial-gradient(ellipse at 62% 40%,
      color-mix(in srgb, var(--blue) 7%, transparent), transparent 62%);
  pointer-events: none;
}
.hero > * { position: relative; z-index: 1; }
/* The one number on the product that is allowed to be big. The tightest
 * tracking in the stylesheet, because at 60px the default spacing of a
 * monospace face looks like a spreadsheet rather than a headline.
 *
 * It used to be gradient-clipped text fading from --fg to 76% --fg. Two
 * problems, and the second is the one that matters:
 *   1. `color: transparent` with no fallback. Any context that drops
 *      background-clip:text -- a print stylesheet, a text-only rendering, an
 *      older engine -- renders the account's equity as nothing at all.
 *   2. The fade ran top to bottom, so the BOTTOM of every glyph was the
 *      lightest part of it. On a figure read at a glance in daylight that is
 *      the wrong half of the number to give away, and the effect exists to
 *      make the number look designed rather than to make it read. */
.hero .nav-value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: clamp(34px, 7vw, 60px);
  font-weight: 600;
  letter-spacing: -0.055em;
  line-height: 1;
  color: var(--fg);
}
.hero .delta { margin-top: 9px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.hero .spark { flex: 1 1 260px; min-width: 200px; height: 76px; }
.hero .spark .cw { height: 76px; }

/* ------------------------------------------------------------------ tables */

table { width: 100%; border-collapse: collapse; font-size: var(--t-12); }
thead th {
  position: sticky;
  top: 0;
  padding: var(--cell-y) var(--cell-x);
  background: color-mix(in srgb, var(--bg) 74%, transparent);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  border-bottom: 1px solid var(--border);
  z-index: 2;
  font-size: var(--t-10);
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--fg-3);
  text-align: left;
  white-space: nowrap;
  cursor: default;
}
thead th.sortable { cursor: pointer; user-select: none; }
thead th.sortable:hover { color: var(--fg); }
tbody td { padding: var(--cell-y) var(--cell-x); border-bottom: 1px solid var(--border); vertical-align: middle; }
/* A second line inside a cell, for the sentence that explains the row. It used
 * to be a full-width colspan row underneath, which doubled the table's height
 * and turned it into alternating data and prose. */
tbody td .sub {
  margin-top: 3px;
  font-size: var(--t-11);
  line-height: 1.45;
  color: var(--fg-3);
  max-width: 46ch;
}
tbody tr:last-child td { border-bottom: 0; }
/* Guarded for the same reason .panel:hover is. Unguarded, a tap on a row of the
 * 300-row blotter leaves that row shaded until you tap somewhere else, and a
 * shaded row in a table of identical rows reads as "this one is selected" --
 * on the surface where the rows are open positions. */
@media (hover: hover) {
  tbody tr:hover { background: var(--bg-1); }
}
/* A bar laid behind a number, inside its own cell. Keeps the exact value --
 * these are percentages someone reasons about -- while making a column of them
 * rankable at a glance instead of one decimal at a time. */
td.cellbar { position: relative; }
td.cellbar > i {
  position: absolute;
  left: 0;
  top: 50%;
  height: 17px;
  margin-top: -8.5px;
  border-radius: 0 3px 3px 0;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--blue) 26%, transparent),
    color-mix(in srgb, var(--blue) 12%, transparent));
  pointer-events: none;
}
td.cellbar > span { position: relative; }
/* Anchor the bar to whichever side its number is on. A right-aligned figure
 * with a bar growing from the far left of a wide cell reads as two unrelated
 * things that happen to share a box. */
td.cellbar.right > i {
  left: auto;
  right: 0;
  border-radius: 3px 0 0 3px;
  background: linear-gradient(270deg,
    color-mix(in srgb, var(--blue) 26%, transparent),
    color-mix(in srgb, var(--blue) 12%, transparent));
}

.scroll-x { overflow-x: auto; }
.scroll-y { max-height: 420px; overflow-y: auto; }

/* ------------------------------------------------------------------- chips */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  background: var(--bg-2);
  font-size: var(--t-11);
  font-weight: 500;
  color: var(--fg-2);
  white-space: nowrap;
}
.chip.pass { border-color: color-mix(in srgb, var(--green) 34%, transparent); color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
.chip.fail { border-color: color-mix(in srgb, var(--red) 30%, transparent); color: var(--red); background: color-mix(in srgb, var(--red) 9%, transparent); }
/* The state between pass and fail. Three separate views were patching an amber
 * chip inline because this did not exist, which put the one verdict a reader
 * most needs to notice outside the chip system entirely. */
.chip.warn {
  border-color: color-mix(in srgb, var(--amber) 38%, transparent);
  color: var(--amber);
  background: color-mix(in srgb, var(--amber) 10%, transparent);
}
.chip.idle { opacity: 0.55; }
.chip.long { border-color: color-mix(in srgb, var(--green) 40%, transparent); color: var(--green); }
.chip.short { border-color: color-mix(in srgb, var(--red) 40%, transparent); color: var(--red); }

/* ------------------------------------------------------------------- bars */

.track {
  position: relative;
  height: 5px;
  border-radius: 3px;
  background: var(--bg-3);
  overflow: hidden;
}
.track > i {
  position: absolute;
  inset: 0 auto 0 0;
  display: block;
  border-radius: 3px;
  background: var(--blue);
  transition: width var(--dur-data) var(--ease-out);
}
.track > i.g { background: var(--green); }
.track > i.a { background: var(--amber); }
.track > i.r { background: var(--red); }

/* ------------------------------------------------------ readiness board */

.ready-row {
  display: grid;
  grid-template-columns: 150px 1fr 48px;
  align-items: center;
  gap: 14px;
  padding: 12px 15px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-out);
}
.ready-row:last-child { border-bottom: 0; }
.ready-row:hover { background: var(--bg-1); }
.ready-row .pair { font-family: var(--mono); font-size: var(--t-13); font-weight: 500; }
.ready-row .blocked {
  margin-top: 3px;
  font-size: var(--t-10);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ready-row .why { margin-top: 7px; display: flex; gap: 5px; flex-wrap: wrap; }
.ready-row .score { font-family: var(--mono); font-size: var(--t-16); text-align: right; letter-spacing: -0.04em; }

@media (max-width: 520px) {
  .ready-row { grid-template-columns: 1fr 44px; }
  .ready-row > :nth-child(2) { grid-column: 1 / -1; }
}

/* -------------------------------------------------------------- instrument */

.inst-card { display: flex; flex-direction: column; }
.inst-card > header { justify-content: space-between; }
.inst-card .chart { width: 100%; height: 168px; display: block; }
.inst-card .adx-strip { width: 100%; height: 34px; display: block; border-top: 1px solid var(--border); }
.inst-card .foot {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  border-top: 1px solid var(--border);
}
.inst-card .foot > div { padding: 9px 11px; border-right: 1px solid var(--border); min-width: 0; }
.inst-card .foot > div:last-child { border-right: 0; }
.inst-card .foot .v { font-family: var(--mono); font-size: var(--t-12); margin-top: 2px; letter-spacing: -0.02em; }

/* ----------------------------------------------------------------- ladder */

/* Inset by half a caption. The stop/entry/target captions are centred on their
 * tick, so one sitting at 0% or 100% hangs half its width outside the panel --
 * which on a phone was the one thing on the whole product that made the page
 * scroll sideways. The rail loses 44px; the labels stay readable. */
.ladder { position: relative; height: 46px; margin: 10px 22px 4px; }
.ladder .rail { position: absolute; inset: 20px 0 auto 0; height: 3px; border-radius: 2px; background: var(--bg-3); }
.ladder .fill { position: absolute; top: 20px; height: 3px; border-radius: 2px; }
.ladder .tick { position: absolute; top: 13px; width: 2px; height: 17px; background: var(--fg-3); border-radius: 1px; }
.ladder .tick.stop { background: var(--red); }
.ladder .tick.tp { background: var(--green); }
.ladder .tick.now { top: 9px; height: 25px; width: 2px; background: var(--fg); }
.ladder .cap { position: absolute; top: 0; font-size: var(--t-10); color: var(--fg-3); transform: translateX(-50%); white-space: nowrap; }
.ladder .cap.b { top: 36px; }

/* ----------------------------------------------------------------- gauges */

.gauge { display: flex; align-items: center; gap: 12px; }
.gauge svg { flex: none; }
.gauge .g-v { font-family: var(--mono); font-size: var(--t-16); letter-spacing: -0.03em; }

/* ---------------------------------------------------------------- console */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  height: 36px;
  padding: 0 16px;
  border: 1px solid var(--border-hi);
  border-radius: var(--pill);
  background: var(--bg-2);
  color: var(--fg);
  font-size: var(--t-13);
  font-weight: 500;
  letter-spacing: -0.01em;
  transition: background var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out),
    opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.btn:hover { background: var(--bg-3); border-color: var(--border-hi); }
.btn:active { transform: scale(0.96); transition-duration: var(--dur-press); }
.btn:disabled { opacity: 0.42; cursor: not-allowed; transform: none; }
/* The one filled button on a screen of instruments -- and the one that submits
 * a live trade, which is why the numbers below are not a matter of taste.
 *
 * It was a 180deg gradient lightening toward the top plus a white specular
 * inset. MEASURED 2026-08-22, #fff against the fill (WCAG 2.1):
 *
 *   DARK   top of gradient 3.13:1   base 3.65:1   on hover 2.61:1
 *   LIGHT  top of gradient 4.04:1   base 4.90:1   on hover 3.17:1
 *
 * Five of those six are under AA 4.5:1 and the worst of them is the HOVER
 * state, i.e. the button got harder to read at the exact moment the pointer
 * was on it. Flat --blue-ink measures 4.91:1 (dark) and 4.90:1 (light), and
 * hover now DARKENS -- 6.0:1 and 5.9:1 -- so the contrast improves under the
 * pointer instead of collapsing.
 *
 * The gradient and the inset specular go with it. A raised fill and a 1px
 * light catch are what a 2013 button did to say "pressable"; the elevation
 * step and the only filled colour on the page already say it. */
.btn.primary {
  background: var(--blue-ink);
  color: #fff;
  border-color: transparent;
  box-shadow: var(--e1);
}
.btn.primary:hover {
  background: color-mix(in srgb, var(--blue-ink) 86%, #000);
  box-shadow: var(--e2);
}
.btn.danger { border-color: color-mix(in srgb, var(--red) 55%, transparent); color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
.btn.danger:hover { background: color-mix(in srgb, var(--red) 18%, transparent); }
.btn.good { border-color: color-mix(in srgb, var(--green) 50%, transparent); color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
.btn.good:hover { background: color-mix(in srgb, var(--green) 17%, transparent); }
.btn.sm { height: 30px; padding: 0 12px; font-size: var(--t-12); }

/* ---------------------------------------------------------- touch targets
 *
 * Apple's minimum is 44x44 and almost nothing here met it: .btn was 36, .btn.sm
 * and .icon-btn were 30, .acct was 30. Raising them everywhere would cost real
 * density on the surface this product is actually designed for, so they are
 * raised only where the pointer is a finger. `pointer: coarse` is the input
 * device, not the screen width -- a phone in landscape at 900px still gets
 * them, and a 1000px browser window on a laptop does not.
 *
 * Nothing here changes a font size or a colour, only a box, so no layout that
 * fits at 44px stops fitting at 36px on the way back. */
@media (pointer: coarse) {
  .btn { height: 44px; padding: 0 18px; }
  .btn.sm { height: 38px; padding: 0 14px; }
  .icon-btn { height: 38px; min-width: 38px; padding: 0 11px; }
  .acct { height: 38px; }
  .seg button { padding: 10px 18px; }
  input[type="text"], input[type="password"], input[type="number"],
  input[type="email"], select { height: 44px; }
  nav.tabs button { padding: 15px 13px 14px; }
  .venues button { padding: 10px 15px; }
}

input[type="text"], input[type="password"], input[type="number"], input[type="email"], select {
  height: 36px;
  padding: 0 12px;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
  color: var(--fg);
  outline: none;
  min-width: 0;
  transition: border-color var(--dur-base) var(--ease-out), box-shadow var(--dur-base) var(--ease-out);
}
/* Two states, not one. The border says "this field has the caret" and shows on
 * a mouse click, because a click into the risk field should visibly land. The
 * RING is :focus-visible only -- it is the keyboard affordance, and firing it
 * on every mouse click is what made it read as decoration and got it
 * suppressed on the command palette input. */
input:focus, select:focus { border-color: var(--blue); }
input:focus-visible, select:focus-visible {
  border-color: var(--blue);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--blue) 20%, transparent);
}
input[type="range"] { accent-color: var(--blue); width: 100%; }

/* ------------------------------------------------------------------ focus
 *
 * ONE ring, on everything focusable. There were four focus declarations in the
 * whole stylesheet, at three different outline-offsets, one of which actively
 * suppressed the ring -- so keyboard focus was a mixture of a designed blue
 * ring, no ring at all, and three variants of the browser default.
 *
 * :where() keeps this at zero specificity on purpose: the four components that
 * need a different ring (an inset one on a full-bleed row, a wider one on a
 * pill) still win without !important, and adding a component here is not a
 * specificity negotiation.
 *
 * Text controls are NOT in this list. They keep the box-shadow ring above,
 * which follows their border-radius; an outline on a 10px-radius field draws a
 * rounded rectangle two pixels outside a rounded rectangle. */
:where(a, button, [tabindex], summary, [role="button"], [role="tab"],
  [role="option"]):focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  /* The ring is drawn outside the box, so on a sticky header or inside a
     scroll container it must not be clipped away by a neighbour's paint. */
  position: relative;
  z-index: 3;
}
/* Rows and cells that run to the panel edge have no room for a 2px outset
   ring -- it lands under the panel's own border. These take it inside. */
:where(.ready-row, .palette .item, tbody tr, .lrn-gate, .lrn-milestone):focus-visible {
  outline-offset: -2px;
}

/* A labelled input inside a grid cell. Column layout so the label sits above
   its control and the whole thing lines up with the metric tiles beside it. */
/* Inputs, then the button, then whatever the form has to say back. Fields grow;
 * the button and the note take exactly what they need. A form laid out in the
 * KPI grid gives a checkbox the same footprint as an equity number. */
.form-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
}
.form-row > .field { flex: 1 1 160px; }
.form-row > .btn { flex: none; }
.note-inline { font-size: var(--t-12); color: var(--fg-3); flex: 1 1 100%; }
.note-inline.neg { color: var(--red); }
.note-inline.pos { color: var(--green); }

.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field > .label { margin: 0; }
.field input { width: 100%; }

pre.json {
  margin: 0;
  padding: 15px;
  max-height: 460px;
  overflow: auto;
  font-family: var(--mono);
  font-size: var(--t-11);
  line-height: 1.55;
  color: var(--fg-2);
  white-space: pre-wrap;
  word-break: break-word;
}

/* --------------------------------------------------------------- palette */

/* SCROLLS. It did not, and the modal it holds carries `margin-top: 4vh` on top
 * of this padding, so 18vh of a short viewport was spent before the dialog
 * began and anything past the fold was simply unreachable -- including the
 * Confirm button of a trade ticket that had grown a `.warn-box` override list
 * and a `.danger-box`. That is the highest-stakes interaction in the product
 * and on a landscape phone it could not be completed.
 *
 * The top inset also drops to 6vh below 700px tall, because 14vh of a 500px
 * viewport is 70px of scrim above a dialog that needs every pixel. */
.overlay {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: start center;
  padding: 14vh 12px calc(24px + env(safe-area-inset-bottom));
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--scrim);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
  z-index: 200;
  animation: fade var(--dur-base) var(--ease-out);
}
@media (max-height: 700px) {
  .overlay { padding-top: 6vh; }
  .modal { margin-top: 0; }
}
@keyframes fade { from { opacity: 0; } }

.palette {
  width: min(560px, 92vw);
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: saturate(180%) blur(30px);
  -webkit-backdrop-filter: saturate(180%) blur(30px);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  animation: rise var(--dur-slow) var(--ease-out);
}
@keyframes rise { from { opacity: 0; transform: translateY(-10px) scale(0.985); } }

.palette input {
  width: 100%;
  height: 50px;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  background: transparent;
  padding: 0 18px;
  font-size: var(--t-16);
}
.palette input:focus { box-shadow: none; border-color: var(--border); }
.palette .items { max-height: 340px; overflow-y: auto; padding: 7px; }
.palette .item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 11px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--t-13);
}
.palette .item[aria-selected="true"] { background: var(--bg-3); }
.palette .item .desc { margin-left: auto; font-size: var(--t-11); color: var(--fg-3); }
.palette .empty { padding: 20px; text-align: center; color: var(--fg-3); font-size: var(--t-13); }

.modal {
  width: min(440px, 92vw);
  margin-top: 4vh;
  padding: 24px;
  background: color-mix(in srgb, var(--bg) 80%, transparent);
  backdrop-filter: saturate(180%) blur(30px);
  -webkit-backdrop-filter: saturate(180%) blur(30px);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  animation: rise var(--dur-slow) var(--ease-out);
}
.modal h3 { margin: 0 0 6px; font-size: var(--t-16); letter-spacing: -0.02em; }
.modal p { margin: 0 0 16px; font-size: var(--t-12); color: var(--fg-2); line-height: 1.6; }
.modal .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.modal label { display: block; font-size: var(--t-11); color: var(--fg-2); margin: 12px 0 6px; }
.modal input { width: 100%; }

/* ---------------------------------------------------------------- toasts */

/* env() insets: every page sets viewport-fit=cover, and without these the
   toast stack sits on the home indicator and the header runs under the notch
   in landscape. */
.toasts { position: fixed; right: calc(16px + env(safe-area-inset-right)); bottom: calc(16px + env(safe-area-inset-bottom)); display: flex; flex-direction: column; gap: 8px; z-index: 300; max-width: min(380px, calc(100vw - 32px)); }
.toast {
  display: flex;
  gap: 9px;
  padding: 12px 14px;
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border: 1px solid var(--border-hi);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  font-size: var(--t-12);
  animation: slide var(--dur-slow) var(--ease-out);
}
.toast.ok { border-left-color: var(--green); }
.toast.err { border-left-color: var(--red); }
.toast.info { border-left-color: var(--blue); }
.toast.warn { border-left-color: var(--amber); }

/* ------------------------------------------------------------- weekly goal */

.goal-row {
  display: grid;
  grid-template-columns: minmax(120px, 160px) 1fr auto;
  gap: 14px;
  align-items: center;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.goal-row:last-of-type { border-bottom: 0; }
.goal-venue { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.goal-venue b { font-size: var(--t-14); letter-spacing: -0.01em; }
.goal-venue .dim { font-size: var(--t-12); }
.goal-mid { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.goal-sub { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: var(--t-12); }
/* 600, not 650. 550/560/650 appeared as one-offs against a 400/500/600/700
 * system: the system font interpolates them silently, so the goal figure, the
 * venue switcher and the strip label were each a weight that exists nowhere
 * else and that any real face -- Geist, which this product self-hosts for its
 * public pages -- would snap to the nearest of the four anyway. Four weights,
 * and the difference between them is now visible rather than notional. */
.goal-num {
  font-size: var(--t-26);
  font-weight: 600;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.goal-num.pos { color: var(--green); }

/* A venue with no goal yet reads as deliberately parked, not broken. */
.goal-row.soon { opacity: 0.62; }
.goal-row.soon .goal-mid { flex-direction: row; align-items: center; gap: 10px; flex-wrap: wrap; }

.goal-foot {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px solid var(--border);
  font-size: var(--t-12);
}
.goal-foot input {
  width: 108px;
  padding: 6px 9px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg);
  font: inherit;
  font-size: var(--t-16);       /* `font: inherit` above resets it to 14px */
  font-variant-numeric: tabular-nums;
}

/* Below ~560px the three-column row wraps to two: the big percentage moves up
   beside the venue name, and the bar takes the full width underneath it.
   All three children are placed explicitly -- with only .goal-mid spanning,
   .goal-num auto-flows onto a third row and the percentage ends up stranded
   under the progress bar instead of beside the name. */
@media (max-width: 560px) {
  .goal-row {
    grid-template-columns: 1fr auto;
    gap: 8px 12px;
    align-items: start;
  }
  .goal-venue { grid-column: 1; grid-row: 1; }
  .goal-num { grid-column: 2; grid-row: 1; font-size: var(--t-22); }
  .goal-mid { grid-column: 1 / -1; grid-row: 2; }
  .goal-foot { gap: 8px; }
  .goal-foot .spacer { display: none; }
}
@keyframes slide { from { opacity: 0; transform: translateX(16px); } }

/* ---------------------------------------------------------------- charts */

/* hidden, not visible: candle bodies at the last bar sit half outside the
 * viewBox and were painting over the panel border. */
svg.chart, svg.adx-strip { display: block; width: 100%; overflow: hidden; }
svg text { font-family: var(--mono); fill: var(--fg-3); }

/* --- the chart frame ------------------------------------------------------
 *
 * Every series chart is an SVG inside a .cw box, with the axis labels, the
 * crosshair and the readout as HTML siblings positioned over it. They have to
 * be HTML: the SVG viewBox is scaled non-uniformly to fill its container, so
 * any text inside it would be stretched by whatever ratio the window happens
 * to be at that moment. */

.cw {
  position: relative;
  width: 100%;
  overflow: hidden;
}
.cw svg.chart { height: 100%; }

/* Axis labels sit in the top-right of their gridline, inset from the edge, so
 * they read as annotations on the plot rather than as a column beside it. */
.cw-ax {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.cw-ax span {
  position: absolute;
  right: 8px;
  transform: translateY(-50%);
  padding: 0 3px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-10);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--fg-3);
  /* Nearly opaque on purpose. A bar reaching the right edge sits directly
   * under its own axis label, and a translucent chip there is unreadable
   * against exactly the chart it is annotating. */
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  border-radius: 3px;
}
/* axisTicks() runs low to high and the scale is inverted, so :first-child is
 * the BOTTOM label and :last-child the top one. Centring works for the middle
 * ticks but hangs the outer two half outside the box, so they are pinned
 * inward instead of centred. */
.cw-ax span:first-child { transform: translateY(-100%); }
.cw-ax span:last-child { transform: translateY(0); }

/* The crosshair. Hidden until the pointer is actually over the plot, and moved
 * by custom properties so the handler sets two numbers rather than touching
 * layout. */
.cw-cross {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.cw.hot .cw-cross { opacity: 1; }
.cw-cross i {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--x, 50%);
  width: 1px;
  background: linear-gradient(180deg, transparent, var(--border-hi) 12%,
    var(--border-hi) 88%, transparent);
}
.cw-cross b {
  position: absolute;
  left: var(--x, 50%);
  top: var(--y, 50%);
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  background: var(--c, var(--blue));
  box-shadow: 0 0 0 2px var(--bg), 0 0 14px 1px var(--c, var(--blue));
}

.cw-read {
  position: absolute;
  top: 6px;
  left: var(--x, 50%);
  display: flex;
  gap: 8px;
  align-items: baseline;
  margin-left: 10px;
  padding: 5px 9px;
  border-radius: 8px;
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  backdrop-filter: var(--glass);
  -webkit-backdrop-filter: var(--glass);
  border: 1px solid var(--border);
  box-shadow: var(--e2);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-11);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-out);
}
.cw.hot .cw-read { opacity: 1; }
.cw-read.flip { transform: translateX(-100%); margin-left: -10px; }
.cw-read b { font-size: var(--t-12); font-weight: 600; letter-spacing: -0.02em; }
.cw-read span { color: var(--fg-3); }

/* The "now" dot at the live end of a series, and its slow halo. Live data is
 * the only thing on this product that gets to move on its own. */
.cw-now {
  filter: drop-shadow(0 0 4px currentColor);
  animation: nowpulse 2.8s var(--ease-out) infinite;
  transform-box: fill-box;
  transform-origin: center;
}
@keyframes nowpulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.45; }
}

.cw-bar { transition: fill-opacity var(--dur-base) var(--ease-out); }
.cw:hover .cw-bar { fill-opacity: 1; }

/* --- rings and donuts ---------------------------------------------------- */

/* Uniformly scaled, unlike the series charts, so these can afford real filters
 * and real text. */
.ring { display: block; overflow: visible; }
/* A donut is a ring that has to hold text in its middle, so it never shrinks
 * below the size that text needs. */
.ring.donut { flex: none; min-width: 128px; }
.ring-arc { transition: stroke-dasharray var(--dur-data) var(--ease-out); }
.ring-t {
  font-family: var(--mono);
  font-size: var(--t-13);
  font-weight: 600;
  letter-spacing: -0.03em;
  fill: var(--fg);
}
.donut-arc { transition: stroke-dasharray var(--dur-data) var(--ease-out), stroke-dashoffset var(--dur-data) var(--ease-out); }
.donut-c {
  font-family: var(--mono);
  font-size: var(--t-19);
  font-weight: 600;
  letter-spacing: -0.04em;
  fill: var(--fg);
}
.donut-s { font-size: var(--t-10); letter-spacing: 0.06em; text-transform: uppercase; fill: var(--fg-3); }

/* An inline key for a chart that draws more than one thing. Without it a
 * dashed line and a shaded band are two mysteries the caption has to explain
 * in a sentence each. */
/* --- a meter with a labelled cap ----------------------------------------- */

/* The bar clamps at the cap, so past it 6.1% and 14% draw identically. The
 * overflow state gives the whole control an unmistakable treatment rather than
 * leaving the difference to a sentence underneath. */
.meter { position: relative; }
.meter-cap {
  display: flex;
  justify-content: space-between;
  margin-top: 5px;
  font-size: var(--t-10);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.meter-cap .cap { color: var(--fg-2); }
.meter.over .track {
  box-shadow: 0 0 0 1px var(--red), 0 0 14px -2px color-mix(in srgb, var(--red) 70%, transparent);
  animation: beat 1.8s ease-in-out infinite;
}
.meter.over .meter-cap .cap { color: var(--red); }

/* The plot column of the session clock. Needs the explicit floor or the SVG
 * refuses to shrink below its intrinsic width inside the grid. */
.sess-plot { min-width: 0; }

/* --- cell strips --------------------------------------------------------- */

/* Countable progress. A bar at 10% and a bar at 15% look the same; two cells
 * filled out of twenty and three cells out of twenty do not. */
.strip { display: flex; gap: 3px; }
.strip > i {
  flex: 1;
  min-width: 0;
  height: 16px;
  border-radius: 3px;
  background: var(--bg-3);
  transition: background var(--dur-base) var(--ease-out);
}
.strip > i.on { background: color-mix(in srgb, var(--blue) 70%, transparent); }
.strip.g > i.on { background: color-mix(in srgb, var(--green) 70%, transparent); }
.strip.a > i.on { background: color-mix(in srgb, var(--amber) 70%, transparent); }
.strip.r > i.on { background: color-mix(in srgb, var(--red) 70%, transparent); }

/* Attendance. Days are narrow and there are a lot of them, so the misses have
 * to be the loud state -- a hollow cell in a run of filled ones disappears. */
.strip.days { gap: 2px; }
.strip.days > i { height: 22px; border-radius: 2px; }
.strip.days > i.on { background: color-mix(in srgb, var(--green) 45%, transparent); }
.strip.days > i.off {
  background: var(--red);
  box-shadow: 0 0 6px color-mix(in srgb, var(--red) 55%, transparent);
}

/* --- status lights ------------------------------------------------------- */

.lamp {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--sans);
  font-size: var(--t-12);
  letter-spacing: -0.005em;
}
.lamp > i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  background: var(--fg-3);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--fg-3) 16%, transparent);
}
.lamp.ok { color: var(--green); }
.lamp.ok > i {
  background: var(--green);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--green) 18%, transparent),
    0 0 8px color-mix(in srgb, var(--green) 60%, transparent);
}
.lamp.warn { color: var(--amber); }
.lamp.warn > i {
  background: var(--amber);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--amber) 18%, transparent),
    0 0 8px color-mix(in srgb, var(--amber) 60%, transparent);
}
/* The only light that moves. Something is wrong and the page should say so
 * without being read. */
.lamp.bad { color: var(--red); }
.lamp.bad > i {
  background: var(--red);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 20%, transparent),
    0 0 10px color-mix(in srgb, var(--red) 70%, transparent);
  animation: beat 1.8s ease-in-out infinite;
}

/* --- notes ---------------------------------------------------------------
 *
 * The explanatory prose this product carries a lot of, in one place. Five
 * views had grown their own copy of the same inline margin/padding/size/colour
 * string, so the notes on Research and the notes on Crypto had quietly drifted
 * to different type sizes. */

ul.note {
  margin: 10px 0 0;
  padding-left: 18px;
  font-size: var(--t-12);
  color: var(--fg-3);
  line-height: 1.6;
}
ul.note li { margin-bottom: 3px; }
ul.note li::marker { color: var(--fg-3); opacity: 0.5; }

p.note-p {
  margin: 12px 0 0;
  font-size: var(--t-12);
  line-height: 1.6;
  color: var(--fg-3);
}
p.note-p em { color: var(--fg-2); font-style: normal; }
p.note-p b { color: var(--fg-2); font-weight: 500; }

/* The row that is currently live, in any comparison table. It was italicised
 * by an inline font-weight, which at six rows of monospace disappeared. */
tr.incumbent > td { background: color-mix(in srgb, var(--blue) 7%, transparent); }
tr.incumbent > td:first-child { box-shadow: inset 2px 0 0 var(--blue); }

/* --- threshold bars ------------------------------------------------------ */

/* A statistic and the bar it must clear. The rule is drawn on every row at the
 * same scale, so "which of these are actually earned" is a single sweep of the
 * eye rather than seven subtractions. */
.tb { display: flex; flex-direction: column; gap: 11px; }
/* The label column is capped rather than proportional. At 0.9fr a wide panel
 * gave five three-letter gate names a third of the screen and pushed every bar
 * -- the thing being compared -- off to the right. */
.tb-row {
  display: grid;
  grid-template-columns: minmax(84px, 210px) minmax(0, 1fr) 62px;
  align-items: center;
  gap: 10px;
}
.tb-l {
  font-size: var(--t-12);
  color: var(--fg-2);
  overflow: hidden;
  text-overflow: ellipsis;
}
.tb-track {
  position: relative;
  height: 16px;
  border-radius: 5px;
  background: var(--bg-2);
  overflow: hidden;
}
.tb-track > i {
  position: absolute;
  left: 0;
  top: 2px;
  bottom: 2px;
  border-radius: 4px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--fg-3) 60%, transparent), color-mix(in srgb, var(--fg-3) 30%, transparent));
}
.tb-track > i.ok {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--green) 90%, transparent), color-mix(in srgb, var(--green) 45%, transparent));
}
/* The threshold itself. Deliberately the highest-contrast mark in the control:
 * everything else on the row is read relative to it. */
.tb-track > u {
  position: absolute;
  top: -1px;
  bottom: -1px;
  width: 2px;
  margin-left: -1px;
  background: var(--amber);
  box-shadow: 0 0 6px color-mix(in srgb, var(--amber) 60%, transparent);
}
.tb-v { font-size: var(--t-12); text-align: right; }
.tb-s { grid-column: 2 / -1; font-size: var(--t-11); margin-top: -4px; line-height: 1.45; }

/* --- stacked bars -------------------------------------------------------- */

.sb { display: flex; flex-direction: column; gap: 10px; }
.sb-key { display: flex; flex-wrap: wrap; gap: 12px; font-size: var(--t-11); color: var(--fg-3); }
/* Swatches, not rules: these name filled segments of a bar, and a 2px dash
 * reads as a line style rather than as a colour. */
.sb-key .key > i { width: 9px; height: 9px; border-radius: 2.5px; }
.sb-row {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr) 54px;
  align-items: center;
  gap: 10px;
}
.sb-l { font-size: var(--t-12); color: var(--fg-2); }
.sb-track {
  display: flex;
  height: 16px;
  border-radius: 5px;
  background: var(--bg-2);
  overflow: hidden;
}
.sb-track > i { display: block; height: 100%; transition: width var(--dur-data) var(--ease-out); }
.sb-track > i + i { box-shadow: inset 1px 0 0 var(--bg); }
.sb-v { font-size: var(--t-12); text-align: right; }
.sb-s { grid-column: 2 / -1; font-size: var(--t-11); margin-top: -4px; }

/* --- diverging bars ------------------------------------------------------ */

.db { display: flex; flex-direction: column; gap: 10px; }
/* max-content on the label column, shared across every row: currency codes get
 * 44px and "Too small" gets what it needs, without either being hardcoded. */
.db-row {
  display: grid;
  grid-template-columns: minmax(44px, max-content) minmax(0, 1fr) 68px;
  align-items: center;
  gap: 10px;
}
.db-l { font-size: var(--t-12); color: var(--fg-2); max-width: 120px; }
.db-track {
  position: relative;
  height: 16px;
  border-radius: 5px;
  background: var(--bg-2);
  overflow: hidden;
}
/* The zero axis. Everything on this control is read relative to it, so it is
 * drawn on top of the bars rather than under them. */
.db-track::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-hi);
}
.db-track > i {
  position: absolute;
  top: 2px;
  bottom: 2px;
  border-radius: 4px;
}
.db-track > i.pos {
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--green) 85%, transparent), color-mix(in srgb, var(--green) 45%, transparent));
}
.db-track > i.neg {
  background: linear-gradient(270deg,
    color-mix(in srgb, var(--red) 85%, transparent), color-mix(in srgb, var(--red) 45%, transparent));
}
.db-v { font-size: var(--t-12); text-align: right; }
.db-s { grid-column: 2 / -1; font-size: var(--t-11); margin-top: -4px; }
.db-axis {
  display: grid;
  grid-template-columns: minmax(44px, max-content) minmax(0, 1fr) 68px;
  gap: 10px;
  margin-top: 2px;
}
/* The ends are named by the caller: "short/long" for exposure, "loss/profit"
 * for a P&L comparison. Baked in as pseudo-element content they were wrong on
 * every chart that was not currency exposure. */
.db-axis b {
  grid-column: 2;
  display: flex;
  justify-content: space-between;
  font-size: var(--t-9);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
}

.key { display: inline-flex; align-items: center; gap: 5px; }
.key > i { width: 14px; height: 2px; border-radius: 2px; flex: none; }
.key > i.band { height: 9px; border-radius: 2px; opacity: 0.28; }
.key > i.dash {
  height: 0;
  border-top: 1px dashed var(--fg-3);
  background: none !important;
}
.key > i.dash.red { border-top-color: var(--red); }

/* Capped: a legend allowed to fill a 1400px panel puts its values a hand-span
 * from their own swatches. */
.legend { display: flex; flex-direction: column; gap: 7px; min-width: 0; max-width: 340px; }
.legend > div { display: flex; align-items: center; gap: 8px; font-size: var(--t-12); min-width: 0; }
.legend i {
  width: 8px;
  height: 8px;
  border-radius: 2.5px;
  flex: none;
}
.legend .n { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend .w {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-12);
  color: var(--fg-2);
}

/* --- the gate heatmap ---------------------------------------------------- */

/* Instruments down, gates across. The readiness board answers "is this pair
 * close?"; this answers "which gate is holding the whole book up?", and a
 * column of red is a far faster read than eight rows of chips. */
.hm { display: flex; flex-direction: column; gap: 3px; }
.hm-row, .hm-head { display: flex; align-items: center; gap: 10px; }
.hm-row > b, .hm-head > b {
  width: 66px;
  flex: none;
  font-family: var(--mono);
  font-size: var(--t-11);
  font-weight: 500;
  color: var(--fg-2);
}
.hm-cells { display: flex; gap: 3px; flex: 1; min-width: 0; }
.hm-cells > i, .hm-cells > span {
  flex: 1;
  min-width: 0;
  height: 18px;
  border-radius: 4px;
  transition: transform var(--dur-fast) var(--ease-press), box-shadow var(--dur-fast) var(--ease-out);
}
.hm-cells > span {
  height: auto;
  font-size: var(--t-9);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--fg-3);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hm-cells > i.on { background: color-mix(in srgb, var(--green) 62%, transparent); }
.hm-cells > i.off { background: color-mix(in srgb, var(--red) 55%, transparent); }
.hm-cells > i.na { background: var(--bg-2); }
@media (hover: hover) {
  .hm-cells > i:hover {
    transform: scaleY(1.22);
    box-shadow: 0 0 0 1px var(--border-hi);
  }
}

/* --- the session clock --------------------------------------------------- */

/* Name gutter, plot, state gutter. The lanes in all three columns are the same
 * fraction of the same height, so they line up without a single magic number. */
.sess { display: grid; grid-template-columns: 74px minmax(0, 1fr) 62px; gap: 12px; }
.sess-names, .sess-states {
  display: grid;
  grid-template-rows: repeat(var(--lanes, 4), 1fr);
  align-items: center;
  font-size: var(--t-12);
  min-width: 0;
}
/* The axis strip under the plot is 20px tall; the gutters have to reserve the
 * same space or their last lane drifts below the band it names. */
.sess-names { padding-bottom: 20px; }
.sess-states { padding-bottom: 20px; text-align: right; position: relative; }
.sess-names > div {
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sess-names > div.on { color: var(--fg); font-weight: 500; }
.sess-states > div {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-11);
  color: var(--fg-3);
}
.sess-states > div.on { color: var(--green); }
.sess-states > i {
  position: absolute;
  right: 0;
  bottom: 2px;
  font-style: normal;
  font-size: var(--t-9);
  letter-spacing: 0.06em;
  color: var(--fg-3);
  opacity: 0.6;
}
/* The now-marker gets a glow the SVG line cannot have: the viewBox is scaled
 * non-uniformly, so a filter there would smear horizontally. */
.sess-now {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--blue);
  box-shadow: 0 0 10px 1px color-mix(in srgb, var(--blue) 70%, transparent);
  pointer-events: none;
}
.sess-now::before {
  content: "";
  position: absolute;
  top: -3px;
  left: -3px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--blue);
  box-shadow: 0 0 10px 1px var(--blue);
}
.sess-axis {
  position: relative;
  height: 20px;
  padding-top: 5px;
}
.sess-axis span {
  position: absolute;
  transform: translateX(-50%);
  font-family: var(--mono);
  font-size: var(--t-10);
  color: var(--fg-3);
}
.sess-axis span:first-child { transform: none; }
.sess-axis span:last-of-type { transform: translateX(-100%); }

@media (max-width: 560px) {
  .sess { grid-template-columns: 58px minmax(0, 1fr) 46px; gap: 8px; }
}

/* --- ranked bars --------------------------------------------------------- */

/* label / count / bar, ranked. Four views had grown their own copy of this. */
.ranked { display: flex; flex-direction: column; gap: 11px; }
.ranked > div > .row { margin-bottom: 5px; }
.ranked .muted { font-size: var(--t-12); min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* --- the funnel ---------------------------------------------------------- */

.fn .fn-labels { position: absolute; inset: 0; pointer-events: none; }
.fn-row {
  position: absolute;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 14px;
  transform: translateY(-50%);
  font-size: var(--t-12);
  color: var(--fg);
  text-shadow: 0 1px 3px var(--bg);
}
.fn-row b { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: var(--t-12); }
.tt {
  position: fixed;
  pointer-events: none;
  z-index: 150;
  padding: 8px 10px;
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  font-family: var(--mono);
  font-size: var(--t-11);
  line-height: 1.6;
  white-space: nowrap;
}

/* ------------------------------------------------------- recommendations */

.rec {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--bg-1);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
}
.rec:hover { border-color: var(--border-hi); }
/* Live proposals earn a coloured edge. Everything else stays neutral, so the
 * one card worth acting on is findable without reading a word. */
.rec.live { border-color: color-mix(in srgb, var(--green) 45%, transparent); }
.rec.live::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 2px;
  background: var(--green);
}
.rec.blocked { opacity: 0.82; }

.rec > header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.rec .side {
  font-family: var(--mono);
  font-size: var(--t-12);
  font-weight: 600;
  letter-spacing: 0.08em;
  padding: 3px 9px;
  border-radius: var(--pill);
}
.rec .side.buy { color: var(--green); background: color-mix(in srgb, var(--green) 15%, transparent); }
.rec .side.sell { color: var(--red); background: color-mix(in srgb, var(--red) 15%, transparent); }
.rec .side.wait { color: var(--fg-3); background: var(--bg-3); }
.rec .pair { font-family: var(--mono); font-size: var(--t-16); font-weight: 500; letter-spacing: -0.01em; }
.rec .headline { font-size: var(--t-12); color: var(--fg-2); padding: 12px 16px 0; }

.rec .levels {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  margin: 13px 0 0;
  background: var(--border);
  border-block: 1px solid var(--border);
}
.rec .levels > div { padding: 10px 16px; background: var(--bg); }
.rec .levels .v { font-family: var(--mono); font-size: var(--t-13); margin-top: 2px; letter-spacing: -0.02em; }

.rec .terms {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  padding: 12px 16px;
  font-size: var(--t-12);
  color: var(--fg-2);
}
.rec .terms b { font-family: var(--mono); color: var(--fg); font-weight: 500; }

.rec .why { padding: 0 16px 13px; display: flex; flex-direction: column; gap: 5px; }
.rec .why div { display: flex; gap: 7px; font-size: var(--t-12); line-height: 1.45; }
.rec .why .ok { color: var(--green); }
.rec .why .no { color: var(--red); }
.rec .why span:last-child { color: var(--fg-2); }

.rec .act { display: flex; gap: 8px; padding: 13px 16px; border-top: 1px solid var(--border); }
.rec .act .btn { flex: 1; }

/* ---------------------------------------------------------- trade ticket */

.ticket { width: min(460px, 94vw); }
.ticket .row-lab {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  font-size: var(--t-12);
  border-bottom: 1px solid var(--border);
}
.ticket .row-lab:last-child { border-bottom: 0; }
.ticket .row-lab span:first-child { color: var(--fg-2); }
.ticket .row-lab span:last-child { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.ticket .warn-box {
  margin: 14px 0 0;
  padding: 12px 13px;
  border: 1px solid color-mix(in srgb, var(--amber) 40%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--amber) 9%, transparent);
  font-size: var(--t-12);
  line-height: 1.55;
  color: var(--fg-2);
}
.ticket .warn-box b { color: var(--amber); }
.ticket .danger-box {
  border-color: color-mix(in srgb, var(--red) 45%, transparent);
  background: color-mix(in srgb, var(--red) 10%, transparent);
}
.ticket .danger-box b { color: var(--red); }

/* --- the ticket preview ---------------------------------------------------
 *
 * What the order actually becomes if Confirm is pressed. It sits inside the
 * ticket modal, under the inputs and above the actions, and it is DENSE on
 * purpose: it is read in the seconds before live money moves, and a preview
 * that needs scrolling is a preview that gets skipped.
 *
 * MARKUP (exact):
 *
 *   <div class="preview-sheet">
 *     <span class="label">If you confirm</span>
 *     <div class="row-lab"><span>Final size</span><span>1,240 units</span></div>
 *     <div class="row-lab"><span>Margin required</span><span>4.12 USD</span></div>
 *     <ul class="ps-guards">
 *       <li class="is-block"><b>Household risk</b>
 *         <span>6.8% against a 6.0% budget</span>
 *         <span class="chip fail">refuses</span></li>
 *       <li class="is-waive"><b>Spread</b>
 *         <span>2.4 pips of a 1.0 pip budget</span>
 *         <span class="chip warn">would be waived</span></li>
 *     </ul>
 *     <p class="ps-afford">Affordable at 3.5% risk: <b>820 units</b></p>
 *   </div>
 *
 * The figures reuse the ticket's own `.row-lab` rather than inventing a pair of
 * classes that would drift away from it -- final size and margin are the same
 * kind of line as entry and stop, and they should keep looking like it.
 *
 * REFUSALS FIRST, then waivers. A ticket that cannot be sent should not make
 * the operator read three amber lines before reaching the red one.
 *
 * The guards list is OMITTED ENTIRELY when no guard has an opinion. It is not
 * rendered empty and it does not render a green "all clear": every guard
 * passing is the ordinary case, and a badge celebrating it would be the loudest
 * thing on the sheet.
 *
 * WAIVED IS NOT QUIET. Amber, filled, with a 2px inset edge and the word in the
 * chip, because a waiver is the operator agreeing to weaken a capital-
 * preservation limit on a live account. The role check, the WARNING journal
 * line and the test that proves the waiver cannot be reached below owner all
 * live in the engine; what this file owes is that it can never be missed. */
.preview-sheet {
  margin: 14px 0 0;
  padding: 10px 12px 11px;
  border: 1px solid var(--border-hi);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.preview-sheet > .label { display: block; margin-bottom: 2px; }
/* Tighter than the ticket's own 8px: these lines are a block to be taken in at
   once rather than a list to be read down. */
.preview-sheet .row-lab { padding: 6px 0; }
.preview-sheet .row-lab:last-of-type { border-bottom: 0; }

.ps-guards {
  list-style: none;
  margin: 9px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.ps-guards li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 7px;
  padding: 7px 9px;
  border-radius: var(--radius-sm);
  font-size: var(--t-12);
  line-height: 1.45;
  color: var(--fg-2);
}
.ps-guards li b { font-weight: 500; }
/* Tone AND edge AND the word in the chip. Three channels for two states,
   because the difference between "this will still go through" and "this will
   not go through" is the whole point of the sheet. */
.ps-guards .is-waive {
  background: color-mix(in srgb, var(--amber) 9%, transparent);
  box-shadow: inset 2px 0 0 var(--amber);
}
.ps-guards .is-waive b { color: var(--amber); }
.ps-guards .is-block {
  background: color-mix(in srgb, var(--red) 10%, transparent);
  box-shadow: inset 2px 0 0 var(--red);
}
.ps-guards .is-block b { color: var(--red); }

/* The size the account could actually carry, always shown, including when it
   is larger than the ticket. It is the answer to "why is this so small", and
   it is quiet: --fg-3 prose with the figure promoted, not a fourth alert. */
.ps-afford {
  margin: 10px 0 0;
  padding-top: 9px;
  border-top: 1px solid var(--border);
  font-size: var(--t-11);
  line-height: 1.5;
  color: var(--fg-3);
}
.ps-afford b {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--fg-2);
}

.seg { display: inline-flex; padding: 2px; background: var(--bg-3); border-radius: var(--pill); }
.seg button {
  padding: 6px 15px;
  border: 0;
  border-radius: var(--pill);
  background: none;
  color: var(--fg-3);
  font-size: var(--t-12);
  font-weight: 500;
  transition: background var(--dur-base) var(--ease-out), color var(--dur-base) var(--ease-out);
}
/* --e1, not a dark-tuned literal. The old `0 1px 3px rgba(0,0,0,0.35)` was
   measured against black and sat under the Buy/Sell selector as a visible grey
   smudge on a white ticket. */
.seg button[aria-pressed="true"] { background: var(--bg); color: var(--fg); box-shadow: var(--e1); }
.seg button.buy[aria-pressed="true"] { color: var(--green); }
.seg button.sell[aria-pressed="true"] { color: var(--red); }

.pos-actions { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 12px; margin-top: 12px; border-top: 1px solid var(--border); }

.empty-state { padding: 44px 20px; text-align: center; color: var(--fg-3); font-size: var(--t-13); }
.empty-state .big { font-size: var(--t-26); margin-bottom: 8px; opacity: 0.35; }

.skel { background: linear-gradient(90deg, var(--bg-2) 25%, var(--bg-3) 50%, var(--bg-2) 75%); background-size: 200% 100%; animation: shimmer 1.4s linear infinite; border-radius: 4px; }
@keyframes shimmer { to { background-position: -200% 0; } }

/* ---------------------------------------------------------------- venues */

.venue { display: flex; flex-direction: column; }
.venue > header { justify-content: space-between; }
.venue .asset {
  font-size: var(--t-10);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.venue .lists { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--border); }
.venue .lists > div { background: var(--bg); padding: 13px 15px; }
@media (max-width: 700px) { .venue .lists { grid-template-columns: 1fr; } }

ul.ticks { list-style: none; margin: 8px 0 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
ul.ticks li { display: flex; gap: 8px; font-size: var(--t-12); line-height: 1.5; color: var(--fg-2); }
ul.ticks li::before { content: "✓"; color: var(--green); flex: none; font-size: var(--t-11); }
ul.ticks.gaps li::before { content: "•"; color: var(--amber); }

.env-need {
  font-family: var(--mono);
  font-size: var(--t-11);
  padding: 2px 7px;
  border-radius: 6px;
  background: var(--bg-3);
  color: var(--fg-2);
}

/* ------------------------------------------------------------------- team */

.team-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 15px;
  border-bottom: 1px solid var(--border);
}
.team-row:last-child { border-bottom: 0; }
.team-row .who { min-width: 0; flex: 1; }
.team-row .who .n { font-size: var(--t-13); font-weight: 500; }
.team-row .who .e { font-size: var(--t-11); color: var(--fg-3); overflow: hidden; text-overflow: ellipsis; }

/* A row on the waiting list. Stacked rather than a flex line, because unlike a
 * member row its content is a PARAGRAPH -- somebody's answer to "why do you
 * need this" -- and the whole point of asking was to read it. */
.wait-row {
  padding: 14px 15px;
  border-bottom: 1px solid var(--border);
}
.wait-row:last-child { border-bottom: 0; }
.wait-row .who .n { font-size: var(--t-13); font-weight: 500; }
.wait-row .who .e { font-size: var(--t-11); color: var(--fg-3); }
.wait-why {
  margin: 9px 0 0;
  font-size: var(--t-12);
  line-height: 1.6;
  color: var(--fg-2);
  /* Their words wrap; they do not get an ellipsis. */
  overflow-wrap: anywhere;
}
.wait-meta {
  margin: 6px 0 0;
  font-size: var(--t-11);
  line-height: 1.55;
  color: var(--fg-3);
}
.wait-meta b { color: var(--fg-2); font-weight: 500; }

/* The reconciliation: cash + held = equity, as an addition the owner can check
 * line by line against the broker's own screen. A total rule under the last
 * addend, because that is what makes a column of numbers read as a sum. */
.rec { display: grid; }
.rec-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: var(--s-3);
  padding: 9px 0;
}
.rec-row .k { font-size: var(--t-13); color: var(--fg-2); min-width: 0; }
.rec-row .k i {
  display: block;
  font-style: normal;
  font-size: var(--t-11);
  color: var(--fg-3);
  margin-top: 2px;
}
.rec-row .v { font-size: var(--t-13); font-variant-numeric: tabular-nums; color: var(--fg); }
.rec-row .v.warn { color: var(--amber); }
.rec-row .v.neg { color: var(--red); }
.rec-row.total {
  border-top: 1px solid var(--fg-3);
  margin-top: 4px;
  padding-top: 11px;
}
.rec-row.total .k { font-weight: 560; color: var(--fg); }
.rec-row.total .v { font-size: var(--t-16); font-weight: 560; }
.role {
  font-size: var(--t-10);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--pill);
  border: 1px solid var(--border);
  color: var(--fg-2);
}
.role.viewer { color: var(--fg-3); }
.role.owner { color: var(--purple); border-color: color-mix(in srgb, var(--purple) 42%, transparent); background: color-mix(in srgb, var(--purple) 11%, transparent); }
.role.trader { color: var(--blue); border-color: color-mix(in srgb, var(--blue) 42%, transparent); background: color-mix(in srgb, var(--blue) 11%, transparent); }

.code-box {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
  padding: 14px 16px;
  border: 1px dashed color-mix(in srgb, var(--green) 45%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--green) 8%, transparent);
}
.code-box .code {
  font-family: var(--mono);
  font-size: var(--t-19);
  letter-spacing: 0.14em;
  color: var(--green);
  user-select: all;
  /* The code must never be clipped: it exists in exactly one place and cannot
     be shown again, so a character hidden past the edge is a lost invite. */
  word-break: break-all;
  min-width: 0;
}

/* ------------------------------------------------------------ venue switcher

   Sits above the tabs and reads as a higher-order choice than they do: these
   are three different products, not three more sections. Segmented rather than
   a dropdown so all three states, and which one you are in, are visible without
   a click. */
.venues {
  display: flex;
  gap: 4px;
  padding: 10px 18px 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.venues::-webkit-scrollbar { display: none; }
.venues button {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--fg-3);
  font: inherit;
  font-size: var(--t-13);
  font-weight: 500;
  letter-spacing: -0.01em;
  padding: 7px 15px;
  border-radius: var(--pill);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-press), background var(--dur-fast) var(--ease-press),
              border-color var(--dur-fast) var(--ease-press);
}
/* Guarded: unguarded, a tap on Crypto leaves FX looking exactly as selected as
 * Crypto does, on the control that decides which book you are looking at. */
@media (hover: hover) {
  .venues button:hover { color: var(--fg); background: var(--bg-2); }
}
/* The selected venue is the only one at 600. Hover and selected shared a fill
 * and a text colour and differed only by a border and a shadow, so on a touch
 * screen -- where the fill latches -- they were indistinguishable. Weight is
 * the one channel a latched hover cannot counterfeit. */
.venues button.on {
  color: var(--fg);
  font-weight: 600;
  background: var(--bg-2);
  border-color: var(--border);
  box-shadow: var(--e1);
}

@media (max-width: 560px) {
  .venues { padding: 8px 12px 0; }
  .venues button { padding: 6px 12px; font-size: var(--t-12); }
}

/* An action that exists but has not happened yet. Amber rather than red: this
   is a window of opportunity, not a fault. */
.queued-panel {
  border-color: color-mix(in srgb, var(--amber) 45%, transparent);
  background: color-mix(in srgb, var(--amber) 5%, transparent);
}

/* Holds the one-time invite code until it is explicitly dismissed. */
.invite-fresh {
  border: 1px solid color-mix(in srgb, var(--green) 40%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--green) 5%, transparent);
  padding: 12px 13px;
  margin: 2px 0;
}
.invite-fresh .code-box { margin-top: 8px; }

@media (max-width: 480px) {
  .code-box { flex-wrap: wrap; }
  .code-box .code { font-size: var(--t-16); letter-spacing: 0.1em; }
}


/* ========================================================= the control surface
 *
 * Three rows that all say the same kind of thing: somebody DECIDED something,
 * and the decision has an end. A mute, a scheduled halt, a household budget.
 *
 * They share one rule, which is the reason they are written together. None of
 * them is a fault, and none of them may render as one. Red in this stylesheet
 * means a loss, a failed gate or a stopped book; spending it on "you turned
 * this off yourself" is how an operator learns to skim past red. */

/* --- a muted instrument ---------------------------------------------------
 *
 * MARKUP (exact). Lives in a `.panel .body.flush`, one row per instrument:
 *
 *   <div class="mute-row">
 *     <span class="mr-inst mono">EUR_USD</span>
 *     <span class="mr-until">muted until <b>Sat 14:30 UTC</b></span>
 *     <span class="mr-left num">3h 12m</span>
 *     <button class="btn sm">Unmute</button>
 *   </div>
 *
 * DELIBERATE: the row keeps the neutral surface every other row has, and its
 * state edge is --fg-3. It is the only inset state edge in the product that is
 * not a semantic colour, and that is the statement -- nothing happened TO this
 * instrument, somebody switched it off.
 *
 * TEMPORARY: the remaining time is a chip rather than prose, at full --fg, and
 * it is the second-loudest thing in the row after the instrument itself. It is
 * also the only part of the row that stops being true while you look at it, so
 * the absolute time sits beside it -- a countdown alone cannot be checked
 * against anything, and a wall-clock alone has to be subtracted from.
 *
 * If a mute has no end, this row is the wrong component: an indefinite mute is
 * a halt and belongs to `.policy-row`, which is built to make a missing expiry
 * impossible to miss. */
.mute-row {
  display: grid;
  grid-template-columns: minmax(88px, max-content) minmax(0, 1fr) max-content max-content;
  align-items: center;
  gap: 6px 12px;
  padding: var(--cell-y) var(--cell-x);
  border-bottom: 1px solid var(--border);
  box-shadow: inset 2px 0 0 var(--fg-3);
}
.mute-row:last-child { border-bottom: 0; }
.mr-inst { font-size: var(--t-12); font-weight: 500; color: var(--fg-2); }
.mr-until { font-size: var(--t-12); color: var(--fg-3); min-width: 0; }
.mr-until b { font-weight: 500; color: var(--fg-2); }
.mr-left {
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--pill);
  background: var(--bg-2);
  font-size: var(--t-12);
  color: var(--fg);
  white-space: nowrap;
}
/* All four children placed explicitly. Implicit flow put the unmute button on
   the second row under the instrument and left the countdown stranded beside
   it, which is the same failure `.goal-row` documents at its own breakpoint. */
@media (max-width: 520px) {
  .mute-row { grid-template-columns: minmax(0, 1fr) max-content; }
  .mr-inst { grid-area: 1 / 1; }
  .mr-left { grid-area: 1 / 2; justify-self: end; }
  .mr-until { grid-area: 2 / 1; }
  .mute-row > .btn { grid-area: 2 / 2; }
}

/* --- a scheduled halt -----------------------------------------------------
 *
 * MARKUP (exact):
 *
 *   <div class="policy-row">
 *     <span class="pl-what">Halt FX</span>
 *     <span class="pl-when num">Fri 21:00 UTC</span>
 *     <span class="pl-until num">lifts Mon 07:00 UTC</span>
 *     <button class="btn sm">Cancel</button>
 *   </div>
 *
 * and, when the schedule carries no expiry, the third span becomes
 *
 *     <span class="pl-until none">no expiry</span>
 *
 * Amber, like `.queued-panel`: an action that exists and has not happened yet
 * is a window, not a fault, and this one is a halt -- the safe direction.
 *
 * THE EXPIRY IS THE POINT OF THE ROW. A halt that never lifts is not dangerous
 * on the day it is set; it is dangerous six weeks later, when the book has been
 * flat and nobody remembers why. So the expiry is never abbreviated, never
 * folded into a tooltip, and its ABSENCE is rendered rather than left blank --
 * a blank cell reads as "nothing to say here", which is the opposite of what a
 * missing expiry means. `.none` says the words, in a red chip, and turns the
 * row's own edge red so the row is findable in a list of twenty. */
.policy-row {
  display: grid;
  grid-template-columns: minmax(96px, max-content) max-content minmax(0, 1fr) max-content;
  align-items: center;
  gap: 6px 12px;
  padding: var(--cell-y) var(--cell-x);
  border-bottom: 1px solid var(--border);
  box-shadow: inset 2px 0 0 var(--amber);
}
.policy-row:last-child { border-bottom: 0; }
.pl-what { font-size: var(--t-12); font-weight: 500; }
.pl-when { font-size: var(--t-12); color: var(--fg-2); white-space: nowrap; }
.pl-until { font-size: var(--t-12); color: var(--fg-3); }
.pl-until.none {
  padding: 2px 8px;
  border: 1px solid color-mix(in srgb, var(--red) 45%, transparent);
  border-radius: var(--pill);
  background: color-mix(in srgb, var(--red) 10%, transparent);
  color: var(--red);
  font-weight: 500;
  justify-self: start;
}
/* Degrades to just the chip where :has() is not available, which is the right
   way round: the chip is the one that carries the words. */
.policy-row:has(.pl-until.none) { box-shadow: inset 2px 0 0 var(--red); }
@media (max-width: 560px) {
  .policy-row { grid-template-columns: minmax(0, 1fr) max-content; }
  .pl-what { grid-area: 1 / 1; }
  .pl-when { grid-area: 1 / 2; justify-self: end; }
  .pl-until { grid-area: 2 / 1; }
  .policy-row > .btn { grid-area: 2 / 2; }
}

/* --- the household risk alert ---------------------------------------------
 *
 * MARKUP (exact). --use is the fraction of the budget in use, as a percentage,
 * and it may exceed 100%:
 *
 *   <div class="house-banner" style="--use: 52%">
 *     <span class="hb-k label">Household risk</span>
 *     <span class="hb-v num">3.1%</span>
 *     <span class="hb-bar"><i></i></span>
 *     <span class="hb-s">of a 6.0% budget across three books.</span>
 *   </div>
 *
 *   <div class="house-banner is-over" style="--use: 113%">
 *     ... <span class="hb-s">6.8% against a 6.0% budget: over by 0.8pp. Nothing
 *     is blocked by this -- it is what the three books add up to.</span>
 *   </div>
 *
 * AN ALERT, NOT A GATE, and the whole design of the over state follows from
 * that one fact. It is amber and never red: red here would say the household
 * had been refused something, and it has not been -- the number simply is what
 * it is. There is no icon, no ring, no pulse and no border on the page around
 * it. Over 6% is a fact about three books that were each sized correctly on
 * their own, which is a thing worth knowing and not a thing that broke.
 *
 * The bar's full width IS the budget, so being over is being full -- the fill
 * clamps at 100% and picks up a hatch. The hatch is the second channel: the
 * same grammar as `.lrn-series-none`, where a hatch means "do not read this as
 * a normal reading". Colour alone on the one figure that spans all three
 * venues would be the least defensible place in the product for it. */
.house-banner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) max-content;
  align-items: center;
  gap: 7px 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
}
.hb-k { grid-area: 1 / 1; }
.hb-v { grid-area: 1 / 2; font-size: var(--t-13); color: var(--fg); }
.hb-bar {
  grid-area: 2 / 1 / 2 / -1;
  height: 5px;
  border-radius: 3px;
  background: var(--bg-3);
  overflow: hidden;
}
.hb-bar > i {
  display: block;
  height: 100%;
  /* Clamped here rather than in the caller, so a caller that passes the true
     140% still draws a full bar instead of one 40% wider than its trough. */
  width: min(100%, var(--use, 0%));
  background: var(--green);
  transition: width var(--dur-data) var(--ease-out);
}
.hb-s { grid-area: 3 / 1 / 3 / -1; font-size: var(--t-11); line-height: 1.5; color: var(--fg-3); }

.house-banner.is-over {
  border-color: color-mix(in srgb, var(--amber) 45%, transparent);
  background: color-mix(in srgb, var(--amber) 8%, transparent);
}
.house-banner.is-over .hb-v { color: var(--amber); }
.house-banner.is-over .hb-bar > i {
  background-color: var(--amber);
  /* Notched toward the GROUND, not toward the ink. Mixing --fg in gave white
     stripes on amber in dark mode, which is hazard tape -- and hazard tape is
     exactly the reading this banner must not have. Mixing --bg in reads as
     notches cut out of the bar in either theme. */
  background-image: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--bg) 34%, transparent) 0 3px,
    transparent 3px 7px);
}

footer.foot { padding: 20px 18px 34px; text-align: center; font-size: var(--t-11); color: var(--fg-3); }

/* ================================================================== motion
 *
 * This is an instrument panel. Motion here has exactly one job: to make a
 * CHANGE OF STATE legible in the half second before the operator has read a
 * word. It is never used to make the page feel alive, and nothing on it is
 * allowed to be a thing you wait for.
 *
 * Three kinds, and there are no others:
 *
 *   ENTRANCE   a view arriving. --dur-slow, 6px of travel, staggered by
 *              --stagger. Ends before the eye has finished the first panel.
 *   STATE      a book stopping. The one place a ring is allowed to pulse, and
 *              it pulses three times and then stops.
 *   AMBIENT    the six infinite loops (beat, breathe, nowpulse, shimmer,
 *              pill-hot). Heartbeats, not responses. See the note on --stagger.
 *
 * Every one of them is switched off under prefers-reduced-motion at the foot
 * of this section, and the switch-off is tested by reading it, not assumed:
 * the blanket 0.01ms duration is NOT sufficient on its own for either an
 * animation that starts at opacity 0 or one that loops forever, and both of
 * those failures were present here.
 */

/* --- entrance ------------------------------------------------------------
 *
 * Panels arrive in sequence rather than all at once, which reads as the page
 * assembling itself rather than as a flash-cut.
 *
 * It was 500ms of animation with a 180ms tail, so the last panel of a view
 * settled 680ms after the tab was pressed -- long enough that on the Portfolio
 * tab you could start reading the first card and have the fourth still moving
 * underneath it. Now: 240ms with a 130ms tail, and the travel is 6px rather
 * than 10px with no scale at all. The scale was the expensive part visually --
 * a panel full of tabular figures rendered at 0.995 for four frames is four
 * frames of blurred text.
 *
 * render() adds .enter only when the TAB changed, never on the sixty-second
 * poll. Panels sliding up under you while you are reading them is the
 * difference between a product that feels alive and one that feels twitchy. */
#view.enter > *,
#view.enter > .stack > * {
  animation: panelIn var(--dur-slow) var(--ease-out) both;
}
#view.enter > *:nth-child(1),
#view.enter > .stack > *:nth-child(1) { animation-delay: 0ms; }
#view.enter > *:nth-child(2),
#view.enter > .stack > *:nth-child(2) { animation-delay: calc(var(--stagger) * 1); }
#view.enter > *:nth-child(3),
#view.enter > .stack > *:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
#view.enter > *:nth-child(4),
#view.enter > .stack > *:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
#view.enter > *:nth-child(5),
#view.enter > .stack > *:nth-child(5) { animation-delay: calc(var(--stagger) * 4); }
/* The tail is capped rather than continued. A view with nineteen panels would
 * otherwise put the last one half a second behind the first, and past about
 * five steps the stagger has already done its work -- everything after that is
 * just a queue. */
#view.enter > *:nth-child(n + 6),
#view.enter > .stack > *:nth-child(n + 6) { animation-delay: calc(var(--stagger) * 5); }

@keyframes panelIn {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}

/* ROWS, for a list inside one panel: the readiness board, the gate list, the
 * milestone list. Opt-in with .rows-in on the CONTAINER, because it is only
 * ever right for a list short enough to take in at once -- putting it on the
 * 300-row blotter would animate 300 elements and say nothing.
 *
 * The travel is 3px and horizontal-free: a row sliding up inside a panel that
 * is itself sliding up doubles the movement, so the row entrance is nearly all
 * opacity. Capped at eight steps for the same reason as the panels. */
/* SCOPED UNDER #view.enter, and this is the load-bearing part. render()
 * replaces #view.innerHTML wholesale on the sixty-second poll, so EVERY
 * animation on anything inside it re-fires every sixty seconds unless it is
 * gated on the marker class that only a tab change sets. An unscoped `.rows-in`
 * is a gate list that re-assembles itself under the operator once a minute.
 *
 * There is exactly ONE exception in this stylesheet, and it is the figure
 * flash below: it wants to fire on a poll, because a poll is when a number
 * changes. It is safe to be the exception only because its class is emitted
 * per figure and only where the value actually moved, so a poll that changed
 * nothing animates nothing. Anything else that wants to fire on a poll needs
 * the same proof before it goes in. */
#view.enter .rows-in > * { animation: rowIn var(--dur-base) var(--ease-out) both; }
#view.enter .rows-in > *:nth-child(1) { animation-delay: 0ms; }
#view.enter .rows-in > *:nth-child(2) { animation-delay: calc(var(--stagger) * 0.6); }
#view.enter .rows-in > *:nth-child(3) { animation-delay: calc(var(--stagger) * 1.2); }
#view.enter .rows-in > *:nth-child(4) { animation-delay: calc(var(--stagger) * 1.8); }
#view.enter .rows-in > *:nth-child(5) { animation-delay: calc(var(--stagger) * 2.4); }
#view.enter .rows-in > *:nth-child(6) { animation-delay: calc(var(--stagger) * 3); }
#view.enter .rows-in > *:nth-child(7) { animation-delay: calc(var(--stagger) * 3.6); }
#view.enter .rows-in > *:nth-child(n + 8) { animation-delay: calc(var(--stagger) * 4.2); }

@keyframes rowIn {
  from { opacity: 0; transform: translateY(3px); }
  to { opacity: 1; transform: none; }
}

/* --- a figure that changed ------------------------------------------------
 *
 * Built 2026-08-22. It shipped once before as `.flash`/`.flash.up`/`.flash.dn`
 * with no markup anywhere emitting it and was deleted for that; this is the
 * contract version, and the half that decides WHICH figures changed lives in
 * app.js because this file cannot see it.
 *
 * THE CONTRACT. app.js emits
 *
 *     <span class="num flash-able" data-figure="fx.nav">42.95</span>
 *
 * and adds `.flash-up` or `.flash-dn` for one animation cycle, then takes it
 * off. `data-figure` is a stable dotted key for the figure across renders; the
 * CSS never reads it. #view is repainted wholesale by render(), so no node
 * survives to diff -- app.js keeps a module-level Map of data-figure -> last
 * value, compares after building the HTML, and puts the class on the freshly
 * inserted nodes. The first render of a figure has nothing to compare against
 * and must flash NOTHING; a key that has never been seen is not a key that
 * went up. The map is keyed by figure and not by view, so a number that moved
 * while the operator was on another tab flashes when they come back to it,
 * which is the right answer -- it moved, and they have not seen it yet.
 *
 * WHY IT IS EMITTED ONLY ON A REAL CHANGE, and why that is a hard rule rather
 * than a preference: emitted unconditionally, every figure on the page flashes
 * every sixty seconds, and within an afternoon the operator has learned to
 * ignore the one signal that says a fill or a stop just landed.
 *
 * .flash-able IS INERT. It carries the duration, the curve, the count and the
 * fill mode but NO animation-name, so an element with it and nothing else runs
 * no animation at all: no paint, no compositing layer, no layout effect, on
 * however many figures a view happens to hold. Only the name arrives with the
 * direction class. That factoring is also what keeps the two states from being
 * a specificity argument with the marker they sit on.
 *
 * NO `to` KEYFRAME, and this is the load-bearing trick. An omitted 100% is
 * filled from the element's own non-animated value, so the tint decays back to
 * whatever colour that figure actually is -- inherited --fg on a neutral one,
 * --green on a `.pos`, --red on a `.neg` -- with no snap at the end and no
 * knowledge here of which. `@keyframes fade` above works the same way.
 *
 * COLOUR ONLY. No transform, no weight, no background: a figure that changes
 * WIDTH while you are reading a column of them is worse than no signal at all,
 * and every one of these is tabular mono precisely so the column holds still.
 * The fill mode is `none` on purpose -- if app.js ever fails to take the class
 * back off, the animation still ends on the element's real colour. */
.flash-able {
  animation-duration: var(--flash-hold);
  animation-timing-function: var(--ease-out);
  animation-iteration-count: 1;
  animation-fill-mode: none;
}
.flash-up { animation-name: flash-up; }
.flash-dn { animation-name: flash-dn; }

/* 40%: the tint is at full strength for the first ~290ms, which is long enough
   to be caught by an eye that arrived late, then spends the remaining ~430ms
   decaying. A flash with no hold at all is a frame of colour nobody sees. */
@keyframes flash-up { 0%, 40% { color: var(--flash-up); } }
@keyframes flash-dn { 0%, 40% { color: var(--flash-dn); } }

/* --- a book stopping ------------------------------------------------------
 *
 * The halt pill is `.pill.bad` sitting in the header behind a `.hidden`, and
 * `.hidden` is `display: none`. An element in display:none runs no animation,
 * and starts its animations from the beginning when it is given a box -- so an
 * animation declared here fires exactly when the pill APPEARS and never on a
 * poll that leaves it up, and no JS is involved in deciding that.
 *
 * THREE rings and then quiet. An indefinitely pulsing ring on a state that can
 * last all weekend stops being read within a minute, and the codebase already
 * has that argument written down at .pill.hot. What carries the state after
 * the rings stop is the fill -- see below. */
.pill.bad,
.pill.live {
  animation: stateIn var(--dur-slow) var(--ease-out) both,
             stateRing 900ms var(--ease-out) 3;
}

/* The halt pill was border-and-text only, exactly like `.pill.ok`. A stopped
 * book and a healthy one differed by hue alone in a 25px chip on a glass bar,
 * which is the one place in this product where a colour-only signal is least
 * acceptable -- and it is the signal that says the money has stopped moving.
 * Filled, and the only pills that are. */
.pill.bad {
  background: color-mix(in srgb, var(--red) 15%, transparent);
  border-color: color-mix(in srgb, var(--red) 60%, transparent);
  font-weight: 600;
  letter-spacing: 0.06em;
}
.pill.live {
  background: color-mix(in srgb, var(--red) 12%, transparent);
}

/* The venue card's halt box gets the same arrival, on a tab change only -- see
   the note on #view.enter above. A halt that latches between two polls lands
   without it, which the CSS cannot help: there is no element that survives the
   repaint to compare against. What carries it in that case is the same thing
   that carries it a minute later -- a red bordered box that was not there
   before, in a card that is otherwise entirely neutral. */
#view.enter .vc-halt { animation: stateIn var(--dur-slow) var(--ease-out) both; }

@keyframes stateIn {
  from { opacity: 0; transform: translateY(-3px); }
  to { opacity: 1; transform: none; }
}
@keyframes stateRing {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 50%, transparent); }
  70%  { box-shadow: 0 0 0 7px color-mix(in srgb, var(--red) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 0%, transparent); }
}

/* --- reduced motion ------------------------------------------------------
 *
 * NON-NEGOTIABLE, and the blanket rule alone does not do it. Two separate
 * failure modes had to be closed:
 *
 *   1. An animation that starts at `opacity: 0` and is given a 0.01ms duration
 *      still has to be told to FINISH rather than to be skipped, or the panel
 *      never appears. Hence the explicit resets.
 *   2. `animation-iteration-count` was never reset, so all six infinite loops
 *      kept cycling -- at 0.01ms each, which is a strobe rather than a fix.
 *      Capping it at 1 lands every one of them on its final keyframe, and
 *      every one of those final keyframes is the resting state (opacity 1, no
 *      ring, a static gradient position).
 *
 *   3. The figure flash carries its information in COLOUR -- which way the
 *      number moved -- and only the fade is the accessibility problem. Under
 *      the blanket rule alone it lasts 0.01ms with `animation-fill-mode: none`,
 *      i.e. it is deleted, and the reader who asked for less motion is the one
 *      reader who gets no signal that a position changed. It is switched to a
 *      STEP instead: same 720ms, no interpolation at any point.
 *
 * One motion cannot be reached from here and is handled in app.js instead:
 * setTab()'s window.scrollTo. It now reads the same query through
 * prefersReducedMotion() and passes behavior:'auto', and it reads it at the
 * moment of the scroll rather than latching it at boot -- a reader who changes
 * the system setting mid-session gets the new behaviour on their next tap. */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  #view.enter > *,
  #view.enter > .stack > *,
  #view.enter .rows-in > *,
  #view.enter .vc-halt,
  #view.enter .lrn-ladder li {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .pill.bad, .pill.live { animation: none; box-shadow: none; }
  /* Not `animation: none`, and not a shorter duration either. The direction
   * class is removed after one CYCLE, so the cycle has to still exist and still
   * take --flash-hold: an element with no animation never finishes one, and on
   * a screen that repaints once a minute that is a figure left tinted for a
   * minute. steps(1, end) holds the start value of a segment for the whole
   * segment, so the tint lands on the first frame, holds flat for 720ms with no
   * interpolation at any point, and the animation ends on the same schedule the
   * unreduced one does with the element back at its own colour.
   *
   * VERIFIED 2026-08-22 in headless Chrome with --force-prefers-reduced-motion:
   * animation-duration resolves to 0.72s rather than the blanket 0.01ms, the
   * timing function to steps(1), and the computed colour is still the full tint
   * at 560ms where the unreduced one is already halfway back. */
  .flash-up, .flash-dn {
    animation-duration: var(--flash-hold) !important;
    animation-timing-function: steps(1, end) !important;
  }
  body::before { display: none; }
}

/* ================================================================ portfolio
 *
 * The venue with no broker. Three books joined in the browser, so every
 * component here has one extra job the single-venue views do not: it must make
 * the SOURCE of each number visible. A combined equity figure that does not
 * say which venues it came from is worse than three separate ones, because it
 * looks authoritative.
 *
 * The venue colour (blue/purple/cyan) is load-bearing rather than decorative:
 * it is the only thing tying a row in the blotter to a slice of the donut to a
 * card on the overview. It is used as a 6px dot, never as a fill, so it stays
 * legible against a colour-blind reader's palette — the dot is positional and
 * the label always sits beside it. */

/* --- liveness strip ------------------------------------------------------
 *
 * Three venues, always all three, even when one has never published. An absent
 * venue rendering as an absent row is how "the crypto machine is dead" reads
 * identically to "there is no crypto venue". */

.vstrip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px;
}
.vs {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg-1);
}
.vs > i { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.vs > div { min-width: 0; flex: 1; }
.vs-n { font-size: var(--t-12); font-weight: 500; }
.vs-s { font-size: var(--t-11); color: var(--fg-3); margin-top: 1px; }
/* State is carried by the left border as well as the dot. A colour-only signal
 * on a status strip is the one place it is least acceptable. */
.vs.good { box-shadow: inset 2px 0 0 var(--green); }
.vs.warn { box-shadow: inset 2px 0 0 var(--amber); }
.vs.bad { box-shadow: inset 2px 0 0 var(--red); }
.vs.idle { opacity: 0.6; }

/* --- venue cards --------------------------------------------------------- */

.vgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
  margin-top: 14px;
}
.vcard {
  display: flex;
  flex-direction: column;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-1);
  transition: border-color var(--dur-base) var(--ease-out), background var(--dur-base) var(--ease-out);
}
.vcard:hover { border-color: var(--border-hi); background: var(--bg-2); }
.vcard.off { opacity: 0.55; }
.vc-eq {
  font-size: var(--t-22);
  font-weight: 600;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  margin: 12px 0 2px;
}
/* Reserved height whether or not there is a curve. Cards that grow a chart the
 * moment a venue closes its first trade make the whole row jump. */
.vc-spark { height: 38px; margin: 4px 0 10px; }
.vc-nospark {
  height: 38px;
  display: flex;
  align-items: center;
  font-size: var(--t-11);
  color: var(--fg-3);
}
.vc-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  font-size: var(--t-12);
  color: var(--fg-2);
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.vc-halt {
  margin-top: 10px;
  padding: 7px 9px;
  border-radius: var(--radius-sm);
  font-size: var(--t-12);
  color: var(--red);
  background: color-mix(in srgb, var(--red) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--red) 26%, transparent);
}

/* --- the watchdog grid ---------------------------------------------------
 *
 * Who is watching whom, drawn as three boxes each listing the other two. The
 * shape is the argument: you can see at a glance that every venue has two
 * independent observers, which is the property that makes a single machine
 * death impossible to miss. */

.wdgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 12px;
}
.wd {
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-1);
}
.wd-peers { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.wd-peer {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  align-items: baseline;
  padding: 6px 9px;
  border-radius: var(--radius-sm);
  background: var(--bg-2);
  font-size: var(--t-12);
}
.wd-peer.good { box-shadow: inset 2px 0 0 var(--green); }
.wd-peer.warn { box-shadow: inset 2px 0 0 var(--amber); }
.wd-peer.bad { box-shadow: inset 2px 0 0 var(--red); }

/* --- misc ---------------------------------------------------------------- */

/* Errors, in a table cell, where a <ul>'s default padding would push the text
 * out of the column. */
ul.errs { margin: 0; padding-left: 15px; font-size: var(--t-12); color: var(--amber); }
ul.errs li { margin: 1px 0; }

/* A donut that has nothing to draw still occupies the donut's space, so the
 * two sit side by side instead of the second one jumping left. */
.donut-empty {
  width: 150px;
  height: 150px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Row-striped tables, for the long ones: the blotter and the combined position
 * list run to hundreds of rows, and at that length tracking a row across ten
 * columns needs more than a hairline. */
/* color-mix off --fg, not a white literal. `rgba(255,255,255,0.017)` is 1.7%
   white: in the LIGHT theme -- the one every page actually ships with -- that
   is white on white and the stripe did not exist at all. The single affordance
   built for the longest table in the product was absent in the only theme
   anyone opens. 4% of --fg resolves to a light stripe on black and a dark one
   on white, and is the same perceived step in both. */
table.rows tbody tr:nth-child(even) > td { background: color-mix(in srgb, var(--fg) 4%, transparent); }
@media (hover: hover) {
  table.rows tbody tr:hover > td { background: var(--bg-2); }
}
td .dot { display: inline-block; margin-right: 6px; vertical-align: middle; }
td.mono.b { font-weight: 500; }

/* Live money. The one chip that must never be mistaken for any other, so it is
 * the only one that is filled rather than outlined. */
.chip.live {
  border-color: color-mix(in srgb, var(--green) 45%, transparent);
  background: color-mix(in srgb, var(--green) 18%, transparent);
  color: var(--green);
  font-weight: 600;
}
.chip.ok { border-color: color-mix(in srgb, var(--green) 34%, transparent); color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
.chip.bad { border-color: color-mix(in srgb, var(--red) 30%, transparent); color: var(--red); background: color-mix(in srgb, var(--red) 9%, transparent); }

/* Prose inside a panel body needs its own top margin when it follows a chart
 * or a table; the note block was written for panels that end in text. */
p.note-p:first-child { margin-top: 0; }
p.note-p.neg { color: var(--red); }
/* The grid carries a top margin because it usually follows the liveness strip.
 * On Overview it is the first thing in the body and the margin is a gap. */
.vgrid.top { margin-top: 0; }

/* A range of outcomes with zero marked, for the concurrency table. The point
 * of the graphic is that some rows straddle zero and some do not, which a pair
 * of numbers in a column does not communicate at a glance. */
.span { position: relative; height: 16px; min-width: 120px; }
.span > i { position: absolute; top: 5px; height: 6px; border-radius: 3px; }
.span > i.zero {
  top: 1px; width: 1px; height: 14px; border-radius: 0;
  background: var(--border-hi);
}
.span > i.bar.g { background: color-mix(in srgb, var(--green) 62%, transparent); }
.span > i.bar.r { background: color-mix(in srgb, var(--red) 62%, transparent); }

/* ------------------------------------------------------------- Virtus-Fin */

/* The formula, set as the thing it is: a definition, not decoration. Shown on
   the page rather than only in the source, because a conviction score nobody
   can read the definition of is exactly the black box this whole feature is
   arguing against. */
.formula {
  padding: 11px 13px;
  border-radius: 7px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-left: 2px solid var(--blue);
  font-size: var(--t-12);
  line-height: 1.55;
  color: var(--fg-2);
  overflow-x: auto;
  white-space: nowrap;
}

/* The formula at display size, for the tab that exists to show it.
 *
 * Wraps by TERM and never mid-term. The first version of this was one nowrap
 * line, which clipped the cost term off the right edge at every width a phone
 * has -- and an equation missing its last term is worse than one on two lines,
 * because nothing tells the reader it was cut. */
.formula-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px 9px;
  font-family: var(--mono);
  font-size: clamp(13px, 2.1vw, 19px);
  line-height: 1.35;
  letter-spacing: -0.01em;
  padding: 18px 0 16px;
}
.formula-hero > span { white-space: nowrap; }
.formula-hero .term { color: var(--fg); font-weight: 500; }
.formula-hero .wt { color: var(--blue); }
.formula-hero .op { color: var(--fg-3); }
.formula-hero .cost { color: var(--red); }
.formula-hero .zero { color: var(--fg-3); opacity: 0.55; }

/* The same equation a second time with this venue's live numbers in it. Smaller,
   because it is the instance and the line above is the definition. */
.formula-live {
  font-family: var(--mono);
  font-size: var(--t-12);
  line-height: 1.7;
  color: var(--fg-2);
  padding: 12px 13px;
  border-radius: 7px;
  background: var(--bg-2);
  border: 1px solid var(--border);
  overflow-x: auto;
}

/* Distance to the promotion bar, as a bar. A count on its own ("60 of 120")
   does not communicate "a bit over halfway" nearly as fast. */
.promo { display: grid; gap: 7px; }
.promo .rail {
  position: relative;
  height: 8px;
  border-radius: 4px;
  background: var(--bg-3);
  overflow: hidden;
}
.promo .rail > i { display: block; height: 100%; background: var(--blue); }
.promo .rail > i.done { background: var(--green); }
.promo .ticks { display: flex; justify-content: space-between; font-size: var(--t-11); color: var(--fg-3); }

/* Five of these sit side by side in a row, so they are shorter and tighter
   than a normal track -- the eye reads the SHAPE across the row, not any one
   bar's exact width. */
.track.vf {
  height: 4px;
  min-width: 44px;
  background: var(--bg-3);
}
.track.vf > i.dim { background: var(--fg-3); opacity: 0.45; }

/* A heading inside a panel body, for panels that carry more than one table. */
h3.sub {
  margin: 20px 0 9px;
  font-size: var(--t-11);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--fg-3);
}
h3.sub:first-child { margin-top: 0; }

/* ================================================================= learning
 *
 * The Learning page has one job that no other view in this product has: it
 * reports on evidence that mostly DOES NOT EXIST YET. Every other surface here
 * shows a number that is true now -- equity, exposure, a fill. This one shows
 * how far the bot is from being entitled to an opinion, and it is on screen
 * for months before it has one.
 *
 * So the whole vocabulary is built around one rule, and it is the rule the
 * rest of the file already states in prose: ABSENCE IS RENDERED AS ABSENCE.
 * Not as a zero, not as a flat line, not as an amber warning, and above all
 * not as a bar sitting near empty that looks like a thing going badly. "Not
 * enough evidence yet" is a first-class state with its own deliberate
 * treatment, and it is the state these components spend most of their life in.
 *
 * The three moves that carry it, used consistently across all seven blocks:
 *
 *   BROKEN vs SOLID. A dashed or hatched mark means "no reading here". A solid
 *   one means "measured". This is structural, not chromatic, so it survives
 *   both themes, a monochrome screen and a colour-blind reader -- and it means
 *   is-none and is-flat, which are the two states most easily confused, differ
 *   in SHAPE and not only in hue.
 *
 *   COLOUR ONLY WHEN EARNED. Green appears when a threshold is actually
 *   reached, red when something is actually degrading. Nothing in progress is
 *   coloured, because a coloured partial bar reads as a verdict.
 *
 *   AMBER IS NOT USED HERE except for a threshold rule. In this stylesheet
 *   amber means WARNING (.pill.warn, .chip.warn, .ticket .warn-box), and a
 *   limitation is not a warning. .lrn-caveat is deliberately the same grey as
 *   every other explanatory line in the product.
 *
 * Everything below is checked at 360px and in both themes. Every colour is a
 * token or a color-mix off one, so nothing needs a second definition.
 */

/* --- the verdict ---------------------------------------------------------
 *
 * The honest headline. One of four states, and the default -- the one it will
 * hold for months -- is `is-none`.
 *
 * is-none is NOT dimmed, NOT amber, and carries no glyph. Its headline sits at
 * full --fg weight and full --fg colour, exactly like the other three, because
 * "there is not enough evidence yet" is a finding stated plainly and not an
 * apology or a fault. What distinguishes it is the RAIL: broken instead of
 * solid, which says at a glance that the reading is incomplete rather than
 * that something is wrong. A dimmed, greyed, apologetic empty state is how a
 * product signals that a number failed to load, and this number did not fail
 * to load -- it does not exist yet, on purpose, and it will not exist for a
 * known number of trades. */
.lrn-verdict {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 15px 16px 15px 20px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-1);
  box-shadow: var(--e1);
}
.lrn-verdict::before {
  content: "";
  position: absolute;
  inset: 12px auto 12px 0;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--fg-3);
}
/* The broken rail. 5px of mark to 5px of gap reads as "interrupted" at a
   glance and still reads as a rail rather than as a row of dots. */
.lrn-verdict.is-none::before {
  background: repeating-linear-gradient(180deg,
    var(--fg-3) 0 5px, transparent 5px 10px);
}
.lrn-verdict.is-learning::before { background: var(--blue); }
.lrn-verdict.is-learning { background: color-mix(in srgb, var(--blue) 6%, transparent); }
/* Flat is a RESULT, not a failure to produce one: the gate was measured and it
   does nothing. Solid rail, neutral colour, no tint -- the distinction from
   is-none is the solid rail, which is exactly the distinction being made. */
.lrn-verdict.is-flat::before { background: var(--fg-2); }
.lrn-verdict.is-degrading::before { background: var(--red); }
.lrn-verdict.is-degrading {
  background: color-mix(in srgb, var(--red) 7%, transparent);
  border-color: color-mix(in srgb, var(--red) 30%, transparent);
}

.lrn-verdict-k {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.lrn-verdict-line {
  margin: 0;
  font-size: var(--t-16);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.3;
  color: var(--fg);
  text-wrap: balance;
}
.lrn-verdict.is-degrading .lrn-verdict-line { color: var(--red); }
.lrn-verdict-sub {
  margin: 0;
  font-size: var(--t-12);
  line-height: 1.55;
  color: var(--fg-2);
  max-width: 66ch;
}
/* Numbers inside the sub-line stay tabular even in a sentence: "18 of 60" and
   "42 more" are figures a person compares between visits. */
.lrn-verdict-sub b {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--fg);
}

/* --- distance to an opinion ----------------------------------------------
 *
 * NOT a progress bar, and the difference is the whole design. A progress bar
 * says "this is going well, it is 30% done". This says "there is not enough
 * here yet, and here is exactly how much is missing", which is a different
 * sentence and has to look like one.
 *
 * Three things stop it reading as success at 30%:
 *
 *   1. The REMAINDER IS DRAWN, not left empty. The unfilled run carries a
 *      hatch, so it is a positive object -- the work still to do -- rather
 *      than the absence of a filled bar. An empty trough reads as a bar that
 *      has not got very far; a hatched one reads as a quantity outstanding.
 *   2. The FILL IS GREY. It is --fg-3, the same value as the caption above it,
 *      and it stays grey until the threshold is actually met. Any colour on a
 *      partial fill is a verdict delivered early. It turns green on .is-met
 *      and only on .is-met.
 *   3. The FOOT STATES THE GAP IN WORDS. The bar is the shape of the answer;
 *      the sentence under it is the answer. "42 more before this can be read"
 *      cannot be misread as 30% of anything. */
.lrn-progress { display: flex; flex-direction: column; gap: 6px; }
.lrn-progress-cap,
.lrn-progress-foot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  font-size: var(--t-11);
  color: var(--fg-3);
  min-width: 0;
}
.lrn-progress-cap > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.lrn-progress-cap b,
.lrn-progress-foot b {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--fg-2);
  white-space: nowrap;
}
.lrn-progress-rail {
  position: relative;
  height: 12px;
  border-radius: 6px;
  overflow: hidden;
  background-color: var(--bg-2);
  background-image: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--fg) 9%, transparent) 0 2px,
    transparent 2px 7px);
}
.lrn-progress-rail > i {
  position: absolute;
  inset: 0 auto 0 0;
  display: block;
  border-radius: 6px;
  background: var(--fg-3);
  transition: width var(--dur-data) var(--ease-out);
}
/* The threshold rule, when the caller asks for one. Same treatment as the one
   on .tb-track: the highest-contrast mark in the control, because everything
   else on the rail is read relative to it. The 1px ground-coloured ring keeps
   it visible where the fill has already passed it. */
.lrn-progress-rail > u {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--at, 100%);
  width: 2px;
  margin-left: -1px;
  background: var(--amber);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--bg) 65%, transparent);
}
/* Reached. This is the ONE state that gets colour, and the hatch goes with it:
   there is nothing outstanding left to draw. */
.lrn-progress.is-met .lrn-progress-rail { background-image: none; }
.lrn-progress.is-met .lrn-progress-rail > i { background: var(--green); }
.lrn-progress.is-met .lrn-progress-foot { color: var(--green); }
.lrn-progress.is-met .lrn-progress-foot b { color: var(--green); }

/* --- one gate ------------------------------------------------------------
 *
 * Name, sample size, clusters against the floor, verdict, mean R. The columns
 * are fixed-width on the right so a column of R figures is scannable as a
 * column; the name is the only elastic one.
 *
 * .is-thin is the important modifier. A mean R computed on n=4 and one
 * computed on n=400 are the same glyphs in the same column, and the whole
 * point of this page is that they are not the same claim -- so below the
 * sample floor the R figure drops to --fg-3 and the row's verdict chip goes to
 * .chip.idle. The number is still SHOWN, because hiding it would be its own
 * kind of dishonesty; it is shown at the weight its evidence earns. */
.lrn-gate {
  display: grid;
  /* The name column is CAPPED, not proportional -- the same lesson as .tb-row.
     At 1.6fr a 1400px panel gave five gate names half the screen and pushed
     every cluster bar, the thing the eye is actually comparing down the
     column, off to the right. */
  grid-template-columns: minmax(150px, 420px) 58px minmax(74px, 1fr) auto 66px;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  transition: background var(--dur-fast) var(--ease-out);
}
.lrn-gate:last-child { border-bottom: 0; }
@media (hover: hover) {
  .lrn-gate:hover { background: var(--bg-1); }
}
/* The column heads. Same grid, no rule of its own, and it is a caption row --
   it must never look like a gate with no data. */
.lrn-gate.is-head {
  padding-top: 6px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
  font-size: var(--t-10);
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.lrn-gate.is-head:hover { background: none; }
.lrn-gate.is-head .lrn-gate-n,
.lrn-gate.is-head .lrn-gate-r,
.lrn-milestone.is-head .lrn-ms-have,
.lrn-milestone.is-head .lrn-ms-need,
.lrn-milestone.is-head .lrn-ms-eta {
  font-family: inherit;
  font-size: inherit;
  font-variant-numeric: normal;
  letter-spacing: inherit;
  color: inherit;
}

.lrn-gate-name { min-width: 0; font-size: var(--t-12); color: var(--fg); }
.lrn-gate-name .sub {
  margin-top: 2px;
  font-size: var(--t-11);
  line-height: 1.45;
  color: var(--fg-3);
  max-width: 46ch;
}
.lrn-gate-n,
.lrn-gate-r {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-12);
  letter-spacing: -0.02em;
  text-align: right;
  white-space: nowrap;
}
.lrn-gate-n { color: var(--fg-2); }
.lrn-gate-verdict { justify-self: start; }
.lrn-gate.is-thin .lrn-gate-r { color: var(--fg-3); }
.lrn-gate.is-thin .lrn-gate-n { color: var(--fg-3); }

/* Clusters against the floor, as a shape. "12" on its own does not say whether
   12 is close; a short bar with the floor drawn on it does. */
.lrn-gate-clusters { display: flex; align-items: center; gap: 8px; min-width: 0; }
.lrn-gate-clusters .rail {
  position: relative;
  flex: 1;
  min-width: 44px;
  height: 6px;
  border-radius: 3px;
  overflow: hidden;
  background-color: var(--bg-2);
  background-image: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--fg) 9%, transparent) 0 2px,
    transparent 2px 7px);
}
.lrn-gate-clusters .rail > i {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: 3px;
  background: var(--fg-3);
  transition: width var(--dur-data) var(--ease-out);
}
.lrn-gate-clusters .rail > i.met { background: var(--green); }
.lrn-gate-clusters .v {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-11);
  color: var(--fg-3);
  white-space: nowrap;
}

/* Every child is placed EXPLICITLY at the reflow. With only some of them
 * placed the rest auto-flow into whatever cell is free next, which is how the
 * weekly goal row ended up with its percentage stranded under the progress bar
 * -- see the note at .goal-row.
 *
 * The head row is removed rather than reflowed: once the columns are gone its
 * captions no longer sit above the things they name, and a caption pointing at
 * the wrong cell is worse than no caption. Which means every cell has to be
 * self-describing on its own -- "n = 41", "12/60 clusters", "+0.18R" -- and
 * the markup contract in CSS-ADDITIONS.md says so. */
@media (max-width: 620px) {
  .lrn-gate {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 6px 10px;
    padding: 11px 13px;
  }
  .lrn-gate.is-head { display: none; }
  .lrn-gate-name { grid-column: 1; grid-row: 1; }
  .lrn-gate-verdict { grid-column: 2; grid-row: 1; justify-self: end; }
  .lrn-gate-clusters { grid-column: 1 / -1; grid-row: 2; }
  .lrn-gate-n { grid-column: 1; grid-row: 3; text-align: left; }
  .lrn-gate-r { grid-column: 2; grid-row: 3; }
}

/* --- the four stages -----------------------------------------------------
 *
 * collecting -> fitted -> tested -> live. The steps NOT yet reached have to be
 * unmistakably not-yet-reached, because the failure mode of every stage
 * indicator ever built is that it looks like four things that all happened.
 *
 * Not-reached is carried three ways at once: a HOLLOW node, a BROKEN
 * connector, and text at --fg-3. Reached is a filled node and a solid
 * connector. The current step additionally gets a halo ring and full --fg text
 * at weight 600. None of that is hue, so the whole control still reads with
 * the colour removed -- which matters more here than anywhere else on the
 * page, because "have we shipped this yet" is the question the operator is
 * actually asking.
 *
 * The connector belongs to the step it leads INTO, so it is solid on .is-done
 * and .is-now and broken on everything after. */
.lrn-ladder {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
.lrn-ladder li {
  position: relative;
  padding: 22px 2px 0;
  min-width: 0;
  text-align: center;
  font-size: var(--t-10);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-3);
}
/* The ladder assembles left to right on a tab change: it is a path, and a path
   that draws itself in the order it is walked says which end is the start.
   Scoped under #view.enter for the reason given in the motion section. */
#view.enter .lrn-ladder li { animation: rowIn var(--dur-base) var(--ease-out) both; }
#view.enter .lrn-ladder li:nth-child(2) { animation-delay: calc(var(--stagger) * 1); }
#view.enter .lrn-ladder li:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
#view.enter .lrn-ladder li:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
/* The node. --bg fill rather than transparent so the connector passing behind
   it is covered and the hollow ring reads as hollow. */
.lrn-ladder li::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 50%;
  margin-left: -6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--fg-3);
  background: var(--bg);
  opacity: 0.6;
  z-index: 1;
}
/* Centre-to-centre with the previous node: every column is 1fr, so -50% to
   +50% of one column is exactly one gap between two nodes. */
.lrn-ladder li::before {
  content: "";
  position: absolute;
  top: 8px;
  right: 50%;
  left: -50%;
  height: 2px;
  background: repeating-linear-gradient(90deg,
    var(--fg-3) 0 3px, transparent 3px 7px);
  opacity: 0.5;
}
.lrn-ladder li:first-child::before { display: none; }

.lrn-ladder li.is-done { color: var(--fg-2); }
.lrn-ladder li.is-done::after {
  background: var(--fg-2);
  border-color: var(--fg-2);
  opacity: 1;
}
.lrn-ladder li.is-done::before,
.lrn-ladder li.is-now::before {
  background: var(--fg-2);
  opacity: 1;
}
.lrn-ladder li.is-now { color: var(--fg); font-weight: 600; }
.lrn-ladder li.is-now::after {
  background: var(--blue);
  border-color: var(--blue);
  opacity: 1;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 22%, transparent);
}

/* Four uppercase words across a 360px screen is 90px a column, and
   "COLLECTING" at --t-10 with 0.06em tracking does not fit in it. */
@media (max-width: 430px) {
  .lrn-ladder li { font-size: var(--t-9); letter-spacing: 0.04em; padding-top: 20px; }
  .lrn-ladder li::after { top: 2px; }
  .lrn-ladder li::before { top: 7px; }
}

/* --- a milestone ---------------------------------------------------------
 *
 * What is being waited for, what there is, what is needed, and when it will
 * arrive. The ETA is the cell that has to be handled honestly: it is a
 * projection off a rate, and a rate needs its own history before it exists.
 *
 * So .is-unknown drops the mono voice entirely. Every computed figure in this
 * product is tabular monospace; an ETA that reads "not enough history to
 * project" in the same face as "6 weeks" is claiming a precision it does not
 * have. Different face, --fg-3, and it is the only cell in the row that is
 * allowed to be a sentence. */
.lrn-milestone {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 52px 56px minmax(88px, auto);
  align-items: baseline;
  gap: 10px;
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
}
.lrn-milestone:last-child { border-bottom: 0; }
.lrn-milestone.is-head {
  padding-top: 6px;
  padding-bottom: 6px;
  font-size: var(--t-10);
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.lrn-ms-what { min-width: 0; font-size: var(--t-12); color: var(--fg); }
.lrn-ms-what .sub {
  margin-top: 2px;
  font-size: var(--t-11);
  line-height: 1.45;
  color: var(--fg-3);
  max-width: 52ch;
}
.lrn-ms-have,
.lrn-ms-need,
.lrn-ms-eta {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-12);
  letter-spacing: -0.02em;
  text-align: right;
  white-space: nowrap;
}
.lrn-ms-have { color: var(--fg); }
.lrn-ms-need { color: var(--fg-3); }
.lrn-ms-eta { color: var(--fg-2); }
.lrn-ms-eta.is-unknown {
  font-family: var(--sans);
  font-variant-numeric: normal;
  letter-spacing: -0.008em;
  font-size: var(--t-11);
  color: var(--fg-3);
  white-space: normal;
  text-align: right;
}
.lrn-milestone.is-met .lrn-ms-have { color: var(--green); }
.lrn-milestone.is-met .lrn-ms-eta { color: var(--green); }

@media (max-width: 560px) {
  .lrn-milestone { grid-template-columns: auto auto minmax(0, 1fr); gap: 5px 12px; }
  .lrn-milestone.is-head { display: none; }
  .lrn-ms-what { grid-column: 1 / -1; grid-row: 1; }
  .lrn-ms-have { grid-column: 1; grid-row: 2; text-align: left; }
  .lrn-ms-need { grid-column: 2; grid-row: 2; text-align: left; }
  .lrn-ms-eta  { grid-column: 3; grid-row: 2; }
}

/* --- a limitation --------------------------------------------------------
 *
 * A caveat is INFORMATION. It is the sentence that says what this measurement
 * cannot see, and it belongs next to the measurement for the same reason an
 * error bar does.
 *
 * Which is why it is deliberately NOT amber and NOT red and carries no glyph
 * and no tinted box. In this stylesheet amber means warning -- .pill.warn,
 * .chip.warn, .ticket .warn-box -- and a page whose every honest limitation is
 * dressed as a warning teaches the operator that honesty is an alarm, so they
 * stop reading them. It is the same --fg-3 as every other explanatory line in
 * the product, set off by a neutral rule rather than by a colour. */
.lrn-caveats {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}
.lrn-caveat {
  margin: 0;
  padding-left: 11px;
  border-left: 2px solid var(--border-hi);
  font-size: var(--t-12);
  line-height: 1.55;
  color: var(--fg-3);
}
.lrn-caveat em { font-style: normal; color: var(--fg-2); }
.lrn-caveat b {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  color: var(--fg-2);
}
/* For the one caveat on a page that changes how the number above should be
   read at all. Still not a warning: it is louder by a single step of grey. */
.lrn-caveat.is-strong { color: var(--fg-2); border-left-color: var(--fg-3); }

/* --- an inline series ----------------------------------------------------
 *
 * A time series with a baseline. The baseline is an HTML sibling of the SVG
 * and not a <line> inside it, for the same reason .cw-ax is: the viewBox is
 * scaled non-uniformly to fill its box, so anything inside it is stretched by
 * whatever ratio the window happens to be at. The same trap is why every
 * stroke below carries vector-effect: non-scaling-stroke -- without it a 2px
 * line renders 2px tall and 6px wide on a wide panel.
 *
 * The plot keeps its height whether or not there is anything to draw. A chart
 * that appears the moment the first month closes makes the whole page jump,
 * and .lrn-series-none is hatched rather than blank so the empty plot reads as
 * reserved space rather than as a chart that failed to render. */
.lrn-series { margin: 0; display: flex; flex-direction: column; gap: 8px; }
.lrn-series-cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 10px;
}
.lrn-series-cap > :not(.label) { font-size: var(--t-11); color: var(--fg-3); }
.lrn-series-plot {
  position: relative;
  width: 100%;
  height: 150px;
  overflow: hidden;
}
@media (max-width: 480px) { .lrn-series-plot { height: 120px; } }

.lrn-series-svg { display: block; width: 100%; height: 100%; overflow: hidden; }
.lrn-series-svg .grid { stroke: var(--grid); stroke-width: 1; vector-effect: non-scaling-stroke; }
.lrn-series-svg .band { fill: color-mix(in srgb, var(--blue) 13%, transparent); stroke: none; }
.lrn-series-svg .line {
  fill: none;
  stroke: var(--blue);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
/* A segment whose points are below the sample floor is drawn broken. Same
   grammar as everywhere else on this page: dashed means "do not read this as
   a measurement", and it costs no colour to say. */
.lrn-series-svg .line.thin { stroke-dasharray: 4 4; opacity: 0.75; }
.lrn-series-svg .pt { fill: var(--blue); }
.lrn-series-svg .pt.thin {
  fill: var(--bg);
  stroke: var(--blue);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}

/* Zero, or whatever the caller's baseline is. Dashed and neutral so it is
   never mistaken for the series itself. */
.lrn-series-base {
  position: absolute;
  left: 0;
  right: 0;
  top: var(--base, 50%);
  height: 0;
  border-top: 1px dashed var(--fg-3);
  opacity: 0.85;
  pointer-events: none;
}
.lrn-series-base > b {
  position: absolute;
  right: 4px;
  top: 0;
  transform: translateY(-50%);
  padding: 0 3px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
  font-size: var(--t-9);
  line-height: 1.4;
  letter-spacing: -0.02em;
  color: var(--fg-3);
  /* Nearly opaque, for the same reason .cw-ax span is: a point sitting on the
     baseline lands directly under this chip. */
  background: color-mix(in srgb, var(--bg) 86%, transparent);
  border-radius: 3px;
}
.lrn-series-foot {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-9);
  letter-spacing: 0.02em;
  color: var(--fg-3);
}
.lrn-series-none {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 12px;
  text-align: center;
  font-size: var(--t-12);
  color: var(--fg-3);
  background-image: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--fg) 6%, transparent) 0 2px,
    transparent 2px 9px);
  border-radius: var(--radius-sm);
}

/* ==========================================================================
 * 2026-08-23 — DENSITY, HIERARCHY, AND A PHONE THAT IS NOT A LIST OF CARDS
 *
 * Rendered at 390px and counted: the Overview spent FOUR full screens on four
 * numbers. Open positions, Closed trades, Expectancy and Profit factor each got
 * a full-width bordered card ~100px tall to hold a value like "0" or "1/3" --
 * because `.g4` collapsed to a single column at 560px and `.g3`/`.g2` at 860px.
 *
 * Every one of those cards also carried the same border, radius, background and
 * elevation as the panels around them, so a screen of twelve elements had
 * exactly one visual plane and no hierarchy at all. An instrument panel whose
 * every element shouts equally is a list, and the operator reads it top to
 * bottom every time instead of jumping to the number they came for.
 *
 * Three changes, in order of how much they matter:
 *   1. two columns on a phone, not one. Four numbers is one screen-third.
 *   2. tiles drop to a LOWER plane than panels, so the page has a foreground
 *      and a background rather than twelve equal cards.
 *   3. the numbers get bigger and the labels get smaller, which is the whole
 *      of the hierarchy inside a tile.
 * ====================================================================== */

/* --- 1. density ---------------------------------------------------------- */

/* Overrides the three collapse rules above. Deliberately NOT `1fr` anywhere:
 * a phone is 390px wide, a tile needs about 150px to hold a label and a
 * six-character number, and two of them fit with room to spare. */
@media (max-width: 860px) {
  .g2, .g3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .g4, .g3, .g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Three-up and four-up rows both become two-up, so a fifth and sixth tile
   * wrap onto a second row rather than becoming two more full-width slabs. */
  .grid { gap: 8px; }
}
/* Under about 340px two columns genuinely stop fitting -- an iPhone SE in
 * landscape-locked accessibility text is the real case. One column there is
 * correct rather than a fallback. */
@media (max-width: 336px) {
  .g4, .g3, .g2 { grid-template-columns: 1fr; }
}

/* --- 2. tiles sit BELOW panels ------------------------------------------- */

@media (max-width: 860px) {
  .metric {
    padding: 10px 12px;
    border-radius: var(--radius-sm);
    /* No border and a flatter ground: the tile is a reading, not a container.
     * The panel it sits in is what has edges. */
    border-color: transparent;
    background: var(--bg-1);
  }
  .metric::before { display: none; }   /* the sheen belongs to real surfaces */
  .metric .label,
  .metric > .label {
    font-size: 10px;
    letter-spacing: 0.055em;
    color: var(--fg-3);
  }
  /* The number is the point of the tile and it was competing with its own
   * caption for weight. */
  .metric .num,
  .metric .v {
    font-size: clamp(19px, 5.4vw, 26px);
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.15;
  }
  .metric .sub,
  .metric .cap { font-size: 10.5px; line-height: 1.35; }
}

/* --- 3. panels tighten, and stop stacking their own chrome ---------------- */

@media (max-width: 640px) {
  .panel > header {
    padding: 10px 12px;
    min-height: 0;
  }
  .panel > header h2 { font-size: 13px; }
  /* The hint is the first thing to go. On a 390px header it wrapped onto its
   * own line under the title and doubled the height of every panel on screen
   * to carry a parenthetical. */
  .panel > header .hint {
    font-size: 10.5px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: 46%;
  }
  .panel > .body { padding: 12px; }
  .panel > .body.flush { padding: 0; }
  main { padding: 10px 10px 96px; }
  .stack { gap: 10px; }
}

/* An empty chart reserving its full height to say "not enough history yet" is
 * 120px of nothing on a screen that is 780px tall. It keeps a shape -- a
 * collapsing panel makes the page jump when data arrives -- but a much smaller
 * one, because the sentence is the content until then. */
@media (max-width: 640px) {
  .empty-state { min-height: 56px; padding: 14px 12px; font-size: 12px; }
}

/* ==========================================================================
 * THE PHONE TAB CONTROL
 *
 * `nav.tabs` is a horizontal scroller with `scrollbar-width: none`. At 390px it
 * shows five of sixteen tabs and gives no indication the other eleven exist --
 * no scrollbar, no edge fade, and no fade is possible on that element because
 * it is translucent glass and a mask-image dissolves the hairline under it.
 *
 * So below 720px the strip is replaced rather than decorated. One control,
 * showing where you are, opening the whole list at once. That is also 44px of
 * vertical chrome returned to the page on the surface that has the least of it.
 * ====================================================================== */

.tab-pick { display: none; }

@media (max-width: 720px) {
  nav.tabs { display: none; }

  .tab-pick {
    display: flex;
    align-items: center;
    gap: 8px;
    width: calc(100% - 20px);
    margin: 6px 10px 2px;
    padding: 0 12px;
    height: 40px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-1);
    color: var(--fg);
    font: 600 14px/1 var(--sans);
    letter-spacing: -0.01em;
    text-align: left;
    transition: background var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out),
                transform var(--dur-press) var(--ease-press);
  }
  .tab-pick:active { transform: scale(0.985); }
  .tab-pick[aria-expanded="true"] { background: var(--bg-2); border-color: var(--border-hi); }
  .tp-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* A caret rather than a chevron glyph: it inherits colour, needs no font, and
   * rotating it is the whole of the open/closed animation. */
  .tp-caret {
    width: 7px; height: 7px; flex: none;
    border-right: 1.6px solid var(--fg-3);
    border-bottom: 1.6px solid var(--fg-3);
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--dur-base) var(--ease-out);
  }
  .tab-pick[aria-expanded="true"] .tp-caret { transform: translateY(1px) rotate(-135deg); }
}

/* --- the sheet ----------------------------------------------------------- */

.tab-sheet {
  width: min(560px, 100%);
  max-height: 82vh;
  overflow-y: auto;
  padding: 6px;
  -webkit-overflow-scrolling: touch;
}
.tab-sheet h3 { margin: 0; padding: 12px 12px 8px; font-size: 15px; }
.ts-group {
  padding: 14px 12px 6px;
  font: 600 10px/1 var(--sans);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--fg-3);
}
.ts-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
@media (max-width: 380px) { .ts-list { grid-template-columns: 1fr; } }

.ts-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 12px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--bg-1);
  color: var(--fg-2);
  font: 500 13.5px/1 var(--sans);
  text-align: left;
  width: 100%;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-press) var(--ease-press);
}
.ts-item:active { transform: scale(0.98); }
.ts-item[aria-current="true"] {
  background: var(--bg-3);
  border-color: var(--border-hi);
  color: var(--fg);
  font-weight: 600;
}
/* The dot is the only ornament, and it exists because the current row and a
 * merely-hovered row would otherwise differ by a background alone -- which on
 * a touch screen latches and makes the last thing tapped look selected. */
.ts-item > i {
  width: 5px; height: 5px; border-radius: 50%; flex: none;
  background: var(--fg-3);
  transition: background var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.ts-item[aria-current="true"] > i {
  background: var(--blue);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue) 22%, transparent);
}
.ts-item .badge { margin-left: auto; }

@media (prefers-reduced-motion: reduce) {
  .tab-pick, .tp-caret, .ts-item, .ts-item > i { transition: none; }
}

/* ==========================================================================
 * THE PHONE'S TWO SPACE HOGS
 *
 * Measured on a 390x1330 render of the Overview: the NAV hero took 230px and
 * the weekly goal 450px, together more than half the screen -- to show one
 * number, an empty chart reading "not enough history yet", and three venue rows
 * two of which say "Coming soon". Everything an operator actually opens the
 * phone for was below the fold.
 *
 * Neither is wrong on a desktop, where there is room for a 320px hero with a
 * curve in it. Both are wrong at 390px, and this is the width the operator
 * actually uses. */

@media (max-width: 640px) {
  .hero {
    height: auto;
    padding: 14px 14px 16px;
  }
  .hero .nav-value { font-size: clamp(34px, 11vw, 46px); }
  .hero .delta { margin-top: 6px; gap: 6px 10px; font-size: 11.5px; }
  /* The sparkline keeps its place when it has a curve to draw and gives the
   * space back when it does not. A reserved 76px band that only ever says "not
   * enough history yet" is the chart claiming a shape it has not earned. */
  .hero .spark { flex-basis: 100%; min-width: 0; height: 58px; }
  .hero .spark .cw { height: 58px; }
  .hero .spark:has(.empty-state) { display: none; }
}

/* The goal rows carry a venue, a broker, a progress bar, a percentage and a
 * status chip. On a phone the broker line and the chip are the two that can go:
 * the venue names its own broker everywhere else in the product, and "Coming
 * soon" beside an em dash is the same sentence twice. */
@media (max-width: 640px) {
  .goal-row {
    padding: 10px 12px;
    gap: 8px;
  }
  .goal-venue .dim { display: none; }
  .goal-venue b { font-size: 13px; }
  .goal-mid { gap: 5px; }
  .goal-sub { font-size: 11px; gap: 6px; }
  .goal-num { font-size: 20px; }
  /* A venue with nothing to report collapses to one line rather than four. */
  .goal-row:has(.chip.idle) .goal-mid { gap: 0; }
  .goal-row:has(.chip.idle) .goal-sub .chip { display: none; }
}

/* The goal row is `minmax(120px, 160px) 1fr auto` -- a 160px name column, a
 * progress column and a figure, side by side. At 390px that leaves the middle
 * column about 120px to hold a bar, a currency amount, a trade count and a
 * countdown, so it wrapped to three lines and the row grew to 110px. Stacking
 * it puts the name and the figure on one line, where they read as a pair, and
 * gives the bar the full width underneath. */
@media (max-width: 640px) {
  .goal-row {
    grid-template-columns: 1fr auto;
    grid-template-areas: "venue num" "mid mid";
    gap: 4px 10px;
  }
  .goal-venue { grid-area: venue; }
  .goal-num { grid-area: num; align-self: center; }
  .goal-mid { grid-area: mid; }
}

/* ======================================================== the wire and rates
 *
 * Two panels added 2026-08-23: MarketPulse (the broker's own analysis desk,
 * read as a feed and tagged to the instrument) and a two-sided rate board.
 */

.wire {
  display: grid;
  gap: 2px;
}

.wire-item {
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}
.wire-item:first-child { padding-top: 2px; }
.wire-item:last-child { border-bottom: 0; padding-bottom: 2px; }

.wire-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-size: 11px;
  color: var(--fg-3);
  margin-bottom: 5px;
}
.wire-when { font-variant-numeric: tabular-nums; }

.wire-title {
  font-size: 15px;
  line-height: 1.35;
  font-weight: 560;
  margin: 0 0 6px;
  letter-spacing: -0.01em;
}
.wire-title a {
  color: inherit;
  text-decoration: none;
  /* The underline appears on hover rather than sitting under every headline:
   * eight underlined links stacked is a wall, and the whole point of this panel
   * is that it can be scanned. */
  border-bottom: 1px solid transparent;
  transition: border-color 0.15s ease;
}
.wire-title a:hover { border-bottom-color: var(--fg-3); }

.wire-sum {
  margin: 0 0 8px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--fg-2);
  /* A summary, not the piece. Three lines is enough to decide whether to open
   * it, and a hard cap keeps every card the same shape so the eye can move
   * down the column instead of re-finding the left edge each time. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* A pair chip that opens a ticket is a button, and has to look like one the
 * moment the pointer is over it -- otherwise it reads as a label and nobody
 * ever discovers that the story is one click from a trade. */
button.chip[data-ticket] {
  cursor: pointer;
  font: inherit;
  font-size: 11px;
  border: 1px solid var(--border-hi);
}
button.chip[data-ticket]:hover {
  border-color: var(--fg-3);
  transform: translateY(-1px);
}

/* ------------------------------------------------------------- rate board */

.rate-table td,
.rate-table th { white-space: nowrap; }

/* Where the current price sits inside the day's range. A 96px rail with a dot
 * on it, which answers "near the high or near the low" in one glance -- the
 * question a number cannot answer without the reader holding two other numbers
 * in their head. */
.rate-range {
  position: relative;
  display: block;
  height: 4px;
  min-width: 72px;
  border-radius: 3px;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--red) 34%, transparent),
    var(--border),
    color-mix(in srgb, var(--green) 34%, transparent)
  );
}
.rate-range i {
  position: absolute;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-left: -3.5px;
  border-radius: 50%;
  background: var(--fg);
  transform: translateY(-50%);
  box-shadow: 0 0 0 2px var(--bg-1);
}

/* On a phone the range rail and the two slowest columns are the first things
 * worth losing: bid, ask, spread and the day's move are the board. */
@media (max-width: 640px) {
  .rate-table th:nth-child(6),
  .rate-table td:nth-child(6),
  .rate-table th:nth-child(7),
  .rate-table td:nth-child(7) { display: none; }
  .wire-title { font-size: 14.5px; }
  .wire-item { padding: 12px 0; }
}

@media (max-width: 480px) {
  .rate-table th:nth-child(8),
  .rate-table td:nth-child(8) { display: none; }
}

/* The feeder row on the Virtus-Fin desk. Set apart rather than hidden: it is
 * the sample the weights are fitted on, so it has to be visible, and it is not
 * a rule under test, so it must not read as one. */
.desk-feeder td {
  opacity: 0.72;
  border-top: 1px dashed var(--border-hi);
}

/* The desk's closing notes. Four claims, and they were one paragraph until a
 * render showed what that looks like: a twelve-line block nobody reaches the
 * end of, with the honest-cost admission buried in the middle of it. */
.desk-notes {
  margin-top: 14px;
  font-size: 11.5px;
  line-height: 1.65;
}
.desk-notes p { margin: 0 0 9px; }
.desk-notes p:last-child { margin-bottom: 0; }
.desk-notes b { color: var(--fg); }

/* On a phone the desk table's prose column takes half the width and pushes
 * R/trade and "vs control" — the only two columns that carry the claim — off
 * the far end of a horizontal scroll nobody drags that far. Measured at 390px:
 * the descriptions wrapped to four lines and made every row 90px tall. */
@media (max-width: 640px) {
  .desk-table th:nth-child(2),
  .desk-table td:nth-child(2),
  .desk-table th:nth-child(9),
  .desk-table td:nth-child(9) { display: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE BLACK REBUILD — 2026-08-24
 * ══════════════════════════════════════════════════════════════════════════
 *
 * The public site was rebuilt on the same day and the two surfaces have to be
 * one product. They were not: the site is set in Geist, self-hosted, so it
 * looks the same to everyone; this file asked for `-apple-system` and therefore
 * rendered in SF Pro on a Mac, Segoe UI on Windows and Roboto on Android. A
 * reader who clicked Get access went from a typeface chosen for the product to
 * whichever one their operating system happened to ship — which is a seam, and
 * it is the seam a person notices without being able to name.
 *
 * Four changes, and they are the same four the site took:
 *
 *   ONE TYPEFACE. Geist and Geist Mono, already served from this origin for the
 *   marketing pages, now declared here too. No new request on a warm cache and
 *   six files on a cold one.
 *
 *   SQUARE. 14/10/18px radii and a 980px pill become 4/3/6 and 4. A capsule is
 *   the shape of a consumer app; this is an instrument.
 *
 *   NO GLASS ON THE CHROME. The header and the tab strip were translucent with
 *   a 24px backdrop blur, which over pure black is a slightly-lighter black
 *   that shimmers as the page moves under it. Solid ground, hairline edge.
 *
 *   NO BEVEL. The one-pixel sheen along the top of every panel and tile was the
 *   cheapest way to stop a flat panel looking like a div, and on a black
 *   rebuild it is the most obvious remaining tell of a 2019 dashboard. A
 *   hairline and a material step do the same job without the highlight.
 *
 * Everything else — the density scale, the type scale, the measured contrast
 * values, the guards — is untouched. This is a change of surface, not of
 * structure.
 */

@font-face {
  font-family: "Geist";
  src: url("/fonts/Geist-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/fonts/Geist-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/fonts/Geist-SemiBold.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist";
  src: url("/fonts/Geist-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("/fonts/GeistMono-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Geist Mono";
  src: url("/fonts/GeistMono-Medium.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
}

:root {
  --sans: "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Text",
    "Helvetica Neue", system-ui, sans-serif;
  --mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;

  --radius: 4px;
  --radius-sm: 3px;
  --radius-lg: 6px;
  --pill: 4px;
}

/* ------------------------------------------------------------------ chrome */

header.bar,
body.scrolled header.bar {
  background: var(--bg);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom: 1px solid var(--border);
  box-shadow: none;
}

.brand .mark { letter-spacing: 0.18em; font-size: var(--t-13); }
.brand .sub { font-size: var(--t-9); }

nav.tabs,
nav.venues {
  background: var(--bg);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* The tab label is a machine word, like every other label in this product. It
 * was 13px sentence-case sans, which is the one place the interface spoke in
 * the same voice as the prose. */
nav.tabs button {
  font-family: var(--mono);
  font-size: var(--t-10);
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  border-radius: 0;
  padding: 14px 11px 12px;
}
/* The travelling indicator goes white. Blue was the only accent on the chrome
 * and it made the current tab look like a link; on a page whose data is
 * coloured by meaning — green for a gain, red for a loss — spending a colour on
 * navigation is spending it in the wrong place. */
nav.tabs::after { background: var(--fg); border-radius: 0; }

.venues button {
  font-family: var(--mono);
  font-size: var(--t-10);
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

/* ------------------------------------------------------------------ panels */

.panel {
  box-shadow: none;
  background: var(--bg-1);
}
/* The bevel. See the header of this section. */
.panel::before,
.metric::before { display: none; }

.panel > header h2 {
  font-family: var(--mono);
  font-size: var(--t-11);
  font-weight: 500;
  letter-spacing: 0.11em;
  text-transform: uppercase;
}

/* A KPI tile is a readout, so its caption is set the way every other caption in
 * the product is and its figure gets the mono cut it was already asking for. */
.metric { box-shadow: none; }
.metric .v { font-family: var(--mono); letter-spacing: -0.01em; }

/* ------------------------------------------------------------------ tables */

/* Column heads were already uppercase 10px; the mono cut is what makes them
 * line up with the figures underneath, which are tabular. */
thead th { font-family: var(--mono); letter-spacing: 0.1em; }

/* ----------------------------------------------------------------- buttons */

.btn {
  font-family: var(--mono);
  font-size: var(--t-11);
  font-weight: 500;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

/* -------------------------------------------------------------- the glows
 *
 * Two blue radial washes: one fixed behind the whole document, one behind the
 * NAV figure at the top of Overview. Both predate the rebuild and both are the
 * same argument the marketing page settled — an accent-coloured blur behind a
 * headline is the most dated effect in software, and over a pure black ground
 * it does something worse than look dated: it stops the ground being black.
 * The top of the page was navy.
 *
 * The figure keeps its scale and its tracking, which is what was carrying it.
 * A hairline and the type do the rest. */
body::before { background: none; }
.hero::after { display: none; }

/* The NAV panel is the one surface in the product that is allowed to be bigger
 * than everything around it, so it is the one that must not also be louder. */
.hero { background: var(--bg-1); border: 1px solid var(--border); }

/* ══════════════════════════════════════════════════════════════════════════
 * THE DECISION STRIP, AND THE HEARTBEAT — 2026-08-24
 * ══════════════════════════════════════════════════════════════════════════
 *
 * The strip is the gate chain from the public site, wired to the journal. See
 * the long note over decisionStrip() in app.js for why it is allowed to move:
 * every dot is a real row, and the gate it stops at is the reason column.
 */

.ds { position: relative; }
.ds-track {
  position: relative;
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 6px;
}
.ds-node {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  padding: 11px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: border-color var(--dur-base) var(--ease-out),
              background var(--dur-base) var(--ease-out);
}
.ds-node b {
  font-size: var(--t-11);
  font-weight: 600;
  letter-spacing: -0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-node i {
  font-style: normal;
  font-family: var(--mono);
  font-size: var(--t-9);
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The tally. How many of the journalled signals this gate actually stopped --
 * the number the animation is a reading of, printed beside it so the strip is
 * still a measurement when nothing is moving. */
.ds-node u {
  position: absolute;
  top: 7px;
  right: 8px;
  font-family: var(--mono);
  font-size: var(--t-9);
  font-weight: 500;
  text-decoration: none;
  color: var(--fg-3);
}
.ds-node.start { border-color: var(--border-hi); }
.ds-node.end { border-color: var(--border-hi); }
/* The dot is standing here. Never coloured: a gate is a question, and the
 * answer is the dot's business. */
.ds-node.at { border-color: var(--fg); background: var(--bg-1); }
.ds-node.no {
  border-color: color-mix(in srgb, var(--red) 65%, transparent);
  background: color-mix(in srgb, var(--red) 11%, transparent);
}
.ds-node.yes {
  border-color: color-mix(in srgb, var(--green) 65%, transparent);
  background: color-mix(in srgb, var(--green) 12%, transparent);
}

/* `translate` for the position and `transform` for anything else, so an inline
 * style written by app.js and a class written by this file compose instead of
 * fighting. The same arrangement the public site's token uses, for the same
 * reason. */
.ds-token {
  position: absolute;
  left: 0;
  top: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--fg);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--fg) 13%, transparent);
  opacity: 0;
  pointer-events: none;
  z-index: 2;
  transition: translate var(--dur-base) var(--ease-out),
              opacity var(--dur-base) var(--ease-out);
}
.ds-token.is-out {
  background: var(--green);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--green) 20%, transparent);
}

/* What the dot currently is. One line, mono, and it holds its height when
 * empty so the panel does not shift as the queue advances. */
.ds-now {
  display: flex;
  align-items: baseline;
  gap: 9px;
  min-height: 18px;
  margin-top: 11px;
  font-family: var(--mono);
  font-size: var(--t-11);
}
.ds-pair { font-weight: 500; color: var(--fg); }
.ds-why { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* On a phone seven gates in a row is seven columns of forty pixels. The strip
 * scrolls rather than shrinking, because a gate whose name is clipped to "Ex…"
 * is a gate nobody can read. */
@media (max-width: 720px) {
  .ds { overflow-x: auto; scrollbar-width: none; }
  .ds::-webkit-scrollbar { display: none; }
  .ds-track { min-width: 560px; }
}

/* ---------------------------------------------------------- the heartbeat */

.mark-svg { width: 17px; height: 17px; display: block; flex: none; }
.mark-svg .m-hex,
.mark-svg .m-f {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform var(--dur-slow) var(--ease-out);
}
@media (hover: hover) {
  .brand:hover .m-hex { transform: rotate(60deg); }
  .brand:hover .m-f { transform: scale(1.08); }
}
/* One pulse per cycle, driven by the cycle NUMBER rather than a timer -- a bot
 * that stopped stops pulsing, which is the only reason this is worth drawing. */
.mark-svg.beat .m-hex { animation: markBeat 900ms var(--ease-out) 1; }
@keyframes markBeat {
  0% { transform: scale(1); opacity: 1; }
  28% { transform: scale(1.22); opacity: 0.55; }
  100% { transform: scale(1); opacity: 1; }
}

/* The paper reading, inside the live ladder. Set apart by a rule rather than by
 * a fill: it is an answer to the same question from a different book, and it
 * must never be mistaken for the live one. */
.promo-paper {
  margin-top: 14px;
  padding-top: 13px;
  border-top: 1px dashed var(--border-hi);
}
