/* A trading terminal, not a document.
 *
 * Three things drive every decision below. The first is density: the numbers
 * on this page are read at a glance, often while something is going wrong, and
 * a figure that needs scrolling to reach is a figure that was not there when it
 * was wanted. The second is that the same page is read one-handed, outdoors, on
 * a phone -- so density is bought out of padding and layout, never out of tap
 * targets or type size. The third is restraint: on a dark ground, every border,
 * fill and shadow competes with the figures, so the surfaces are separated by
 * the quietest thing that still separates them and nothing else.
 *
 * Dark first. The light palette is still here and still complete, under a
 * `prefers-color-scheme: light` query; the dark one is the default because it
 * is what this thing is looked at in.
 *
 * `--surface`, `--text`, `--muted`, `--border`, `--accent`, `--ok` and
 * `--danger` are read by charts.js and handed to the charting library, whose
 * `fade()` understands `#rgb` and `#rrggbb` and nothing else. Those seven stay
 * hex -- which is why `--border` is written as the hex that a 6% white rule
 * resolves to over a card, rather than as the `rgb(255 255 255 / 0.06)` it is
 * conceptually. The two are the same colour; only one of them can also be a
 * gridline.
 *
 * One typeface, at one scale, stated once:
 *
 *   page title    20px / 600     section label  11px / 600 / tracked / upper
 *   metric        28px / 600     body           14px
 *
 * The monospace face is for prices, quantities and code. It is never used for
 * a label, because a label is a word and words are not columns.
 */

