/*
 * tradevoir.com — the company page.
 *
 * The same visual language as the portal, on purpose: a product whose whole claim is precision
 * should not have a front door that looks like a template. Colours, fonts, spacing and radii are
 * the portal's (src/Voir.Web/src/styles/tokens.css), copied rather than imported because this
 * page is deployed on its own and must not depend on the app's build.
 *
 * Two differences from the portal, both deliberate:
 *   - Display sizes. The portal's scale stops at 26px because it is a dense tool read at arm's
 *     length. A page read once, at a glance, needs a headline, so the four display sizes below
 *     exist here and nowhere in the app.
 *   - No JavaScript at all. The page is static, and the CSP in _headers refuses every script.
 *
 * Still true here: no gradients, no decorative shadows, 1px structural rules, 2px corners.
 */

/* --- fonts ------------------------------------------------------------------------------------
 * Self-hosted, the files the portal bundles. No third-party font host: a company page that
 * tells every visitor's browser to call Google before it can render the word "evidence" has
 * made a small, avoidable disclosure on its first request. Licences sit beside the files. */

@font-face {
  font-family: 'IBM Plex Sans Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 700;
  src: url('fonts/ibm-plex-sans-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'IBM Plex Sans Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 700;
  src: url('fonts/ibm-plex-sans-latin-ext-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304,
    U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0,
    U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'JetBrains Mono Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 800;
  src: url('fonts/jetbrains-mono-latin-wght-normal.woff2') format('woff2-variations');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
    U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
    U+FEFF, U+FFFD;
}

/* --- tokens: the portal's dark palette, which is also this page's default ---------------------- */

:root {
  --bg-0: #0b1017;
  --bg-1: #10161f;
  --bg-2: #151d28;
  --bg-3: #1b2532;
  --line: #1e2836;
  --line-strong: #2c3b4e;
  --fg-0: #d7dee8;
  --fg-1: #93a2b5;
  --fg-2: #7b8ca1;
  --blue: #2f6bff;
  --blue-bright: #5e8fff;
  --blue-deep: #1b3f9e;
  --blue-wash: rgba(47, 107, 255, 0.14);
  --teal: #2ee6b0;
  --focus-ring: #8fb6ff;

  /* The rest of the portal's palette, copied for the screen depiction below. The figure is drawn
     as real elements from these tokens rather than shipped as a screenshot: it stays sharp at any
     density, follows the visitor's light or dark setting the way the product does, and cannot go
     stale against a portal that has moved on. A PNG would fail all three. */
  --fg-3: #43556b;

  /* A line somebody has to be able to see, as opposed to one that merely separates two surfaces.
     Its own token because no existing one can do the job: --line and --line-strong are picked to
     be quiet, and --line-strong measured 1.67:1 on the page, which is fine for the edge between
     two panels and not fine for anything carrying meaning.

     It is used in exactly two places, both of which WCAG holds to 3:1 rather than to taste — the
     border of a form control, which is what tells somebody where to type, and the strokes in the
     diagrams that are themselves the information: the empty slots in Fig 0.2, which are what make
     it a row of twelve rather than a scatter, and the links in Fig 0.5, which are the entire
     claim being drawn. Picked as the least-light value clearing 3:1 on --bg-0. */
  --control-line: #55677d;

  --amber: #e5a53a;
  --red: #ff4d4f;
  --buy: #2ee6b0;
  --sell: #ff7a6b;
  --statusbar-bg: #1b3f9e;
  --statusbar-fg: #cfe0ff;

  /* The link light, picked against the status bar rather than against the page, and deliberately
     not redefined in the light theme. The bar stays a strong blue in both themes because it is
     chrome rather than surface, so a dot that followed the page would be the light theme's dark
     teal on a dark blue fill: measured 1.65:1, a light nobody can see. These two are the dark
     hues in both themes, which is the same reasoning --focus-ring-on-accent exists for.
     Measured on --statusbar-bg: teal 5.80, amber 6.17. */
  --statusbar-ok: #2ee6b0;
  --statusbar-warn: #e5a53a;

  /* Session identity, for the grid's 3px left stripe. Deliberately excludes teal, amber, red and
     the buy/sell hues: those mean established, recovering, faulted and side. A session assigned
     amber would read as degraded when it is perfectly healthy. */
  --sess-0: #6ea8ff;
  --sess-1: #b98cff;
  --sess-2: #38bdf8;
  --sess-3: #f472b6;

  /* The portal's pulse rates. Rate encodes urgency, so a warn light is more insistent than a
     live one; all of it is off under prefers-reduced-motion. */
  --pulse-live: 2.4s;
  --pulse-warn: 1.3s;

  /* The button's own fill, not --blue. White on the portal's --blue measured 4.50:1 here —
     on the line, not over it — and the portal's hover, --blue-bright, took it to 3.07:1,
     because brighter blue means less contrast with white. So the fill is a shade deeper than
     the brand blue and hover goes darker still: the state a pointer is on is the one with the
     most contrast rather than the least. Measured in the page's own sweep:
       dark   rest 5.38   hover 6.67        light  rest 6.83   hover 9.39 */
  --btn-bg: #2a5fe8;
  --btn-bg-hover: #1f4fd6;
  --on-btn: #ffffff;

  --font-ui: 'IBM Plex Sans Variable', 'IBM Plex Sans', 'Segoe UI Variable Text', 'Segoe UI',
    system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono Variable', 'JetBrains Mono', 'IBM Plex Mono', 'Cascadia Mono',
    'Consolas', ui-monospace, monospace;

  /* The portal's scale, used for everything that is not a headline. */
  --fs-micro: 10px;
  --fs-xs: 11px;
  --fs-sm: 12px;
  --fs-md: 13px;
  --fs-lg: 15px;
  --fs-xl: 20px;

  /* The four sizes this page adds. Fluid between a phone and a wide screen, so a headline
     never wraps to one word a line on a 360px screen or sits small on a 1600px one. */
  --fs-body: 17px;
  --fs-lede: clamp(17px, 1.6vw, 20px);
  --fs-h2: clamp(26px, 3.4vw, 40px);
  --fs-display: clamp(38px, 6vw, 68px);

  /* Weight and tracking are a pair, and at display sizes they are the whole difference between
     a headline that reads as confident and one that reads as loud. Semibold is correct at 20px
     and wrong at 68px: the strokes thicken faster than the counters open, and the line turns
     into a block. So display type steps *down* to 500 as it grows, and the tracking closes to
     compensate — large type at default tracking looks gappy because metrics are drawn for
     reading sizes, not for headlines.

     These are display-only. Every other weight on the page stays 600, and the portal is
     untouched: this is the one thing a page read once at a glance needs that a dense tool read
     all day does not. */
  --display-weight: 500;
  --display-tracking: -0.022em;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
  --sp-8: 72px;
  --sp-9: 112px;
  --sp-10: 152px;

  --radius: 2px;
  --measure: 62ch;
  --page: 1120px;

  /* Motion. The portal's easing curve and two of its durations, plus one slower step that only
     this page has: the app animates state changes a user is waiting on, where anything above
     200ms reads as lag, and this page animates arrivals nobody is waiting on, where 200ms reads
     as a flinch. Everything below is switched off entirely under prefers-reduced-motion. */
  --dur-med: 200ms;
  --dur-slow: 620ms;
  --ease: cubic-bezier(0.4, 0, 0.2, 1);

  color-scheme: dark;
}

/* The portal's light palette, re-picked for a light ground rather than inverted. System choice
   only: a page with no JavaScript has no toggle to remember, and the visitor's own setting is
   the right answer anyway. */
@media (prefers-color-scheme: light) {
  :root {
    --bg-0: #eef1f6;
    --bg-1: #ffffff;
    --bg-2: #f4f7fb;
    --bg-3: #e8edf5;
    --line: #dde4ee;
    --line-strong: #bcc9da;
    --fg-0: #17202c;
    --fg-1: #46566a;
    --fg-2: #5c6d81;
    --blue: #1a4fd0;
    --blue-bright: #123ca8;
    --blue-deep: #b9cbf5;
    --blue-wash: rgba(26, 79, 208, 0.09);
    --teal: #0a7857;
    --focus-ring: #1a4fd0;
    --btn-bg: #1a4fd0;
    --btn-bg-hover: #123ca8;

    /* The light values, re-picked in the portal rather than derived from the dark ones. #2ee6b0
       is about 1.6:1 on white; an inverted palette would put the screen depiction's status
       colours below legibility on exactly the theme where they are easiest to check. */
    --fg-3: #9aa8b9;
    /* Darker than the dark theme's, for the same reason every status hue is re-picked here:
       contrast on a light ground is measured against white. #7f8fa4 was 2.91:1 on --bg-0 — under
       the 3:1 bar by a margin small enough to argue about, which is the wrong way to settle it. */
    --control-line: #78879d;

    /* Darkened the same way every other hue is for a light ground. */
    --sess-0: #2563c9;
    --sess-1: #7c3fd4;
    --sess-2: #0d7fb5;
    --sess-3: #bf3f86;
    --amber: #8e5b00;
    --red: #c62a2f;
    --buy: #0a7857;
    --sell: #bd3c2c;
    --statusbar-bg: #17439c;
    --statusbar-fg: #eaf1ff;

    color-scheme: light;
  }
}

/* --- base --------------------------------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg-0);
  color: var(--fg-0);
  font-family: var(--font-ui);
  font-size: var(--fs-body);
  line-height: 1.6;
  font-feature-settings: 'kern', 'liga';
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
p {
  margin: 0;
}

a {
  color: inherit;
}

:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
  border-radius: var(--radius);
}

/* Above the sticky header, or the first thing a keyboard user reaches would appear behind it. */
.skip {
  position: absolute;
  left: var(--sp-4);
  top: -100px;
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-2);
  border: 1px solid var(--line-strong);
  z-index: 30;
}

.skip:focus {
  top: var(--sp-3);
}

.wrap {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--sp-5);
}