:root {
  color-scheme: dark light;

  /* -- surfaces. Four steps, and every one of them is meant to be *seen*. The
     sheet used to run from #0d1117 to #232a33 and the whole platform read as
     one flat sheet with lines drawn on it: the page, the cards and the tiles
     were separated by a border and by nothing else, so a card with a quiet
     border was a card that was not there. The whole ramp moves up a step and
     spreads, and the border comes up with it.

       --bg         #161b22   the page
       --surface    #1c2128   a card, a panel, a chart pane -- the objects
       --surface-2  #242930   *above* a card: the recommended plan, an open
                              drawer's head, the lit top of a metric tile,
                              the raised face of an outlined button
       --surface-3  #2d323b   the lit inset a control, a meter track or a
                              tile's own top edge is set in

     The numbers are a sequence, not a rank -- `--surface` is what charts.js
     reads for a pane and cannot be renamed without renaming it there too, so
     the step above a card is the one that had to keep the number 2. Read the
     list rather than the digits. */
  --bg: #161b22;
  --surface: #1c2128;
  --surface-2: #242930;
  --surface-3: #2d323b;

  /* A 10% white rule over a card, and a 15% one where a box has to hold its
     own edge against the page -- an input, a picker, a button that is not
     filled. It used to be 6%, which is a hairline you have to look for; a card
     whose edge cannot be found is a card that is not there. Written as hex for
     charts.js -- the gridlines are this colour at 60% -- so it is the hex a
     10% white rule resolves to over `--surface`, rather than the
     `rgb(255 255 255 / 0.1)` it is conceptually. */
  --border: #33373e;
  --border-strong: #3d434c;

  --text: #e8eaed;
  --muted: #9aa0a6;
  /* The third rank, and it is deliberately almost unused. It measures 3.6:1
     against a card, which is under the 4.5:1 a sentence needs -- so it is not
     allowed on one. The footnotes, hints and timestamps that a third rank is
     usually reached for all take `--muted` instead, which is 6.5:1 and still
     reads as quiet. What is left for this is marks whose meaning is carried by
     something beside them: the pip's dot, which also says its state in the
     colour of its word. A rank that cannot hold a sentence is not a text
     colour, and pretending otherwise is how a palette ends up unreadable in
     daylight. */
  --dim: #6b7280;

  --accent: #4d90f8;
  --accent-soft: rgb(77 144 248 / 0.1);
  --accent-line: rgb(77 144 248 / 0.35);
  /* Text laid on the accent. White on #4d90f8 is 3.2:1 and fails at body size;
     this is 6:1 and passes. A saturated blue carrying near-black is also what
     the fill looks like on every terminal this is modelled on. */
  --on-accent: #0a1020;

  /* Profit, loss, caution. Standard values on purpose: this is the one part of
     the palette a reader arrives already knowing, and a house green is a green
     somebody has to learn. */
  --ok: #22c55e;
  --ok-bg: rgb(34 197 94 / 0.12);
  --warn: #f59e0b;
  --warn-bg: rgb(245 158 11 / 0.12);
  --danger: #f15c5c;
  --danger-bg: rgb(241 92 92 / 0.12);
  --idle: #9aa0a6;
  --idle-bg: rgb(255 255 255 / 0.06);
  --unknown-bg: rgb(255 255 255 / 0.06);

  /* -- the plan a rail's foot is badged with. Three tiers and a fourth state
     for an account on none of them, and the colour is the whole of the
     signal: the word beside it is the plan's own label, which on a 240px rail
     is two syllables the eye skips.

     `--plan-pro` is a violet and is *not* `--accent-2`. That one is
     decoration -- the palette says so where it is declared, and it measures
     4.1:1 -- and this one carries a word, so it is a lighter step held to the
     same 4.5:1 floor as every other rank that lands under text. 6.4:1 over
     the page, and the rail's own gradient is lighter than the page rather
     than darker, so the badge only ever gets more contrast than that.

     Grey for free rather than for nothing: an account with no plan at all is
     the fourth case and takes the badge's default border and `--muted`, so
     "free" and "no plan" do not read as the same thing. */
  --plan-free: #9aa0a6;
  --plan-free-bg: rgb(255 255 255 / 0.06);
  --plan-basic: #4d90f8;
  --plan-basic-bg: rgb(77 144 248 / 0.12);
  --plan-pro: #a78bfa;
  --plan-pro-bg: rgb(167 139 250 / 0.14);

  /* -- series colours, for a chart that draws more than one thing.
     Assigned in this order and never cycled: slot 1 is whichever pair sorted
     first, and it stays slot 1 when a second pair appears, because a colour
     that follows rank rather than identity repaints every line the moment the
     set changes. Read by rti.js off the stylesheet, like the seven above.

     Checked rather than chosen: each step sits inside the dark mode's
     lightness band, clears the chroma floor, clears 3:1 against `--surface`,
     and neighbouring slots stay apart under simulated protanopia and
     deuteranopia. Colour is never the only channel anyway -- each series is
     drawn in its own pane with its pair named above it. */
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #c98500;
  --series-5: #d55181;
  --series-6: #008300;
  --series-7: #9085e9;
  --series-8: #e66767;

  /* -- one spacing scale, so a gap somewhere is the same gap everywhere. */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;

  /* -- one radius scale. A card, a control, a badge: three sizes, and nothing
     invents a fourth. */
  --radius: 8px;
  --radius-sm: 6px;
  --radius-xs: 4px;
  /* The panel radius, for the surfaces that are read as objects rather
     than as boxes: the landing page's cards and the pricing columns. */
  --radius-lg: 14px;
  --tap: 44px;
  --rail: 240px;
  --rail-collapsed: 56px;

  /* -- type. */
  --sans: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
          "Liberation Mono", monospace;
  --fs-title: 1.25rem;
  --fs-metric: 1.75rem;
  --fs-body: 0.875rem;
  --fs-sm: 0.8125rem;
  --fs-xs: 0.75rem;
  --fs-label: 0.6875rem;

  /* No drop shadows on the dark theme: over a near-black ground they read as
     smudge rather than as lift, and the border already says where the box
     ends. The token survives because the light theme still wants one. */
  --shadow: none;
  /* The two exceptions, which are not lift but occlusion: a thing floating over
     the page has to prove it is over it. */
  --shadow-pop: 0 12px 32px rgb(0 0 0 / 0.5);

  /* -- the effects layer.
     Every glow, gradient and lift on this platform resolves to one of the
     tokens below, and the palette is the only place any of them is written
     down. That is the same bargain the colours already make: a shadow typed
     into a rule is a shadow the light theme cannot answer, and a glow written
     three ways is three glows.

     The gradients pair the brand blue with a violet. The violet is decoration
     only -- it never carries a reading, never lands under text, and never
     appears in the palette a chart is handed -- so it is not held to the
     contrast floor the seven text ranks are. */
  --accent-2: #8b5cf6;
  --accent-grad: linear-gradient(135deg, #4d90f8 0%, #6366f1 50%, #8b5cf6 100%);
  --accent-grad-lit: linear-gradient(135deg, #60a5fa 0%, #818cf8 50%, #a78bfa 100%);
  /* A wide ramp for the animated fills, which slide a 300%-wide background
     across a box rather than interpolating stops -- gradient stops are not
     animatable, and a background-position slide is composited. */
  --accent-ramp: linear-gradient(100deg, #4d90f8, #8b5cf6, #22d3ee, #4d90f8);
  /* The filled button's own gradient, and the reason it is not
     `--accent-grad`: a label sits on this one. `--on-accent` clears 4.5:1
     against the accent and against anything *lighter* than it, and fails
     against the indigo and violet the decorative ramp passes through. So the
     button ramps towards white and never away from it. */
  --btn-grad: linear-gradient(135deg, #4d90f8 0%, #74aefb 100%);
  --btn-grad-lit: linear-gradient(135deg, #74aefb 0%, #9cc7fd 100%);

  /* What a panel does under a pointer, and it is the same thing on both
     themes now: `--glow`, a lit 1px edge and a 0.15 bloom around it with no
     offset in either, so a raised panel reads as light coming off it rather
     than as a shadow cast under it. `--glow-lit` is the same halo at hover
     strength. There was a `--lift` here, a black drop shadow for the panels;
     it went when the last rule that set it took the halo instead, which is
     the whole of the dark theme's argument about shadows applied to the one
     place that was still making an exception to it. */
  --glow: 0 0 0 1px rgb(77 144 248 / 0.3), 0 0 28px rgb(77 144 248 / 0.15);
  --glow-lit: 0 0 0 1px rgb(77 144 248 / 0.55), 0 0 40px rgb(77 144 248 / 0.28);
  /* The focus ring, named rather than typed: it is the one box-shadow that
     appears on every control on the platform. */
  --focus-ring: 0 0 0 3px rgb(77 144 248 / 0.28);
  --focus-ring-danger: 0 0 0 3px rgb(241 92 92 / 0.28);
  /* The inset hairline that turns a flat fill into a lit surface. */
  --sheen: linear-gradient(180deg, rgb(255 255 255 / 0.06), rgb(255 255 255 / 0) 60%);
  --sheen-strong: linear-gradient(180deg, rgb(255 255 255 / 0.1), rgb(255 255 255 / 0) 55%);
  /* Frosted glass. The fill is what a browser without `backdrop-filter` gets,
     so it is opaque enough to read on its own; the blur is the enhancement.

     Two of them, because there are two grounds being frosted. `--glass` is a
     *card* -- it resolves to `--surface` over the page, and its alpha is high
     enough that the mesh behind it cannot drag it back down to the page
     colour, which is the whole failure the surfaces above were re-stepped to
     fix. `--glass-page` is the top bar, which is the page itself with content
     running under it and must not read as a panel laid across the top. */
  --glass: rgb(30 36 45 / 0.9);
  --glass-page: rgb(22 27 34 / 0.8);
  --glass-solid: #1d232c;
  --glass-line: rgb(255 255 255 / 0.1);

  /* The semantic glows. `text-shadow`, not `box-shadow`: what glows is the
     figure, and a box around a P&L would be a second rectangle inside a
     tile that is already one. */
  --glow-text-ok: 0 0 18px rgb(34 197 94 / 0.45);
  --glow-text-danger: 0 0 18px rgb(241 92 92 / 0.45);
  --glow-text-accent: 0 0 22px rgb(77 144 248 / 0.4);
  /* The neon rank, and the landing page is the only thing that gets it. Three
     layers rather than one, because the sample report's headline figure is set
     at twice the size of the tiles around it and a single 18px halo at that
     size reads as a glyph that is out of focus rather than as light coming off
     one: the tight layer keeps the edge, the middle one is the glow, and the
     wide one is what the tile it sits in picks up. A dashboard where every
     P&L did this would be a dashboard where none of them stood out. */
  --glow-text-ok-neon:
    0 0 6px rgb(34 197 94 / 0.55),
    0 0 18px rgb(34 197 94 / 0.4),
    0 0 44px rgb(34 197 94 / 0.22);

  /* The page's ground. A mesh rather than a flat fill -- three very wide
     radials that never resolve into shapes, which is the whole point: it
     should read as depth and never as a picture. */
  --mesh:
    radial-gradient(70rem 40rem at 12% -8%, rgb(77 144 248 / 0.09), transparent 60%),
    radial-gradient(60rem 36rem at 96% 4%, rgb(139 92 246 / 0.07), transparent 62%),
    radial-gradient(50rem 40rem at 50% 108%, rgb(34 211 238 / 0.045), transparent 60%);
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #eef1f5;
    --surface: #ffffff;
    --surface-2: #f6f8fa;
    --surface-3: #e9edf2;
    --border: #d8dee6;
    --border-strong: #c3ccd7;
    --text: #12151a;
    --muted: #5b6675;
    --dim: #6b7280;
    --accent: #1f5fd0;
    --accent-soft: rgb(31 95 208 / 0.1);
    --accent-line: rgb(31 95 208 / 0.35);
    --on-accent: #ffffff;
    --ok: #12764a;
    --ok-bg: rgb(18 118 74 / 0.1);
    --warn: #8a5300;
    --warn-bg: rgb(138 83 0 / 0.1);
    --danger: #b3261e;
    --danger-bg: rgb(179 38 30 / 0.1);
    --idle: #4a5261;
    --idle-bg: rgb(16 24 40 / 0.06);
    --unknown-bg: rgb(16 24 40 / 0.06);

    /* The three plan colours, stepped for a white card rather than flipped.
       The violet darkens to 6.3:1 over the page here, where the dark theme's
       lightens to 6.4:1 over its own -- the same reading, arrived at from the
       opposite end. */
    --plan-free: #5b6675;
    --plan-free-bg: rgb(16 24 40 / 0.06);
    --plan-basic: #1f5fd0;
    --plan-basic-bg: rgb(31 95 208 / 0.12);
    --plan-pro: #6d28d9;
    --plan-pro-bg: rgb(109 40 217 / 0.12);
    --shadow: 0 1px 2px rgb(16 24 40 / 0.06);
    --shadow-pop: 0 12px 32px rgb(16 24 40 / 0.14);

    /* The same eight, stepped for a white surface rather than flipped from the
       dark ones. Slots 3, 4 and 5 sit under 3:1 here, which is why the pane
       label and the current value are text next to every line rather than a
       legend swatch: the reading never depends on telling two lines apart by
       colour. */
    --series-1: #2a78d6;
    --series-2: #eb6834;
    --series-3: #1baf7a;
    --series-4: #eda100;
    --series-5: #e87ba4;
    --series-6: #008300;
    --series-7: #4a3aa7;
    --series-8: #e34948;

    /* The effects layer, answered for a white ground rather than inverted
       from the dark one. A glow over white is a tint and a bloom, not a halo:
       the same rule that reads as light in a dark room reads as dirt in a
       bright one, so the opacities come down: the halo is a tint and a bloom
       here rather than the light it is over near-black. */
    --accent-2: #6d28d9;
    --accent-grad: linear-gradient(135deg, #1f5fd0 0%, #4f46e5 50%, #6d28d9 100%);
    --accent-grad-lit: linear-gradient(135deg, #1a52b8 0%, #4338ca 50%, #5b21b6 100%);
    --accent-ramp: linear-gradient(100deg, #1f5fd0, #6d28d9, #0e7490, #1f5fd0);
    /* White on this accent, so the ramp darkens rather than lightens. */
    --btn-grad: linear-gradient(135deg, #1f5fd0 0%, #1a52b8 100%);
    --btn-grad-lit: linear-gradient(135deg, #1a52b8 0%, #164aa6 100%);

    --glow: 0 0 0 1px rgb(31 95 208 / 0.3), 0 0 24px rgb(31 95 208 / 0.15);
    --glow-lit: 0 0 0 1px rgb(31 95 208 / 0.5), 0 0 32px rgb(31 95 208 / 0.22);
    --focus-ring: 0 0 0 3px rgb(31 95 208 / 0.22);
    --focus-ring-danger: 0 0 0 3px rgb(179 38 30 / 0.22);
    --sheen: linear-gradient(180deg, rgb(255 255 255 / 0.9), rgb(255 255 255 / 0) 60%);
    --sheen-strong: linear-gradient(180deg, rgb(255 255 255 / 0.95), rgb(255 255 255 / 0) 55%);
    --glass: rgb(255 255 255 / 0.92);
    --glass-page: rgb(246 248 250 / 0.8);
    --glass-solid: #ffffff;
    --glass-line: rgb(16 24 40 / 0.1);

    /* A glowing figure on white is a blurred figure. The semantic glows are
       kept in the palette so the rules that use them need no theme branch,
       and set to nothing at all here. */
    --glow-text-ok: none;
    --glow-text-danger: none;
    --glow-text-accent: none;

    --mesh:
      radial-gradient(70rem 40rem at 12% -8%, rgb(31 95 208 / 0.07), transparent 60%),
      radial-gradient(60rem 36rem at 96% 4%, rgb(109 40 217 / 0.05), transparent 62%),
      radial-gradient(50rem 40rem at 50% 108%, rgb(14 116 144 / 0.04), transparent 60%);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: var(--fs-body)/1.5 var(--sans);
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  /* The ground is not flat. See `body::before`. */
  position: relative;
}

/* The mesh, as a fixed layer behind everything rather than as a background on
   the body itself. `background-attachment: fixed` is the obvious way to write
   this and the wrong one: it is repainted on every scroll frame on a phone,
   and iOS ignores it outright inside a scrolling container. A fixed
   pseudo-element is composited once and costs nothing to scroll past.

   Inert in every sense -- no pointer events, behind the stacking order, and
   `aria-hidden` by virtue of being a pseudo-element. Nothing on the page reads
   it, so nothing is lost when a browser declines to paint it. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: var(--mesh);
}

/* -- motion --------------------------------------------------------------
 *
 * Five keyframes, and every one of them is a loop that a reader is meant to
 * stop noticing after the first cycle. Nothing here moves layout: they animate
 * `opacity`, `transform` and `background-position`, which are the three the
 * compositor can do without laying the page out again. A dashboard that
 * dropped frames while a P&L was moving would be a dashboard that animated the
 * wrong properties.
 *
 * All of it is switched off at the foot of this sheet under
 * `prefers-reduced-motion`, which is why none of it may ever be the only thing
 * that carries a meaning. */

/* An accent halo breathing behind a mark. Opacity and scale only. */
@keyframes aura {
  0%, 100% { opacity: 0.55; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: 0.9;  transform: translate(-50%, -50%) scale(1.08); }
}

/* A wide gradient slid across its own box. Gradient *stops* cannot be
   interpolated; a background three times wider than the box, walked from one
   end to the other, is how a gradient is animated without repainting it. */
@keyframes ramp {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}

/* The quietest of them: the rail's mark, lit and unlit. */
@keyframes breathe {
  0%, 100% { opacity: 0.86; }
  50%      { opacity: 1; }
}

/* What an accordion body does when it is opened. A fade with a few pixels of
   travel, so the panel arrives rather than appearing. */
@keyframes reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* A dot that is telling you something is live.

   The halo is repeated in every frame, and that is the load-bearing part: an
   animated `box-shadow` *replaces* the one the rule set, so a keyframe list
   carrying only the ring would leave the pulsing dot dimmer at rest than the
   still one beside it -- the pip's own `0 0 8px` glow switched off for the
   whole of the animation. The ring expands past a lamp that stays lit. */
@keyframes ping {
  0%, 100% { box-shadow: 0 0 8px rgb(34 197 94 / 0.6), 0 0 0 0 rgb(34 197 94 / 0.5); }
  70%      { box-shadow: 0 0 8px rgb(34 197 94 / 0.6), 0 0 0 5px rgb(34 197 94 / 0); }
}

/* -- the landing page's four, and they are all atmosphere: nothing below
   carries a reading, so the page is complete when the reduced-motion rule at
   the foot of this sheet switches every one of them off.

   Each one is written so that its *stopped* state is the state the rule
   already declares -- an orb at its resting offset, a beam parked off the left
   edge of its own line, a bloom at the middle of its range. An animation whose
   first frame is the only frame that looks right is an animation that breaks
   the page for the reader who asked not to see it. */

/* An orb, drifting. Twenty pixels over twenty-odd seconds, which is slow
   enough that it is never caught moving and only ever noticed as a page that
   is not quite still. Two of them, in opposite phase, so four orbs sharing one
   keyframe set do not rise and fall as one object. */
@keyframes drift {
  0%, 100% { transform: translateY(-20px); }
  50%      { transform: translateY(20px); }
}
@keyframes drift-back {
  0%, 100% { transform: translateY(18px); }
  50%      { transform: translateY(-18px); }
}

/* A bright spot travelling along a one-pixel rule. Thirty seconds for one
   pass, and the seams are given staggered negative delays below so the five of
   them are never lit at the same moment -- five beams in step would read as
   the page refreshing rather than as the page breathing. */
@keyframes beam {
  0%   { background-position: -30% 0, 0 0; }
  100% { background-position: 130% 0, 0 0; }
}

/* The stats bar's scan. The pass itself is three seconds and the cycle is
   nine, so the strip is still for twice as long as it is swept: a highlight
   that arrived every three seconds without a rest is a strip that flickers in
   the corner of the eye of somebody reading the section below it. */
@keyframes scan {
  0%   { background-position: -80% 0; }
  33%  { background-position: 180% 0; }
  100% { background-position: 180% 0; }
}

/* The bloom behind the call to action. The range is a pair of custom
   properties rather than two keyframe sets, so the hover can widen it without
   restarting the pulse -- the same argument the ramp's `animation-duration`
   makes one rule further down. */
@keyframes bloom {
  0%, 100% { opacity: var(--bloom-lo); }
  50%      { opacity: var(--bloom-hi); }
}

a { color: var(--accent); }

h1, h2, h3, h4 { letter-spacing: -0.011em; }

/* Almost every `<code>` on this dashboard holds an identifier somebody else
   chose -- a contract, an instance id, a strategy name, a vault fingerprint --
   and none of them carry a break opportunity. In a card 296px wide that is the
   difference between a page and a page that scrolls sideways. */
code { font-family: var(--mono); font-size: 0.92em; overflow-wrap: anywhere; }
/* Except in the guide's rule trees, which are pre-formatted and scroll inside
   their own box on purpose; see `.guide__code`. */
.guide__code code { overflow-wrap: normal; }

.muted { color: var(--muted); }
.small { font-size: var(--fs-sm); }
/* The quietest rank, for a sentence that qualifies a panel rather than being
   one. `.muted.small` together is what the templates already write for it. */
.muted.small { color: var(--muted); }
.neg { color: var(--danger); }
.pos { color: var(--ok); }
.tone-ok { color: var(--ok); }
.tone-warn { color: var(--warn); }
.tone-danger { color: var(--danger); font-weight: 600; }
.tone-unknown, .tone-idle { color: var(--muted); }

/* Every figure on this dashboard is a number in a column of numbers. */
dd, td, .metrics dd, .chip, .badge { font-variant-numeric: tabular-nums; }

/* A price, a quantity, a size. The one place the monospace face is used
   outside `<code>`, and it is used for the reason monospace exists: so that a
   column of figures lines up on the decimal without a table telling it to. */
.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* The keyboard's way past the navigation, on every page. Off-screen until it
   is focused, which is the only time it is of use to anybody. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 60;
  padding: var(--s2) var(--s3);
  background: var(--surface-2);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
}
.skip:focus { left: var(--s3); top: var(--s3); }

/* -- the shell ----------------------------------------------------------
 *
 * A fixed rail and a column of content beside it. The rail is `fixed` rather
 * than `sticky` because it is the full height of the viewport at every scroll
 * position, and because that is what lets the content column own the whole
 * scroll -- one scrollbar on the page, not two side by side.
 *
 * Three widths, and the middle one is the interesting one:
 *
 *   >= 64rem   the rail, 240px, labelled
 *   48-64rem   the rail, 56px, icons only -- a tablet is wide enough to keep
 *              navigation on screen and too narrow to spend 240px on it
 *   <  48rem   off-canvas, over the content, with a scrim
 *
 * The open state is a checkbox, not a script. Everything else on this
 * dashboard that discloses -- the tabs, the split controls, the settings
 * accordions -- is CSS-only for the same reason: the navigation of a trading
 * dashboard is not something to lose because a bundle did not arrive. */

.nav-switch {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  opacity: 0;
  /* Not `display: none` and not `pointer-events: none`: it is the control, and
     a keyboard has to be able to reach and toggle it. */
}

.shell { display: block; }

.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 40;
  width: var(--rail);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s3) var(--s2);
  /* Still the ground rather than a panel -- but a lit one. The gradient runs
     from a shade above the page colour at the top to the page colour itself at
     the foot, which is what stops a 240px column of flat near-black reading as
     a hole beside the content. It resolves to `--bg` at both ends of the
     scale, so the rail and the page never disagree about where the edge is:
     the hairline does that, and the glow beside it is what makes the hairline
     look intentional rather than like a seam. */
  background:
    linear-gradient(180deg, rgb(255 255 255 / 0.035) 0%, rgb(255 255 255 / 0) 42%),
    /* The landing page's own mesh, narrowed to a 240px column: an accent bloom
       behind the mark at the top and a violet one at the foot, both wide enough
       that neither ever resolves into a shape. It is the same ground the hero
       is painted on, which is what makes the rail read as part of the product
       rather than as a chrome bar bolted to the side of it. */
    radial-gradient(24rem 20rem at 50% -10%, rgb(77 144 248 / 0.14), transparent 65%),
    radial-gradient(20rem 24rem at 30% 108%, rgb(139 92 246 / 0.1), transparent 68%),
    var(--bg);
  border-right: 1px solid var(--border);
  /* The lit edge. An inset rule rather than a second border, so it cannot
     widen the rail and cannot be undone by the collapse at tablet width. */
  box-shadow: inset -1px 0 0 var(--glass-line);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.sidebar__brand {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--s2);
}
/* The mark, at the height the row already was. The same 36px survives the
   collapse to a 56px rail, which is the point of using the mark here and the
   lockup nowhere in the shell: it is the one thing up here that still reads
   once the labels are gone. `width: auto` off a square file is 36px either
   way; it is written so a re-cut file cannot stretch the row. */
.sidebar__brand img {
  display: block;
  height: 36px;
  width: auto;
  /* Lit rather than moved. A mark that changed size on a loop would drag the
     row's baseline with it forty times a minute; opacity and a halo do not
     lay anything out. `drop-shadow` rather than `box-shadow` because the file
     has transparency in it and a box glow would light its bounding box. */
  filter: drop-shadow(0 0 10px rgb(77 144 248 / 0.35));
  animation: breathe 4.5s ease-in-out infinite;
}

.sidebar__nav {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.sidebar__nav a {
  display: flex;
  align-items: center;
  gap: var(--s2);
  /* 36px under a mouse. A coarse pointer takes the full target back, below. */
  min-height: 36px;
  padding: 0 12px;
  font-size: var(--fs-body);
  font-weight: 500;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  border-radius: var(--radius-sm);
  position: relative;
  /* The active item's left edge. Transparent rather than absent, so lighting
     it does not move the label by three pixels. */
  border-left: 2px solid transparent;
  transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}

/* The icons. Attribute selectors rather than a class on each anchor, and a
   mask rather than an `<img>`: the glyph then takes `currentColor`, so it
   lights with the label instead of needing a second copy in the accent. */
.sidebar__nav a::before {
  content: "";
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  background: currentColor;
  -webkit-mask: var(--icon) center / 16px 16px no-repeat;
  mask: var(--icon) center / 16px 16px no-repeat;
  opacity: 0.9;
}
.sidebar__nav a[href="/"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='7.5' height='7.5' rx='1.5'/%3E%3Crect x='13.5' y='3' width='7.5' height='7.5' rx='1.5'/%3E%3Crect x='3' y='13.5' width='7.5' height='7.5' rx='1.5'/%3E%3Crect x='13.5' y='13.5' width='7.5' height='7.5' rx='1.5'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/instances"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4.5' y='4.5' width='15' height='15' rx='2'/%3E%3Crect x='9.5' y='9.5' width='5' height='5' rx='1'/%3E%3Cpath d='M9.5 2.5v2M14.5 2.5v2M9.5 19.5v2M14.5 19.5v2M2.5 9.5h2M2.5 14.5h2M19.5 9.5h2M19.5 14.5h2'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/strategies"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.75 2.75 7.5 12 12.25 21.25 7.5 12 2.75Z'/%3E%3Cpath d='m2.75 12.5 9.25 4.75 9.25-4.75'/%3E%3Cpath d='m2.75 17 9.25 4.75L21.25 17'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/backtest"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.25 2.75v16.5a1.5 1.5 0 0 0 1.5 1.5h16.5'/%3E%3Cpath d='m7 15.25 3.75-4.5 3 2.75 4.5-6'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/credentials"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7.75' cy='15.75' r='4.5'/%3E%3Cpath d='m11 12.5 8.25-8.25M16.5 6.75 19 9.25M13.75 9.5l2.5 2.5'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/account"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='8' r='4'/%3E%3Cpath d='M4.5 20.5a7.5 7.5 0 0 1 15 0'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/admin/users"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2.75 4.75 5.5v6c0 4.4 3 8.4 7.25 9.75 4.25-1.35 7.25-5.35 7.25-9.75v-6L12 2.75Z'/%3E%3C/svg%3E");
}
.sidebar__nav a[href="/guide"] {
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.25 4.75A2.5 2.5 0 0 1 6.75 2.25h13v16h-13a2.5 2.5 0 0 0-2.5 2.5v-16Z'/%3E%3Cpath d='M4.25 18.25a2.5 2.5 0 0 1 2.5-2.5h13'/%3E%3C/svg%3E");
}

.sidebar__nav a[aria-current="page"] {
  color: var(--text);
  font-weight: 600;
  /* The fill fades out across the row rather than filling it evenly: the
     weight belongs at the lit edge, and a flat accent rectangle 240px wide is
     louder than the page it is pointing into. */
  background: linear-gradient(90deg, var(--accent-soft), rgb(77 144 248 / 0));
  border-left-color: var(--accent);
  /* Square against the rule and rounded away from it: the fill reads as
     something attached to the edge of the rail rather than a floating pill. */
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  /* The glow on that edge. Inset and 2px wide, sitting exactly under the
     border, so it lights the rule rather than adding a second one. */
  box-shadow: inset 2px 0 8px -2px rgb(77 144 248 / 0.7);
}
@media (hover: hover) {
  .sidebar__nav a:hover {
    color: var(--text);
    background: linear-gradient(90deg, rgb(255 255 255 / 0.06), rgb(255 255 255 / 0.02));
  }
  .sidebar__nav a[aria-current="page"]:hover {
    background: linear-gradient(90deg, var(--accent-soft), rgb(77 144 248 / 0.04));
  }
}

/* Who is signed in, and the way out. Pushed to the bottom of the rail by the
   margin rather than by a spacer element. */
.sidebar__foot {
  margin-top: auto;
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}
.sidebar__user {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 0 12px;
  min-width: 0;
}
.sidebar__avatar {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--muted);
  text-transform: uppercase;
}
.sidebar__who { min-width: 0; }
.sidebar__name {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The role and the plan on one line under the name. Wrapping rather than
   truncating: at 240px "administrator" and a plan badge do not always fit
   across, and a clipped role is worse than a second line. */
.sidebar__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
}
.sidebar__role {
  font-size: var(--fs-xs);
  color: var(--muted);
}

/* The plan, badged by tier. The colour is the signal and the word is the
   confirmation -- never the colour alone, which is the rule every other mark
   on this dashboard is held to. Smaller than a `.badge` because it sits in a
   24px row beside a role, and the inner glow rather than a fill so a lit
   pill on a lit rail still reads as a pill. */
.sidebar__plan {
  display: inline-block;
  font-size: 0.625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  white-space: nowrap;
}
.sidebar__plan--free {
  border-color: var(--border-strong);
  background: var(--plan-free-bg);
  color: var(--plan-free);
}
.sidebar__plan--basic {
  border-color: var(--accent-line);
  background: var(--plan-basic-bg);
  color: var(--plan-basic);
  box-shadow: inset 0 0 8px rgb(77 144 248 / 0.22);
}
.sidebar__plan--pro {
  border-color: rgb(167 139 250 / 0.45);
  background: var(--plan-pro-bg);
  color: var(--plan-pro);
  box-shadow: inset 0 0 8px rgb(167 139 250 / 0.26);
}
.sidebar__foot .btn {
  justify-content: flex-start;
  padding: 0 12px;
  min-height: 36px;
  font-weight: 500;
}
.sidebar__foot form { display: block; }

/* The scrim behind the open drawer. Present at every width and inert until the
   drawer is open, which is what lets it fade rather than appear. */
.nav-scrim {
  position: fixed;
  inset: 0;
  z-index: 39;
  background: rgb(3 6 12 / 0.6);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s ease, visibility 0.15s ease;
}

.main { margin-left: var(--rail); min-width: 0; }

/* The strip above the content. On a phone it carries the drawer's handle and
   the brand; at every width it carries the live-updates pip, which is the one
   piece of state that belongs to the whole page rather than to any panel on
   it. Sticky, because "is this still live" is asked from halfway down a long
   page, which is exactly where an unpinned indicator is not. */
.appbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 36px;
  padding: var(--s3) var(--s5) 0;
  /* Opaque, because content scrolls under it, and borderless: on a wide screen
     this is the page's top gutter with a status pip in the corner of it, not a
     bar. It becomes a real bar on a phone, where it also carries the drawer's
     handle -- see the narrow-screen block. */
  background: var(--bg);
}
/* Frosted where it can be. On a wide screen this bar is the page's top gutter
   and the blur is barely visible; on a phone it is a real bar with content
   running under it, which is where it earns its keep. Opaque fallback above,
   like the cards. */
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .appbar {
    background: var(--glass-page);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
.appbar__toggle, .appbar__brand { display: none; }
.appbar__right { margin-left: auto; display: flex; align-items: center; gap: var(--s2); }

.appbar__toggle {
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  margin-left: calc(var(--s2) * -1);
  border-radius: var(--radius-sm);
  color: var(--muted);
  cursor: pointer;
}
.appbar__toggle::before {
  content: "";
  width: 20px;
  height: 20px;
  background: currentColor;
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round'%3E%3Cpath d='M3.5 6.5h17M3.5 12h17M3.5 17.5h17'/%3E%3C/svg%3E");
  -webkit-mask: var(--icon) center / 20px 20px no-repeat;
  mask: var(--icon) center / 20px 20px no-repeat;
}
.appbar__brand {
  align-items: center;
}
/* Smaller than the rail's, because this bar is 36px tall and the mark is
   sharing it with a drawer handle and the live pip. */
.appbar__brand img {
  display: block;
  height: 28px;
  width: auto;
}
/* The checkbox is off-screen, so the thing that shows it has focus is the
   handle it is labelled by. */
.nav-switch:focus-visible ~ .shell .appbar__toggle {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* -- the tablet rail. Wide enough to keep navigation on screen, too narrow to
   spend 240px on it: the labels go and the icons stay. The text is not removed
   -- `font-size: 0` leaves it in the document, so the accessible name of every
   link is still its word rather than its picture. */
@media (min-width: 48rem) and (max-width: 63.99rem) {
  .sidebar { width: var(--rail-collapsed); padding-left: var(--s1); padding-right: var(--s1); }
  .main { margin-left: var(--rail-collapsed); }
  .sidebar__nav a { justify-content: center; padding: 0; font-size: 0; gap: 0; border-left-width: 0; border-radius: var(--radius-sm); }
  /* The fill goes flat and the left rule goes with the labels: on a 40px row
     a gradient that fades out over 240px is one flat colour anyway, and a
     2px rule down the edge of an icon-only square reads as a clipped box.
     The inner glow stays, so the row is still lit rather than merely tinted. */
  .sidebar__nav a[aria-current="page"] {
    border-radius: var(--radius-sm);
    background: var(--accent-soft);
    box-shadow: inset 0 0 10px rgb(77 144 248 / 0.3);
  }
  /* The plan badge goes with the name it qualifies -- `.sidebar__who` is
     already hidden here, and this is inside it. Written anyway, because the
     row's `justify-content: center` is what a stray 30px pill would fight. */
  .sidebar__meta { justify-content: center; }
  .sidebar__brand { justify-content: center; padding: 0; }
  .sidebar__user { justify-content: center; padding: 0; }
  .sidebar__who, .sidebar__foot .btn span { display: none; }
  /* The row's `font-size: 0` zeroes its children too, and one of them is not
     a label: the dot that says this account has no second factor. It is the
     reason the Account link is worth looking at, so it is given its size back
     and moved to the corner the label no longer occupies. */
  .sidebar__nav a .dot { position: absolute; top: 5px; right: 9px; font-size: 12px; }
  .sidebar__foot .btn { justify-content: center; padding: 0; font-size: 0; }
  .sidebar__foot .btn::before {
    content: "";
    width: 16px;
    height: 16px;
    background: currentColor;
    --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 3.75h4.25a1.5 1.5 0 0 1 1.5 1.5v13.5a1.5 1.5 0 0 1-1.5 1.5H15'/%3E%3Cpath d='m10 8-4 4 4 4M6 12h9.5'/%3E%3C/svg%3E");
    -webkit-mask: var(--icon) center / 16px 16px no-repeat;
    mask: var(--icon) center / 16px 16px no-repeat;
  }
}

/* -- the phone. The rail comes off the canvas and returns as a drawer over the
   content, with a scrim behind it. 200ms, which is long enough to read as a
   movement and short enough not to be a wait. */
@media (max-width: 47.99rem) {
  .sidebar {
    transform: translateX(-100%);
    transition: transform 0.15s ease;
    box-shadow: var(--shadow-pop);
  }
  .main { margin-left: 0; }
  .appbar {
    padding: var(--s2) var(--s3);
    border-bottom: 1px solid var(--border);
  }
  .appbar__toggle, .appbar__brand { display: inline-flex; }
  .nav-switch:checked ~ .shell .sidebar { transform: none; }
  .nav-switch:checked ~ .shell .nav-scrim { opacity: 1; visibility: visible; }
  /* Nothing behind the drawer scrolls while it is open. */
  .nav-switch:checked ~ .shell { overflow: hidden; height: 100vh; }
}

/* A coarse pointer takes the full target back, at every width. The rail is
   36px because it is read with a mouse; a phone and a tablet with a touch
   screen are not, and that is not what the density is bought out of. */
@media (pointer: coarse) {
  .sidebar__nav a, .sidebar__foot .btn { min-height: var(--tap); }
}

.page { max-width: 90rem; margin: 0 auto; padding: var(--s4) var(--s5) 4rem; }
/* The guide is prose. A 1400px measure is unreadable, whatever the rest of the
   dashboard wants. */
.page--prose { max-width: 56rem; }
.page__title {
  font-size: var(--fs-title);
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0 0 var(--s4);
}
.crumb { margin: 0; font-size: var(--fs-sm); }
/* The back link is the most-pressed control on every page that has one, and on
   a phone it is pressed one-handed at the top of the screen. It gets a real
   target rather than the 17px an inline anchor is by default. */
.crumb a {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  min-height: var(--tap);
  color: var(--muted);
  text-decoration: none;
}
.crumb a::before { content: "\2190"; }
@media (hover: hover) { .crumb a:hover { color: var(--text); } }

.footer {
  max-width: 90rem;
  margin: 0 auto;
  padding: var(--s4) var(--s5) var(--s5);
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: var(--fs-xs);
}

/* -- the layout grid ----------------------------------------------------
 *
 * One class, used everywhere two panels belong beside each other. Everything
 * inside gets `min-width: 0` because a grid track's default minimum is its
 * content, and a contract id is wider than half a phone. */

.cols { display: grid; gap: var(--s4); align-items: start; }
.cols > * { min-width: 0; }
/* Cards inside a grid are spaced by the grid, not by their own margin. */
.cols > .card, .cols > * > .card:last-child { margin-bottom: 0; }
.cols--2 { grid-template-columns: 1fr; }

@media (min-width: 60rem) {
  .cols--2 { grid-template-columns: 1fr 1fr; }
}

/* The overview's 2x2. The live region is `display: contents` so the two panels
   the websocket swaps sit in the same grid as the two it does not -- a wrapper
   with a box of its own would make them one cell between them. */
.dash__live { display: contents; }
.dash > .banner, .dash__live > .banner { grid-column: 1 / -1; }

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

/* A lit fill, a hairline, and a lit top edge.
 *
 * The fill is a very shallow sheen over the surface colour rather than the
 * flat colour alone: on a near-black ground an unlit rectangle reads as a hole
 * cut in the page, and the whole reason the panels are separated at all is so
 * the figures on them are read as belonging together.
 *
 * The top edge is a pseudo-element rather than a `border-image`, and that is
 * the load-bearing choice. `border-image` replaces the whole border, so a card
 * with a gradient top would lose the hairline down its other three sides --
 * and those are what say where the box ends. This paints one gradient rule
 * over the top border instead, inset by the corner radius at both ends so it
 * fades out before the curve rather than being clipped by it. */
.card {
  position: relative;
  background: var(--sheen), var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s4);
  margin-bottom: var(--s4);
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.card::before {
  content: "";
  position: absolute;
  top: -1px;
  left: var(--radius);
  right: var(--radius);
  /* Two pixels, which is the weight `.status` and `.metric--hero` already use
     for the same mark. At one it was a hairline sitting on a hairline border
     and the two read as one slightly paler line. */
  height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--accent-line), transparent);
}
/* Frosted, where the browser has it. Deliberately an enhancement rather than
   the design: the fill above is opaque and complete, and everything below only
   swaps it for a translucent one *once* the blur that makes translucency
   legible is known to exist. Without the guard, a browser with no
   `backdrop-filter` would render the panels as plain transparency over a
   gradient mesh, which is the one way to make a dashboard of figures harder to
   read rather than easier. */
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .card {
    background: var(--sheen), var(--glass);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
/* The lift. Two pixels, which is enough to read as a response and not enough
   to move a figure somebody is in the middle of reading -- and the edge takes
   the accent while it is raised, so the panel under the pointer is the one
   thing on the page wearing the brand colour. The halo is `--glow`: a lit
   1px edge and a 0.15 bloom with no offset in either, which is light coming
   off a panel rather than a shadow cast under it. Pointer only: there is no
   hover on a phone, and a lift that stuck after a tap would leave one panel on
   the page permanently raised. */
@media (hover: hover) {
  .card:hover {
    transform: translateY(-2px);
    border-color: var(--accent-line);
    box-shadow: var(--glow);
  }
}
.card--stale { opacity: 0.85; border-style: dashed; }
/* The sign-in card is not a panel in a grid of panels -- it is the page. It
   keeps the surface and gives up the lift and the lit edge, because there is
   nothing beside it for either to distinguish it from. */
.card--narrow::before { content: none; }
@media (hover: hover) {
  .card--narrow:hover { transform: none; box-shadow: none; border-color: var(--border); }
}

/* The signed-out pages: one card, centred on the page colour, with the brand
   above it. No illustration and no marketing -- this is the door to something
   that moves money, and it should read like one. */
.auth { min-height: 100vh; display: grid; place-items: center; padding: var(--s5) var(--s4); }
/* The full lockup, in the three places that get one: the landing page, the
   sign-in door and the guide. All three are pages a reader can reach without
   having been told what this is, which is the whole argument for spending the
   tagline on them and for giving the rail the bare mark instead. Sized
   together because they are the same object doing the same job -- and kept as
   one rule so the phone size below cannot be undone by whichever of them the
   sheet declares last. */
.auth__brand, .guide__brand, .landing__logo {
  display: block;
  width: 100%;
  max-width: 400px;
  height: auto;
}
/* Centred over a centred card; left-aligned over left-aligned prose. */
.auth__brand { margin: 0 auto var(--s5); }
.guide__brand { margin: 0 0 var(--s4); }

@media (max-width: 47.99rem) {
  .auth__brand, .guide__brand, .landing__logo { max-width: 280px; }
}
.card--narrow {
  width: 100%;
  max-width: 23rem;
  margin: 0 auto;
  padding: var(--s5);
  background: var(--surface-2);
}
.card--narrow h1 {
  margin: 0 0 var(--s4);
  font-size: 1.0625rem;
  font-weight: 600;
}
.auth__foot { margin: var(--s4) auto 0; max-width: 23rem; text-align: center; }

/* Panel headings. `h3` is a section label -- Positions, Working orders, Trade
   log -- and reads as one: 11px, tracked, and quiet enough that the numbers
   under it are what the eye lands on. `h2` is a result and stays a heading. */
.card > h2, .card > h3, .card > form > h2 {
  margin: 0 0 var(--s3);
}
.card > h2, .card > form > h2 { font-size: 1.125rem; font-weight: 600; letter-spacing: -0.01em; }
/* `.card__label` is the same eyebrow for a heading the child selector cannot
   reach -- one nested inside a column of a grid, say. */
.card > h3, .card__label {
  margin: 0 0 var(--s3);
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
/* The guide's own headings are headings, not panel labels. Written with the
   element as well as the class so they outrank `.card > h3` rather than tying
   with it and losing on source order. */
h2.guide__heading, h3.guide__heading {
  font-size: 1.0625rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: -0.015em;
  color: var(--text);
}
/* The child combinator plus the one place a subheading is legitimately a step
   further down: a long form inside a card divides itself into named parts, and
   its `h4` is the same heading as one sitting directly in the card. Without the
   second selector it renders at the browser's default 1.5em with the browser's
   default margins, which on the notification panel is a subheading twice the
   size of the panel's own title. */
.card > h4, .card form > h4 { margin: var(--s4) 0 var(--s2); font-size: var(--fs-body); font-weight: 600; }
.card > p:last-child { margin-bottom: 0; }
/* A 1400px card is the right width for a table of figures and the wrong one
   for a sentence. The panel keeps the width; its prose does not. */
.card p, .card li, .accordion__body p { max-width: 84ch; }

/* The bots list: a grid of bot cards rather than a column of them. Four across
   on a wide screen, one on a phone, and no arithmetic anywhere -- `auto-fill`
   with a sane minimum decides. */
.cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
  gap: var(--s4);
}
.cards > .card { margin-bottom: 0; }
/* A bot card is a link to a bot, so it answers a pointer more strongly than a
   panel does: the accent halo as well as the lift. */
@media (hover: hover) {
  .cards > .card:hover { border-color: var(--accent-line); box-shadow: var(--glow); }
}
.card__title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  font-size: 1.0625rem;
  font-weight: 600;
  text-decoration: none;
  color: var(--text);
  min-height: var(--tap);
}
.card__chips { display: flex; flex-wrap: wrap; gap: var(--s1); margin: 0.2rem 0; }
.card__detail { margin: var(--s2) 0 0; font-size: var(--fs-sm); }

.panel__header {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: flex-start;
  justify-content: space-between;
}
.panel__title {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
}
.panel__detail { margin: var(--s2) 0 0; font-size: var(--fs-sm); }

/* -- the instance header row --------------------------------------------
 *
 * Name, badges, status and every control on one line, because on this page
 * they are one thought: what this bot is, and what can be done to it. */

.inst__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s3);
}
.inst__id { flex: 1 1 16rem; min-width: 0; }
.inst__meta { margin: 0.15rem 0 0; font-size: var(--fs-xs); color: var(--muted); }

/* -- chips, badges, banners -------------------------------------------- */

/* Pill-shaped, and lit from inside in its own colour. The fill was always
   the status; the inset glow is the same fill concentrated at the edges,
   which is what stops a 10px word on a dark ground reading as flat ink. */
.chip {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  font-family: var(--sans);
  font-size: var(--fs-xs);
  font-weight: 600;
  line-height: 1.5;
  background: var(--unknown-bg);
  color: var(--muted);
  white-space: nowrap;
}
.chip--ok { background: var(--ok-bg); color: var(--ok); box-shadow: inset 0 0 8px rgb(34 197 94 / 0.28); }
.chip--warn { background: var(--warn-bg); color: var(--warn); box-shadow: inset 0 0 8px rgb(245 158 11 / 0.28); }
.chip--danger { background: var(--danger-bg); color: var(--danger); box-shadow: inset 0 0 8px rgb(241 92 92 / 0.28); }
.chip--idle { background: var(--idle-bg); color: var(--idle); box-shadow: inset 0 0 8px rgb(255 255 255 / 0.06); }

.badge {
  display: inline-block;
  font-family: var(--sans);
  font-size: 0.625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--muted);
  white-space: nowrap;
}
.badge--live {
  border-color: rgb(241 92 92 / 0.5);
  background: var(--danger-bg);
  color: var(--danger);
  font-weight: 700;
  /* The loudest badge on the platform: this bot is trading real money. It gets
     the inner glow *and* an outer one, which is the only place on the
     dashboard both are used together. */
  box-shadow: inset 0 0 10px rgb(241 92 92 / 0.35), 0 0 12px rgb(241 92 92 / 0.22);
}
/* Accent rather than a status colour. Read-only is not a fault and not an
   alarm -- it is a deliberate configuration -- but it does change what every
   other number on the card means, so it has to be the thing the eye lands on
   after the name. */
.badge--readonly { border-color: var(--accent-line); background: var(--accent-soft); color: var(--accent); font-weight: 700; box-shadow: inset 0 0 10px rgb(77 144 248 / 0.28); }

/* Who placed an order. Accent for the operator's own, because in a table of
   mostly-strategy rows the exception is what has to be findable; the bot badge
   deliberately keeps the default muted border, so a page of them reads as
   texture rather than as forty highlighted rows. Not a status colour either
   way: a manual order is neither a warning nor a fault. */
.badge--manual { border-color: var(--accent-line); color: var(--accent); font-weight: 700; box-shadow: inset 0 0 10px rgb(77 144 248 / 0.2); }
.badge--strategy { color: var(--muted); }

/* A rule down the left edge rather than a full tinted box: a banner is read
   next to the panel it qualifies, and four saturated rectangles in a column
   are what a page of warnings looks like when nobody reads any of them. */
.banner {
  border-radius: var(--radius-sm);
  padding: var(--s3);
  margin: var(--s3) 0;
  border: 1px solid var(--border);
  border-left: 2px solid var(--border-strong);
  background: var(--surface-2);
  font-size: var(--fs-sm);
  line-height: 1.55;
}
.banner p { margin: 0.4rem 0 0; }
.banner p:first-child { margin-top: 0; }
.banner ul { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.banner--ok { background: var(--ok-bg); border-color: var(--border); border-left-color: var(--ok); }
.banner--warn { background: var(--warn-bg); border-color: var(--border); border-left-color: var(--warn); }
.banner--danger { background: var(--danger-bg); border-color: var(--border); border-left-color: var(--danger); }
.banner--info { background: var(--surface-2); border-left-color: var(--accent); }
.card > .banner:first-child { margin-top: 0; }

/* -- metrics ------------------------------------------------------------
 *
 * The row this dashboard is opened for. Every variant below is the same
 * reading at a different weight: a label above, a figure under it, aligned on
 * the decimal because they are read down a column as often as across a row. */

.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6.5rem, 1fr));
  gap: var(--s2);
  margin: var(--s4) 0 0;
}
.metrics > div { min-width: 0; }
.metrics dt {
  color: var(--muted);
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.metrics dd {
  margin: 0.2rem 0 0;
  font-size: 1rem;
  line-height: 1.25;
  letter-spacing: -0.01em;
  /* The `dt` above is clipped to an ellipsis; the `dd` must not be, because it
     is the figure. So it is allowed to break instead -- and it needs to be
     told it may, because a formatted money value has no break opportunity in
     it at all. `-$12,345.67` at the headline size is wider than a backtest
     result tile on a 375px screen, and without this it does not wrap, it does
     not clip, it simply extends past the tile and takes the width of the page
     with it. A wrapped P&L is ugly; a report page that scrolls sideways is
     the thing being traded away for it. */
  min-width: 0;
  overflow-wrap: anywhere;
}
/* The same reading, packed to the left instead of stretched into equal
   columns. A four-figure row spread across 1400px reads as four unrelated
   panels; the point of `--compact` is that it is one line of figures. */
.metrics--compact {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3) var(--s5);
}
.metrics--compact dd { font-size: 0.9375rem; }
/* The quieter row under a reading: things that qualify the figures rather than
   being them. A flex row rather than a grid, so five short values sit next to
   each other instead of being stretched into five equal columns across
   1400px -- which reads as five unrelated panels rather than one footnote. */
.metrics--meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3) var(--s5);
  margin-top: var(--s4);
}
.metrics--meta dd { font-size: var(--fs-body); }

/* The tiled row -- the terminal strip. Each figure in a box of its own, which
   is what turns eight numbers from a paragraph of numbers into a row that can
   be read across. 12px of padding rather than 16: these are compact by
   intention, and the figure is what the space is for. */
.metrics--tiles {
  /* 7rem rather than 8: at 320px the card is 264px wide inside its padding,
     and 8rem tiles are one pixel too wide to sit two abreast -- which turns a
     row of readings into a column of them on the narrowest screen there is. */
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: 1px;
  /* The tiles are separated by the grid gap showing the border colour through,
     not by eight separate outlines: one strip with hairlines in it reads as a
     row, and eight bordered boxes read as eight cards. */
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* A ring rather than a shadow. The strip clips its children, so an *inset*
     shadow on it would be painted over by the first tile's own fill and an
     offset one would put a smudge under a console. A zero-offset hairline of
     light sits outside the border and simply lifts the whole instrument off
     the panel. */
  box-shadow: 0 0 0 1px rgb(255 255 255 / 0.05);
  overflow: hidden;
}
.metrics--tiles > div {
  padding: var(--s3);
  /* Each tile lit from its own top edge. The strip is one object -- the gap
     between the tiles is the strip's background showing through -- so the
     gradient has to be per-tile for the row to read as instruments in a
     console rather than as one long panel with rules drawn on it. */
  background: linear-gradient(180deg, var(--surface-2), var(--surface) 70%);
  border: 0;
  transition: background 0.15s ease, box-shadow 0.15s ease;
}
/* The panels' hover answer, in the one form a tile can take it. The strip
   clips its children, so the halo a card gets outside its border would be cut
   off at the first hairline: the tile lifts a step instead and lights its own
   top edge in the accent, which is the same reading from inside the box. */
@media (hover: hover) {
  .metrics--tiles > div:hover {
    background: linear-gradient(180deg, var(--surface-3), var(--surface-2) 70%);
    box-shadow: inset 0 2px 0 rgb(77 144 248 / 0.55);
  }
}
.metrics--tiles dd { font-size: 1.125rem; font-weight: 600; }
/* The glow is on the figure, in the figure's own meaning. `--glow-text-*`
   is `none` under a light theme, where a bloomed number is just a blurred
   one -- so these rules need no theme branch of their own. */
.metrics--tiles dd.pos, .metrics--headline dd.pos { text-shadow: var(--glow-text-ok); }
.metrics--tiles dd.neg, .metrics--headline dd.neg { text-shadow: var(--glow-text-danger); }

/* The one figure the page exists to report. Twice the size of the tiles around
   it and coloured by its sign, because the first question is never "how much"
   -- it is "up or down".

   It also gets the console's one lit tile: an accent wash under the figure and
   a rule along its top edge. Both are decoration over a reading that is
   already carried by size, colour and position, which is what makes it safe
   for either to be switched off. */
.metric--hero { grid-column: span 2; position: relative; }
.metrics--tiles > .metric--hero {
  background:
    linear-gradient(180deg, rgb(77 144 248 / 0.1), rgb(77 144 248 / 0.02) 60%),
    linear-gradient(180deg, var(--surface-3), var(--surface) 70%);
}
@media (hover: hover) {
  .metrics--tiles > .metric--hero:hover {
    background:
      linear-gradient(180deg, rgb(77 144 248 / 0.2), rgb(77 144 248 / 0.05) 60%),
      linear-gradient(180deg, var(--surface-3), var(--surface-2) 70%);
    box-shadow: none;
  }
}
.metrics--tiles > .metric--hero::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent-grad);
  opacity: 0.9;
}
.metric--hero dd {
  font-size: var(--fs-metric);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.025em;
  /* The hero figure glows even when it is neither up nor down -- a flat zero
     is still the number the page was opened for. `.pos` and `.neg` above
     override it with the colour that means something. */
  text-shadow: var(--glow-text-accent);
}

/* The headline block of a backtest: six figures, three across and two down on
   anything wider than a phone, two across below that. Stated rather than
   auto-fit, because these six are the result and a row of eight thin columns
   is not how a result should read. */
.metrics--headline { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 48rem) {
  .metrics--headline { grid-template-columns: repeat(3, 1fr); }
}
/* A backtest's six figures are the result the page was opened for, so they are
   set at the figure size the rest of the product uses for one rather than a
   step under it. */
.metrics--headline dd { font-size: var(--fs-metric); font-weight: 600; letter-spacing: -0.02em; }

/* And they are six panels rather than six cells of one instrument.
 *
 * That is the difference between this block and the bot page's strip, and it
 * is a difference in what is being read. The strip is eight live readings of
 * one running thing, glanced across in one movement, so it is one object with
 * hairlines in it. These six are the *result* of a finished run -- read one at
 * a time, compared against another run's, screenshotted into a message -- and
 * each one is its own answer. So the strip's shared background and clipped
 * corners come off, and each tile takes the card treatment the rest of the
 * platform uses for a thing that is its own object: frosted fill, hairline,
 * lit top edge, and a lift under a pointer. */
.metrics--headline {
  gap: var(--s3);
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
}
.metrics--headline > div {
  position: relative;
  padding: var(--s4);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--sheen), var(--surface);
  overflow: hidden;
  transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .metrics--headline > div {
    background: var(--sheen), var(--glass);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
.metrics--headline > div::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg, var(--accent-line), transparent);
}
@media (hover: hover) {
  .metrics--headline > div:hover {
    transform: translateY(-2px);
    border-color: var(--accent-line);
    box-shadow: var(--glow);
    background: var(--sheen), var(--surface);
  }
}

/* Exposure against its cap. Still a hairline rather than a block -- it
   qualifies the figure above it and must not out-weigh it -- but a lit one:
   the track is sunk into the tile and the fill is a ramp that runs cyan to
   violet across the cap, so how full the bar is reads from its colour as well
   as from its length. One pixel taller than it was, which is what the ramp
   needs to be visible as a ramp at all. */
.meter {
  height: 4px;
  border-radius: 999px;
  background: var(--surface-3);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.4);
  overflow: hidden;
  margin: var(--s2) 0 0;
}
.metrics--tiles .meter { margin: var(--s2) 0 0; }
.meter__fill {
  height: 100%;
  /* The default ramp, for the bars that are progress rather than a reading --
     a backtest's tick count is not closer to anything bad at 90%. The three
     semantic bands below are what a *cap* gets. */
  background: linear-gradient(90deg, #22d3ee, #4d90f8 55%, #8b5cf6);
  border-radius: 999px;
  box-shadow: 0 0 10px rgb(77 144 248 / 0.5);
  transition: width 0.15s ease, background 0.15s ease;
}

/* A bar drawn against a cap, in the colour of how close to it this is: green
   under two thirds, amber past it, red at it. The band comes off the same
   fraction the width does, so the two channels cannot disagree -- and the
   width is still the one that carries the reading, which is what a reader who
   cannot separate the three colours is left with. The tone is on the track
   rather than on the fill so an empty bar in a red state is still visibly the
   bar of something that has reached its cap. */
.meter--ok .meter__fill {
  background: linear-gradient(90deg, #16a34a, var(--ok));
  box-shadow: 0 0 10px rgb(34 197 94 / 0.45);
}
.meter--warn .meter__fill {
  background: linear-gradient(90deg, var(--ok), var(--warn) 85%);
  box-shadow: 0 0 10px rgb(245 158 11 / 0.5);
}
.meter--danger .meter__fill {
  background: linear-gradient(90deg, var(--ok), var(--warn) 55%, var(--danger));
  box-shadow: 0 0 10px rgb(241 92 92 / 0.55);
}

/* Widths as classes rather than inline styles: the page's CSP has no
   unsafe-inline, which blocks style attributes outright. */
.meter__fill--0 { width: 0%; }
.meter__fill--5 { width: 5%; }
.meter__fill--10 { width: 10%; }
.meter__fill--15 { width: 15%; }
.meter__fill--20 { width: 20%; }
.meter__fill--25 { width: 25%; }
.meter__fill--30 { width: 30%; }
.meter__fill--35 { width: 35%; }
.meter__fill--40 { width: 40%; }
.meter__fill--45 { width: 45%; }
.meter__fill--50 { width: 50%; }
.meter__fill--55 { width: 55%; }
.meter__fill--60 { width: 60%; }
.meter__fill--65 { width: 65%; }
.meter__fill--70 { width: 70%; }
.meter__fill--75 { width: 75%; }
.meter__fill--80 { width: 80%; }
.meter__fill--85 { width: 85%; }
.meter__fill--90 { width: 90%; }
.meter__fill--95 { width: 95%; }
.meter__fill--100 { width: 100%; }

/* -- the backtest's progress bar ---------------------------------------- */
/* The one bar on the platform that is watched rather than read. A replay is
   minutes long, and the question somebody is holding this page open to answer
   is not "how far" but "how much longer" -- so this is a bar, a line of
   figures and a sentence, and the sentence is the part that answers it.

   Wider and louder than `.meter`, which is the 4px reading a metric tile
   carries. This is the subject of its own card. */

.progress {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin: var(--s3) 0 0;
}
.progress__track {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-3);
  /* Inset, so the track reads as a channel cut into the card rather than as a
     grey bar laid on it. An offset shadow here would be a smudge; see the
     sheet's rule about drop shadows. */
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.4);
  overflow: hidden;
}
.progress__fill {
  height: 100%;
  /* 1% of width, out of two classes rather than out of a `style` attribute:
     the page's Content-Security-Policy has no `unsafe-inline`, so an inline
     width is dropped and the bar reads zero for ever. `--bar-tens` and
     `--bar-units` are set by the twenty-one classes below and added up here.

     The fallbacks are load-bearing: a `calc()` with an unresolved custom
     property in it is invalid at computed-value time and takes the whole
     declaration with it, so a bar rendered without its classes would be full
     width rather than empty. */
  width: calc((var(--bar-tens, 0) + var(--bar-units, 0)) * 1%);
  border-radius: 999px;
  /* The accent ramp, walked along a background three times the width of the
     box. Gradient stops are not animatable; a background-position slide is,
     and it is composited. The movement is the point: a bar whose width has not
     changed for forty seconds because the chunk in flight is a big one still
     has to look like something is happening. */
  background: var(--accent-ramp);
  background-size: 300% 100%;
  animation: ramp 3s linear infinite;
  box-shadow: 0 0 10px rgb(77 144 248 / 0.5);
  transition: width 0.3s ease;
}
/* A finished run keeps its bar and loses its motion. The animation is what
   says "in flight", so leaving it running under a result would be the page
   saying two different things about the same run. */
.progress--final .progress__fill {
  animation: none;
  background: linear-gradient(90deg, #22d3ee, #4d90f8 55%, #8b5cf6);
}

/* The figures. Percentage first because it is what the bar above it just
   said, the counts pushed to the other end, and both on one line -- this is a
   caption for the bar, not a second reading of it. */
.progress__line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  margin: 0;
  font-size: var(--fs-sm);
}
.progress__percent {
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-body);
  font-weight: 700;
  color: var(--text);
}
.progress__counts {
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* The sentence. What is being replayed, how far through it is, and how much
   longer -- the three clauses somebody watching wants, in the order they ask
   for them. Separated by a middle dot rather than by three labelled fields,
   because at 375px a definition list of three is three rows and this is one. */
.progress__status {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.progress__status strong { color: var(--text); font-weight: 600; }

.bar-t0 { --bar-tens: 0; }
.bar-t1 { --bar-tens: 10; }
.bar-t2 { --bar-tens: 20; }
.bar-t3 { --bar-tens: 30; }
.bar-t4 { --bar-tens: 40; }
.bar-t5 { --bar-tens: 50; }
.bar-t6 { --bar-tens: 60; }
.bar-t7 { --bar-tens: 70; }
.bar-t8 { --bar-tens: 80; }
.bar-t9 { --bar-tens: 90; }
.bar-t10 { --bar-tens: 100; }
.bar-u0 { --bar-units: 0; }
.bar-u1 { --bar-units: 1; }
.bar-u2 { --bar-units: 2; }
.bar-u3 { --bar-units: 3; }
.bar-u4 { --bar-units: 4; }
.bar-u5 { --bar-units: 5; }
.bar-u6 { --bar-units: 6; }
.bar-u7 { --bar-units: 7; }
.bar-u8 { --bar-units: 8; }
.bar-u9 { --bar-units: 9; }

/* -- the console's small marks -------------------------------------------
 *
 * A pip, a sparkline, a proportion bar, a row of beats and a trend arrow.
 * Everything below is drawn by `partials/macros.html` as inline SVG or as an
 * empty span, and every dimension in the markup is a presentation attribute
 * rather than a `style` -- the dashboard's Content-Security-Policy has no
 * `unsafe-inline`, and a blocked style attribute is a silent failure that
 * leaves an instrument reading zero.
 *
 * Colour is never the only channel. A pip sits beside the word it agrees
 * with, a sparkline beside the figure it draws, a bar beside the number it is
 * a proportion of. That is what makes it safe for the pulse below to be
 * switched off under `prefers-reduced-motion`, and for a reader who cannot
 * separate green from red to lose nothing at all. */

/* The pip. Eight pixels, flush with the cap height of the word beside it,
   with a halo in its own colour so a lit dot on a near-black panel reads as a
   lamp rather than as a full stop. */
.pip {
  flex: 0 0 auto;
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 0.45rem;
  vertical-align: 0.05em;
  background: var(--idle);
}
.pip--ok      { background: var(--ok);     box-shadow: 0 0 8px rgb(34 197 94 / 0.6); }
.pip--warn    { background: var(--warn);   box-shadow: 0 0 8px rgb(245 158 11 / 0.6); }
.pip--danger  { background: var(--danger); box-shadow: 0 0 8px rgb(241 92 92 / 0.6); }
.pip--idle,
.pip--unknown { background: var(--idle); }

/* The pulse, and it is only ever on a state that is *happening*: a bot that is
   trading, a link that is up, a halt that is in force. A stopped bot is grey
   and still, which is the reading. `ping` animates a zero-offset shadow ring
   out from the dot -- no layout, no repaint of anything but the dot itself. */
.pip--pulse.pip--ok     { animation: ping 2s ease-out infinite; }
.pip--pulse.pip--danger { animation: ping-danger 1.4s ease-out infinite; }
.pip--pulse.pip--warn   { animation: ping-warn 2s ease-out infinite; }

/* Both carry the pip's halo in every frame, for the reason `ping` states. */
@keyframes ping-danger {
  0%, 100% { box-shadow: 0 0 8px rgb(241 92 92 / 0.6), 0 0 0 0 rgb(241 92 92 / 0.55); }
  70%      { box-shadow: 0 0 8px rgb(241 92 92 / 0.6), 0 0 0 6px rgb(241 92 92 / 0); }
}
@keyframes ping-warn {
  0%, 100% { box-shadow: 0 0 8px rgb(245 158 11 / 0.6), 0 0 0 0 rgb(245 158 11 / 0.5); }
  70%      { box-shadow: 0 0 8px rgb(245 158 11 / 0.6), 0 0 0 6px rgb(245 158 11 / 0); }
}

/* The sparkline. A stroke and nothing else -- no fill, no axis, no baseline:
   the figure beside it is the measurement and this is the shape. Two pixels,
   round-joined, and `vector-effect` so a line drawn into a box stretched by
   `preserveAspectRatio="none"` keeps its weight instead of being scaled into a
   hairline at one end and a slab at the other. */
.spark { display: block; overflow: visible; }
.spark__line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.spark--ok .spark__line     { stroke: var(--ok); }
.spark--danger .spark__line { stroke: var(--danger); }
.spark--accent .spark__line { stroke: var(--accent); }
.spark--idle .spark__line   { stroke: var(--border-strong); }
/* The wash under the stroke, where a tile asked the macro for one. It needs a
   rule of its own and not just a tone: an SVG shape with no `fill` declared
   falls back to *black*, so a polygon shipped without this reads as a solid
   slab across the bottom of the tile rather than as a chart. Stated as a
   colour and a separate opacity so the tone rules below can override the one
   without restating the other. */
.spark__area { fill: var(--accent); fill-opacity: 0.14; }
.spark--ok .spark__area     { fill: var(--ok); }
.spark--danger .spark__area { fill: var(--danger); }
.spark--accent .spark__area { fill: var(--accent); }
.spark--idle .spark__area   { fill: var(--border-strong); fill-opacity: 0.22; }

/* The proportion bar. The track is the sunk inset every meter on this platform
   uses; the ramp is declared in user space over the whole bar in the macro, so
   the colour at the fill's right-hand end is where in its range the reading
   is -- green under two thirds, amber at it, red at the cap. */
.xbar { display: block; }
.xbar__track { fill: var(--surface-3); }
.xbar__lo  { stop-color: var(--ok); }
.xbar__mid { stop-color: var(--warn); }
.xbar__hi  { stop-color: var(--danger); }

/* The heartbeat row. Five beats, newest at the right and brightest, fading
   back through the four before it -- and a beat that has not happened is drawn
   as an unlit socket rather than removed, so the dial is the same width at
   every reading. */
.beats { display: block; }
.beats__dot { fill: var(--ok); }
.beats--warn .beats__dot    { fill: var(--warn); }
.beats--danger .beats__dot  { fill: var(--danger); }
.beats--idle .beats__dot,
.beats--unknown .beats__dot { fill: var(--muted); }
.beats__dot--1 { opacity: 0.2; }
.beats__dot--2 { opacity: 0.4; }
.beats__dot--3 { opacity: 0.6; }
.beats__dot--4 { opacity: 0.8; }
.beats__dot--5 { opacity: 1; }
/* An unlit socket. Drawn in the border colour rather than in a text rank:
   this is the shape of a beat that did not happen, and it is read against the
   lit dots beside it rather than on its own. */
.beats__dot--out { fill: var(--border-strong); opacity: 0.9; }

/* Which way a figure went. Decoration over a reading already carried by the
   sign in the number and by the colour of it. */
.trend { margin-left: 0.3rem; vertical-align: 0.06em; }
.trend--up path   { fill: var(--ok); }
.trend--down path { fill: var(--danger); }

/* -- section labels ------------------------------------------------------
 *
 * Eleven pixels, tracked, upper, with a small accent square in front. The
 * square is the one piece of brand colour a panel heading carries, and it is a
 * pseudo-element rather than a character so it cannot be read aloud or
 * selected with the words. */
/* Every panel label on the dashboard, not just the ones that ask for it: the
   eyebrow is already 11px, tracked and upper, and the square is the one piece
   of brand colour a section heading carries. Written over `.card > h3` and
   `.card__label` together, which between them are every panel label on the
   platform -- so a template that writes a plain `<h3>` inside a card gets the
   mark without knowing about it, and there is one rule rather than a class
   somebody has to remember.

   The guide's headings opt out below. They are prose headings that happen to
   sit in a card, and a marker in front of every paragraph of a knowledge base
   is a page of bullets. */
.card > h3,
.card__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.card > h3::before,
.card__label::before {
  content: "";
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-xs);
  background: var(--accent-grad);
  box-shadow: 0 0 8px rgb(77 144 248 / 0.55);
}
/* The one opt-out. The guide's headings are prose headings that happen to sit
   in a card, and a marker in front of every section of a knowledge base is a
   page of bullets. They already declare their own `display: flex`, so only the
   square has to be taken back. */
h2.guide__heading::before,
h3.guide__heading::before { content: none; }

/* -- the system status strip --------------------------------------------
 *
 * Three readings across the top of the overview: this box's link to the
 * trading engine, the trading engine's link to the venue, and this box's own
 * archive. Three cards rather than three rows of a definition list, because
 * they are independent -- any one can be red while the other two are green --
 * and a row of three is what says so. */

.statuses {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s3);
  margin-top: var(--s3);
}
@media (min-width: 48rem) {
  .statuses { grid-template-columns: repeat(3, 1fr); }
}
.status {
  position: relative;
  min-width: 0;
  padding: var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, var(--surface-2), var(--surface) 72%);
  overflow: hidden;
  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