.mono {
  font-family: var(--font-mono);
}

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

/* Sticky, because the one route into the product is in it and a visitor who has read four
   screens should not have to scroll back up to find the way in.
   The rule under it is earned rather than permanent: at the top of the page the header is part
   of the hero and a line across it is noise; once anything has scrolled beneath it, the line is
   what says the header is floating over content rather than sitting in it. Done on the scroll
   timeline so it stays a stylesheet with no script — the CSP in _headers refuses every script,
   and that is worth more than this effect. Where the timeline is unsupported the line is simply
   always there, which is what this page shipped with. */
.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--bg-0);
  border-bottom: 1px solid var(--line);
}

@supports (animation-timeline: scroll()) {
  .site-head {
    border-bottom-color: transparent;
    animation: head-rule linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 56px;
  }
}

@keyframes head-rule {
  to {
    border-bottom-color: var(--line);
  }
}

.site-head .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  height: 64px;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  text-decoration: none;
}

.brand svg {
  display: block;
}

/* The portal's wordmark: the UI face at 0.14em, with the trailing letter-space cancelled so the
   word sits in its own box rather than floating left of it. */
.wordmark {
  font-weight: 600;
  font-size: var(--fs-lg);
  letter-spacing: 0.14em;
  margin-right: -0.14em;
  text-transform: uppercase;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: var(--sp-5);
  font-size: var(--fs-lg);
}

.site-nav a {
  color: var(--fg-1);
  text-decoration: none;
}

.site-nav a:hover {
  color: var(--fg-0);
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 44px;
  padding: 0 var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: 600 var(--fs-lg) / 1 var(--font-ui);
  text-decoration: none;
  white-space: nowrap;
}

.btn-primary {
  background: var(--btn-bg);
  color: var(--on-btn);
}

.btn-primary:hover {
  background: var(--btn-bg-hover);
}

.btn-quiet {
  border-color: var(--line-strong);
  color: var(--fg-0);
}

.btn-quiet:hover {
  background: var(--bg-2);
}

.site-nav .btn {
  min-height: 36px;
  padding: 0 var(--sp-4);
  color: var(--fg-0);
}

/* --- hero --------------------------------------------------------------------------------------- */

/* No rule under the hero, and none between the sections below it. A 1px line every screenful
   chops the page into slabs and makes it read as a stack of separate documents; what separates
   one claim from the next here is space. The borders that remain are the ones doing structural
   work — the header, and the edges of the panels that depict the product. */
.hero {
  padding: var(--sp-10) 0 var(--sp-9);
}

.eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-5);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.hero h1 {
  max-width: 16ch;
  font-size: var(--fs-display);
  font-weight: var(--display-weight);
  line-height: 1.02;
  letter-spacing: var(--display-tracking);
  text-wrap: balance;
}

.hero .lede {
  max-width: var(--measure);
  margin-top: var(--sp-5);
  font-size: var(--fs-lede);
  color: var(--fg-1);
  text-wrap: pretty;
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin-top: var(--sp-6);
}

/* --- sections ----------------------------------------------------------------------------------- */

.section {
  padding: var(--sp-9) 0;
}

.section-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: var(--sp-7);
  align-items: start;
}

.kicker {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
  margin-bottom: var(--sp-3);
}

.section h2 {
  font-size: var(--fs-h2);
  font-weight: var(--display-weight);
  line-height: 1.1;
  letter-spacing: var(--display-tracking);
  text-wrap: balance;
}

.prose p {
  max-width: var(--measure);
  color: var(--fg-1);
  text-wrap: pretty;
}

.prose p + p {
  margin-top: var(--sp-4);
}

.prose strong {
  color: var(--fg-0);
  font-weight: 600;
}

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

/* Four columns separated by a hairline and nothing else. The panel this used to sit in made the
   four steps read as one object; they are four decisions taken separately, and the rule between
   them says that better than a box around all of them did. */