/* The lit top edge, in the colour of the reading rather than in the accent.
   Two pixels, the full width of the card: this is the one place on the
   platform where the state of a thing is what the gradient means. */
.status::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: linear-gradient(90deg, var(--idle), transparent);
}
.status--ok::before     { background: linear-gradient(90deg, var(--ok), transparent); }
.status--warn::before   { background: linear-gradient(90deg, var(--warn), transparent); }
.status--danger::before { background: linear-gradient(90deg, var(--danger), transparent); }
@media (hover: hover) {
  .status:hover {
    transform: translateY(-2px);
    border-color: var(--accent-line);
    box-shadow: var(--glow);
  }
}
.status__name {
  display: flex;
  align-items: center;
  margin: 0;
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  min-width: 0;
}
/* A state is a pill and a count is a number, and the distinction is the whole
   of the hierarchy here: "down" is one of three words a reader already knows,
   and a tinted pill is how it is said everywhere else on this platform -- the
   chip carries the tone *and* the sentence explaining it, as a `title`. A tick
   count is a figure and is set as one. */
.status__value .chip {
  font-size: var(--fs-body);
  padding: 0.2rem 0.6rem;
  letter-spacing: 0.02em;
}
.status__value {
  margin: 0.3rem 0 0;
  font-size: 1.375rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow-wrap: anywhere;
}
.status--ok .status__value     { color: var(--ok); text-shadow: var(--glow-text-ok); }
.status--danger .status__value { color: var(--danger); text-shadow: var(--glow-text-danger); }
.status--warn .status__value   { color: var(--warn); }
.status__meta { margin: 0.15rem 0 0; font-size: var(--fs-xs); }
.status-panel p + p { margin-top: var(--s2); }

/* -- the bot strips ------------------------------------------------------
 *
 * One row per bot: state and name on the left, the shape in the middle, the
 * figure on the right. The columns are stated rather than auto-fit, because
 * the whole value of this shape is that the P&L is in the same place on every
 * row and can be read straight down.
 *
 * The whole row is the link. There is no second control on it, so there is
 * nothing to miss and nothing to hit by accident. */

.strips { list-style: none; margin: var(--s3) 0 0; padding: 0; }
.strip + .strip { border-top: 1px solid var(--border); }
.strip--stale { opacity: 0.75; }
.strip__link {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s2) var(--s4);
  /* A real target on a phone, and the row height on a desktop. */
  min-height: var(--tap);
  padding: var(--s2) var(--s2);
  border-radius: var(--radius-sm);
  color: var(--text);
  text-decoration: none;
  transition: background 0.15s ease, box-shadow 0.15s ease;
}
@media (min-width: 48rem) {
  .strip__link { grid-template-columns: minmax(0, 1fr) 100px auto; }
}
@media (hover: hover) {
  .strip__link:hover {
    background: var(--surface-2);
    box-shadow: inset 2px 0 0 var(--accent);
  }
}
.strip__link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.strip__id { display: flex; flex-direction: column; min-width: 0; }
.strip__name {
  display: flex;
  align-items: center;
  font-weight: 600;
  min-width: 0;
  overflow-wrap: anywhere;
}
.strip__sub {
  font-size: var(--fs-xs);
  /* Indented past the pip, so the two lines align on the name rather than on
     the lamp. */
  padding-left: 1.05rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The sparkline column is the first thing to go when the row runs out of
   width: it is the shape, and the shape is the part a phone has no room for
   and no need of -- the figure beside it says the same thing. */
.strip__spark { display: none; }
@media (min-width: 48rem) {
  .strip__spark { display: block; }
}
.strip__pnl {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  white-space: nowrap;
}
.strip__pnl.pos { text-shadow: var(--glow-text-ok); }
.strip__pnl.neg { text-shadow: var(--glow-text-danger); }

/* -- the activity feed ---------------------------------------------------
 *
 * Five lines of what this dashboard was asked to do. A list rather than a
 * table: there are three fields and one of them is a timestamp that keeps
 * ticking, and folding a three-column table on a phone costs more than it
 * buys. */

.feed { list-style: none; margin: var(--s3) 0 0; padding: 0; }
.feed__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--s2);
  padding: var(--s2) var(--s1);
  font-size: var(--fs-sm);
  min-width: 0;
}
.feed__row + .feed__row { border-top: 1px solid var(--border); }
.feed__what { font-weight: 600; }
.feed__row--denied .feed__what { color: var(--warn); }
.feed__row--error .feed__what { color: var(--danger); }
.feed__on {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  min-width: 0;
  overflow-wrap: anywhere;
}
.feed__when { margin-left: auto; font-size: var(--fs-xs); white-space: nowrap; }

/* -- the instrument strip ------------------------------------------------
 *
 * `.metrics--tiles` with the column count stated rather than fitted. The
 * bot page's eight readings are one instrument, and `auto-fit` is the wrong
 * tool for an instrument: at 1400px it makes ten columns and puts two empty
 * cells at the end of the console, and at 1100px it makes seven and wraps the
 * eighth onto a row of its own. Three widths, stated:
 *
 *   >= 64rem   eight across, one row -- the whole reading at a glance
 *   48-64rem   four across, two rows -- a tablet's honest half of that
 *   <  48rem   two across -- the narrowest a labelled figure survives
 *
 * The hero gives up its double width here. It is the same figure it is
 * everywhere else on the platform and it keeps the lit tile and the accent
 * rule that say so, but a tile spanning two of eight columns is a strip of
 * nine cells, which is not a row of eight readings. */
.metrics--strip { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 48rem) {
  .metrics--strip { grid-template-columns: repeat(4, 1fr); }
}
@media (min-width: 64rem) {
  .metrics--strip { grid-template-columns: repeat(8, 1fr); }
}
.metrics--strip > .metric--hero { grid-column: auto; }
/* One size for all eight, and it is the figure size rather than the headline
   one: eight readings at 28px in a 1150px card is a row that wraps, and a
   strip whose tiles are different heights is not a strip. */
.metrics--strip dd,
.metrics--strip > .metric--hero dd {
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: -0.025em;
}
/* The mark under the figure, and it is pinned to the foot of the tile rather
   than hung under the number.
 *
 * That is what keeps the strip an instrument. The eight figures are different
 * lengths and one of them wraps on a narrow screen, so a mark spaced from the
 * *figure* leaves eight marks on eight different lines; spaced from the
 * *tile*, they are one row across the bottom of the console whatever the
 * numbers above them do. `margin-top: auto` in a flex column is the whole
 * mechanism -- it works for a 22px sparkline and a 4px bar alike, because what
 * is being aligned is the bottom edge. */
.metrics--strip > div { display: flex; flex-direction: column; }
.tile__viz {
  display: flex;
  align-items: flex-end;
  height: 22px;
  overflow: hidden;
}
.metrics--strip .tile__viz,
.metrics--strip .meter { margin-top: auto; }
/* A meter in a tile is a mark rather than a control, so it keeps clear of the
   figure above it even when the tile is tall enough not to need the room. */
.metrics--strip .meter { margin-bottom: 0.55rem; }
.metrics--strip .tile__viz .spark,
.metrics--strip .tile__viz .beats { align-self: flex-end; }
/* A tile whose reading has gone past two thirds of its cap, and one that has
   reached it. The tint is on the tile rather than on the figure, because what
   has changed is not the number -- it is how much room is left. */
.metrics--tiles > .tile--warn {
  background:
    linear-gradient(180deg, rgb(245 158 11 / 0.12), rgb(245 158 11 / 0.02) 60%),
    linear-gradient(180deg, var(--surface-2), var(--surface) 70%);
}
.metrics--tiles > .tile--danger {
  background:
    linear-gradient(180deg, rgb(241 92 92 / 0.14), rgb(241 92 92 / 0.03) 60%),
    linear-gradient(180deg, var(--surface-2), var(--surface) 70%);
}
.tile--warn dd { color: var(--warn); }
.tile--danger dd { color: var(--danger); }
/* The feed lamp sits on the baseline of the word beside it rather than being
   pushed up by the figure size the other seven tiles use. */
.metrics--strip dd .pip { vertical-align: 0.02em; }
.metrics--strip dd:has(.pip) { font-size: var(--fs-body); font-weight: 600; }

/* -- the control bar -----------------------------------------------------
 *
 * One line at desktop width and no wrapping: the identity shrinks, the
 * controls do not move, and Start is in the same place on every bot's page.
 * Below 64rem it wraps again, because six 44px targets do not fit a phone and
 * a control bar that scrolled sideways would put the kill switch off the
 * right-hand edge of the one page where that matters. */
@media (min-width: 64rem) {
  .inst__bar { flex-wrap: nowrap; }
  .inst__bar .controls--inline { flex-wrap: nowrap; flex: 0 0 auto; }
  /* The identity is the column that gives: a long bot name ellipses, and the
     buttons keep their width. */
  .inst__bar .inst__id { flex: 1 1 auto; }
  .inst__bar .inst__meta {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* -- a chart that fills its card ----------------------------------------
 *
 * The panes take the card's side padding back, so the chart runs to the
 * card's own edges. A chart inset inside a box inside a panel is three nested
 * rectangles and only the innermost one is carrying a reading. The heading and
 * the sentence under it keep the padding -- they are prose, and prose against
 * the edge of a panel is not readable. */
.card--chart > [data-rti] { margin: var(--s3) calc(var(--s4) * -1) 0; }
.card--chart .rti__head { padding: 0 var(--s4); }
.card--chart .rti__empty { padding: 0 var(--s4); }
.card--chart .chart--rti { border-radius: 0; border-left: 0; border-right: 0; }
/* And the card's own top edge goes from a hairline to a real 2px gradient
   rule, because this panel is the one on the page that is a chart rather than
   a table of figures. */
.card--chart::before {
  height: 2px;
  left: 0;
  right: 0;
  top: 0;
  /* The card does not clip its children, so a rule run to both edges has to
     round its own top corners or it pokes out past the panel's. */
  border-radius: var(--radius) var(--radius) 0 0;
  background: linear-gradient(90deg, var(--accent), var(--accent-2), transparent);
  opacity: 0.75;
}

/* -- blotter rows --------------------------------------------------------
 *
 * A coloured edge per row: a position by whether it is up or down, an order
 * by which way it is going. An inset shadow rather than a border, so the edge
 * costs the row no width and the columns stay aligned down the table -- the
 * same trick `.row--manual` already uses one rule below. */
.grid tbody tr.row--up   { box-shadow: inset 2px 0 0 var(--ok); }
.grid tbody tr.row--down { box-shadow: inset 2px 0 0 var(--danger); }
.grid tbody tr.row--flat { box-shadow: inset 2px 0 0 var(--border-strong); }
/* A blotter, packed. The instance page shows two tables side by side inside
   half a page each, and the default row height is a table read as a document
   rather than as a ledger.

   The modifier is on the *card* rather than on the `<table>`, and that is not
   a style preference: the responsive rules are attached to `class="grid"` and
   a second class in that attribute is a table the phone check cannot see. See
   `test_mobile.ResponsiveTableTests`. */
.card--blotter .grid { font-size: var(--fs-xs); }
.card--blotter .grid th,
.card--blotter .grid td { padding: 0.3rem 0.5rem; }

/* The exception to the packing above. Open positions is the primary table on
   the bot page -- it is what the page is for -- and reading it should not feel
   like reading the working-orders list beside it. It keeps the blotter's card
   and takes back the default row height. */
.card--positions .grid { font-size: var(--fs-sm); }
.card--positions .grid th,
.card--positions .grid td { padding: 0.45rem 0.6rem; }

/* -- the account bar -----------------------------------------------------
 *
 * Four figures under the controls: balance, unrealised, realised today,
 * exposure. It is the same tile strip the status card uses -- one object with
 * hairlines through it rather than four boxes -- pinned to four across so the
 * bar keeps its shape as the window moves. `auto-fit` would reflow it to five
 * or three at widths where the strip below is right to.
 *
 * At 44rem it folds to two across, which is two lines of two: a phone's
 * account bar. Nothing is dropped -- the exposure meter and the P&L shape both
 * come with their tile -- because a figure that disappears on a narrow screen
 * is a figure somebody stops looking for on a wide one. */
.account-bar { grid-template-columns: repeat(4, 1fr); }
.account-bar__item { display: flex; flex-direction: column; }
/* The mark sits on the floor of the tile whatever the figure above it does, so
   the four meters and sparklines line up across the bar rather than stepping
   with the label lengths. */
.account-bar__item .tile__viz,
.account-bar__item .meter,
.account-bar__note,
.account-bar__split { margin-top: auto; }
.account-bar__item .meter { margin-bottom: 0.35rem; }
.account-bar__item .tile__viz .spark { align-self: flex-end; }
.account-bar__note { padding-top: var(--s1); }
.account-bar__split {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s2);
  padding-top: var(--s1);
  font-size: var(--fs-xs);
}
@media (max-width: 44rem) {
  .account-bar { grid-template-columns: repeat(2, 1fr); }
}

/* The YES/NO leg, as a tag rather than as a word in a column of words. A
   binary has no short side, so this is the whole of a position's direction and
   it has to survive being scanned down forty rows.

   Bordered rather than filled: a filled chip in every row of a blotter is a
   second colour competing with the one the P&L column is using to mean up and
   down. */
.side {
  display: inline-block;
  padding: 0 0.35rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xs);
  font-size: var(--fs-label);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.side--yes { border-color: var(--ok); color: var(--ok); }
.side--no { border-color: var(--accent); color: var(--accent); }

/* -- a blotter that scrolls inside itself --------------------------------
 *
 * For the two lists on the bot page with no natural end: closed positions and
 * the trade log. Both are read as "the last few" and both used to run down the
 * page, which is what put the settings four screens below a live position.
 *
 * A height in `rem` rather than in rows: the rows are a different height on a
 * phone, where each one is a block rather than a line, and a box measured in
 * rows would be a third of the screen on one and a tenth on the other.
 *
 * `tabindex="0"` is on the element in the markup. A scroll container that only
 * a wheel can reach cannot be read from a keyboard at all, and the browser
 * only puts a scrollable box in the tab order if it is told to. */
.blotter__scroll {
  max-height: 22rem;
  overflow-y: auto;
  overscroll-behavior-y: contain;
  border-radius: var(--radius-xs);
}
/* The table's own top margin would be a gap that scrolls away inside the box,
   and on a wide screen it would put the pinned header a quarter-inch down. */
.blotter__scroll .grid { margin-top: 0; }
.blotter__scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The header stays put while the rows move under it. Only on a wide screen:
   the phone layout hides the header row entirely and labels each cell instead,
   so there is nothing to pin. */
@media (min-width: 40.0625rem) {
  .blotter__scroll .grid thead th {
    position: sticky;
    top: 0;
    z-index: 1;
    /* The lit strip the header already carries is transparent, and rows
       scrolling under a transparent header read as a smear. This is the card's
       own fill painted behind the same gradient. */
    background-image: linear-gradient(180deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0));
    background-color: var(--surface);
  }
}
@media (max-width: 40rem) {
  /* Shorter on a phone, where every row is a block of four labelled lines and
     22rem of them is most of the screen. */
  .blotter__scroll { max-height: 16rem; }
}

/* -- tables -------------------------------------------------------------
 *
 * A blotter. No row rules: the rows are separated by alternating fills a few
 * percent apart, which is enough to follow a row across nine columns and not
 * enough to draw a grid over the figures. The header is a label row, not a
 * heading. */

.grid { width: 100%; border-collapse: collapse; margin-top: var(--s3); font-size: var(--fs-sm); }
.grid th, .grid td { text-align: left; padding: 0.45rem 0.6rem; }
.grid thead th {
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  /* A lit strip rather than a fill: the header is a label row, and a solid
     bar across the top of a blotter competes with the figures under it. The
     gradient fades to nothing before the last row of the header, so what the
     eye actually sees is the rule -- lit from above. */
  background: linear-gradient(180deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0));
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.grid thead th:first-child { border-top-left-radius: var(--radius-xs); }
.grid thead th:last-child { border-top-right-radius: var(--radius-xs); }
.grid tbody tr { border-bottom: 0; }
.grid tbody tr:nth-child(even) { background: rgb(255 255 255 / 0.018); }
@media (hover: hover) {
  .grid tbody tr { transition: background-color 0.15s ease, box-shadow 0.15s ease; }
  .grid tbody tr:hover, .grid tbody tr:nth-child(even):hover {
    background: rgb(255 255 255 / 0.045);
    /* A wash from inside the row rather than an outline around it. A ring
       would draw a box around one row of a blotter, which is a grid line the
       rest of the table deliberately does not have. */
    box-shadow: inset 0 0 30px rgb(77 144 248 / 0.09);
  }
  /* The operator's own trades keep their left edge *and* take the wash. Two
     shadows, because the hover rule above would otherwise replace the marker
     with the glow on exactly the rows it matters most on. */
  .grid tbody tr.row--manual:hover,
  .grid tbody tr.row--manual:nth-child(even):hover {
    box-shadow: inset 2px 0 0 var(--accent), inset 0 0 30px rgb(77 144 248 / 0.09);
  }
}
@media (prefers-color-scheme: light) {
  .grid tbody tr:nth-child(even) { background: rgb(16 24 40 / 0.02); }
  .grid tbody tr:hover, .grid tbody tr:nth-child(even):hover { background: rgb(16 24 40 / 0.045); }
}
.row--differs { background: var(--warn-bg); }
/* A trade an operator caused. A left edge rather than a background: the P&L
   cell already uses colour to mean lost money, and tinting the whole row would
   put two unrelated meanings on one patch of colour. This one survives being
   read at a glance down the side of the table, which is how the log is
   actually scanned. */
.row--manual { box-shadow: inset 2px 0 0 var(--accent); }

/* Narrow screens: each row becomes a labelled block. No horizontal scroll,
   no 9-point type, nothing hidden behind a swipe. */
@media (max-width: 40rem) {
  .grid thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .grid tbody tr { display: block; padding: var(--s2) 0; border-bottom: 1px solid var(--border); }
  .grid tbody tr:last-child { border-bottom: 0; }
  .grid td {
    display: flex;
    justify-content: space-between;
    gap: var(--s4);
    padding: 0.2rem 0;
    border: 0;
    /* The values in these cells are contract ids, vault fingerprints and
       strategy identifiers -- thirty characters with nothing to break at. A
       flex item will not shrink below its content unless it is allowed to, so
       without this the row is wider than the card, the card is wider than the
       page, and every page with a table on it scrolls sideways. */
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .grid td::before {
    content: attr(data-label);
    color: var(--muted);
    font-size: var(--fs-xs);
    /* Never shrinks and never wraps: the label is what makes a headless row
       readable, and a "Contract" broken over two lines to make room for the
       value it labels is the wrong thing to sacrifice. */
    flex: 0 0 auto;
  }
  .grid td:empty { display: none; }
  /* The value side of the row, when a cell wraps its content in an element of
     its own rather than leaving it as text. */
  .grid td > * { min-width: 0; overflow-wrap: anywhere; }
}

/* The opt-out, for the tables that are genuinely tabular -- a backtest's
   trades, the guide's preset comparison. Those are columns of figures that
   only mean anything read across, and folding one into forty labelled blocks
   loses the comparison that is the entire point of it. They keep their columns
   and scroll inside their own box, which is a swipe over the table rather than
   a page that moves sideways under everything else. */
.scroll-x, .guide__scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* The standard scroll-shadow pair, and it is the only thing that says this
     box scrolls at all: a phone paints no scrollbar until it is being
     scrolled, so a table cut off at the edge is indistinguishable from a table
     with nothing more in it. The `local` gradients are covers painted in the
     surface colour and pinned to the content, the `scroll` ones are the
     shadows pinned to the box -- so a cover slides away exactly as far as
     there is something more to see. */
  background:
    linear-gradient(to right, var(--surface) 40%, rgb(0 0 0 / 0)) left center / 24px 100% no-repeat local,
    linear-gradient(to left, var(--surface) 40%, rgb(0 0 0 / 0)) right center / 24px 100% no-repeat local,
    radial-gradient(farthest-side at 0 50%, rgb(0 0 0 / 0.3), rgb(0 0 0 / 0)) left center / 12px 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, rgb(0 0 0 / 0.3), rgb(0 0 0 / 0)) right center / 12px 100% no-repeat scroll;
}
.scroll-x > .grid, .guide__scroll .grid { min-width: 34rem; }
@media (max-width: 40rem) {
  .scroll-x .grid thead, .guide__scroll .grid thead {
    position: static; width: auto; height: auto; clip: auto;
  }
  .scroll-x .grid tbody tr, .guide__scroll .grid tbody tr { display: table-row; border-bottom: 0; }
  .scroll-x .grid td, .guide__scroll .grid td {
    display: table-cell;
    padding: 0.45rem 0.6rem;
    border: 0;
    white-space: nowrap;
  }
  .scroll-x .grid td::before, .guide__scroll .grid td::before { content: none; }
  .scroll-x .grid th, .guide__scroll .grid th { white-space: nowrap; }
}

/* -- forms and buttons --------------------------------------------------
 *
 * One control shape, one focus state, one set of sizes. A select, a text box,
 * a date field and a multi-line box are the same box with different contents,
 * and anything that looks like an input on this platform is one. */

.stack { display: flex; flex-direction: column; gap: var(--s4); }
/* A column flex container stretches its children, which turned every Save in a
   `.stack` into a 1400px-wide button. The ones that are *meant* to be full
   width say so with `--block`, and `width: 100%` still wins there. */
.stack > .btn { align-self: flex-start; }
.stack > .btn--block { align-self: stretch; }
.fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--s4); }
/* Denser variants of the same grid. `--dense` is the modifier; `.order-ticket`
   reaches its own `.fields` rather than carrying the modifier, because the
   manual order form's field grid is asserted by class in the mobile tests and
   a compound class would read as a different one. */
.fields--dense,
.order-ticket .fields { grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: var(--s3) var(--s4); }
.order-ticket { gap: var(--s3); }
/* The label sits above its control, always. A floating label is a label that
   is not there when the box has something in it, which on a form that sends an
   order is the moment it is most wanted. */
.field { display: flex; flex-direction: column; gap: var(--s1); }
/* A field whose *value* is long, in a grid sized for fields whose values are
   not. `.fields` packs as many 7.5rem columns as fit, which on a 375px screen
   is two of about 148px -- right for a quantity, a price, YES/NO, and wrong
   for the two controls on the order ticket that carry a sentence: a contract
   is `KXBTCD-26AUG1712-B109999.99`, and an order type is "Limit — rest until
   filled or cancelled". A `<select>` does not wrap and does not scroll; it
   truncates. So at 148px the reader picks a contract they cannot read the end
   of, on the one form here that sends money.

   Stated as a span rather than as a narrow-screen override, because it is not
   really a phone problem: those two values are longer than a column at every
   width the grid produces, and a full row is what they want on a laptop too. */
.field--wide { grid-column: 1 / -1; }
.field > span {
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

/* Styled by exclusion rather than by enumeration, and that is a fix rather
   than a tidy-up. The list this replaces named text, password, number and
   time -- so the backtest form's two `type="date"` fields got no tap height,
   no padding, no border and no width, and rendered as a squeezed native
   widget on a phone while looking fine beside a mouse. The list was always
   going to drift: `paramform` picks the input type from a schema Server B
   declares, so a type added there arrives here with no rule waiting for it.
   The exclusions are the four inputs that are genuinely not text boxes, plus
   hidden, which has no box at all. */
input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="submit"]):not([type="button"]),
select, textarea {
  /* 36px under a mouse; a coarse pointer takes the full target back below. */
  min-height: 36px;
  padding: 0.35rem 0.6rem;
  font: inherit;
  font-size: var(--fs-body);
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  width: 100%;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
/* A figure typed into a box is still a figure. */
input[inputmode="numeric"], input[inputmode="decimal"], input[type="number"], input[type="date"], input[type="time"] {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
textarea { min-height: 8rem; font-family: var(--mono); font-size: var(--fs-sm); padding: var(--s3); }
/* A list box, not a dropdown: the padding that suits one line is wrong for
   seven, and the rows want to be tappable rather than tightly stacked. */
select[multiple] { padding: var(--s1); height: auto; }
select[multiple] option { padding: 0.35rem; border-radius: var(--radius-xs); }

/* One focus state for every control on the platform: the border takes the
   accent and a soft ring grows outside it. Loud enough to find with a thumb on
   a phone in daylight, quiet enough not to look like an error. */
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  /* The ring outside, and a wash of the accent inside the box. The inner half
     is what makes a focused field read as *lit* rather than as outlined, and
     it is an inset shadow rather than a background so it composes with the
     field's own fill instead of replacing it -- which matters because an
     invalid field, a changed field and a disabled one all set that fill. */
  box-shadow: var(--focus-ring), inset 0 0 12px rgb(77 144 248 / 0.14);
}
/* And one error state, keyed off the attribute a screen reader reads, so the
   two cannot say different things. */
[aria-invalid="true"] { border-color: var(--danger); }
[aria-invalid="true"]:focus {
  border-color: var(--danger);
  box-shadow: var(--focus-ring-danger), inset 0 0 12px rgb(241 92 92 / 0.14);
}
::placeholder { color: var(--muted); opacity: 1; }

.check, .radio { display: flex; align-items: center; gap: var(--s2); min-height: var(--tap); }
.radios { display: flex; gap: var(--s5); flex-wrap: wrap; }
.radio--disabled { opacity: 0.55; }
input[type="checkbox"], input[type="radio"] { width: 1.05rem; height: 1.05rem; accent-color: var(--accent); }

/* `inline-flex`, and that is the load-bearing part rather than a tidy-up.
   Half the buttons on this dashboard are `<a class="btn">` -- New bot, Export
   CSV, Clone to live, Platform kill switch, every Cancel and Try again -- and
   an anchor is inline, where `min-height` does not apply at all. Those read as
   buttons and were about 43px of padding-box tall by accident, sitting a
   couple of pixels under a thumb target and a couple of pixels off the
   baseline of the real buttons beside them. `text-decoration` for the same
   reason: without it an anchor button is an underlined link in a box.

   Three weights and no more: filled for the one action a page is for, outlined
   for everything else, and a red outline for the ones that cannot be undone.
   A second filled button on a page is a page with no primary action. */
/* -- the two-way toggle --------------------------------------------------
 *
 * Two pressed faces sharing one track, over a radio group. The radios are the
 * state -- not a script, not a `data-` attribute -- so the control posts,
 * restores and announces itself the way a browser already knows how to, and
 * survives a bundle that never arrived like every other disclosure on this
 * dashboard.
 *
 * The input is moved off-screen rather than hidden: `display: none` and
 * `visibility: hidden` both take a radio out of the tab order, and the group
 * would then be operable by a pointer and by nothing else. The focus ring is
 * drawn on the face the input is labelled by. */

.field--toggle { display: flex; flex-direction: column; gap: var(--s1); min-width: 0; }
.field__legend {
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.toggle {
  /* The radios are moved off-screen rather than hidden, so they need a
     containing block that is this control and not whatever panel happens to be
     positioned above it. */
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.35);
}
.toggle__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  /* Not `display: none`: it is the control, and a keyboard has to reach it. */
}
.toggle__face {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: 0 var(--s2);
  border-radius: var(--radius-xs);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
}
@media (hover: hover) {
  .toggle__face:hover { color: var(--text); background: rgb(255 255 255 / 0.04); }
}
/* The pressed face. Lit from inside rather than outlined: this is a control
   whose two halves sit in one sunk track, and an outline on one of them would
   read as a second box rather than as the chosen one. */
.toggle__input:checked + .toggle__face {
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  color: var(--text);
  box-shadow: inset 0 0 0 1px var(--border-strong), 0 0 10px rgb(0 0 0 / 0.25);
}
/* And the two that carry a meaning carry it in colour as well as in position.
   Never *only* in colour: the pressed face is also the lit one, which is the
   channel a reader who cannot separate green from red is left with. */
.toggle__input:checked + .toggle__face--yes,
.toggle__input:checked + .toggle__face--buy {
  color: var(--ok);
  box-shadow: inset 0 0 0 1px rgb(34 197 94 / 0.45), inset 0 0 12px rgb(34 197 94 / 0.18);
}
.toggle__input:checked + .toggle__face--no,
.toggle__input:checked + .toggle__face--sell {
  color: var(--danger);
  box-shadow: inset 0 0 0 1px rgb(241 92 92 / 0.45), inset 0 0 12px rgb(241 92 92 / 0.18);
}
.toggle__input:focus-visible + .toggle__face { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The ticket itself, packed. It is a form of six short controls and it should
   read as an instrument on the page rather than as an essay with a button at
   the bottom -- so the grid is tight and the prose under it is quiet. */
.card--ticket .fields { gap: var(--s3) var(--s3); }
.card--ticket .order-ticket { gap: var(--s3); }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-height: var(--tap);
  padding: 0.4rem 0.8rem;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  color: var(--text);
  /* Lit at rest. The outlined weight used to be an outline and nothing else,
     which on a lit page reads as a hole rather than as a control. `--quiet`
     takes the transparency back below, because that one is meant to recede. */
  background: linear-gradient(180deg, var(--surface-2), var(--surface));
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease,
              box-shadow 0.15s ease, transform 0.15s ease;
}
@media (hover: hover) {
  .btn:hover {
    background: var(--sheen-strong), var(--surface-2);
    border-color: var(--muted);
  }
  .btn--primary:hover {
    background: var(--btn-grad-lit);
    border-color: transparent;
    box-shadow: var(--glow-lit);
  }
  .btn--danger:hover { background: var(--danger-bg); border-color: var(--danger); box-shadow: 0 0 0 1px rgb(241 92 92 / 0.35); }
  .btn--warn:hover { background: var(--warn-bg); border-color: var(--warn); }
  .btn--quiet:hover { background: var(--sheen-strong), var(--surface-2); border-color: transparent; color: var(--text); }
  /* One pixel, on press rather than on hover, so a button that has been
     pressed says so before its handler has answered. */
  .btn:active { transform: translateY(1px); }
}
/* The one filled weight on the platform, and now a lit one. The ramp runs
   towards white rather than towards the violet the decorative gradients use:
   `--on-accent` is a near-black label, and it clears 4.5:1 on this accent and
   on anything lighter than it. See `--btn-grad`. */
.btn--primary {
  background: var(--btn-grad);
  border-color: transparent;
  color: var(--on-accent);
  box-shadow: var(--glow);
}
.btn--danger { border-color: rgb(241 92 92 / 0.45); color: var(--danger); }
.btn--warn { border-color: rgb(245 158 11 / 0.45); color: var(--warn); }
.btn--quiet { background: transparent; border-color: transparent; color: var(--muted); }
/* The outlined weight, with the fill taken back out. `--danger --ghost` is
   the delete in the control bar: it has to be reachable from the row that
   starts and stops the bot, and it must not compete with the Kill switch
   beside it -- which is the control somebody wants in a hurry, and which
   keeps its lit face for exactly that reason. The colour and the border stay,
   so this is still legible as the destructive one; only the raised face
   goes. Under a pointer it takes the tinted background back, because a
   control that never answers a hover reads as disabled. */
.btn--ghost { background: none; }
.btn--block { width: 100%; }
.btn--small { min-height: 36px; padding: 0.3rem 0.65rem; font-size: var(--fs-xs); }
/* The compact button exists so a Cancel in a table row does not shout, and 36px
   is fine under a mouse. Under a thumb it is not: this is the control that
   withdraws a resting order, and it sits in a list of them. Where the pointer
   is coarse, or the screen is phone-width, it gets the full target back and
   keeps the quieter type and padding. */
@media (pointer: coarse), (max-width: 40rem) {
  .btn--small { min-height: var(--tap); padding: 0.3rem 0.9rem; }
}
.btn[disabled] { opacity: 0.4; cursor: not-allowed; }

/* The same bargain the compact button makes, for every text control on the
   platform: 36px beside a mouse, a full target under a thumb. */
@media (pointer: coarse) {
  input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="submit"]):not([type="button"]),
  select { min-height: var(--tap); }
}

.inline { display: inline; }
.controls { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s4); }
.controls form { display: inline-flex; }
/* The same row of controls, sitting on the header line it belongs to rather
   than under it. */
.controls--inline { margin-top: 0; margin-left: auto; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.page__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  align-items: center;
  justify-content: space-between;
}
.page__head .page__title { margin: 0 0 var(--s4); }

/* -- pending -------------------------------------------------------------
 *
 * Two places, one mechanism, and it is a placeholder rather than a spinner. A
 * spinner says "something is happening"; a shape in the size of the thing that
 * is coming says what is happening and stops the page moving under a thumb
 * when it arrives. Everything else here is server-rendered and arrives with
 * its numbers already in it, so there is no third case. */

@keyframes pulse { 50% { opacity: 0.35; } }

.skeleton, .spinner {
  display: inline-block;
  min-width: 3.5rem;
  height: 0.85em;
  vertical-align: -0.1em;
  border-radius: var(--radius-xs);
  background: var(--surface-3);
  animation: pulse 1.4s ease-in-out infinite;
}

/* -- parameter fields --------------------------------------------------- */
/* Rendered from Server B's schema, so this styles shapes -- a labelled input,
   a hint, a help paragraph, an error -- and never a particular parameter. */

.param { margin: 0 0 var(--s4); }
.param:last-child { margin-bottom: 0; }
.param__label {
  display: block;
  font-weight: 600;
  font-size: var(--fs-sm);
  margin-bottom: var(--s1);
}
.param__check .param__label { margin: 0; }
.param__hint { margin: var(--s1) 0 0; font-size: var(--fs-xs); color: var(--muted); }
.param__help { margin: var(--s1) 0 0; }

.param--invalid input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
.param--invalid select,
.param--invalid textarea {
  border-color: var(--danger);
}
.param__errors {
  margin-top: var(--s2);
  padding: var(--s2) var(--s3);
  border-radius: var(--radius-sm);
  border-left: 2px solid var(--danger);
  background: var(--danger-bg);
}
.param__error {
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--danger);
}
.param__error + .param__error { margin-top: var(--s1); }

/* A field that differs from the preset it is measured against. Deliberately
   quieter than an error: an edit is not a fault, it is the point of loading a
   preset and then forking it. A left rule rather than a border swap, so a
   field that is both changed and invalid still reads as invalid first. */
.param--changed { border-left: 2px solid var(--warn); padding-left: var(--s3); }
/* The one chip that is allowed to wrap. Chips are nowrap because a status word
   broken over two lines stops reading as a chip -- but this one carries the
   baseline value inside it ("changed from 0.35"), which makes it a sentence
   rather than a word, and a sentence that cannot wrap pushes the label row
   past the edge of a phone. */
.chip--changed { margin-left: 0.35rem; font-weight: 600; white-space: normal; border-radius: var(--radius-sm); }
.chip--changed[hidden] { display: none; }

/* -- tooltips ------------------------------------------------------------
 *
 * A (?) beside a label, opening the description Server B declared for that
 * parameter. CSS only, and that is the whole design: `:hover` covers a mouse,
 * `:focus-within` covers a tap -- pressing the button focuses it, pressing
 * anywhere else blurs it shut -- and a keyboard gets it from tabbing, for
 * free. Nothing here waits on app.js, which is not loaded on the guide page at
 * all and is deferred everywhere else.
 *
 * The body stays in the document when closed rather than being removed:
 * `aria-describedby` on the input points at it, so a screen reader reads the
 * explanation as part of the field whether or not anything is hovering. It is
 * hidden with `visibility`, not `display: none`, for the same reason. */

.param__head {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  /* The label carries the bottom margin in every other layout; here the row
     does, or the (?) sits a third of a line high against it. */
  margin-bottom: var(--s1);
}
/* The label takes the row and the (?) is pushed to its end. Not cosmetic: the
   panel below is anchored to the icon's right edge and grows leftwards, so an
   icon sitting hard against a three-word label near the left margin would open
   a 22rem panel off the side of the screen. */
.param__head .param__label { flex: 1 1 auto; min-width: 0; margin-bottom: 0; }
.param__head .param__check { flex: 1 1 auto; min-width: 0; }

.tip { position: relative; display: inline-flex; flex: 0 0 auto; }

.tip__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Smaller than a 44px tap target on purpose: it sits inline with a label
     forty times on one screen, and at tap size it would be the loudest thing
     on the form. The hit area is padded out below. */
  width: 1.15rem;
  height: 1.15rem;
  padding: 0;
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 700;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  cursor: pointer;
}
/* The pressable area, without the visible circle growing: a 22px dot is hard
   to hit with a thumb, and a 22px dot with 11px of invisible margin is not. */
.tip__toggle::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--tap);
  height: var(--tap);
  transform: translate(-50%, -50%);
}
.tip__toggle--sample { cursor: default; }

.tip__body {
  position: absolute;
  z-index: 41;
  top: calc(100% + 0.35rem);
  /* Anchored to the right edge of the (?) and grown leftwards, because the
     icon sits at the end of a full-width label row: opening rightwards would
     put every tooltip on the form off the side of the screen. */
  right: 0;
  width: max-content;
  max-width: min(22rem, 80vw);
  padding: var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1.5;
  text-align: left;
  box-shadow: var(--shadow-pop);
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.1s ease, visibility 0.1s ease;
}
.tip:focus-within .tip__body { visibility: visible; opacity: 1; }
@media (hover: hover) {
  .tip:hover .tip__body { visibility: visible; opacity: 1; }
  .tip__toggle:hover { color: var(--text); border-color: var(--muted); }
}

.tip__relevance { display: block; margin-bottom: var(--s1); white-space: normal; color: var(--muted); }
.tip__text { display: block; }

/* -- radio cards (strategy picker) -------------------------------------- */

.choices { display: grid; grid-template-columns: 1fr; gap: var(--s2); }
@media (min-width: 60rem) {
  .choices { grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); }
}
.choice {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  padding: var(--s3);
  min-height: var(--tap);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.choice:has(input:checked) {
  border-color: var(--accent);
  background: linear-gradient(180deg, rgb(77 144 248 / 0.14), var(--accent-soft));
  box-shadow: var(--glow);
}
.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
/* The panels' answer to a pointer, on the controls that are read as panels.
   A picked strategy already wears the accent above; this is the one under the
   pointer, at the weight a card gets. */
@media (hover: hover) {
  .choice:hover { border-color: var(--accent-line); box-shadow: var(--glow); }
}
.choice__body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.choice__title { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; font-weight: 600; }
.choice__detail { margin: 0; font-size: var(--fs-sm); }

/* -- market picker ------------------------------------------------------ */
/* Two levels of heading, then checkboxes. The nesting is the whole point: a
   flat list of forty series is a wall, and "BTC -> 15m -> one tick" is a
   decision somebody can make with a thumb. */

/* The market picker: a collapsible group per timeframe, and the pairs inside
   it. One column and not a grid -- the groups are disclosures now rather than
   cards, and two columns of `<details>` would move the second one up the page
   every time the first was closed. */
.picker { display: flex; flex-direction: column; gap: var(--s2); }

.picker__group {
  min-width: 0;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
/* The lit edge, on the group holding a selection. The same language the rail's
   active row speaks: a rule down the leading edge and an inner glow under it,
   never an offset shadow. `:has()` rather than a class, so it follows the
   boxes rather than the render. */
.picker__group:has(input:checked) {
  border-color: var(--accent-line);
  box-shadow: inset 2px 0 8px -2px rgb(77 144 248 / 0.4);
}
.picker__summary {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: 0 var(--s3);
  cursor: pointer;
  list-style: none;
  transition: background-color 0.15s ease;
}
/* The default triangle, replaced by one that can be rotated. Safari needs the
   `-webkit-` pseudo-element as well as `list-style: none` to drop its own. */
.picker__summary::-webkit-details-marker { display: none; }
.picker__summary::before {
  content: "";
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  background: currentColor;
  color: var(--muted);
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m9 5 7 7-7 7'/%3E%3C/svg%3E");
  -webkit-mask: var(--icon) center / 14px 14px no-repeat;
  mask: var(--icon) center / 14px 14px no-repeat;
  transition: transform 0.15s ease;
}
.picker__group[open] > .picker__summary::before { transform: rotate(90deg); }
@media (hover: hover) {
  .picker__summary:hover { background: var(--sheen-strong); }
}
.picker__summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The count, pushed to the far end of the summary. Tabular figures, so a
   group that goes from "9 of 12" to "10 of 12" does not shuffle the row. */
.picker__count {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.picker__body {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: 0 var(--s3) var(--s3);
}

/* Select all. Quieter than a `.btn` on purpose: it sits above a row of
   checkboxes and is a convenience, not the thing being decided. Hidden in the
   markup and unhidden by app.js -- it is the only control in this picker that
   needs a script, so it is not offered where there is not one. */
.picker__all[hidden] { display: none; }
.picker__all {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0.2rem 0.5rem;
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted);
  background: none;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-xs);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}
@media (hover: hover) {
  .picker__all:not([disabled]):hover { color: var(--text); border-color: var(--muted); }
}
.picker__all[aria-pressed="true"] { color: var(--accent); border-color: var(--accent-line); }
.picker__all[disabled] { opacity: 0.4; cursor: not-allowed; }
/* The same bargain every compact control on this platform makes. */
@media (pointer: coarse), (max-width: 40rem) {
  .picker__all { min-height: var(--tap); padding: 0.2rem 0.8rem; }
}

.picker__cadence-label {
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text);
}
.picker__options { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.pick {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 0.35rem 0.6rem;
  min-height: var(--tap);
  font-size: var(--fs-sm);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}
.pick:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: inset 0 0 12px rgb(77 144 248 / 0.18);
}
.pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) {
  .pick:not(:has(input:disabled)):hover { border-color: var(--accent-line); }
}
.pick:has(input:disabled) { opacity: 0.6; cursor: not-allowed; }
.pick__label { min-width: 0; }

/* -- tabs --------------------------------------------------------------- */
/* CSS-only: a radio per tab, panels shown by sibling selector. No script, so
   the tabs work before app.js loads and if it never does. The radios are
   visually hidden rather than display:none, which would take them out of the
   tab order and off a screen reader. */

.tabs__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.tabs__strip {
  display: flex;
  gap: var(--s1);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--s4);
  /* A long tab strip scrolls rather than wrapping into an unpredictable
     second row that moves the panel down as you switch. */
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs__strip::-webkit-scrollbar { display: none; }
.tabs__tab {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--tap);
  padding: 0.4rem 0.8rem;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.tabs__panel { display: none; }

/* One rule per tab: which radio is checked decides which label is lit and
   which panel is shown. */
#tab-basic:checked ~ .tabs__strip label[for="tab-basic"],
#tab-advanced:checked ~ .tabs__strip label[for="tab-advanced"] {
  color: var(--text);
  border-bottom-color: var(--accent);
  /* The lit underline. Inset so it sits on the 2px border rather than beside
     it, and confined to the bottom of the box so it does not halo the word. */
  box-shadow: inset 0 -6px 10px -6px rgb(77 144 248 / 0.85);
}
#tab-basic:checked ~ .tabs__panel--basic,
#tab-advanced:checked ~ .tabs__panel--advanced { display: block; }

#tab-basic:focus-visible ~ .tabs__strip label[for="tab-basic"],
#tab-advanced:focus-visible ~ .tabs__strip label[for="tab-advanced"] {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* -- the wizard ---------------------------------------------------------- */
/* One long form, read one step at a time. The same mechanism as the tabs
   above and for the same reasons: a radio per step, panels shown by a sibling
   selector, no script anywhere in the switching. Which matters more here than
   it does for the tabs -- every field of every step stays in the DOM and stays
   submitted, so a browser that never ran a line of our JavaScript still posts
   a complete backtest, and a step nobody opened still carries its defaults.

   The radios are visually hidden rather than `display: none`, which would take
   them out of the tab order: the step strip is how a keyboard moves through
   this form.

   Back and Next are `<label>`s pointing at the step they select, not buttons.
   A button inside a form submits it, and a Next that ran a backtest would be
   the worst control on the platform. */

.wizard { display: block; }

.wizard__radio {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The progress bar. A track and a fill whose width is set by whichever radio
   is checked -- see the six rules below. Decorative: the step strip under it
   carries the same reading in words, and the current step is marked there with
   `aria-current` rather than by colour alone. */
.wizard__progress {
  height: 4px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
  margin-bottom: var(--s3);
}
.wizard__fill {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--btn-grad);
  transition: width 0.25s ease;
}

/* The steps themselves. A scrolling strip rather than a wrapping row, like the
   tab strip: six labels wrapping into a second line move the panel down as you
   switch, which is the one thing a progress indicator must never do. */
.wizard__steps {
  display: flex;
  gap: var(--s1);
  list-style: none;
  margin: 0 0 var(--s4);
  padding: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.wizard__steps::-webkit-scrollbar { display: none; }

.wizard__step {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  cursor: pointer;
  transition: color 0.15s ease, background 0.15s ease;
}
.wizard__step:hover { color: var(--text); background: var(--surface-2); }

/* The number in front of the label. Round, quiet, and lit when its step is the
   one on screen -- the one place a step's own index is stated, because "step 4
   of 6" is the question somebody halfway through a form is asking. */
.wizard__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

/* A step whose fields are refused. The chip beside it says how many, so this
   is the mark and not the message. */
.wizard__step--invalid { color: var(--danger); }
.wizard__step--invalid .wizard__num {
  border-color: var(--danger);
  color: var(--danger);
}

.wizard__panel { display: none; }

/* The foot of every panel: back on the left, next on the right, and on the
   last step the one button that actually runs something. */
.wizard__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
  justify-content: space-between;
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--border);
}

/* Back and Next are labels, so they need the button's box drawn on them
   rather than inherited from it. `.btn` gives them the rest. */
.wizard__go { cursor: pointer; }

/* One rule per step: which radio is checked decides which label is lit, which
   panel is shown, and how far the bar has moved. Written out rather than
   generated, because a stylesheet is not a program and six is not many. */
#step-1:checked ~ .wizard__steps .wizard__step--1,
#step-2:checked ~ .wizard__steps .wizard__step--2,
#step-3:checked ~ .wizard__steps .wizard__step--3,
#step-4:checked ~ .wizard__steps .wizard__step--4,
#step-5:checked ~ .wizard__steps .wizard__step--5,
#step-6:checked ~ .wizard__steps .wizard__step--6 {
  color: var(--text);
  background: var(--accent-soft);
}
#step-1:checked ~ .wizard__steps .wizard__step--1 .wizard__num,
#step-2:checked ~ .wizard__steps .wizard__step--2 .wizard__num,
#step-3:checked ~ .wizard__steps .wizard__step--3 .wizard__num,
#step-4:checked ~ .wizard__steps .wizard__step--4 .wizard__num,
#step-5:checked ~ .wizard__steps .wizard__step--5 .wizard__num,
#step-6:checked ~ .wizard__steps .wizard__step--6 .wizard__num {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent);
}