.ladder {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-top: var(--sp-7);
  padding: 0;
  list-style: none;
}

/* The ladder is pulled out by exactly the padding each step carries, rather than the outer steps
   having their outer padding removed. Both put the first step's text on the page margin; only this
   one leaves all four steps with the same content width, which is what makes the four schematics
   render at the same size. Zeroing the outer padding instead made the end columns 24px wider, and
   their drawings 24px larger than the middle two. */
.ladder {
  margin-left: calc(var(--sp-5) * -1);
  margin-right: calc(var(--sp-5) * -1);
}

.rung {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  padding: 0 var(--sp-5);
}

.rung + .rung {
  border-left: 1px solid var(--line);
}

.rung-step {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.rung-art {
  margin: var(--sp-4) 0 var(--sp-3);
}

/* --- the mini screens ----------------------------------------------------------------------------
 * A step of the ladder shows the screen it produces, at the size a column allows. Built from the
 * same tokens as the hero panel and the same vocabulary as the product, because the alternative,
 * which sat here first, was four abstract line drawings: on a page arguing that this product is
 * precise, a picture of nothing in particular argues the opposite.
 *
 * Every label is kept short enough to survive a 200px column, which is what these are capped at on
 * a phone. That is a real constraint on the wording rather than something the CSS can fix: a
 * screen too small to read is not a screenshot, it is a texture. */

.mini {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-1);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1.3;
  user-select: none;
}

.mini-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--line);
  background: var(--bg-2);
  color: var(--fg-2);
}

/* The one step available today gets the brand blue on its header, which is the only place on this
   row where colour is doing any selling. */
.rung--live .mini-head {
  color: var(--blue-bright);
}

.mini-ok {
  color: var(--teal);
}

.mini-rows {
  margin: 0;
  padding: 0;
  list-style: none;
}

.mini-rows li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-3);
  padding: 0 var(--sp-3);
  height: 24px;
  border-top: 1px solid var(--line);
}

.mini-rows li:first-child {
  border-top: 0;
}

.mini-k {
  position: relative;
  padding-left: var(--sp-3);
  color: var(--fg-0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The same rule the blotter follows: the hue goes on the stripe, never on the label. */
.mini-k::before {
  content: '';
  position: absolute;
  left: 0;
  top: 4px;
  bottom: 4px;
  width: 2px;
  background: var(--amber);
}

.mini-k.bad::before {
  background: var(--red);
}

.mini-k.live::before {
  background: var(--teal);
}

.mini-k.plain {
  padding-left: 0;
}

.mini-k.plain::before {
  content: none;
}

.mini-v {
  color: var(--fg-1);
  white-space: nowrap;
}

.rung h3 {
  font-size: var(--fs-xl);
  font-weight: 600;
  letter-spacing: -0.01em;
}

.rung p {
  font-size: var(--fs-lg);
  color: var(--fg-1);
  flex: 1;
}

/* What the customer risks at each rung, which is the column product-thesis.html SS I puts in its
   ladder and the reason the ladder persuades rather than lists. Without it the intro's claim that
   each step is "a separate decision" is asserted; with it, Nothing / Nothing / Reputation only /
   Their books makes the argument on its own. */
.rung-risk {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--sp-3);
  margin-top: auto;
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--fg-0);
}

.rung-risk-k {
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-2);
}

/* Three identical "after exchange certification" labels read as "not built" unless something
   says what the gate is. This turns it into a procurement fact with an actor and a process. */
.ladder-note {
  max-width: var(--measure);
  margin-top: var(--sp-6);
  font-size: var(--fs-lg);
  color: var(--fg-1);
  text-wrap: pretty;
}

/* A fact can carry the same availability label the ladder uses. Two of these four describe the
   capture layer, and an unlabelled capture claim in the section headed "why it settles arguments"
   is exactly the kind of quiet oversell site/README.md exists to stop. */
.fact {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
}

.fact p {
  flex: 1;
}

.fact .status {
  margin-top: var(--sp-1);
}

/* A status says so in words, never by colour alone: the dot is the portal's live teal, and the
   label carries the meaning for anyone who cannot rely on it. */
.status {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.status::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  border: 1px solid var(--fg-2);
}

.status-now {
  color: var(--fg-0);
}

.status-now::before {
  background: var(--teal);
  border-color: var(--teal);
}

/* --- the split: what each clearer can and cannot see --------------------------------------------
 * Twelve marks a row, flexing rather than fixed, so the drawing fits any width without a media
 * query and without text that shrinks below reading size — which is the reason none of the
 * diagrams on this page are SVG. Labels are real text in the flow, at the same sizes as
 * everything else.
 *
 * The rows carry no colour that means anything: a mark is present or it is an outline, and the
 * only hue is the brand blue on the row that is the whole day. Status hues stay reserved for
 * status, here as everywhere. */

.split {
  display: grid;
  gap: var(--sp-3);
  padding: var(--sp-5);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-1);
}

.split-row {
  display: grid;
  grid-template-columns: 11rem minmax(0, 1fr) 2.5rem;
  align-items: center;
  gap: var(--sp-4);
}

/* The complete day sits apart from the groupings under it. A rule rather than a gap, because the
   point is that everything below is a subset of this one, not a sibling of it. */
.split-row--truth {
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--sp-1);
}

/* "By exchange" and "By clearing firm". The diagram makes its point twice, on two independent
   splits of the same twelve fills, and without these the six rows below read as one list of six
   sources rather than as two complete accounts of the same day. */