#step-1:checked ~ .wizard__panel--1,
#step-2:checked ~ .wizard__panel--2,
#step-3:checked ~ .wizard__panel--3,
#step-4:checked ~ .wizard__panel--4,
#step-5:checked ~ .wizard__panel--5,
#step-6:checked ~ .wizard__panel--6 { display: block; }

#step-1:focus-visible ~ .wizard__steps .wizard__step--1,
#step-2:focus-visible ~ .wizard__steps .wizard__step--2,
#step-3:focus-visible ~ .wizard__steps .wizard__step--3,
#step-4:focus-visible ~ .wizard__steps .wizard__step--4,
#step-5:focus-visible ~ .wizard__steps .wizard__step--5,
#step-6:focus-visible ~ .wizard__steps .wizard__step--6 {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Sixths, so the bar is full on the last step rather than on the one after it
   -- there is no step after Review, and a bar that stops at five sixths under
   a Run button reads as something left undone. */
#step-1:checked ~ .wizard__progress .wizard__fill { width: 16.6%; }
#step-2:checked ~ .wizard__progress .wizard__fill { width: 33.3%; }
#step-3:checked ~ .wizard__progress .wizard__fill { width: 50%; }
#step-4:checked ~ .wizard__progress .wizard__fill { width: 66.6%; }
#step-5:checked ~ .wizard__progress .wizard__fill { width: 83.3%; }
#step-6:checked ~ .wizard__progress .wizard__fill { width: 100%; }

/* The review step's summary: a label and a value per choice, with the value
   kept live by app.js as the boxes above change. With no script it is the
   state the page was rendered in, which is the same bargain every other live
   figure on this dashboard makes -- and the run is bounded by the inputs
   either way, never by this. */
.wizard__summary { display: grid; gap: var(--s2); margin: 0; }
.wizard__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
  align-items: baseline;
  justify-content: space-between;
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--border);
}
.wizard__row:last-child { border-bottom: 0; }
.wizard__term {
  font-size: var(--fs-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.wizard__value { min-width: 0; text-align: right; font-weight: 600; }

/* A phone reads the strip as six chips it scrolls through, and the step foot
   stacks so neither control ends up half a thumb wide. */
@media (max-width: 40rem) {
  .wizard__nav { flex-direction: column; align-items: stretch; }
  .wizard__row { justify-content: flex-start; }
  .wizard__value { text-align: left; }
}

/* -- the create wizard -------------------------------------------------
 *
 * The same strip, the same progress bar and the same review rows as the
 * first-run flow above, driven differently. That flow switches its panels in
 * the stylesheet, so the step it is on is `#step-N:checked`; the create wizard
 * renders one screen per request, because two of its screens depend on an
 * answer from an earlier one and a stylesheet cannot know a strategy name. So
 * the step it is on is the one the server marked with `aria-current`.
 *
 * The attribute is the selector rather than a class beside it deliberately:
 * `aria-current="step"` is what tells a screen reader which of the five this
 * is, and a class carrying the same fact would be a second place to get it
 * wrong -- with the lit chip agreeing with the class and the announcement
 * agreeing with the attribute. */

.wizard__step--static { cursor: default; }
.wizard__step--static:hover { color: var(--muted); background: transparent; }

/* A screen already answered. Marked, but quietly: it is behind you, and the
   only thing the strip has to make obvious is where you are. */
.wizard__step--done,
.wizard__step--done:hover { color: var(--text); background: transparent; }
.wizard__step--done .wizard__num { border-color: var(--ok); color: var(--ok); }

.wizard__step[aria-current="step"],
.wizard__step[aria-current="step"]:hover {
  color: var(--text);
  background: var(--accent-soft);
}
.wizard__step[aria-current="step"] .wizard__num {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent);
}

/* Fifths, and full on the review for the same reason the first-run flow's bar
   is full on its last step: there is nothing after Review, and a bar stopped
   short under a create button reads as something left undone. */
.wizard__fill--1 { width: 20%; }
.wizard__fill--2 { width: 40%; }
.wizard__fill--3 { width: 60%; }
.wizard__fill--4 { width: 80%; }
.wizard__fill--5 { width: 100%; }

/* The review's three buttons. They sit together on the right of the step foot
   with the primary one last, which is where the eye finishes a left-to-right
   row -- and on a phone they stack, so none of them ends up half a thumb wide. */
.wizard__actions { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }

/* A reading rather than a value: a sentence set in the value column's bold
   right-aligned type would be a paragraph pretending to be a number. */
.wizard__value--prose { font-weight: 400; }

.wizard__aside { margin-top: var(--s3); }

/* The preset the wizard opens on. Accent rather than a status colour: a
   recommendation is not a state the bot is in, and the other starting points
   beside it are not faults. */
.badge--recommended { border-color: var(--accent-line); color: var(--accent); }

@media (max-width: 40rem) {
  .wizard__actions { flex-direction: column; align-items: stretch; }
}

/* The first-run flow's own head: the lockup, the welcome and the sentence
   under it, centred over the wizard rather than beside it. The mark is sized
   by `.auth__brand`, which is the same rule the sign-in page and the guide
   use -- this only decides where it sits. */
.onboard { text-align: center; margin-bottom: var(--s5); }
.onboard__logo { margin: 0 auto var(--s4); }
.onboard__skip { margin-top: var(--s5); text-align: center; }

/* -- reveal -------------------------------------------------------------- */
/* A checkbox that shows its own controls, with no script -- the same reasoning
   as the tabs above. `:has()` rather than a sibling selector because the
   checkbox lives inside its `<label class="check">`, where it has to be for the
   label text to be its own hit target on a phone.

   The controls stay in the DOM when hidden, and stay submitted. That is
   deliberate: the handler reads them only when the toggle is ticked, so a
   stale value in a hidden box cannot reach a run, and a rejected form comes
   back with what was typed still in it. */

.reveal__body { display: none; }
.reveal:has(.reveal__toggle:checked) .reveal__body { display: flex; }

/* A form that came back with an error inside the hidden half must show it, or
   the page reports a problem with a control nobody can see. Belt and braces
   with the handler, which only sets `errors.split` on a submission that had
   the toggle on in the first place. */
.reveal.param--invalid .reveal__body { display: flex; }

/* The backtest's date step: "everything this box holds" states the span it
   resolves to, "pick a range" shows the calendar, and neither is on screen
   while the other is. The same `:checked` mechanism as the split controls
   below, and for the same reason -- two date boxes visible under a mode that
   ignores them is an invitation to fill in the fields being ignored. */
.dates__all, .dates__custom { display: none; }
.dates:has(.dates__mode--all:checked) .dates__all { display: block; }
.dates:has(.dates__mode--custom:checked) .dates__custom { display: flex; }
/* A range refused must show the boxes it was refused about, whichever mode is
   ticked -- otherwise the page reports a problem with a control nobody can
   see. */
.dates.param--invalid .dates__custom { display: flex; }

/* The split controls: one mode's field at a time, chosen by its own radio. */
.split__modes { display: flex; flex-wrap: wrap; gap: var(--s4); }
.split__fraction, .split__date { display: none; }
.split:has(.split__mode--fraction:checked) .split__fraction { display: flex; }
.split:has(.split__mode--date:checked) .split__date { display: flex; }

/* -- accordion ---------------------------------------------------------- */
/* <details>, so it opens on tap with no script, is keyboard-reachable and is
   announced as expandable. Exclusivity comes from the shared name attribute,
   with app.js closing siblings where that is not yet honoured. */

.accordion {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--sheen), var(--surface);
  margin-bottom: var(--s2);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.accordion__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  /* Full width and a real target: this is the control most often pressed on a
     phone, one-handed, and it must not need aim. */
  min-height: var(--tap);
  padding: var(--s2) var(--s3);
  font-size: var(--fs-body);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  border-radius: var(--radius-sm);
  transition: background 0.15s ease, color 0.15s ease;
}
.accordion__summary::-webkit-details-marker { display: none; }
.accordion__summary::after {
  content: "";
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  background: var(--muted);
  --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
  -webkit-mask: var(--icon) center / 14px 14px no-repeat;
  mask: var(--icon) center / 14px 14px no-repeat;
  transition: transform 0.15s ease, background 0.15s ease;
}
.accordion[open] > .accordion__summary {
  border-bottom: 1px solid var(--border-strong);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  /* The elevated surface, so an open drawer's head sits above the card it is
     on rather than level with it. */
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
}
.accordion[open] > .accordion__summary::after { transform: rotate(180deg); }
.accordion__title { min-width: 0; }
.accordion__meta {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin-left: auto;
  min-width: 0;
}
.accordion__body {
  padding: var(--s4);
  /* The panel arrives rather than appearing. A fade with four pixels of
     travel, on the body only -- the box itself is sized by the browser's own
     disclosure, so this cannot fight it or leave a panel half-open. */
  animation: reveal 0.2s ease;
}

/* The box, opening smoothly, where the browser can do it -- and on the FAQ
 * only.
 *
 * `<details>` has never been animatable: its content goes from `display: none`
 * to laid out in one frame, and every script-free workaround for that (a
 * max-height guess, a grid-rows trick) either clips a long panel or animates
 * to a height that is not the panel's. `::details-content` plus
 * `interpolate-size` is the first way to say "animate to whatever height this
 * turns out to be", so it is gated on exactly that and nothing else degrades
 * without it -- a browser that does not have it opens the panel the way it
 * always did, instantly, with the fade above still playing on the contents.
 *
 * Scoped to the landing page's questions rather than applied to every
 * accordion on the platform, and that restriction is load-bearing. Animating a
 * height means clipping what overflows it, and what overflows a *settings*
 * accordion is the parameter tooltips: `.tip__body` is absolutely positioned
 * below its label, and the last row of an open drawer opens its help text past
 * the bottom edge of the panel. Under `overflow: hidden` that help would be
 * cut off -- on the form where a wrong number costs money, to buy a quarter of
 * a second of easing. The FAQ has no tooltips and nothing that escapes its own
 * box, so it is the one place the trade is free. */
@supports (interpolate-size: allow-keywords) {
  .landing__section--faq { interpolate-size: allow-keywords; }
  .landing__section--faq .accordion::details-content {
    block-size: 0;
    overflow: hidden;
    content-visibility: hidden;
    transition: block-size 0.25s ease, content-visibility 0.25s allow-discrete;
  }
  .landing__section--faq .accordion[open]::details-content {
    block-size: auto;
    content-visibility: visible;
  }
}
.accordion--expert .accordion__title { color: var(--warn); }
/* The one accordion that sits among cards rather than in a stack of its own
   siblings: it is spaced like the panels around it -- and, since it is the
   account of what a run actually did rather than a drawer of settings, it is
   given the panels' surface and their lit top edge too. */
.accordion--diagnostics {
  margin-bottom: var(--s4);
  border-radius: var(--radius);
  background: var(--sheen), var(--surface);
}
.accordion--diagnostics::before {
  content: "";
  position: absolute;
  top: -1px;
  left: var(--radius);
  right: var(--radius);
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--accent-line), transparent);
}
.accordion--diagnostics > .accordion__summary {
  color: var(--muted);
  border-radius: var(--radius);
}
.accordion--diagnostics[open] > .accordion__summary {
  color: var(--text);
  border-radius: var(--radius) var(--radius) 0 0;
}

/* Two columns of settings on a wide screen. Most of what B declares is a short
   number or a toggle, and one of those per 1300px row is a form that scrolls
   for no reason. Anything that is genuinely wide -- a JSON textarea, a
   multi-select, the market picker -- takes the full row back. */
@media (min-width: 60rem) {
  .accordion__body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0 var(--s5);
    align-items: start;
  }
  /* Descendant rather than child: a governed parameter now sits inside a
     `display: contents` wrapper so its switch can reach it, and a `>` here
     would leave every one of those without its spacing. Nothing nests a
     `.param` inside another `.param`, so the broader selector costs nothing.
     Same reason for each full-row rule below. */
  .accordion__body .param { margin-bottom: var(--s4); }
  .accordion__body > p,
  .accordion__body > .banner,
  .accordion__body > .param__errors,
  .accordion__body > .grid,
  .accordion__body > form,
  .accordion__body > textarea,
  .accordion__body .param:has(textarea),
  .accordion__body .param:has(select[multiple]),
  .accordion__body .param:has(.picker),
  .accordion__body .param:has(.choices) { grid-column: 1 / -1; }
  /* The opt-out, for a body that is not a settings form. The backtest's
     diagnostics are panels and tables, and a two-column settings grid is the
     wrong container for them. */
  .accordion__body--flow { display: block; }
}

/* No hover-dependent *affordance* anywhere: the border and the caret already
   say this is pressable, and a phone has no hover to reveal anything with. The
   accent edge below is the panels' answer to a pointer, not the thing that
   tells a reader the drawer opens -- which is why it can be there on a mouse
   and absent on a thumb without either reader losing anything. */
@media (hover: hover) {
  .accordion:hover { border-color: var(--accent-line); box-shadow: var(--glow); }
  .accordion__summary:hover { background: var(--surface-2); }
  .accordion[open] > .accordion__summary:hover {
    background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
  }
  .tabs__tab:hover { color: var(--text); }
}

/* -- a setting under a switch -------------------------------------------
 *
 * Three hundred parameters, and on a typical bot most of them are having no
 * effect at all: an indicator that is switched off still shows its eleven
 * settings, a combination left on `none` still shows its twenty-seven. The
 * schema knows which -- every governed parameter arrives from Server B naming
 * the one that governs it -- and this is what that knowledge looks like on a
 * page.
 *
 * **The switch reaches its fields through the DOM, not through a script.**
 * CSS cannot ask "what is the value of the input this element names in a
 * `data-` attribute", so the route from a checkbox to the eleven fields it
 * makes inert is a common ancestor and `:has()`. That ancestor is `.depend`,
 * and the two wrappers inside it are `display: contents` -- they are handles
 * for a selector and they change no layout: the fields stay direct grid items
 * of the section body, laid out exactly as they were before this existed.
 *
 * **A choice is handled by marking its options.** The one string comparison
 * CSS can do is "is this element the one currently `:checked`", so the server
 * marks the option values that switch a block off and the selector asks that
 * question instead. `combination_mode` is the case; it works with no script.
 *
 * **A switch in another section is JavaScript's.** `hold_to_expiry` is a basic
 * exit setting and two of the parameters it makes inert are advanced ones, a
 * section and a tab away, where no ancestor selector can reach. Those carry a
 * server-rendered `param--inert`, correct when the page loads, and app.js is
 * what keeps them correct after a click.
 *
 * **Greyed is not the same as locked.** Everything inert is greyed. What is
 * also put out of reach is everything inert *except* the fields marked
 * `param--clearable` -- the ones carrying an error, and the ones whose
 * off-state is "empty" and which are not empty. Both are fields somebody may
 * have to change before the form can be saved at all, and a locked field is a
 * form that cannot be corrected. The `(?)` stays reachable throughout: an
 * explanation is worth reading whether or not the setting is doing anything.
 */

.depend,
.depend__switch,
.depend__body { display: contents; }

/* The four ways a field ends up inert. The first is the server's word, for a
   switch CSS cannot see; the other three are the switch itself, live. */
.param--inert,
.depend--off:has(> .depend__switch input[type="checkbox"]:not(:checked)) > .depend__body > .param,
.depend--on:has(> .depend__switch input[type="checkbox"]:checked) > .depend__body > .param,
.depend--choice:has(> .depend__switch option.opt--inert:checked) > .depend__body > .param {
  opacity: 0.45;
  cursor: not-allowed;
  transition: opacity 0.15s ease;
}

/* And the same four, put out of reach -- minus the two exceptions, and minus
   the help button, which is never one of them. `pointer-events` rather than
   `disabled`: a disabled input is not submitted, and a parameter missing from
   a submission is one this box would have to reconstruct from what it had
   stored. The value stays in the form, unchanged, and goes back exactly as it
   came. app.js takes these out of the tab order to finish the job. */
.param--inert:not(.param--clearable) :is(input, select, textarea),
.depend--off:has(> .depend__switch input[type="checkbox"]:not(:checked)) > .depend__body > .param:not(.param--clearable) :is(input, select, textarea),
.depend--on:has(> .depend__switch input[type="checkbox"]:checked) > .depend__body > .param:not(.param--clearable) :is(input, select, textarea),
.depend--choice:has(> .depend__switch option.opt--inert:checked) > .depend__body > .param:not(.param--clearable) :is(input, select, textarea) {
  pointer-events: none;
}

/* The sentence, revealed with the greying. Hidden rather than absent so it
   appears the moment the box is unticked, in the same frame, with nothing
   fetched and nothing rebuilt. */
.depend__note,
.param__inert { display: none; }
/* The per-field version, for a switch in another section. Shown off the class
   the server writes and app.js maintains, which is the only signal that
   reaches across a section boundary at all. */
.param--inert > .param__inert { display: block; margin: var(--s1) 0 0; }
.depend--off:has(> .depend__switch input[type="checkbox"]:not(:checked)) > .depend__note,
.depend--on:has(> .depend__switch input[type="checkbox"]:checked) > .depend__note,
.depend--choice:has(> .depend__switch option.opt--inert:checked) > .depend__note {
  display: block;
  margin: 0 0 var(--s3);
  padding-left: var(--s3);
  border-left: 2px solid var(--border-strong);
}
@media (min-width: 60rem) {
  /* A note about the whole block takes the whole row. */
  .accordion__body .depend__note { grid-column: 1 / -1; }
}

/* An open drawer wears an accent edge down its left-hand side.
 *
 * The settings form is eight or nine of these stacked, one of them open, and
 * with nothing but a caret to say which, a reader scrolled halfway down a long
 * panel has to look back up to find out where they are. The edge is a border
 * rather than an inset shadow because it is on the *box* and not on a table
 * row: it is allowed to take the two pixels, and taking them keeps the body's
 * text off the rule.
 *
 * The border is declared shut as well as open, in the border colour, so
 * opening a drawer does not move its contents two pixels to the right. */
.accordion { border-left: 2px solid var(--border); }
.accordion[open] {
  border-left-color: var(--accent);
  box-shadow: var(--glow);
}
.accordion[open] > .accordion__summary { color: var(--text); }
/* The caret takes the accent with it, so the two marks that say "this one is
   open" agree. */
.accordion[open] > .accordion__summary::after { background: var(--accent); }
/* The diagnostics drawer sits among cards rather than in a stack of siblings,
   so it keeps its own hairline: there is nothing beside it to be told apart
   from, and a 2px accent rule down a full-width panel reads as an alert. */
.accordion--diagnostics { border-left-width: 1px; }
.accordion--diagnostics[open] { border-left-color: var(--accent-line); }
/* Same for the landing page's questions -- a marketing FAQ is not a settings
   form and does not want a form's chrome. */
.landing__section--faq .accordion { border-left-width: 1px; }
.landing__section--faq .accordion[open] { border-left-color: var(--border); }

/* The help mark, in the accent rather than in the third text rank. It is the
   one thing on a settings row a reader is looking *for* -- forty labels and
   forty quiet grey circles is forty things that read as punctuation. */
.tip__toggle { color: var(--accent); border-color: var(--accent-line); background: var(--accent-soft); }
@media (hover: hover) {
  .tip__toggle:hover { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }
}

.settings__actions { margin-top: var(--s5); }
.settings__actions p { margin: var(--s2) 0 0; }
/* A block button is the right shape under a thumb and the wrong one across a
   1400px card, where it stops reading as a button at all. */
@media (min-width: 60rem) {
  .settings__actions .btn--block { width: auto; min-width: 16rem; }
}

/* A short form laid out in two columns rather than one long column of single
   fields. Anything that is not a short field -- a heading, a banner, the
   market picker -- takes the row back. */
@media (min-width: 60rem) {
  .form-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--s4) var(--s5);
    align-items: start;
  }
  .form-grid > h2,
  .form-grid > p,
  .form-grid > button,
  .form-grid > .banner,
  .form-grid > .settings__actions,
  .form-grid > .param--invalid:not(:has(input, select, textarea)),
  .form-grid > .check,
  .form-grid > .param:has(.picker),
  .form-grid > .param:has(.choices),
  .form-grid > .param:has(textarea),
  .form-grid > .field:has(textarea) { grid-column: 1 / -1; }
}

/* -- live-update indicator --------------------------------------------- */

.stream-state {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--fs-xs);
  font-weight: 500;
  color: var(--muted);
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  white-space: nowrap;
}
.stream-state::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--dim);
}
.stream-state[data-state="open"]::before {
  background: var(--ok);
  /* The one thing on the dashboard that is allowed to keep moving: it is
     saying that the connection is still there, and a static dot says that
     just as convincingly when it is not. The colour and the word beside it
     carry the reading on their own where the animation is switched off. */
  animation: ping 2.4s ease-out infinite;
}
.stream-state[data-state="connecting"]::before { background: var(--warn); }
.stream-state[data-state="closed"]::before { background: var(--danger); }
.stream-state[data-state="closed"] { color: var(--danger); border-color: rgb(241 92 92 / 0.35); }

.fingerprint { font-family: var(--mono); font-size: var(--fs-sm); word-break: break-all; }

/* -- backtest charts ---------------------------------------------------
 *
 * The height is stated here and nowhere else. Lightweight Charts sizes itself
 * to its container (`autoSize`), so a height in the JavaScript would be a
 * second place for the same number to live and the one that a stylesheet could
 * not override. Width is never stated: the container is a block inside a card,
 * so it is already the width of the page or of its column.
 *
 * Deliberately short on a phone. A chart taller than the thumb's reach turns
 * the page into two scroll regions that fight each other, and the library
 * captures vertical drags inside its own pane. */

.chart {
  position: relative;
  height: 240px;
  margin: var(--s3) 0 var(--s4);
  /* The pane paints itself in `--surface`, which is the card colour: the chart
     is part of the panel rather than a picture pasted into it, and a second
     rectangle inside the card is exactly what a dashboard should not look
     like. This is what a pane looks like before the library has painted, and
     what one that failed to load looks like for good. */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* Lit from the top edge, like every other surface here. An inset shadow
     rather than a gradient background: the fill has to stay exactly
     `--surface`, because that is the colour charts.js reads and hands the
     library for the pane it paints inside this box. A gradient here would put
     a visible rectangle inside the pane the moment the chart loaded. */
  box-shadow: inset 0 1px 0 var(--glass-line);
  overflow: hidden;
}
.chart--price { height: 300px; }
.chart:last-child { margin-bottom: 0; }

/* A pane with nothing in it yet. `:empty` is exactly the right condition and
   costs no script: charts.js either appends a canvas or appends the sentence
   explaining why it could not, and either one ends this state. A placeholder
   in the shape of the chart rather than a spinner -- it says what is coming
   and it does not move the page when it arrives. */
.chart:empty {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  animation: pulse 1.4s ease-in-out infinite;
}
/* The sentence stays. A shape that pulses says something is coming; it does
   not say what, and it says nothing at all to a reader who is being read the
   page rather than looking at it. */
.chart:empty::after { content: "Waiting for the run's figures…"; }

/* The placeholder a pane shows instead of a chart: no library, no archived
   book, no data. Centred so it reads as the state of the pane rather than as a
   caption that lost its picture. */
.chart__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  margin: 0;
  padding: 0 var(--s4);
  text-align: center;
}

/* Two panes side by side. Equity and drawdown are the same series measured two
   ways, and reading one under the other is reading them a screen apart. */
.charts__pair { display: grid; grid-template-columns: 1fr; gap: 0 var(--s4); }
@media (min-width: 60rem) {
  .charts__pair { grid-template-columns: 1fr 1fr; }
}
.charts h2 {
  margin: var(--s4) 0 var(--s1);
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.charts h2:first-of-type { margin-top: 0; }
.charts p { margin: var(--s1) 0 0; }

.chart__controls {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  margin: var(--s3) 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.chart__controls select { max-width: 100%; }

/* Positioned by script, with `transform` rather than `top`/`left` so a
   tooltip that follows a crosshair does not lay out the page on every frame.
   `pointer-events: none` is load-bearing: without it the tooltip sits under
   the cursor, takes the hover from the marker that opened it, and flickers. */
.chart__tip {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  pointer-events: none;
  max-width: 15rem;
  padding: var(--s2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  color: var(--text);
  font-size: var(--fs-xs);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  box-shadow: var(--shadow-pop);
}
.chart__tip-head {
  font-family: var(--mono);
  font-size: 0.6875rem;
  color: var(--muted);
  word-break: break-all;
}

@media (min-width: 48rem) {
  .chart { height: 280px; }
  .chart--price { height: 340px; }
}

/* -- the settlement index chart -----------------------------------------
 *
 * One pane per pair, not one pane with several lines on it. Bitcoin trades
 * near $100,000 and Solana near $200: on a shared axis the second line is a
 * flat mark along the bottom, and the only way to give both a readable scale
 * is a second y-axis -- which makes the two lines' crossings and gaps mean
 * nothing at all. Faceting keeps one axis per chart, keeps each strike line
 * next to the index it is a strike on, and costs vertical space that a page
 * this dense can afford for the one thing on it that moves every second.
 *
 * Short panes, because there are several of them and because the useful range
 * of a fifteen-minute index is narrow: the shape is in the horizontal. */

.rti__pane { margin-top: var(--s4); }
.rti__pane:first-of-type { margin-top: var(--s3); }

.rti__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s1) var(--s3);
  margin-bottom: var(--s1);
}

/* The pair, in the colour of its own line. The name is what identifies the
   pane -- the colour agrees with it rather than carrying it, which is what
   keeps this readable for a reader who cannot tell two of the eight apart. */
.rti__pair {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.rti__swatch {
  width: 0.7rem;
  height: 2px;
  border-radius: 999px;
  background: var(--muted);
}

/* The current reading, in text. A number a reader can quote is worth more than
   a pixel they have to measure against an axis, and it is also the fallback
   when the library did not load. */
.rti__now {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  color: var(--text);
}
.rti__now--stale { color: var(--warn); }

.chart--rti { height: 150px; margin: 0; }
@media (min-width: 48rem) {
  .chart--rti { height: 170px; }
}

/* The card's own empty state, for a page whose script never ran or whose
   Server B is not sending an index. The chart's pulsing placeholder is
   deliberately not reused: that one says "any moment now", and this one may be
   the answer. */
.rti__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: var(--s3) 0 0;
  /* Height out of padding rather than a `min-height`: every one of those in
     this sheet is the shared tap token or an explained exception, and a
     placeholder is not a tap target. */
  padding: var(--s5) var(--s4);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--muted);
  text-align: center;
}

/* -- two-factor enrolment ------------------------------------------------ */

/* The QR code carries its own black-on-white, whatever the page theme is: a
   code inverted by a dark-mode rule is one many scanners refuse outright. It is
   capped rather than fluid because a QR bigger than a phone camera's frame is
   harder to scan, not easier. */
.enrol { display: flex; flex-wrap: wrap; gap: var(--s5); align-items: flex-start; }
.enrol__qr {
  flex: 0 0 auto;
  width: min(14rem, 70vw);
  padding: var(--s2);
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.enrol__qr .qr { display: block; width: 100%; height: auto; }
.enrol__manual { flex: 1 1 14rem; min-width: 0; }

.secret {
  font-family: var(--mono);
  font-size: 0.9375rem;
  letter-spacing: 0.06em;
  word-break: break-all;
  user-select: all;  /* one click selects the whole key to copy */
}

.codes {
  list-style: none;
  margin: var(--s4) 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.35rem var(--s4);
}
.codes code { font-size: 0.9375rem; letter-spacing: 0.04em; user-select: all; }

/* -- admin ---------------------------------------------------------------
   The account list and the platform kill switch. Nothing here invents a
   colour: `admin` is an accent badge like `read-only`, and the one thing on
   these pages that has to stop the eye -- a password shown once -- gets a
   border rather than a new palette entry. */

.badge--admin { border-color: var(--accent-line); color: var(--accent); font-weight: 700; box-shadow: inset 0 0 10px rgb(77 144 248 / 0.2); }

.card--callout { border-color: var(--accent-line); background: var(--surface-2); }

/* A plan picker sitting on one line beside its button, on a screen wide enough
   for that. It wraps to two on a phone like everything else here does. */
.stack--inline { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: flex-end; margin-top: var(--s4); }
.field--inline { flex: 0 1 12rem; }

/* The nav's 2FA-off marker. A dot, not a word: it has to be noticeable without
   turning every page header into a lecture. */
.dot { font-size: 1.1em; line-height: 1; }
.dot--warn { color: var(--warn); }

/* -- the guide -----------------------------------------------------------
 *
 * A reference page rather than a dashboard: long prose, a contents index, and
 * definition lists. It reuses the cards, chips and accordions the rest of the
 * app already has, so it cannot drift into looking like a different product,
 * and adds only what a document needs and a dashboard does not. */

.guide__heading { margin: 0 0 var(--s3); display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
/* The one chip on the platform that carries a sentence rather than a word --
   "Mostly noise: included because users expect it" -- so it is the one that
   has to wrap. Nowrap here is 353px of chip on a 375px screen. */
.guide__heading .chip { white-space: normal; }
.guide__toc-title { margin: 0 0 var(--s3); font-size: var(--fs-label); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.guide__toc-list, .guide__toc-list ol { margin: 0; padding-left: 1.2rem; }
.guide__toc-list li { margin: 0.1rem 0; }
/* Tight under a mouse, a real target under a thumb -- the same bargain
   `.btn--small` makes, and for the same reason: this is the control most used
   on a phone, and twenty-three 44px rows is a contents page you scroll. */
.guide__toc-list a { display: inline-block; padding: var(--s1) 0; }
@media (pointer: coarse), (max-width: 40rem) {
  .guide__toc-list a { display: flex; align-items: center; min-height: var(--tap); }
}
/* Two columns on a wide screen. Twenty-three entries in one column is a
   contents page you have to scroll, which is the one thing a contents page
   exists not to be. */
@media (min-width: 60rem) {
  .guide__toc-list { column-count: 2; column-gap: var(--s5); }
  .guide__toc-list > li { break-inside: avoid; }
}

.guide__ranking {
  margin: var(--s4) 0;
  padding: var(--s3);
  border-left: 2px solid var(--accent);
  background: var(--surface-2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.guide__ranking-title { margin: 0 0 var(--s1); font-size: var(--fs-label); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.guide__ranking p { margin: 0.35rem 0 0; }
.guide__ranking p:first-of-type { margin-top: 0; }

.guide__facts { margin: 0; }
.guide__facts dt {
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin-top: var(--s4);
}
.guide__facts dt:first-child { margin-top: 0; }
.guide__facts dd { margin: 0.15rem 0 0; }
.guide__facts-title { margin: var(--s5) 0 var(--s1); font-size: var(--fs-body); font-weight: 600; }

.guide__produces { margin: 0 0 var(--s3); }

.guide__settings { margin: var(--s3) 0 0; }
.guide__setting-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  font-weight: 600;
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--border);
}
.guide__settings > .guide__setting-name:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.guide__setting-body { margin: var(--s1) 0 0; }
.guide__setting-body p { margin: var(--s1) 0 0; }
.guide__edge { font-size: var(--fs-sm); color: var(--muted); }
.guide__edge strong { color: var(--text); }

.guide__code {
  margin: var(--s2) 0 0;
  padding: var(--s3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  /* A rule tree is written wide and must not widen the page with it. */
  overflow-x: auto;
  font-size: var(--fs-xs);
  line-height: 1.55;
}

/* The guide's account of the same five conditions. Numbered, and deliberately
   not `.gates`: that list carries a met/not-met chip per row, and the guide
   has no session to answer it from. Ordered because the gates are met in an
   order -- cloning first, the bot stopped last -- and a reader following the
   path wants the position as well as the name. */
.guide__gates { margin: var(--s3) 0 0; padding-left: 1.3rem; }
.guide__gates > li { margin: var(--s4) 0 0; }
.guide__gates > li:first-child { margin-top: 0; }
.guide__gates .guide__facts-title { margin-top: 0; }
.guide__gates p { margin: var(--s1) 0 0; }

/* The live-mode conditions. A list rather than a table: each row is a
   statement with a sentence under it, and the chip carries the state in a
   word as well as a colour -- "met"/"not met" is legible to somebody who
   cannot tell the green from the amber. */
.gates { list-style: none; margin: 0; padding: 0; }
.gates .gate {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s1) var(--s2);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--border);
}
.gates .gate:last-child { border-bottom: 0; }
.gates .gate .chip { grid-row: 1; align-self: center; }
.gates .gate .muted { grid-column: 2; }

/* -- narrow screens ------------------------------------------------------
   The page's own margins. 12px either side rather than 24: on a 375px screen
   the difference is a tenth of the width of every figure on the page. */
@media (max-width: 47.99rem) {
  .page { padding: var(--s4) var(--s3) 3rem; }
  .footer { padding: var(--s4) var(--s3) var(--s5); }
  .card { padding: var(--s3); }
  .cols, .cards { gap: var(--s3); }
  .card, .cards > .card { margin-bottom: var(--s3); }
  /* Two per row, never one: a pair of figures is still scannable and a column
     of one is a page you scroll to compare two numbers. */
  .metrics, .metrics--tiles, .metrics--headline { grid-template-columns: repeat(2, 1fr); }
  .metric--hero { grid-column: 1 / -1; }
  .page__title { margin-bottom: var(--s3); }
}

/* -- the landing page ----------------------------------------------------
 *
 * The one page here with no session behind it, and the only one whose job is
 * to be read rather than watched.
 *
 * Full-bleed bands, each with its own measure inside. `.landing` itself is the
 * width of the window and holds no content directly: every section paints its
 * own ground edge to edge and stops its *contents* at `.landing__inner`. That
 * is what the gradients need -- a mesh that ends at a 68rem column is a
 * rectangle sitting on a page rather than a page that is lit -- and it costs
 * nothing, because the column is one wrapper rather than a measure repeated on
 * every rule inside it.
 *
 * Written mobile-first, unlike the rest of this sheet, and deliberately: the
 * dashboard is a desktop instrument with a phone case and this is the
 * opposite. Every rule below is the small-screen one, and the `min-width`
 * block at the end is what a wide screen adds.
 *
 * Everything that moves on this page is decoration over a reading that is
 * already complete without it: the aura behind the mark, the ramp around the
 * call to action, the line joining the three steps. All of it stops under
 * `prefers-reduced-motion` at the foot of this sheet, and nothing is lost when
 * it does. */

.landing { position: relative; overflow-x: clip; }

/* -- the orbs.
 *
 * Four soft discs drifting behind the whole page, and the depth on this page
 * that the bands cannot give it: a section's own light is nailed to that
 * section and stops at its edges, so a page made only of those reads as a
 * stack of lit rectangles. These cross the seams.
 *
 * Pseudo-elements on four elements that already exist rather than four empty
 * `<div>`s, and not only for tidiness: `test_welcome` reads `.landing__grid`
 * and `.landing__tiers` by slicing to their *first* `</div>`, so a decorative
 * wrapper anywhere near them silently truncates what those assertions can see.
 * Decoration that has to be in the markup is decoration that can break a test
 * about pricing.
 *
 * `z-index: 0` rather than `-1`. Every band on this page paints its light in a
 * pseudo-element at the same level, so the orbs interleave with those in
 * document order and all of it stays under `.landing__inner`, which is the one
 * thing on the page holding `z-index: 1`. A negative level would have put them
 * behind the page's own mesh on any browser where a band happens to establish
 * a stacking context, which is to say invisibly and unpredictably. */
.landing::before,
.landing::after,
.landing__stats::before,
.landing__foot::after {
  content: "";
  position: absolute;
  z-index: 0;
  border-radius: 50%;
  pointer-events: none;
}
/* Top left, over the hero and the first seam. */
.landing::before {
  top: 8%;
  left: -7%;
  width: 400px;
  height: 400px;
  background: radial-gradient(closest-side, rgb(77 144 248 / 0.06), transparent 70%);
  filter: blur(80px);
  animation: drift 22s ease-in-out infinite;
}
/* Right, about level with the plans. */
.landing::after {
  top: 54%;
  right: -8%;
  width: 340px;
  height: 340px;
  background: radial-gradient(closest-side, rgb(139 92 246 / 0.06), transparent 70%);
  filter: blur(80px);
  animation: drift-back 27s ease-in-out -9s infinite;
}
/* Behind the figures, off to the left of them. Hung off the stats band because
   that band is the seam between the hero and everything under it, and an orb
   sitting in a seam is the one that reads as depth rather than as a light
   somebody left on. */
.landing__stats::before {
  top: -140px;
  left: 4%;
  width: 260px;
  height: 260px;
  background: radial-gradient(closest-side, rgb(34 211 238 / 0.06), transparent 70%);
  filter: blur(80px);
  animation: drift-back 19s ease-in-out -4s infinite;
}
/* The last one, under the foot, so the page fades out lit rather than simply
   running out of content. */
.landing__foot::after {
  bottom: -80px;
  left: 26%;
  width: 320px;
  height: 320px;
  background: radial-gradient(closest-side, rgb(139 92 246 / 0.05), transparent 70%);
  filter: blur(80px);
  animation: drift 24s ease-in-out -14s infinite;
}

/* The measure. `position: relative` and a stacking context of its own, so the
   contents sit above the mesh each band paints behind them. */
.landing__inner {
  position: relative;
  z-index: 1;
  max-width: 68rem;
  margin: 0 auto;
  padding: 0 var(--s4);
}
/* Prose, and the questions. 68rem is right for three pricing columns and much
   too wide for a sentence. */
.landing__inner--narrow { max-width: 48rem; }

/* -- the hero.
 *
 * A band with a height rather than a stack with padding. Everything in it is
 * centred in the space it is given, which is what the six parts -- lockup,
 * eyebrow, claim, subhead, actions, qualifier -- need to stop reading as a
 * list: the gaps below are all larger than the gaps anywhere else on the page,
 * deliberately, because this is the one screen a reader is given before they
 * decide whether to have another. The viewport height itself is added in the
 * wide-screen block; on a phone the content is taller than the screen anyway
 * and a minimum would only push the first thing below it out of reach. */
.landing__hero {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 4rem 0 3.5rem;
  text-align: center;
}
/* The band's own light. Three radials, wide enough that none of them ever
   resolves into a shape -- the moment one reads as a circle it stops being
   depth and starts being a graphic. */
.landing__hero::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* The first of them is centred on the lockup rather than on the top edge of
     the band, and is wide enough that its own falloff is the blur: the mark
     sits in a pool of light instead of under a band of it. */
  background:
    radial-gradient(64rem 46rem at 50% 34%, rgb(77 144 248 / 0.16), transparent 70%),
    radial-gradient(38rem 26rem at 6% 14%, rgb(139 92 246 / 0.11), transparent 70%),
    radial-gradient(36rem 24rem at 94% 8%, rgb(34 211 238 / 0.08), transparent 70%);
}
/* The grid over it. One-pixel rules every 40px, at three per cent of white --
   which over this ground is about four levels per channel on the line and a
   third of one averaged over the square, so it is texture rather than pattern.
   It is what stops the radials above reading as a wash: a gradient with
   nothing in it has no scale, and the eye takes the ruling as the scale.
 *
 * Masked to an ellipse over the middle of the band rather than run to its
 * edges. A repeating grid that simply stops at a section boundary is a visible
 * horizontal edge across the page, and this one is meant to be found rather
 * than seen. A browser with no `mask-image` gets the grid to the band's edges,
 * where the seam under the stats bar is already drawing a line anyway. */
.landing__hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.03) 0 1px, transparent 1px 40px),
    repeating-linear-gradient(180deg, rgb(255 255 255 / 0.03) 0 1px, transparent 1px 40px);
  -webkit-mask-image: radial-gradient(62% 58% at 50% 44%, #000, transparent 78%);
          mask-image: radial-gradient(62% 58% at 50% 44%, #000, transparent 78%);
}

.landing__eyebrow {
  margin: 0 0 var(--s4);
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent);
}

/* The frame the glow is painted on. The halo is a pseudo-element behind the
   artwork rather than a `filter` on it: the lockup has transparency in it, and
   a drop-shadow would trace the letterforms instead of lighting the space they
   sit in. */
.landing__aura {
  position: relative;
  display: block;
  width: 100%;
  max-width: 280px;
  margin: 0 auto 2.5rem;
}
.landing__aura::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: -1;
  /* Sized so the halo plus its blur still clears the edge of the narrowest
     screen. At 375px the lockup is 280px in a 343px column, which leaves 47px
     of margin either side: a 12% overhang is 17px of that and the 20px blur is
     most of the rest. `overflow-x: clip` on `.landing` is the belt, this is
     the braces -- a glow that needed clipping would be a glow cut off by a
     straight line down the side of the page. */
  width: 112%;
  height: 190%;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgb(77 144 248 / 0.45), rgb(139 92 246 / 0.22) 55%, transparent 78%);
  filter: blur(20px);
  animation: aura 6s ease-in-out infinite;
}
/* Sized with `.auth__brand` and `.guide__brand`, in one rule up in the auth
   block, so the three pages that carry the lockup cannot drift apart. This
   only takes back the centring, which the frame above now owns. */
.landing__logo { position: relative; margin: 0 auto; }
/* The hero's eyebrow is a caption under the mark rather than a label over the
   page, so it sits close to the thing it captions -- the generous gap in this
   band is the one *above* it, between the lockup and everything else. */
.landing__hero .landing__eyebrow { margin-bottom: var(--s3); }

.landing__headline {
  margin: 0 0 var(--s5);
  /* 36px on a phone, 56px from tablet width up -- stated as rem so it still
     answers a reader who has changed their browser's text size. The tracking
     is the compression the size is for: at 56px the default letter-fit reads
     as a document's heading rather than as a claim. */
  font-size: 2.25rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.08;
  color: var(--text);
}
.landing__lede {
  /* 600px rather than the band's full width: a subhead set to the measure of a
     three-column grid is a subhead nobody finishes. */
  max-width: 37.5rem;
  margin: 0 auto;
  font-size: 1.125rem;
  line-height: 1.6;
  color: var(--muted);
}
/* The gap before the buttons is the largest on the page and is meant to be:
   everything above it is the argument and everything below it is the door. */
.landing__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s4);
  margin: 3rem 0 0;
}
/* The qualifier under the buttons, and the one under the sample figures. */
.landing__note {
  max-width: 34rem;
  margin: var(--s5) auto 0;
  font-size: var(--fs-sm);
  line-height: 1.55;
  color: var(--muted);
}

/* The call to action.
 *
 * Three layers, and the order of them is the whole trick. The button keeps the
 * filled weight's own gradient, because a label sits on it and `--on-accent`
 * only clears 4.5:1 against the accent and lighter -- the animated ramp passes
 * through an indigo it would fail against. The ramp is therefore a *ring*
 * around the fill, masked to the border area so it never lands under the
 * label, and the bloom is a blurred copy of the same ramp behind the whole
 * button.
 *
 * The mask is the standard two-layer exclusion: one gradient over the padding
 * box, one over the border box, composited so only the difference between them
 * -- the ring -- is painted. A browser without `mask-composite` paints the
 * ramp as a solid rectangle behind the fill, which is invisible, so the
 * failure mode is a button that simply has no ring. */
.landing__cta {
  position: relative;
  padding: 1rem 2.5rem;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  border-radius: var(--radius);
  border-color: transparent;
}
.landing__cta > span { position: relative; }
.landing__cta::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 2px;
  border-radius: var(--radius);
  background: var(--accent-ramp);
  background-size: 300% 100%;
  animation: ramp 6s linear infinite;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
/* The bloom. Behind the button's own background, which is what `z-index: -1`
   buys here: the parent sets no stacking context of its own, so this paints
   under the fill and is only visible where it extends past it. */
.landing__cta::after {
  content: "";
  position: absolute;
  inset: -12px;
  z-index: -1;
  border-radius: var(--radius-lg);
  background: var(--accent-ramp);
  background-size: 300% 100%;
  animation: ramp 6s linear infinite;
  filter: blur(16px);
  /* The resting value, and the one the reduced-motion reader is left holding
     when the pulse below is switched off: the middle of the range rather than
     either end of it. */
  opacity: 0.45;
  /* The pulse. Two animations on one element -- the ramp walks the gradient
     along the box and this one breathes the whole thing -- which is only
     possible because they drive different properties. The range is a pair of
     custom properties so the hover can open it without naming a second set of
     keyframes and restarting the cycle. */
  --bloom-lo: 0.3;
  --bloom-hi: 0.6;
  animation: ramp 6s linear infinite, bloom 2s ease-in-out infinite;
  pointer-events: none;
}
@media (hover: hover) {
  /* The colours shift faster and the bloom opens up. Not a new animation --
     the same one, wound on -- so a pointer arriving mid-cycle does not restart
     the ramp from blue, and the bloom brightens from wherever in its breath it
     happened to be. */
  .landing__cta:hover::before { animation-duration: 2.5s; }
  .landing__cta:hover::after {
    animation-duration: 2.5s, 2s;
    --bloom-lo: 0.5;
    --bloom-hi: 0.85;
  }
}
/* The second action is a link that has been given a button's target. It must
   not read as a second primary: no fill, no ring, no bloom. */
.landing__ghost {
  padding: 1rem 2.5rem;
  font-size: 1rem;
  border-radius: var(--radius);
  background: var(--sheen), var(--surface-2);
  border-color: var(--border-strong);
  color: var(--text);
}

/* -- the stats bar. Frosted, and sitting in the seam between the hero and what
   follows it: it summarises the claim above rather than introducing the
   sections below, so it is given a band of its own and no heading. */
.landing__stats { position: relative; padding: 3.5rem 0 0; }

.statbar {
  position: relative;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  margin: 0;
  padding: var(--s3) var(--s2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  /* A wash across the whole bar rather than four tinted cells: the strip is
     one object and the figures in it are one reading. Opaque enough to read on
     its own; the blur below is the enhancement. */
  background:
    linear-gradient(180deg, rgb(77 144 248 / 0.1), rgb(139 92 246 / 0.04) 60%, transparent),
    var(--sheen), var(--glass);
  box-shadow: var(--glow);
}
@supports ((-webkit-backdrop-filter: blur(14px)) or (backdrop-filter: blur(14px))) {
  .statbar {
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
  }
}
/* The two rules the strip is set between, drawn 16px clear of it rather than
   on its own border: a border is the edge of an object and these are the edge
   of the *band*, which is why they run the full measure and the bar's own
   border does not. One pseudo-element and two background layers, pinned to the
   top and bottom of a box that overhangs the bar by 16px either side. */
.statbar::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -16px;
  bottom: -16px;
  pointer-events: none;
  opacity: 0.45;
  background:
    linear-gradient(90deg, transparent, var(--accent), transparent),
    linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 100% 1px, 100% 1px;
  background-position: 0 0, 0 100%;
  background-repeat: no-repeat;
}
/* The scan. A soft highlight the width of two cells, swept across the strip
   once and then parked off the right-hand edge for twice as long again -- the
   rest is the whole difference between an instrument and a novelty. Clipped by
   its own `border-radius` rather than by `overflow` on the bar: the pseudo is
   the same box, so its background stops at the same rounded corners and the
   bar keeps its halo. */