.split-head {
  margin-top: var(--sp-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.split-head:first-of-type {
  margin-top: 0;
}

.split-label,
.split-count {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--fg-2);
}

.split-count {
  text-align: right;
}

.split-row--truth .split-label,
.split-row--truth .split-count {
  color: var(--fg-0);
}

.split-marks {
  display: flex;
  gap: 6px;
}

.split-marks i {
  flex: 1;
  height: 16px;
  border: 1px solid var(--control-line);
  border-radius: var(--radius);
}

/* A carried fill is solid, an uncarried one is the outline it would have filled. Measured against
   the panel they sit on — which is the ground each is actually adjacent to — the outline is 3.13:1
   and the solid 5.28:1, both over the 3:1 a meaningful graphic is held to.
   The two are only 1.69:1 against *each other*, and that is fine rather than overlooked: they are
   never adjacent, always separated by a gap of panel, and the difference a reader is reading is
   solid against hollow, which survives being unable to tell two greys apart at all. */
.split-marks i.on {
  background: var(--fg-2);
  border-color: var(--fg-2);
}

.split-row--truth .split-marks i.on {
  background: var(--blue);
  border-color: var(--blue);
}

/* --- the day assembling ----------------------------------------------------------------------
 * The diagram makes its point twice: first that a day arrives split across five sources, and then
 * that the whole of it is the thing none of them has. Animating it in that order says both without
 * a word: the source bricks land in their own places, and only when the last one is down does the
 * record above fill in.
 *
 * Only the filled bricks animate. The empty slots are the frame and are there from the first
 * frame, so the drawing never changes shape: bricks arrive into a grid rather than a grid growing.
 *
 * On the same 12s cycle as the hero, so the two things moving on this page share a tempo instead
 * of beating against each other. All of it inside prefers-reduced-motion: no-preference; with
 * motion off every brick is simply there, which is also what a browser supporting none of this
 * renders. */

/* Percentages rather than seconds, so they all move together if the cycle length below changes.
   At 15s: the pop is 0.24s, and each brick clears between 10.2s and 11.1s into its own cycle.

   The clear has to finish that early because every brick's cycle is offset by its own delay, so
   the whole thing is a rolling wave rather than a synchronised one: a brick that cleared at 90%
   was still on screen while the next cycle's bricks were already arriving. That looked like a
   record whose sources had vanished underneath it, which is precisely the opposite of the point
   the drawing makes. Clearing at 68-74% leaves the last brick gone by 14.2s and the first new one
   not due until 15.4s, so there is a real moment with nothing on screen and the two phases can
   never overlap. Verified by scrubbing: no instant has both a clearing brick and an arriving one. */
@keyframes brick-in {
  0% {
    opacity: 0;
    transform: scale(0.55);
  }
  1.6% {
    opacity: 1;
    transform: none;
  }
  68% {
    opacity: 1;
    transform: none;
  }
  74% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: no-preference) {
  /* 15s: about 3s of building and then twelve of sitting finished.
     There is no objectively right number here, only a trade-off between two ways of being wrong.
     Much under ten and the drawing rebuilds itself while somebody is still reading the paragraph
     beside it, which is the kind of movement that makes a page feel restless. Much over twenty and
     somebody who scrolls to it lands in the middle of the hold, sees nothing move for a long time,
     and reasonably concludes it is a static picture. Fifteen keeps the worst case — arriving just
     after a cycle finished — at about eleven seconds, and the build itself is over in three.

     Deliberately not the hero's 12s. The two are never both fully in view, so a shared tempo buys
     nothing, and their jobs differ: the hero is glanced at for a few seconds and wants life, this
     is read alongside prose and wants patience. */
  .split-row:not(.split-row--truth) .split-marks i.on,
  .split-row--truth .split-marks i {
    animation: brick-in 15s var(--ease) infinite both;
  }

  /* The sources, column by column: 0.40s to 1.72s. */
  .split-row:not(.split-row--truth) .split-marks i:nth-child(1) { animation-delay: 0.40s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(2) { animation-delay: 0.52s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(3) { animation-delay: 0.64s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(4) { animation-delay: 0.76s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(5) { animation-delay: 0.88s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(6) { animation-delay: 1.00s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(7) { animation-delay: 1.12s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(8) { animation-delay: 1.24s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(9) { animation-delay: 1.36s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(10) { animation-delay: 1.48s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(11) { animation-delay: 1.60s; }
  .split-row:not(.split-row--truth) .split-marks i:nth-child(12) { animation-delay: 1.72s; }

  /* The record, once the day is complete: 2.30s to 3.13s. */
  .split-row--truth .split-marks i:nth-child(1) { animation-delay: 2.300s; }
  .split-row--truth .split-marks i:nth-child(2) { animation-delay: 2.375s; }
  .split-row--truth .split-marks i:nth-child(3) { animation-delay: 2.450s; }
  .split-row--truth .split-marks i:nth-child(4) { animation-delay: 2.525s; }
  .split-row--truth .split-marks i:nth-child(5) { animation-delay: 2.600s; }
  .split-row--truth .split-marks i:nth-child(6) { animation-delay: 2.675s; }
  .split-row--truth .split-marks i:nth-child(7) { animation-delay: 2.750s; }
  .split-row--truth .split-marks i:nth-child(8) { animation-delay: 2.825s; }
  .split-row--truth .split-marks i:nth-child(9) { animation-delay: 2.900s; }
  .split-row--truth .split-marks i:nth-child(10) { animation-delay: 2.975s; }
  .split-row--truth .split-marks i:nth-child(11) { animation-delay: 3.050s; }
  .split-row--truth .split-marks i:nth-child(12) { animation-delay: 3.125s; }
}

/* --- the chain: the raw log, linked by hash ------------------------------------------------------
 * The one claim on the page that is hard to believe as a sentence and obvious as a picture. Drawn
 * with the connector as a border on the gap rather than as an arrowhead glyph, so it costs no
 * font and no image and survives both themes. */

.chain {
  padding: var(--sp-5);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-1);
}

.chain-links {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  align-items: stretch;
  gap: var(--sp-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.chain-link {
  position: relative;
  display: grid;
  gap: var(--sp-1);
  padding: var(--sp-3);
  border: 1px solid var(--control-line);
  border-radius: var(--radius);
  background: var(--bg-2);
}

/* The link between one entry and the next, drawn into the gap the grid already leaves. */
.chain-link + .chain-link::before {
  content: '';
  position: absolute;
  top: 50%;
  right: 100%;
  width: var(--sp-5);
  border-top: 1px solid var(--control-line);
}

.chain-link--head {
  border-color: var(--blue);
}

.chain-seq {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--fg-2);
}

.chain-hash {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--fg-0);
}

.chain-tag {
  position: absolute;
  top: calc(var(--sp-3) * -1);
  right: var(--sp-2);
  padding: 0 var(--sp-2);
  background: var(--bg-1);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--blue-bright);
}

.chain-anchor {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-top: var(--sp-5);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--fg-1);
}

.chain-anchor-mark {
  flex: none;
  width: 9px;
  height: 9px;
  border: 1px solid var(--teal);
  background: var(--teal);
  border-radius: 50%;
}

/* The one disclosure the captions were carrying, moved inside the drawing so it stays attached to
   the thing it is disclosing about. The hashes are the shape of a SHA-256 prefix and not any real
   digest; see the claims rules in site/README.md before removing it. */
.chain-note {
  margin-left: auto;
  color: var(--fg-2);
}

/* --- the breaks panel: the product's own vocabulary, as it appears in the product ------------- */

.panel {
  margin-top: var(--sp-7);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-1);
}

.panel-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-3) var(--sp-5);
  border-bottom: 1px solid var(--line);
  background: var(--bg-2);
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  color: var(--fg-2);
}

.breaks {
  margin: 0;
  padding: 0;
  list-style: none;
}

.breaks li {
  display: grid;
  grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: var(--sp-5);
  padding: var(--sp-4) var(--sp-5);
  border-top: 1px solid var(--line);
}

.breaks li:first-child {
  border-top: 0;
}

.breaks .kind {
  font-family: var(--font-mono);
  font-size: var(--fs-md);
  color: var(--fg-0);
}

.breaks .means {
  font-size: var(--fs-lg);
  color: var(--fg-1);
}

/* --- facts: what the architecture already is ------------------------------------------------- */

.facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  margin-top: var(--sp-7);
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* Three across rather than the facts grid's two by two, because there are three destinations and
   a two by two with a hole in it reads as a missing fourth. Same hairline construction: the gap is
   the background showing through. */
.dests {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  margin-top: var(--sp-7);
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.dest {
  display: flex;
  flex-direction: column;
  padding: var(--sp-5);
  background: var(--bg-0);
}

/* Identical to .fact's, because the two card grids sit four sections apart and any difference
   between them reads as two components rather than one page. */
.dest h3 {
  font-size: var(--fs-lg);
  font-weight: 600;
  margin-bottom: var(--sp-2);
}

.dest p {
  font-size: var(--fs-lg);
  color: var(--fg-1);
}

/* The concrete artifact, so the card names a thing rather than a capability. Pushed to the bottom
   by the auto margin, which is why .dest is a column flex: the three cards hold different amounts
   of prose and the wire lines should still line up with each other. Written against both classes
   because .dest p would otherwise outrank a bare .dest-wire and take back the size and colour. */
.dest .dest-wire {
  margin-top: auto;
  padding-top: var(--sp-4);
  font-size: var(--fs-xs);
  color: var(--fg-2);
  word-break: break-word;
}

.fact {
  padding: var(--sp-5);
  background: var(--bg-0);
}

.fact h3 {
  font-size: var(--fs-lg);
  font-weight: 600;
  margin-bottom: var(--sp-2);
}

.fact p {
  font-size: var(--fs-lg);
  color: var(--fg-1);
}

/* --- roles -------------------------------------------------------------------------------------- */

.roles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-6);
  margin-top: var(--sp-7);
}

.role {
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line-strong);
}

.role .kicker {
  margin-bottom: var(--sp-2);
}

.role h3 {
  font-size: var(--fs-xl);
  font-weight: 600;
  margin-bottom: var(--sp-3);
}

.role p {
  font-size: var(--fs-lg);
  color: var(--fg-1);
}

/* --- close -------------------------------------------------------------------------------------- */

.close {
  padding: var(--sp-9) 0 var(--sp-10);
  border-top: 1px solid var(--line);
}

.close h2 {
  max-width: 20ch;
  font-size: var(--fs-h2);
  font-weight: var(--display-weight);
  line-height: 1.1;
  letter-spacing: var(--display-tracking);
}

.close p {
  max-width: var(--measure);
  margin-top: var(--sp-4);
  color: var(--fg-1);
}

.contact {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--sp-7);
  align-items: start;
}

.contact-alt {
  margin-top: var(--sp-5);
  font-size: var(--fs-lg);
}

.contact-alt a {
  color: var(--blue-bright);
}

/* --- the form ------------------------------------------------------------------------------------
 * Plain HTML controls, styled with the portal's own tokens, and no script behind any of it: what
 * validates here is what the browser validates for free (`required`, `type=email`, `maxlength`),
 * and everything else is checked on the edge in functions/enquiry.js. A field that only a
 * stylesheet knows is invalid is a field that lets a real enquiry through wrong.
 *
 * Controls are taller than the portal's 26px. The app is a dense tool used with a pointer all day
 * by somebody who has learnt it; this is six fields filled once, often on a phone, by somebody who
 * has not. 44px is the touch target, and the density rule is not worth a mis-tap here. */

.form {
  display: grid;
  gap: var(--sp-4);
}

.field {
  display: grid;
  gap: var(--sp-2);
}

.field label {
  font-size: var(--fs-md);
  color: var(--fg-1);
}

.field .opt {
  color: var(--fg-2);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--control-line);
  border-radius: var(--radius);
  background: var(--bg-1);
  color: var(--fg-0);
  font: inherit;
  font-size: var(--fs-lg);
}

.field textarea {
  min-height: 0;
  resize: vertical;
  line-height: 1.45;
}

/* The placeholder is an example, not a label, so it is quieter than the value that replaces it —
   and --fg-2 rather than --fg-3, because a placeholder nobody can read is a hint nobody gets. */
.field ::placeholder {
  color: var(--fg-2);
  opacity: 1;
}

.field input:focus-visible,
.field select:focus-visible,
.field textarea:focus-visible {
  border-color: var(--focus-ring);
}

.form .btn {
  justify-self: start;
  margin-top: var(--sp-2);
}

.form-note {
  font-size: var(--fs-md);
  color: var(--fg-2);
  max-width: var(--measure);
}

/* Off-screen rather than display:none: a bot reading the markup fills what it can find, and a
   field that is not rendered at all is a field the cruder ones skip. Out of the tab order and out
   of the accessibility tree, so nobody who is not a bot ever meets it. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- the standalone note pages (thanks, and the function's failure page) ----------------------- */

.note {
  padding: var(--sp-9) 0 var(--sp-10);
}

.note h1 {
  max-width: 18ch;
  margin-top: var(--sp-3);
  font-size: var(--fs-h2);
  font-weight: var(--display-weight);
  line-height: 1.1;
  letter-spacing: var(--display-tracking);
}

.note .lede {
  max-width: var(--measure);
  margin-top: var(--sp-4);
  font-size: var(--fs-lede);
  color: var(--fg-1);
  text-wrap: pretty;
}

.note .lede a {
  color: var(--blue-bright);
}

.site-foot {
  padding: var(--sp-6) 0 var(--sp-7);
  font-size: var(--fs-md);
  color: var(--fg-2);
}

.site-foot .wrap {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--sp-4);
}