.statbar::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  pointer-events: none;
  background: linear-gradient(100deg,
    transparent,
    rgb(255 255 255 / 0.05) 42%,
    rgb(173 209 255 / 0.11) 50%,
    rgb(255 255 255 / 0.05) 58%,
    transparent);
  background-size: 42% 100%;
  background-repeat: no-repeat;
  background-position: -80% 0;
  animation: scan 9s linear infinite;
}

/* Value above label, said with `column-reverse` rather than by writing the
   `<dd>` first: a description list whose term follows its description is not
   one, and the order that is read out is the order in the markup. */
.statbar__cell {
  display: flex;
  flex-direction: column-reverse;
  align-items: center;
  gap: var(--s1);
  padding: var(--s3) var(--s2);
  text-align: center;
}
/* The hairlines between the figures -- the `·` of the line this section is,
   drawn rather than typed, so it lands between the cells at every width and
   never at the end of a row. */
.statbar__cell:nth-child(even) { border-left: 1px solid var(--border-strong); }
.statbar dt {
  margin: 0;
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
}
.statbar dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.03em;
  color: var(--text);
  /* Lit rather than gradient-filled. A figure painted with `background-clip:
     text` is a figure whose contrast varies along its own width, and these
     four are the page's headline numbers. */
  text-shadow: var(--glow-text-accent);
}

/* -- the bands below the fold. Each gets its own light, from a different
   corner, so scrolling the page moves through it rather than past a repeat. */
.landing__section { position: relative; padding: 4.5rem 0; }
.landing__section::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
}
/* The seam. A rule that is brightest in the middle and gone at both ends,
   drawn on the section rather than between two of them so a band added or
   moved carries its own -- and a gradient rather than a border, which is the
   one thing a border cannot be. Inset to the measure so it reads as the end of
   a section and not as a line ruled across the window. */
.landing__section::after {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: min(64rem, 100% - 2rem);
  height: 1px;
  transform: translateX(-50%);
  pointer-events: none;
  opacity: 0.5;
  /* Two layers: the seam itself, and a short bright spot that walks along it
     once every thirty seconds. The spot is a second background sized to a
     fifth of the line and moved by `background-position`, which is composited
     -- a 1px element cannot be given a moving child, and animating a gradient
     stop is not a thing a browser will do.

     Parked at `-30%` in the rule as well as in the first keyframe, so the
     reader who has asked for no motion gets the seam and no spot rather than a
     bright mark frozen a third of the way along a line for no reason. */
  background:
    linear-gradient(90deg, transparent, rgb(190 218 255 / 0.95) 45%, #ffffff 50%,
                    rgb(190 218 255 / 0.95) 55%, transparent),
    linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 20% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  background-position: -30% 0, 0 0;
  animation: beam 30s linear infinite;
}
/* Staggered, so the five beams are never travelling together. Negative delays
   rather than positive ones: the page opens with them already spread along
   their lines instead of with all five queued at the left-hand edge. */
.landing__section--how::after     { animation-delay: -6s; }
.landing__section--proof::after   { animation-delay: -12s; }
.landing__section--pricing::after { animation-delay: -18s; }
.landing__section--faq::after     { animation-delay: -24s; }
/* Except the first, which has none. The stats bar above it now draws its own
   rule 16px under the strip, and that rule is the boundary between the two
   bands; a second seam twenty pixels below the first is two lines where the
   page means one. */
.landing__section--features::after { content: none; }

.landing__section--features::before {
  background: radial-gradient(40rem 26rem at 82% 0%, rgb(77 144 248 / 0.09), transparent 68%);
}
/* The even bands take a diagonal ruling over their light, and the odd ones do
   not. That is the whole of the rhythm: scrolling this page alternates between
   a band that is lit and a band that is lit *and* woven, so each one arrives as
   a different room rather than as more of the last one. One per cent and a bit
   of white on a 1px line every 14px -- about a third of a level per channel
   averaged over the weave, which is why it needs no mask at the band edges:
   there is no edge to see. */
.landing__section--how::before {
  background:
    repeating-linear-gradient(45deg, rgb(255 255 255 / 0.02) 0 1px, transparent 1px 14px),
    radial-gradient(40rem 26rem at 14% 50%, rgb(139 92 246 / 0.09), transparent 68%);
}
.landing__section--proof::before {
  background:
    radial-gradient(44rem 26rem at 50% 0%, rgb(34 197 94 / 0.07), transparent 66%),
    radial-gradient(30rem 20rem at 88% 90%, rgb(77 144 248 / 0.08), transparent 68%);
}
.landing__section--pricing::before {
  background:
    repeating-linear-gradient(45deg, rgb(255 255 255 / 0.02) 0 1px, transparent 1px 14px),
    radial-gradient(46rem 28rem at 50% 8%, rgb(77 144 248 / 0.11), transparent 66%);
}
.landing__section--faq::before {
  background: radial-gradient(36rem 24rem at 88% 12%, rgb(139 92 246 / 0.07), transparent 68%);
}

.landing__title {
  margin: 0 0 3rem;
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.12;
  text-align: center;
}
.landing__section .landing__eyebrow { text-align: center; }
/* The sentence under a section title, before the thing the section is. */
.landing__standfirst {
  max-width: 44rem;
  margin: -2rem auto 3rem;
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--muted);
  text-align: center;
}

/* -- the feature cards. `.card` already carries the frosted fill, the lit top
   edge and the lift; these are the same panels at a larger radius, with the
   edge turned into the animated ramp and an accent wash added inside. */
.landing__grid { display: grid; grid-template-columns: 1fr; gap: var(--s4); }
.landing__grid > .card {
  margin-bottom: 0;
  padding: var(--s5);
  border-radius: var(--radius-lg);
  box-shadow: inset 0 0 40px rgb(77 144 248 / 0.05);
}
.landing__grid > .card::before {
  top: 0;
  left: 14px;
  right: 14px;
  height: 2px;
  border-radius: 0 0 var(--radius-xs) var(--radius-xs);
  background: var(--accent-ramp);
  background-size: 300% 100%;
  opacity: 0.35;
  transition: opacity 0.2s ease;
}
@media (hover: hover) {
  .landing__grid > .card:hover {
    border-color: var(--accent-line);
    box-shadow: inset 0 0 44px rgb(77 144 248 / 0.12), var(--glow-lit);
  }
  .landing__grid > .card:hover::before { opacity: 1; animation: ramp 4s linear infinite; }
  .landing__grid > .card:hover .landing__icon {
    filter: drop-shadow(0 0 16px rgb(77 144 248 / 0.6));
    background: rgb(77 144 248 / 0.18);
    border-color: var(--accent);
  }
}

/* -- what a card emanates.
 *
 * A disc half again the size of the card, centred on it, coming up under the
 * pointer. `.card` already lights *inside* -- the inset wash above goes from
 * 0.05 to 0.12 on hover -- and this is the other half of that reading: light
 * getting out.
 *
 * Shaped as a ring rather than as a blob, and that is not a style choice. Both
 * of these panels are frosted, and `backdrop-filter` establishes a stacking
 * context; so does the two-pixel lift the pricing column takes on hover. Inside
 * one, a `z-index: -1` child paints *above* its own element's background
 * instead of behind it, which is the one thing this effect must not do. A
 * gradient that is transparent across the card's own footprint and only starts
 * at its edge cannot wash the panel it is lighting, on a browser that puts it
 * behind or on a browser that does not.
 *
 * The stops are in the pseudo-element's own half-width: the card occupies the
 * middle two thirds of a box inset by -25%, so its edge is at 0.67 and the
 * light runs from 0.52 -- just inside the border, where a rim belongs -- out to
 * the corner. */
.landing__grid > .card::after,
.tier::after {
  content: "";
  position: absolute;
  inset: -25%;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
  background: radial-gradient(50% 50% at 50% 50%,
    transparent 0 52%, rgb(77 144 248 / 0.1) 70%, transparent 100%);
}
@media (hover: hover) {
  .landing__grid > .card:hover::after,
  .tier:hover::after { opacity: 1; }
}

/* The icons. Inline SVG taking `currentColor`, so one file answers both
   themes and lights with the card it is on -- and set in a disc of its own
   accent, because a 26px line drawing at the top of a 24rem card is a mark the
   eye skips on its way to the heading. The disc is the padding box: the glyph
   is still drawn at 24px inside a 48px target, so nothing about the artwork
   changes and no file is touched. */
.landing__icon {
  display: block;
  width: 48px;
  height: 48px;
  padding: 11px;
  margin: 0 0 var(--s5);
  border: 1px solid var(--accent-line);
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  filter: drop-shadow(0 0 12px rgb(77 144 248 / 0.35));
  transition: filter 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}

/* A feature card's heading is a heading, not a panel label. Written to outrank
   `.card > h3` on specificity rather than to tie with it and win on source
   order -- the same correction `h2.guide__heading` makes, and the reason the
   eyebrow's `text-transform` and tracking are undone here explicitly: an
   inherited rule you did not mean to inherit is not turned off by silence. */
.landing__grid .card h3, .landing__steps > li h3 {
  margin: 0 0 var(--s3);
  font-size: 1.125rem;
  font-weight: 600;
  text-transform: none;
  letter-spacing: -0.01em;
  color: var(--text);
}
.landing__grid .card p, .landing__steps > li p { margin: 0; line-height: 1.6; }

/* -- the steps. Numbered by the list rather than by the markup, so a step
   inserted in the middle does not need three edits to stay counted -- and
   joined by a line the steps draw themselves, so it cannot be left pointing at
   a step that is no longer there. */
.landing__steps {
  counter-reset: step;
  list-style: none;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s5);
  margin: 0;
  padding: 0;
}
.landing__steps > li {
  position: relative;
  counter-increment: step;
  padding: var(--s5);
  padding-top: 4.25rem;
  background: var(--sheen), var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
}
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .landing__steps > li {
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
/* The figure. Large enough to be the thing the eye lands on, which is what
   makes the three cards read as a sequence rather than as a grid of three. */
.landing__steps > li::before {
  content: counter(step);
  position: absolute;
  top: var(--s4);
  left: var(--s5);
  font-family: var(--mono);
  font-size: 2.25rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--accent);
  /* Lit with a `filter` rather than a `text-shadow`, and the gradient fill
     below is the reason. `background-clip: text` paints the fill between the
     shadow and the glyph, so a text-shadow on this figure would be a glow
     drawn *over* the gradient it is meant to be behind. A drop-shadow is taken
     from the rendered shape, which is the gradient, so it lights the number
     that is actually on the page. Two of them, blue then violet, matching the
     two ends of the fill -- and no offset in either, which is the same halo
     rather than smudge bargain the rest of this sheet makes. */
  filter:
    drop-shadow(0 0 14px rgb(77 144 248 / 0.5))
    drop-shadow(0 0 30px rgb(139 92 246 / 0.32));
}
/* Filled with the gradient where the browser can paint text with one, and left
   as the flat accent where it cannot. Gated rather than assumed: `color:
   transparent` with no clip is a step number that is not there. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .landing__steps > li::before {
    background: var(--accent-grad);
    -webkit-background-clip: text;
            background-clip: text;
    color: transparent;
  }
}
/* The join. Vertical on a phone, where the cards are stacked, and horizontal
   from tablet width up -- see the wide-screen block. Drawn in the gap between
   two cards, and never after the last one. */
.landing__steps > li::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  width: 2px;
  height: var(--s5);
  transform: translateX(-50%);
  background: linear-gradient(180deg, var(--accent), rgb(139 92 246 / 0.1));
}
.landing__steps > li:last-child::after { content: none; }

/* -- the sample report. The tiles are the loudest thing on the page and are
   meant to be; what keeps that honest is the badge in the heading and the note
   under them, not a quieter treatment. */
.badge--sample {
  vertical-align: middle;
  margin-left: var(--s2);
  border-color: var(--border-strong);
  color: var(--muted);
}

.proof {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s3);
  margin: 0;
}
.proof__cell {
  position: relative;
  padding: var(--s4);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--sheen), var(--glass);
  box-shadow: inset 0 0 30px rgb(77 144 248 / 0.05);
  min-width: 0;
}
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .proof__cell {
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
.proof__cell dt {
  margin: 0 0 var(--s2);
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.proof__cell dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text);
  /* Same figure, same wrapping bargain as the dashboard's tiles: a money value
     has no break opportunity in it, and a tile it cannot fit in is a page that
     scrolls sideways. */
  min-width: 0;
  overflow-wrap: anywhere;
  text-shadow: var(--glow-text-accent);
}
.proof__cell dd.pos { color: var(--ok); text-shadow: var(--glow-text-ok); }
.proof__cell dd.neg { color: var(--danger); text-shadow: var(--glow-text-danger); }
/* The headline figure of the report, across the full row and at twice the
   size, with its own lit edge. */
.proof__cell--hero { grid-column: 1 / -1; }
.proof__cell--hero dd { font-size: var(--fs-metric); }
/* The one figure on the page that gets the neon rank. Scoped to the hero tile
   rather than to `.pos`, so the five readings underneath it keep the single
   halo: the point of the treatment is that this number is the headline, and a
   headline every tile is wearing is not one. */
.proof__cell--hero dd.pos { text-shadow: var(--glow-text-ok-neon); }
.proof__cell--hero::before {
  content: "";
  position: absolute;
  top: 0;
  left: 14px;
  right: 14px;
  height: 2px;
  border-radius: 0 0 var(--radius-xs) var(--radius-xs);
  background: linear-gradient(90deg, transparent, var(--ok), transparent);
}

/* -- the plans. Three columns on a wide screen, one on a phone, and the order
   in the markup is the order they stack in -- cheapest first, so the column a
   reader lands on is the one that costs nothing.

   `align-items: start` rather than stretch: the tiers do not carry the same
   number of features and a short column padded out to match a tall one is
   whitespace pretending to be a feature. The button is still flush with the
   bottom of its own card, which is what `margin-top: auto` below is for. */
.landing__tiers {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s4);
  align-items: start;
}

.tier {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--s5);
  background: var(--sheen), var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .tier {
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
@media (hover: hover) {
  .tier:hover { transform: translateY(-2px); border-color: var(--accent-line); box-shadow: var(--glow); }
}
/* The recommendation, and it is given every channel the page has: the surface
   above a card's, a wash of the brand across the top of it, the accent edge,
   the halo, a ring that moves -- and, once there is a row to be raised out of,
   two per cent of scale. The scale is in the wide-screen block and nowhere
   else: stacked on a phone there is nothing beside it to be larger than, and a
   column scaled inside a one-column grid is only a column with less room.

   The ring is the same masked two-layer trick the call to action uses, at 1px
   -- so the featured column has an accent edge on a browser that cannot
   composite masks too, by way of the halo below it. */
.tier--featured {
  border-color: var(--accent-line);
  box-shadow: var(--glow);
  background:
    linear-gradient(180deg, rgb(77 144 248 / 0.13), rgb(139 92 246 / 0.05) 55%, transparent),
    var(--sheen-strong), var(--surface-2);
}
.tier--featured::before {
  content: "";
  position: absolute;
  inset: 0;
  padding: 1px;
  border-radius: var(--radius-lg);
  background: var(--accent-ramp);
  background-size: 300% 100%;
  animation: ramp 8s linear infinite;
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
@media (hover: hover) {
  .tier--featured:hover { box-shadow: var(--glow-lit); }
}

.tier__head {
  padding-bottom: var(--s4);
  margin-bottom: var(--s4);
  border-bottom: 1px solid var(--border-strong);
}
.tier__flag { margin: 0 0 var(--s2); }
.badge--popular {
  border-color: transparent;
  /* The button's ramp, not the decorative one. A label sits on this fill, and
     `--on-accent` clears 4.5:1 against the accent and against anything lighter
     than it -- which the decorative ramp's indigo and violet are not. The one
     flag on the page is not worth an unreadable word. */
  background: var(--btn-grad);
  color: var(--on-accent);
  font-weight: 700;
  box-shadow: 0 0 16px rgb(77 144 248 / 0.5);
}
.tier__name {
  margin: 0 0 var(--s3);
  font-size: 1.125rem;
  font-weight: 600;
  /* `.card > h3` is the panel eyebrow -- tracked, uppercase, muted. A plan
     name is a name, so both are turned off here rather than left to silence,
     the same correction the feature cards make above. */
  text-transform: none;
  letter-spacing: -0.01em;
  color: var(--text);
}
/* The price is the figure on this card, so it is set at the figure size the
   rest of the product uses for one -- and in the same face, because a price is
   a number read against two other numbers in the row. */
.tier__price {
  display: flex;
  align-items: baseline;
  gap: var(--s1);
  margin: 0;
}
.tier__amount {
  font-family: var(--mono);
  /* 48px. The price is the one figure on this card and the only thing on the
     page set larger than a section heading. */
  font-size: 3rem;
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.03em;
  color: var(--text);
}
.tier--featured .tier__amount { text-shadow: var(--glow-text-accent); }
.tier__period { font-size: var(--fs-sm); color: var(--muted); }

.tier__features {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--fs-body);
  line-height: 1.6;
  color: var(--muted);
}
.tier__features > li {
  position: relative;
  padding-left: var(--s5);
}
.tier__features > li + li { margin-top: var(--s3); }
/* The tick is decoration: every line in this list is included, so a mark that
   never varies carries no information and is hidden from the reader who is
   being read to. */
.tier__features > li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--accent);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-shadow: 0 0 10px rgb(77 144 248 / 0.6);
}

/* Not shipped yet. Deliberately the quiet badge rather than a warn colour:
   this is a date, not a fault, and three of these in accent would outshout the
   one thing on the card that is meant to stand out. `baseline` rather than the
   badge's default so it sits on the line of the text it qualifies instead of
   riding above it. */
.badge--soon {
  margin-left: var(--s1);
  vertical-align: baseline;
  color: var(--muted);
  border-color: var(--border-strong);
}

/* Pushed to the foot of its own card, so three cards of different heights
   still put their buttons on their own bottom edge. */
.tier__actions { margin: auto 0 0; padding-top: var(--s5); }

/* -- the questions. The dashboard's own accordion, on the page that sells the
   dashboard, so the two cannot drift into looking like different products. */
.landing__section--faq .accordion {
  margin-bottom: var(--s3);
  border-color: var(--glass-line);
  border-radius: var(--radius);
  background: var(--sheen), var(--glass);
}
@supports ((-webkit-backdrop-filter: blur(12px)) or (backdrop-filter: blur(12px))) {
  .landing__section--faq .accordion {
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
  }
}
.landing__section--faq .accordion__summary {
  padding: var(--s3) var(--s4);
  font-size: 1rem;
  border-radius: var(--radius);
}
.landing__section--faq .accordion[open] > .accordion__summary {
  border-bottom-color: var(--glass-line);
  border-radius: var(--radius) var(--radius) 0 0;
}
/* The open question is marked by its caret rather than by its words. Accent
   text on a frosted panel measures 4.2:1 at the top of the sheen -- under the
   floor, and the top of the panel is exactly where this line sits. The caret
   is a shape next to a heading that has not moved, so it can carry the colour
   the heading cannot. */
.landing__section--faq .accordion[open] > .accordion__summary::after {
  background: var(--accent);
}
.landing__section--faq .accordion__body { padding: 0 var(--s4) var(--s4); }
.landing__section--faq .accordion__body p { max-width: 62ch; }
@media (hover: hover) {
  .landing__section--faq .accordion:hover { border-color: var(--accent-line); }
}

/* -- the foot. The separator is a gradient rule rather than a border, which is
   the one thing a border cannot be. */
.landing__foot {
  position: relative;
  margin-top: 0;
  padding: 4.5rem 0 3rem;
  text-align: center;
}
.landing__foot::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  /* The same beam the section seams carry, on the last rule of the page. */
  background:
    linear-gradient(90deg, transparent, rgb(190 218 255 / 0.95) 45%, #ffffff 50%,
                    rgb(190 218 255 / 0.95) 55%, transparent),
    linear-gradient(90deg, transparent, var(--accent), transparent);
  background-size: 20% 100%, 100% 100%;
  background-repeat: no-repeat, no-repeat;
  background-position: -30% 0, 0 0;
  animation: beam 30s linear infinite -3s;
  opacity: 0.55;
}
.landing__foot-mark {
  display: block;
  width: 40px;
  height: 40px;
  margin: 0 auto var(--s3);
  opacity: 0.85;
  filter: drop-shadow(0 0 10px rgb(77 144 248 / 0.35));
}
.landing__wordmark { margin: 0 0 var(--s2); font-size: var(--fs-sm); color: var(--muted); }
.landing__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s3);
  margin: 0;
}
/* A footer link is read with a thumb as often as with a mouse, and an inline
   anchor is 17px tall whatever its font size. */
.landing__links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 var(--s2);
  font-size: var(--fs-sm);
}
/* The risk warning. Quiet, and deliberately not hidden behind a link: it
   qualifies the figures three sections above it, and a disclaimer a reader has
   to ask for is one they will not read. */
.landing__disclaimer {
  max-width: 52rem;
  margin: var(--s4) auto 0;
  font-size: var(--fs-xs);
  line-height: 1.6;
  color: var(--muted);
}

/* -- what a wide screen adds. */
@media (min-width: 48rem) {
  .landing__inner { padding: 0 var(--s5); }
  /* The band is the screen. Everything in it is centred in that height rather
     than stacked from the top of it, which is what buys the gaps above without
     any of them having to be typed as a margin. */
  .landing__hero { min-height: 100vh; padding: 6rem 0 5rem; }
  .landing__aura { max-width: 400px; margin-bottom: 3.5rem; }
  /* 56px, the size the claim is made at. */
  .landing__headline { font-size: 3.5rem; }
  .landing__lede { font-size: 1.25rem; }
  .landing__actions { margin-top: 3.5rem; }
  /* 80px of clear air between the claim and the four figures under it. */
  .landing__stats { padding-top: 5rem; }
  /* 120px between bands. Each section is meant to arrive rather than to
     continue, and that is bought with space and a seam and nothing else. */
  .landing__section { padding: 7.5rem 0; }
  .landing__foot { padding: 6rem 0 3rem; }
  /* 36px. */
  .landing__title { font-size: 2.25rem; }

  .statbar { grid-template-columns: repeat(4, 1fr); }
  .statbar__cell + .statbar__cell { border-left: 1px solid var(--border-strong); }
  /* 36px, which is the size the four claims are made at. */
  .statbar dd { font-size: 2.25rem; }

  /* Three across and two down, stated rather than auto-fit: six cards are a
     3x2 grid, and a fourth column at 1600px would leave two cards alone on a
     second row. */
  .landing__grid { grid-template-columns: repeat(3, 1fr); }

  .landing__steps { grid-template-columns: repeat(3, 1fr); gap: var(--s5); }
  /* The join turns to run along the row, drawn in the gap between two cards
     and level with the figures it connects. */
  .landing__steps > li::after {
    top: 3rem;
    left: 100%;
    width: var(--s5);
    height: 2px;
    transform: none;
    background: linear-gradient(90deg, var(--accent), rgb(139 92 246 / 0.15));
  }

  /* The cockpit strip: the headline figure across the top, the five readings
     that qualify it in one row under it. */
  .proof { grid-template-columns: repeat(5, 1fr); }
  .proof__cell--hero dd { font-size: 2.25rem; }

  /* Three explicit columns rather than auto-fit: these three are compared with
     each other and a tier that wrapped onto its own row would be compared with
     nothing. They hold at 48rem because a 15rem column still fits three across
     there, which is the same floor the feature grid is built on. */
  .landing__tiers { grid-template-columns: repeat(3, 1fr); }
  /* Raised out of the row, now that there is a row. Two per cent: enough that
     the eye lands on the middle column first, and little enough that the three
     prices are still read across a line. */
  .tier--featured { transform: scale(1.02); }
}

/* The featured column keeps its scale while it answers the pointer, or the
   hover would put it back in line with its neighbours -- which is the one
   thing the scale exists to stop. */
@media (min-width: 48rem) and (hover: hover) {
  .tier--featured:hover { transform: scale(1.02) translateY(-2px); }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}