.site-foot a {
  color: var(--fg-1);
}

.site-foot nav {
  display: flex;
  gap: var(--sp-5);
}

/* --- figures ------------------------------------------------------------------------------------
 * A numbered caption under every depiction of the product, which is not decoration: this page
 * sells a record you can put in front of an auditor, and the same page inventing a screenshot
 * would undercut the claim in the act of making it. The number makes the figure citable and the
 * caption says, in words, that the data is sample data. See site/README.md — no figures that read
 * as real. */

.figure {
  margin: var(--sp-7) 0 0;
}

/* The figure now owns the space above it, so whatever it wraps must stop claiming its own or the
   gap is counted twice. */
.figure > .ladder,
.figure > .panel {
  margin-top: 0;
}

/* No captions under the drawings, and no figure numbers. Each one sat directly under the section
   that had just explained it in prose, so it restated the argument in smaller type and read as a
   footnote on a point already made. A drawing beside the sentence it illustrates does not need to
   be introduced.
   Anything a caption was carrying that nothing else said goes into the drawing itself, which is
   where somebody looking at it will see it: "Sample data" is a badge in the hero's title bar, and
   "Digests illustrative" is a line inside the chain panel. */

/* --- the screen ----------------------------------------------------------------------------------
 * The portal, copied rather than approximated. Every rule below is the corresponding rule from
 * src/Voir.Web/src/styles/app.css and global.css, with its real values; the earlier version of
 * this section invented its own spacing and type and looked like a different product.
 *
 * Scoped under .app throughout, because the portal's class names collide with this page's own:
 * .btn here is a 44px marketing button, .filter-bar and .micro-label and .pill are the page's.
 * Inside .app they must be the app's.
 *
 * Two deliberate departures, both because this is a picture rather than a running screen:
 *   - .dg__scroll is overflow:hidden rather than auto. Nothing here scrolls, and a scrollbar
 *     on a hero is a control that does not work.
 *   - the activity bar's items are 30px tall rather than 44px. At the real height nine items
 *     over three make the rail 528px, which would set the height of the whole panel to about
 *     double its content. The 48px width, the grouping and the 2px accent are what carry it.
 */

.app {
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--bg-1);
  overflow: hidden;
  user-select: none;
}

/* --- title bar ---------------------------------------------------------------------------- */

.app .title-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 38px;
  padding: 0 12px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line);
}

/* The wordmark is the UI face at 0.14em with the trailing letter-space cancelled, which is how
   Logo.tsx draws it. */
.tb-brand {
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: 0.14em;
  margin-right: -0.14em;
  text-transform: uppercase;
  color: var(--fg-0);
}

.tb-sep {
  width: 1px;
  height: 16px;
  background: var(--line-strong);
}

.tb-product {
  font-size: 12px;
  color: var(--fg-1);
}

/* The app has no breadcrumb to copy, so this follows the one rule its title bar does have:
   everything beside the wordmark is 12px mono. A step dimmer than the product name, because it
   says where you are rather than what you are looking at. Unstyled it inherited the 17px body
   size at full --fg-0 and was the loudest thing in a 38px bar. */
.tb-crumb {
  font-size: 12px;
  color: var(--fg-2);
  white-space: nowrap;
}

.tb-spacer {
  flex: 1;
}

.tb-tenant {
  font-size: 12px;
}

.tb-tenant b {
  font-weight: 400;
  color: var(--fg-0);
}

/* Blue, not grey. This is the portal's own badge and the disclosure rides on it: "Certification"
   is the environment, "Sandbox · synthetic data" is the control plane's mark on a tenant whose
   fills are invented. Keep both, at every width. */
.app .env-badge {
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  padding: 2px 6px;
  border: 1px solid var(--blue-deep);
  border-radius: var(--radius);
  background: var(--blue-wash);
  color: var(--blue-bright);
  white-space: nowrap;
}

.app-body {
  display: flex;
  align-items: stretch;
  min-height: 0;
}

/* --- activity bar ------------------------------------------------------------------------- */

.activity-bar {
  flex: none;
  width: 48px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 4px;
  background: var(--bg-2);
  border-right: 1px solid var(--line);
}

.ab-group {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.ab-group--foot {
  margin-top: auto;
  padding-top: 4px;
  padding-bottom: 4px;
  border-top: 1px solid var(--line);
}

/* One item per view, carrying the portal's own icon (components/Icons.tsx): 20px on a 24 grid at
   stroke 1.6, stroke:currentColor, so the item's colour drives the drawing. --fg-2 at rest and
   --blue-bright on the current view, which is .activity-bar__item's own behaviour. */
.activity-bar i {
  position: relative;
  width: 48px;
  height: 30px;
  display: grid;
  place-items: center;
  color: var(--fg-2);
}

.activity-bar i svg {
  display: block;
}

.activity-bar i.on {
  color: var(--blue-bright);
}

/* The current view's 2px accent, inset from the top and bottom of its own item. */
.activity-bar i.on::before {
  content: '';
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: 2px;
  background: var(--blue);
}

.app-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* --- the screen's filter bar ---------------------------------------------------------------- */

.app .filter-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  background: var(--bg-1);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.filter-bar__group {
  display: flex;
  align-items: center;
  gap: 6px;
}

.app .micro-label {
  font-size: var(--fs-micro);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--fg-2);
}

/* Inputs and selects, drawn rather than made real: a control nobody can operate is better than
   one that opens onto nothing. Values from global.css's input/select rule. */
.ctl {
  display: inline-flex;
  align-items: center;
  height: 22px;
  padding: 4px 7px;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--bg-0);
  color: var(--fg-0);
}

.ctl.sel::after {
  content: '';
  margin-left: 6px;
  border-left: 3px solid transparent;
  border-right: 3px solid transparent;
  border-top: 4px solid var(--fg-2);
}

/* The app's button, which has to beat this page's 44px marketing .btn inside the panel. */
.app .btn {
  display: inline-flex;
  align-items: center;
  min-height: 0;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 400;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--fg-1);
  background: var(--bg-0);
}

.app .btn--icon {
  padding: 2px 7px;
  line-height: 15px;
}

/* --- the data grid -------------------------------------------------------------------------- */

.dg {
  display: flex;
  flex-direction: column;
  min-height: 0;

  /* The grid's two metrics. The app sets these inline on this element; here they have to live in
     the stylesheet, because site/_headers ships style-src 'self' with no 'unsafe-inline' and that
     blocks the style attribute too. Undefined, --dg-font falls back to the 17px body size and the
     whole blotter renders at reading size. */
  --dg-row-h: 26px;
  --dg-font: 12px;
}

/* The Trade Feed's column widths, straight from views/tradeColumns.tsx. The app puts each of
   these inline on all three of a column's cells; the CSP forbids that here, so they are classes
   and every cell carries its own. They must stay identical across the header, the filter row and
   every body row or the columns stop lining up — which is exactly what the inline version did
   once the policy dropped it. */
.col-cap { width: 112px; }
.col-venue { width: 62px; }
.col-sym { width: 138px; }
.col-side { width: 60px; }
.col-qty { width: 70px; }
.col-px { width: 96px; }
.col-trd { width: 86px; }
.col-acct { width: 104px; }
.col-clr { width: 92px; }
.col-state { width: 104px; }
.col-lat { width: 96px; }

.dg__toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px;
  background: var(--bg-1);
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

.dg__count {
  font-size: 11px;
  color: var(--fg-1);
}

.dg__spacer {
  flex: 1;
}

.dg__zoom {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.dg__zoomv {
  font-size: 11px;
  color: var(--fg-2);
  min-width: 34px;
  text-align: center;
}

.dg__scroll {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  background: var(--bg-1);
}

/* The real grid sets this width inline, as the sum of its column widths, and lets min-width:100%
   stretch it when the viewport is wider — which is why bare --bg-1 shows to the right of the last
   column rather than the columns growing. Carried as a custom property here instead of a literal
   inline width, so the narrow layouts can restate it without having to out-shout an inline style
   with !important. */
.dg__inner {
  width: var(--dg-w, auto);
  min-width: 100%;
}

/* The sum of the visible column widths, carried as a class rather than as the app's inline
   style attribute. Not cosmetic: an inline custom property outranks every stylesheet rule for
   that property, so with the width inline the narrow layouts could not restate it at all and the
   grid stayed 1020px wide on a 375px screen. A class loses to a later rule, which is the whole
   point. */
.dg__inner--w924 {
  --dg-w: 924px;
}

.dg__head {
  position: sticky;
  top: 0;
  z-index: 3;
}

.dg__row {
  display: flex;
  height: var(--dg-row-h);
  border-bottom: 1px solid var(--line);
  background: var(--bg-1);
}

.dg__row--head,
.dg__row--filter {
  background: var(--bg-2);
}

.dg__row--head {
  border-bottom: 1px solid var(--line-strong);
}

.dg__row--filter {
  height: calc(var(--dg-row-h) + 2px);
}

/* No vertical borders anywhere: column edges are implied by alignment. The header reserves 1px of
   transparent right border and spends it on nothing, which is what the app does. */
.dg__cell,
.dg__hcell,
.dg__fcell {
  flex: 0 0 auto;
  padding: 0 9px;
  display: flex;
  align-items: center;
  font-size: var(--dg-font);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.dg__cell {
  color: var(--fg-0);
  font-variant-numeric: tabular-nums;
}

.dg__cell > * {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Cells are flex containers, so text-align alone would not move anything. */
.dg__cell--r {
  justify-content: flex-end;
  text-align: right;
}

/* 10.08px at the default magnification, derived rather than taken from the --fs-* scale, and
   uppercased by CSS rather than in the column definitions. */
.dg__hcell {
  gap: 5px;
  font-family: var(--font-mono);
  font-size: calc(var(--dg-font) * 0.84);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--fg-2);
  border-right: 1px solid transparent;
}

.dg__htext {
  overflow: hidden;
  text-overflow: ellipsis;
}

.dg__fcell {
  padding: 0 5px;
}

.dg__finp,
.dg__fsel {
  display: flex;
  align-items: center;
  width: 100%;
  min-width: 0;
  height: 18px;
  padding: 1px 5px;
  font-family: var(--font-mono);
  font-size: calc(var(--dg-font) * 0.92);
  background: var(--bg-0);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--fg-0);
}

.dg__body {
  position: relative;
}

/* The left identity stripe, by capture session. Same mechanism the selected row uses, which is
   why in the app selecting a row replaces its session colour rather than showing both. */
.dg__row--sess-0 { box-shadow: inset 3px 0 0 var(--sess-0); }
.dg__row--sess-1 { box-shadow: inset 3px 0 0 var(--sess-1); }
.dg__row--sess-2 { box-shadow: inset 3px 0 0 var(--sess-2); }
.dg__row--sess-3 { box-shadow: inset 3px 0 0 var(--sess-3); }

.side-buy { color: var(--buy); }
.side-sell { color: var(--sell); }

.app .dim { color: var(--fg-2); }

/* The tone class sets only `color`; the border is currentColor and so is the dot, so one
   declaration drives all three. The ground is always --bg-1 and never the hue. */
.app .pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 6px;
  border: 1px solid currentColor;
  border-radius: var(--radius);
  background: var(--bg-1);
  font-family: var(--font-mono);
  font-size: var(--fs-micro);
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 16px;
}

.pill--warn { color: var(--amber); }
.pill--bad { color: var(--red); }

.pill__dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
}

/* --- status bar ----------------------------------------------------------------------------- */

.app .status-bar {
  display: flex;
  align-items: center;
  gap: 0;
  height: 24px;
  background: var(--statusbar-bg);
  border-top: 1px solid var(--blue);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--statusbar-fg);
}

.status-bar__item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  height: 100%;
  white-space: nowrap;
}

.status-bar__spacer {
  flex: 1;
}

/* Two different shapes, not two colours of the same shape: ok is a filled disc, warn is a ring.
   The label beside it says LIVE or CONNECTING in words either way. */
.status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
  background: currentColor;
}

.status-dot--ok {
  color: var(--statusbar-ok);
}

.status-dot--warn {
  color: var(--statusbar-warn);
  background: transparent;
  border: 2px solid currentColor;
}

/* The two link states occupy the same cell so the bar does not resize as they swap. Without
   motion the settled one is the one with opacity, which is why neither is display:none. */
.st-swap {
  display: grid;
}

.st-run,
.st-done {
  grid-area: 1 / 1;
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

.st-run {
  opacity: 0;
}

/* --- the import, running -------------------------------------------------------------------
 * The panel fills the way an import fills it: empty, then a row at a time, then done. It is the
 * one thing on this page that shows the product working rather than describing it, and it is an
 * import run, which is the part available today, rather than a live exchange session.
 *
 * One 12s cycle shared by every animated part so the rows, the link state and its dot cannot
 * drift apart: rows land over the first ~1.7s, the status flips at 14%, everything holds, and the
 * last second resets. Now that rows are real elements, each one carries its own delay directly;
 * this used to be eight ranges of :nth-child arithmetic that had to be recomputed by hand every
 * time a column was added.
 *
 * Every rule below is inside prefers-reduced-motion: no-preference. With motion off nothing moves,
 * the rows are simply there, and the bar reads LIVE. */

@keyframes trade-in {
  0% {
    opacity: 0;
    transform: translateY(-4px);
  }
  3% {
    opacity: 1;
    transform: none;
  }
  92% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes st-running {
  0%, 12% { opacity: 1; }
  16%, 100% { opacity: 0; }
}

@keyframes st-finished {
  0%, 12% { opacity: 0; }
  16%, 92% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes st-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0.25; }
}

@media (prefers-reduced-motion: no-preference) {
  .dg__body .dg__row {
    animation: trade-in 12s var(--ease) infinite both;
  }

  .dg__body .dg__row:nth-child(1) { animation-delay: 0.6s; }
  .dg__body .dg__row:nth-child(2) { animation-delay: 0.74s; }
  .dg__body .dg__row:nth-child(3) { animation-delay: 0.88s; }
  .dg__body .dg__row:nth-child(4) { animation-delay: 1.02s; }
  .dg__body .dg__row:nth-child(5) { animation-delay: 1.16s; }
  .dg__body .dg__row:nth-child(6) { animation-delay: 1.3s; }
  .dg__body .dg__row:nth-child(7) { animation-delay: 1.44s; }
  .dg__body .dg__row:nth-child(8) { animation-delay: 1.58s; }

  .st-run {
    animation: st-running 12s steps(1, end) 0.6s infinite both;
  }

  .st-done {
    animation: st-finished 12s steps(1, end) 0.6s infinite both;
  }

  .status-dot--warn {
    animation: st-blink 0.9s steps(1, end) infinite;
  }

  .pill__dot {
    animation: st-blink var(--pulse-warn, 1.3s) var(--ease) infinite;
  }
}

/* --- motion --------------------------------------------------------------------------------------
 * Two kinds, both pure CSS, because the CSP refuses every script and an effect is not worth
 * weakening it for.
 *
 * The hero arrives on load, staggered, so the page assembles rather than appears. Everything
 * below arrives on the scroll timeline as it enters the viewport — which is the honest way to do
 * it: tied to where the page actually is rather than to a timer that fires whether or not the
 * reader got there. Anything already on screen at load has finished its range and renders
 * settled, so nothing is ever stuck invisible.
 *
 * All of it is inside prefers-reduced-motion: no-preference. Not damped — absent. */

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .hero .eyebrow,
  .hero h1,
  .hero .lede,
  .hero .actions,
  .hero .figure {
    animation: rise var(--dur-slow) var(--ease) both;
  }

  .hero h1 {
    animation-delay: 70ms;
  }

  .hero .lede {
    animation-delay: 140ms;
  }

  .hero .actions {
    animation-delay: 210ms;
  }

  .hero .figure {
    animation-delay: 280ms;
  }

  @supports (animation-timeline: view()) {
    .reveal {
      animation: rise linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 62%;
    }
  }
}

/* --- narrow screens -----------------------------------------------------------------------------
 * The grid gets its own ladder of stops, separate from the page's two, because fixed-width columns
 * either fit or are cut and there is no middle. Each stop is sized to the narrowest viewport in
 * its own range:
 *
 *   >= 1040   10 columns, 924px   cap venue sym side qty px trd acct clr state
 *   <= 1039    7 columns, 630px   drops Trader, Account, State
 *   <=  744    4 columns, 356px   drops Captured, Qty, Clearing
 *   <=  600    5 columns, 320px   activity bar hidden; widths narrowed so a real blotter still
 *                                 fits - venue, symbol, side, qty and price, which is the least
 *                                 that is recognisably a fill
 *
 * Usable width is the viewport minus 113: a 15px scrollbar, the 48px page gutter, 2px of panel
 * border and the 48px activity bar. The scrollbar is the easy one to forget, and forgetting it
 * is what put the first stop 15px too low and clipped the State column from 1022 to 1036. Media
 * queries here match the viewport WITH the scrollbar, not documentElement.clientWidth.
 *
 * Above each range's floor, bare --bg-1 shows to the right of the last column. That is not a gap
 * to close: it is what the real grid does, because .dg__inner is min-width 100% while the cells
 * stay flex:0 0 auto. */

@media (max-width: 1039px) {
  .dg__inner {
    --dg-w: 630px;
  }

  .col-trd,
  .col-acct,
  .col-state {
    display: none;
  }
}

@media (max-width: 744px) {
  .dg__inner {
    --dg-w: 356px;
  }

  .col-cap,
  .col-qty,
  .col-clr {
    display: none;
  }
}

@media (max-width: 900px) {
  .section-head {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-5);
  }

  /* Two across. Padding is untouched, so the columns stay equal and the drawings stay the same
     size; only which step carries the dividing rule changes. */
  .ladder {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    row-gap: var(--sp-6);
  }

  .rung:nth-child(odd) + .rung {
    border-left: 1px solid var(--line);
  }

  .rung:nth-child(even) + .rung {
    border-left: 0;
  }

  .roles {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-5);
  }

  .dests {
    grid-template-columns: minmax(0, 1fr);
  }

  .dest .dest-wire {
    padding-top: var(--sp-3);
  }

  /* The form goes under the pitch rather than beside it, and stays the full measure: a
     half-width form on a narrow screen is how a company ends up with half-typed enquiries. */
  .contact {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-6);
  }

  /* The chain stacks rather than wrapping to two across. Wrapping looks like the obvious fold and
     is wrong: entry 3 would land underneath entry 1, so the line drawn into it would join the
     wrong pair and the drawing would state a different chain than the one it is describing. */
  .chain-links {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-4);
  }

  .chain-link + .chain-link::before {
    top: auto;
    right: auto;
    bottom: 100%;
    left: var(--sp-5);
    width: 0;
    height: var(--sp-4);
    border-top: 0;
    border-left: 1px solid var(--control-line);
  }

  .chain-link {
    grid-template-columns: 9rem minmax(0, 1fr);
    align-items: baseline;
    gap: var(--sp-3);
  }

  .chain-tag {
    top: var(--sp-2);
  }
}

@media (max-width: 600px) {
  .wrap {
    padding: 0 var(--sp-4);
  }

  .site-nav .hide-narrow {
    display: none;
  }

  .hero {
    padding: var(--sp-8) 0 var(--sp-7);
  }

  /* One column. Every step is a full-width block, so no step has a side to be an edge of. */
  .ladder,
  .facts {
    grid-template-columns: minmax(0, 1fr);
  }

  .ladder {
    row-gap: var(--sp-5);
    margin-left: 0;
    margin-right: 0;
  }

  .rung {
    padding-left: 0;
    padding-right: 0;
    border-left: 0;
  }

  .rung + .rung {
    padding-top: var(--sp-5);
    border-top: 1px solid var(--line);
  }

  .rung-art {
    max-width: 200px;
  }

  .breaks li {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--sp-1);
  }

  .actions .btn {
    flex: 1 1 100%;
  }

  /* Qty and Price come back by narrowing the columns rather than dropping more of them. A blotter
     with no quantity and no price is not a blotter, and this is the width most first looks happen
     at. 52 + 104 + 50 + 46 + 84 = 336, inside the 341 a 375px screen leaves. This is the one place
     the page departs from the app's widths: the real grid never narrows a column, it scrolls, and
     a hero that needs a horizontal drag to show a second column is worse than one that fits. */
  .dg__inner {
    --dg-w: auto;
  }

  .col-qty {
    display: flex;
  }

  .col-venue { width: 50px; }
  .col-sym { width: 98px; }
  .col-side { width: 48px; }
  .col-qty { width: 44px; }
  .col-px { width: 80px; }

  /* The activity bar, the product name, the breadcrumb and the tenant are chrome at this width.
     The two environment badges are not chrome and stay: they are the disclosure, so the title bar
     is allowed to grow rather than clip them. */
  .activity-bar,
  .tb-product,
  .tb-crumb,
  .tb-sep,
  .tb-tenant {
    display: none;
  }

  .app .title-bar {
    height: auto;
    flex-wrap: wrap;
    row-gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
  }

  .app .title-bar .tb-spacer {
    display: none;
  }

  .app .filter-bar,
  .dg__toolbar {
    gap: 6px;
    padding: 6px 8px;
  }

  /* Written against both parents rather than as a bare `.hide-tight`, which is one class and
     loses to `.filter-bar__group` on specificity. */
  .app .hide-tight,
  .status-bar .hide-tight,
  .hide-tight {
    display: none;
  }

  /* The split's label column costs more than it carries at this width, so the label goes above
     its own row of marks and the count goes with it. */
  .split-row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--sp-2) var(--sp-3);
  }

  .split-marks {
    grid-column: 1 / -1;
    gap: 4px;
  }

  .split-marks i {
    height: 14px;
  }

  .split {
    gap: var(--sp-4);
    padding: var(--sp-4);
  }

  .chain {
    padding: var(--sp-4);
  }
}
