/* ══════════════════════════════════════════════════════════════════════════
   MetaTrader Control — "Deep Signal" theme
   Near-black neutral base, electric teal + violet accents. Every surface is a
   visibly different lightness from the one behind it, and every border is
   drawn — no element is allowed to blend into its container.

   The ground used to be ink *blue*, several rungs of it, with two coloured
   blooms across the top. It photographed well and it was tiring to read for an
   hour: a saturated mid-blue behind white text is a low-contrast pairing the
   eye keeps trying to focus, and the journal is a page people sit on all day.
   The blue is now a hint in the neutrals rather than the colour of the page,
   the text is white outright, and the accents do the colouring — which is what
   accents are for.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  /* Ground */
  --bg:         #06080C;
  /* Barely there. Enough to stop a flat black page looking dead under the
     sidebar, nowhere near enough to tint the type sitting on it. */
  --bg-grad:    radial-gradient(1200px 600px at 12% -10%, #0E1526 0%, transparent 62%),
                radial-gradient(900px 500px at 95% 0%, #141020 0%, transparent 58%),
                #06080C;
  --surface:    #0C0F16;
  --surface-2:  #11151E;
  --surface-3:  #191E2A;

  /* Lines — always visible, never guessed at */
  --border:     #242B39;
  --border-hi:  #3C4658;

  /* Type — three rungs, each one comfortably readable on --surface.
     Nothing in the UI is allowed to sit below --mute. Neutral rather than
     blue-tinted: on a near-black ground a blue-white reads as dimmer than it
     measures, and this is a screen that gets stared at. */
  --text:       #FFFFFF;
  --dim:        #D8DDE7;
  --mute:       #A6AEBE;

  /* Type scale — one place to nudge the whole app up or down. */
  --fs-body:    16px;
  --fs-sm:      14.5px;
  --fs-label:   11.5px;   /* uppercase micro-labels */
  --fw-body:    500;
  --fw-med:     600;
  --fw-bold:    700;

  /* Accents */
  --teal:       #00E5D0;
  --violet:     #9B6BFF;
  --pink:       #FF4D9D;
  --lime:       #B8F53C;
  --amber:      #FFB627;
  /* Money colours. Bright enough to carry a number at 14px on a near-black
     ground and read as a *result* rather than as decoration, and deliberately
     short of the primaries: #00FF00 and #FF0000 on this background are the
     pair that make an hour on the page hurt. Both clear 5:1 against --bg. */
  --up:         #00F5A0;
  --down:       #FF3D5F;
  --info:       #4D93FF;

  --accent-grad: linear-gradient(100deg, var(--teal) 0%, var(--violet) 100%);

  --radius:     12px;
  --radius-sm:  8px;
  --shadow:     0 10px 30px rgba(0, 0, 0, .45);
  --glow-teal:  0 0 0 1px color-mix(in srgb, var(--teal) 35%, transparent), 0 0 22px color-mix(in srgb, var(--teal) 18%, transparent);

  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;

  /* How tall a rows-scroll-inside-it box gets, before anything measures.
     freeze.js overwrites this per box with the height that actually makes the
     card end at the fold — this value cannot know how tall the pager under it
     is, and getting that wrong is what left a short page-scroll between the end
     of the rows and the pager.

     Kept, and kept sane, because it is what a box gets with the script blocked:
     one number for the trades table and the journal both, since they sit under
     the same filter bar and the same card head. `max()` rather than a bare
     calc — on a short window the subtraction goes small enough to show two
     rows, and a box scrolled two rows at a time is worse than no frozen header
     at all. */
  --freeze-h: max(320px, calc(100vh - 250px));
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background: var(--bg-grad);
  background-attachment: fixed;
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-body);
  font-weight: var(--fw-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

a { color: var(--teal); text-decoration: none; font-weight: var(--fw-med); }
a:hover { color: #6BFFF0; text-decoration: underline; }

strong, b { font-weight: var(--fw-bold); }

h1, h2, h3 { margin: 0 0 .6rem; font-weight: var(--fw-bold); letter-spacing: -.01em; color: var(--text); }
h1 { font-size: 1.6rem; }
h2 { font-size: 1.2rem; }
h3 { font-size: .95rem; color: var(--dim); text-transform: uppercase; letter-spacing: .08em; font-weight: 800; }

/* Inline SVG icons. Stroke-driven so they inherit whatever colour the
   surrounding text has — no icon ever drifts away from its label. */
.ico {
  width: 18px; height: 18px; flex: 0 0 auto;
  fill: none; stroke: currentColor;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.ico.sm { width: 14px; height: 14px; stroke-width: 2.2; }
.ico.xs { width: 11px; height: 11px; stroke-width: 2.4; }

code, .mono, .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ── Layout ──────────────────────────────────────────────────────────────── */

.shell { display: flex; min-height: 100vh; }

.sidebar {
  width: 232px;
  flex: 0 0 232px;
  background: rgba(9, 13, 26, .92);
  border-right: 1px solid var(--border);
  padding: 18px 14px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overflow-x: hidden;
  transition: width .18s ease, flex-basis .18s ease, padding .18s ease;
}

.brand {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 6px 18px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 16px;
}
.brand-mark {
  width: 32px; height: 32px; border-radius: 9px;
  background: var(--accent-grad);
  display: grid; place-items: center;
  font-weight: 800; color: #05121A; font-size: 14px;
  box-shadow: var(--glow-teal);
  flex: 0 0 32px;
}
.brand-name { font-weight: 800; letter-spacing: -.02em; font-size: 15px; color: var(--text); }
.brand-sub {
  font-size: 9.5px; color: var(--mute); text-transform: uppercase;
  letter-spacing: .14em; font-weight: 700;
}

/* ── Nav: section label vs. clickable item ───────────────────────────────────
   The two must never read alike. Labels are small, spaced-out, capped, muted,
   and carry a hairline rule to the right edge. Items are large, solid-weight,
   boxed on hover, and the only things with an icon that lights up.          */

.navgroup { margin-bottom: 20px; }
.navgroup > .navlabel {
  display: flex; align-items: center; gap: 7px;
  padding: 0 8px 9px;
  font-size: 10px; text-transform: uppercase; letter-spacing: .2em;
  font-weight: 800; color: var(--mute);
  user-select: none; cursor: default;
}
.navgroup > .navlabel .ico { color: var(--violet); opacity: .9; }
.navgroup > .navlabel::after {
  content: ""; flex: 1; height: 1px; min-width: 8px;
  background: linear-gradient(90deg, var(--border) 0%, transparent 100%);
}

.navlink {
  position: relative;
  display: flex; align-items: center;
  gap: 11px; padding: 9px 11px; margin-bottom: 3px;
  border-radius: var(--radius-sm);
  color: var(--dim);
  font-size: 14.5px; font-weight: var(--fw-med); letter-spacing: -.005em;
  border: 1px solid transparent;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.navlink .navtext { flex: 1; min-width: 0; }
.navlink .ico { color: var(--mute); transition: color .14s ease; }
.navlink:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.navlink:hover .ico { color: var(--teal); }
.navlink.active {
  background: linear-gradient(90deg, color-mix(in srgb, var(--teal) 16%, transparent), color-mix(in srgb, var(--violet) 11%, transparent));
  border-color: var(--border-hi);
  color: #FFFFFF; font-weight: var(--fw-bold);
}
.navlink.active .ico { color: var(--teal); }
.navlink.active::before {
  content: ""; position: absolute; left: -1px; top: 8px; bottom: 8px;
  width: 3px; border-radius: 3px; background: var(--accent-grad);
}
.navlink .pill { font-size: 10.5px; font-weight: 700; }

/* Collapsed rail. The class lands on <html> from an inline script in <head>,
   so a collapsed sidebar never flashes open on load. */
.nav-toggle {
  display: inline-grid; place-items: center;
  width: 34px; height: 34px; flex: 0 0 34px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--dim); cursor: pointer;
  transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.nav-toggle:hover { color: var(--text); border-color: var(--border-hi); background: var(--surface-2); }

html.nav-collapsed .sidebar { width: 70px; flex: 0 0 70px; padding: 18px 11px; }
html.nav-collapsed .brand { justify-content: center; padding: 4px 0 18px; }
html.nav-collapsed .brand-text,
html.nav-collapsed .navlink .navtext,
html.nav-collapsed .navlabel-text { display: none; }
html.nav-collapsed .navgroup > .navlabel {
  justify-content: center; gap: 0;
  padding: 0 0 9px; margin: 0 6px 8px;
  border-bottom: 1px solid var(--border);
}
html.nav-collapsed .navgroup > .navlabel::after { display: none; }
html.nav-collapsed .navlink { justify-content: center; gap: 0; padding: 10px 0; }
html.nav-collapsed .navlink .pill {
  position: absolute; top: 0; right: 2px;
  padding: 0 4px; min-width: 16px; justify-content: center;
  font-size: 9px; line-height: 15px; letter-spacing: 0;
  box-shadow: 0 0 0 2px rgba(9, 13, 26, .92);
}
html.nav-collapsed .navlink.active::before { left: 2px; }

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

.topbar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 12px 22px;
  border-bottom: 1px solid var(--border);
  background: rgba(7, 10, 19, .75);
  backdrop-filter: blur(10px);
  position: sticky; top: 0; z-index: 20;
}
.topbar-left { display: flex; align-items: center; gap: 14px; min-width: 0; }
.topbar-title { font-size: 1.02rem; font-weight: var(--fw-bold); color: var(--text); letter-spacing: -.01em; }
.topbar .who { color: var(--dim); font-size: var(--fs-sm); font-weight: var(--fw-body); }

/* ── User menu ───────────────────────────────────────────────────────────── */

.usermenu { position: relative; }
.userbtn {
  display: flex; align-items: center; gap: 10px;
  padding: 5px 9px 5px 5px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text); font-family: inherit; cursor: pointer;
  transition: border-color .14s ease, background .14s ease;
}
.userbtn:hover { border-color: var(--border-hi); background: var(--surface-2); }
.userbtn[aria-expanded="true"] { border-color: var(--teal); background: var(--surface-2); }
.avatar {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent-grad); color: #05121A;
  font-size: 11.5px; font-weight: 800; letter-spacing: .02em;
}
.userbtn .uname {
  font-size: 13.5px; font-weight: var(--fw-bold); color: var(--text);
  max-width: 170px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.userbtn .chev { width: 15px; height: 15px; color: var(--mute); transition: transform .16s ease; }
.userbtn[aria-expanded="true"] .chev { transform: rotate(180deg); color: var(--teal); }

.usermenu-pop {
  position: absolute; right: 0; top: calc(100% + 9px);
  min-width: 236px; padding: 6px;
  background: var(--surface-2);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .6);
  z-index: 40;
}
.usermenu-pop[hidden] { display: none; }
.usermenu-pop .pop-head {
  padding: 9px 11px 11px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 6px;
}
.usermenu-pop .pop-name { font-size: 14px; font-weight: var(--fw-bold); color: var(--text); }
.usermenu-pop .pop-mail {
  font-size: 12.5px; color: var(--dim); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis;
}
.usermenu-pop .pop-role {
  display: inline-block; margin-top: 7px;
  font-size: 9.5px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase;
  color: var(--teal);
}
.popitem {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; border-radius: var(--radius-sm);
  color: var(--dim); font-size: 14px; font-weight: var(--fw-med);
}
.popitem:hover { background: var(--surface-3); color: var(--text); text-decoration: none; }
.popitem .ico { color: var(--mute); }
.popitem:hover .ico { color: var(--teal); }
.popitem.danger:hover { color: var(--down); background: color-mix(in srgb, var(--down) 14%, transparent); }
.popitem.danger:hover .ico { color: var(--down); }

.content { padding: 22px 24px 60px; flex: 1; }

/* A page with no shell around it — see the `bare` flag in base.html. It owns
   the viewport exactly: no page padding, no page scroll, and whatever is inside
   takes the height and does its own scrolling. The journal wall is the one
   screen built this way, and being unable to scroll the page is the point of
   it — the lines scroll, the furniture does not move. */
.content.bare { padding: 0; height: 100vh; overflow: hidden; }

/* ── Cards ───────────────────────────────────────────────────────────────── */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  margin-bottom: 18px;
  /* `clip` rather than `hidden`, and the difference matters exactly once: a
     card that clips with `hidden` becomes a scroll container, and a scroll
     container is what a `position: sticky` descendant sticks to — so the pager
     at the foot of a paged card had nothing to pin to but a box that never
     scrolls, and quietly did nothing. `clip` clips the same way, respects the
     radius the same way, and creates no such container, so the pager sticks to
     the screen. Nothing here has ever wanted to scroll a card itself.

     `hidden` first, for browsers without `clip` — they get exactly what they
     had before, which is a pager that scrolls with the page. */
  overflow: hidden;
  overflow: clip;
}
.card-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.card-head h2, .card-head h3 { margin: 0; }
/* A card that has to let something escape it — a dropdown drawn over the page
   rather than inside the box. Cards clip so the head's flat background follows
   the rounded corner; this one cannot clip, so the head is given the radius
   itself instead and comes out looking identical. */
.card.pop-out { overflow: visible; }
.card.pop-out > .card-head { border-radius: var(--radius) var(--radius) 0 0; }
/* A card the script empties out — queued commands, once they are all acked.
   Spelled out for the grid case too: the rule below gives a card in a grid its
   own display, at the same specificity and further down the file, and would
   otherwise win and draw a card that asked to be hidden. */
.card[hidden], .grid > .card[hidden] { display: none; }
.card-body { padding: 16px; }
.card-body.tight { padding: 0; }

.grid { display: grid; gap: 16px; }
/* The grid owns the spacing between cards. A card's own bottom margin on top
   of the gap made every row of a grid sit 18px lower than the one beside it,
   which is what stopped two side-by-side cards from lining up at the bottom. */
.grid > .card { margin-bottom: 0; }
/* Side-by-side cards run to the same height, and their bodies stretch with
   them, so a short card is a card with room in it rather than a card that
   stops early and leaves a step in the layout. */
.grid > .card { display: flex; flex-direction: column; }
.grid > .card > .card-body { flex: 1 1 auto; }
.grid.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid.cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
@media (max-width: 1100px) {
  .grid.cols-4, .grid.cols-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid.cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .grid.cols-2, .grid.cols-3, .grid.cols-4, .grid.cols-6 { grid-template-columns: 1fr; }

  /* On phones the sidebar is a drawer driven by the same toggle button:
     off-canvas by default, slid in when the shell is *not* collapsed. */
  .sidebar {
    position: fixed; top: 0; left: 0; z-index: 60;
    width: 250px; flex-basis: 250px;
    box-shadow: 0 0 60px rgba(0, 0, 0, .7);
    transform: translateX(-100%);
    transition: transform .2s ease;
  }
  html:not(.nav-collapsed) .sidebar { transform: translateX(0); }
  html.nav-collapsed .sidebar { width: 250px; flex-basis: 250px; padding: 18px 14px; }
  html.nav-collapsed .brand-text,
  html.nav-collapsed .navlink .navtext,
  html.nav-collapsed .navlabel-text { display: revert; }
  html.nav-collapsed .navgroup > .navlabel {
    justify-content: flex-start; gap: 7px; padding: 0 8px 9px;
    margin: 0; border-bottom: none;
  }
  html.nav-collapsed .navgroup > .navlabel::after { display: block; }
  html.nav-collapsed .navlink { justify-content: flex-start; gap: 11px; padding: 9px 11px; }
  html.nav-collapsed .navlink .pill { position: static; padding: 2.5px 9px; font-size: 10.5px; line-height: normal; }

  .nav-scrim {
    position: fixed; inset: 0; z-index: 50;
    background: rgba(4, 6, 14, .62); backdrop-filter: blur(2px);
  }
  html.nav-collapsed .nav-scrim { display: none; }
}
/* Above the phone breakpoint the scrim never participates. */
@media (min-width: 721px) { .nav-scrim { display: none; } }

/* ── Stat tiles ──────────────────────────────────────────────────────────── */

/* Every tile carries a --tint. The rail, the icon and the meter all read from
   that one variable, so a tile is recoloured by setting a single property
   rather than by three rules that can drift apart. Teal is the default. */
.stat {
  --tint: var(--teal);
  background:
    radial-gradient(120% 100% at 100% 0%, color-mix(in srgb, var(--tint) 9%, transparent) 0%,
                    transparent 58%),
    linear-gradient(168deg, var(--surface-2) 0%, var(--surface) 62%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 15px 17px 16px;
  position: relative;
  overflow: hidden;
  transition: border-color .18s ease, transform .18s ease;
}
.stat:hover { border-color: var(--border-hi); transform: translateY(-1px); }
.stat::before {
  content: ""; position: absolute; inset: 0 auto 0 0; width: 3px;
  background: linear-gradient(180deg, var(--tint), color-mix(in srgb, var(--tint) 35%, transparent));
}
.stat .label {
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .13em;
  font-weight: var(--fw-bold); color: var(--mute); margin-bottom: 6px;
}
/* Sans, not mono. These are three or four digits read at a glance from across a
   desk, and the mono face draws a squared-off 0 and a seriffed 1 that at this
   size read as two different fonts sitting next to each other. Tabular figures
   keep the column alignment that was the only reason to use mono here. */
.stat .value {
  font-family: var(--sans); font-variant-numeric: tabular-nums;
  font-size: 2.1rem; line-height: 1.05; font-weight: var(--fw-bold);
  letter-spacing: -.025em;
  /* No colour of its own. P&L tiles put .up / .down on this element and a
     colour here would outrank both of them. */
}
/* A tile whose headline is a time range rather than a figure. "6:30 PM – 10:30
   PM" at 2.1rem wraps on every screen narrower than a desk, and a wrapped time
   range reads as two times. */
.stat .value.time {
  font-size: 1.32rem; letter-spacing: -.01em; line-height: 1.2;
}
.stat .sub { font-size: 12.5px; color: var(--dim); margin-top: 6px; }
.stat .sub a { color: var(--dim); text-decoration: none; border-bottom: 1px dotted var(--border-hi); }
.stat .sub a:hover { color: var(--tint); border-bottom-color: var(--tint); }

/* The label and its icon, on one line. */
.stat-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.stat-ico {
  flex: none; width: 30px; height: 30px; margin-top: -2px;
  display: grid; place-items: center; border-radius: 9px;
  background: color-mix(in srgb, var(--tint) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--tint) 26%, transparent);
  color: var(--tint);
}
.stat-ico .ico { width: 16px; height: 16px; }
/* The denominator. Same line, deliberately quieter — "9" is the number, "/ 12"
   is the context, and at the same weight the pair reads as a date. */
.stat-of {
  font-size: 1.15rem; font-weight: var(--fw-med); color: var(--mute);
  letter-spacing: 0; margin-left: 6px;
}
/* The same fraction as a width. Reading a proportion off two numbers is work;
   this is the answer without it. */
.stat-bar {
  height: 4px; border-radius: 3px; margin-top: 11px; overflow: hidden;
  background: color-mix(in srgb, var(--border) 85%, transparent);
}
.stat-bar > i {
  display: block; height: 100%; border-radius: 3px; min-width: 0;
  background: linear-gradient(90deg, color-mix(in srgb, var(--tint) 55%, transparent), var(--tint));
  transition: width .55s cubic-bezier(.22, .61, .36, 1);
}
/* A number that just changed. Same pulse the heartbeat rings use, so "this
   moved" looks the same everywhere on the site. */
.stat.beat { animation: statbeat .62s ease-out; }
@keyframes statbeat {
  0%   { border-color: var(--tint); box-shadow: 0 0 0 0 color-mix(in srgb, var(--tint) 38%, transparent); }
  100% { border-color: var(--border); box-shadow: 0 0 0 10px transparent; }
}

.stat.t-teal   { --tint: var(--teal); }
.stat.t-violet { --tint: var(--violet); }
.stat.t-amber  { --tint: var(--amber); }
.stat.t-up     { --tint: var(--up); }
/* Nothing to report — an empty queue should not be lit up like a problem. */
.stat.t-quiet  { --tint: var(--border-hi); }
.stat.t-quiet .value { color: var(--mute); }

.stat.up    { --tint: var(--up); }
.stat.down  { --tint: var(--down); }
.stat.warn  { --tint: var(--amber); }

.up   { color: var(--up); }
.down { color: var(--down); }
.flat { color: var(--dim); }

/* ── Tables ──────────────────────────────────────────────────────────────── */

.table-wrap { overflow-x: auto; }
table.tbl { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
table.tbl thead th {
  text-align: left; padding: 11px 14px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-hi);
  color: var(--dim); font-weight: 800;
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .11em;
  white-space: nowrap;
}
table.tbl tbody td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  /* Stated rather than inherited. A table is the one place a muted colour on
     some ancestor turns every figure on the page to grey at once, and that is
     exactly what used to happen — see the note on tr.thin. */
  color: var(--text);
}
table.tbl tbody tr:hover { background: var(--surface-2); }
table.tbl tbody tr:last-child td { border-bottom: none; }
table.tbl td.num, table.tbl th.num { text-align: right; font-family: var(--mono); }
.empty { padding: 34px 16px; text-align: center; color: var(--dim); font-weight: var(--fw-med); }

/* ── A table that keeps its header ─────────────────────────────────────────
   Seventeen columns of numbers are unreadable once the header has scrolled
   off: "0.01 / 4369.145 / +1166 / +1.06" is four numbers and no answer to
   which is which. So the rows scroll inside the wrap and the header stays.

   The wrap has to own the vertical scroll for this to work at all. `sticky`
   sticks to the nearest scrolling ancestor, and `overflow-x: auto` alone
   already makes this element one — it just never scrolls vertically, so a
   sticky header inside it would ride up the page with everything else and
   look like it did nothing. Capping the height is what gives it something to
   stick to.

   The height is --freeze-h, shared with the journal's own scroll box: the two
   sit under the same filter bar and the same card head, so there is only one
   right answer and it should only be written down once. It is a maximum, so a
   page with four trades on it is still four rows tall. */
.table-wrap.freeze {
  overflow: auto;
  max-height: var(--freeze-h);
  overscroll-behavior: contain;      /* stop at the ends; do not scroll the page on */
}
.table-wrap.freeze thead th {
  position: sticky; top: 0; z-index: 3;
  /* The border is drawn as an inset shadow because `border-collapse: collapse`
     hands cell borders to the table, which does not travel with a sticky cell —
     the header would lose its underline the moment it started floating. */
  border-bottom: none;
  box-shadow: inset 0 -1px 0 var(--border-hi);
}

/* ── Runs ─────────────────────────────────────────────────────────────────────
   One sitting of the algo — the EA attached, trading for an hour, detached.

   The Trades table is read one run at a time and had no way to say where one
   ended: a terminal attached twice in a day produced one flat list, and picking
   this afternoon's rows out of it meant knowing the clock times by heart.

   Two marks, and deliberately only two. The cell says *which* run — it is a
   number and a link into that run on its own. The rail says *where it changes*
   — and for that it needs no identity at all, only enough contrast to break at
   the join, which is why it alternates between two tints rather than taking a
   colour per run. Twelve run colours down the side of a table is bunting, and
   it would collide with the P&L greens and reds that are already the only
   colours on this page carrying meaning.

   A separator row would have been the obvious thing and is the wrong one here:
   an injected row with a colspan is a row the pager splits at a page boundary
   and any future sort scatters. The rail is a property of each row, so it
   survives both. The journal, which is a list of blocks and not a table, gets
   the full-width divider instead — see .runbreak below. */

table.tbl tbody tr.runrow > td:first-child {
  /* Drawn inside the cell rather than as a border: `border-collapse: collapse`
     would have this fight the row's own bottom border for the corner, and the
     rail is meant to run unbroken down the whole run. */
  box-shadow: inset 3px 0 0 var(--run-rail, transparent);
}
tr.run-a { --run-rail: color-mix(in srgb, var(--violet) 55%, transparent); }   /* violet, at rail weight */
tr.run-b { --run-rail: color-mix(in srgb, var(--teal) 45%, transparent); }     /* teal */
/* The join, drawn as an L. A run's first row carries the line that says the run
   above it ended — across the full width of the table, which is what the eye
   catches when scrolling — and it is in that run's own tint rather than a
   neutral grey, so the horizontal arm and the vertical rail beneath it are one
   colour and read as a bracket opening around the run. A grey rule said only
   "something changed here" and left you to work out which side of it the new
   tint started on.

   Applied by the adjacent sibling test rather than by a class, so nothing has
   to be computed per row: two rows in different tints are, by definition, a
   boundary. The corner cell draws both arms in the one shadow — a border would
   fight `border-collapse: collapse` for exactly that corner, which is why the
   rail is a shadow to begin with. */
table.tbl tbody tr.run-a + tr.run-b > td,
table.tbl tbody tr.run-b + tr.run-a > td {
  border-top: none;
  box-shadow: inset 0 3px 0 var(--run-rail);
}
table.tbl tbody tr.run-a + tr.run-b > td:first-child,
table.tbl tbody tr.run-b + tr.run-a > td:first-child {
  box-shadow: inset 3px 0 0 var(--run-rail), inset 0 3px 0 var(--run-rail);
}

/* The number itself. Monospace and quiet: it is an index, not a value, and it
   sits at the head of a row whose actual news is further along. */
.runtag {
  display: inline-flex; align-items: center; gap: 3px;
  font-family: var(--mono); font-size: 12.5px; font-weight: var(--fw-med);
  color: var(--mute); text-decoration: none;
  padding: 2px 7px; border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  white-space: nowrap;
}
.runtag:hover { color: var(--text); border-color: var(--border-hi); background: var(--surface-3); }
/* The run the terminal is attached to right now. A dot, not a word — the
   picker has room to say "live" and a table cell does not. */
.runlive { color: var(--up); font-size: 15px; line-height: 0; }

/* ── Where one run ends, in the journal ───────────────────────────────────────
   The trades table cannot have a divider row; this page can, because it is a
   stack of blocks rather than a table. Same fact, drawn in the shape the page
   can carry: a rule across the full width with the run's number and clock in
   the middle of it, and the number is a link into that run alone. */
.runbreak {
  display: flex; align-items: center; gap: 14px;
  margin: 26px 0 14px;
}
/* The first one labels the top of the page rather than separating anything, so
   it does not need the air a real break does. */
.runbreak.first { margin-top: 4px; }
.runbreak-line {
  flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent 0%, var(--border-hi) 18%,
                              var(--border-hi) 82%, transparent 100%);
}
.runbreak-tag {
  display: inline-flex; align-items: baseline; gap: 9px;
  padding: 5px 13px; border-radius: 999px;
  border: 1px solid var(--border-hi); background: var(--surface-2);
  color: var(--dim); text-decoration: none;
  font-size: 12.5px; font-weight: var(--fw-bold);
  letter-spacing: .04em; white-space: nowrap;
}
a.runbreak-tag:hover { color: var(--text); background: var(--surface-3); }
/* The closing half of a pair. Same divider, deliberately quieter: the opening
   one is what you scan for, and two identical rules around every sitting would
   double the furniture on a page that is otherwise lines of text. The margins
   are the opening one's, mirrored — the air belongs between two runs, not
   inside one. */
.runbreak.closing { margin: 14px 0 26px; }
.runbreak.closing .runbreak-tag {
  border-style: dashed; color: var(--mute); background: transparent;
}
.runbreak-when {
  font-family: var(--mono); font-size: 11.5px; font-weight: var(--fw-body);
  color: var(--mute); letter-spacing: 0;
}
/* Narrow screens: the clock drops under the number rather than pushing the
   rules off both ends. */
@media (max-width: 700px) {
  .runbreak-tag { flex-direction: column; align-items: flex-start; gap: 2px; }
}

/* ── Badges / pills ──────────────────────────────────────────────────────── */

.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2.5px 9px; border-radius: 999px;
  font-size: 11.5px; font-weight: var(--fw-bold); letter-spacing: .02em;
  border: 1px solid transparent; white-space: nowrap;
}
.pill.approved, .pill.active, .pill.ok {
  background: color-mix(in srgb, var(--up) 14%, transparent); color: var(--up); border-color: color-mix(in srgb, var(--up) 42%, transparent);
}
.pill.pending {
  background: color-mix(in srgb, var(--amber) 14%, transparent); color: var(--amber); border-color: color-mix(in srgb, var(--amber) 45%, transparent);
}
.pill.blocked, .pill.suspended, .pill.expired, .pill.error {
  background: color-mix(in srgb, var(--down) 14%, transparent); color: var(--down); border-color: color-mix(in srgb, var(--down) 45%, transparent);
}
/* Account type, straight from ACCOUNT_TRADE_MODE as the terminal reported it. */
.pill.real {
  background: color-mix(in srgb, var(--pink) 14%, transparent); color: var(--pink); border-color: color-mix(in srgb, var(--pink) 45%, transparent);
}
.pill.demo, .pill.contest {
  background: color-mix(in srgb, var(--info) 14%, transparent); color: var(--info); border-color: color-mix(in srgb, var(--info) 45%, transparent);
}
.pill.unknown { background: var(--surface-3); color: var(--dim); border-color: var(--border-hi); }

/* Trade direction. Its own colours — a sell is not a "real account". */
.pill.sell {
  background: color-mix(in srgb, var(--pink) 14%, transparent); color: var(--pink); border-color: color-mix(in srgb, var(--pink) 45%, transparent);
}
.pill.buy {
  background: color-mix(in srgb, var(--info) 14%, transparent); color: var(--info); border-color: color-mix(in srgb, var(--info) 45%, transparent);
}
.pill.info  { background: color-mix(in srgb, var(--violet) 14%, transparent); color: var(--violet); border-color: color-mix(in srgb, var(--violet) 45%, transparent); }
.pill.warn  { background: color-mix(in srgb, var(--amber) 14%, transparent);  color: var(--amber);  border-color: color-mix(in srgb, var(--amber) 45%, transparent); }
.pill.debug { background: var(--surface-3); color: var(--dim); border-color: var(--border-hi); }
.pill.neutral { background: var(--surface-3); color: var(--dim); border-color: var(--border-hi); }

.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dot.on  { background: var(--up);   box-shadow: 0 0 0 5px color-mix(in srgb, var(--up) 20%, transparent); animation: pulse 2.4s infinite; }
.dot.off { background: var(--mute); box-shadow: 0 0 0 3px rgba(115,127,171,.14); }
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .45 } }

/* ── Buttons ─────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-hi);
  background: var(--surface-2); color: var(--text);
  font-size: 13.5px; font-weight: var(--fw-bold); font-family: inherit;
  cursor: pointer; white-space: nowrap;
  transition: transform .08s ease, background .15s ease, border-color .15s ease;
}
.btn:hover { background: var(--surface-3); border-color: var(--teal); text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn.primary {
  background: var(--accent-grad); color: #05121A; border-color: transparent;
  box-shadow: 0 4px 18px color-mix(in srgb, var(--teal) 22%, transparent);
}
.btn.primary:hover { filter: brightness(1.1); }
.btn.danger  { background: color-mix(in srgb, var(--down) 16%, transparent); color: var(--down); border-color: color-mix(in srgb, var(--down) 50%, transparent); }
.btn.danger:hover  { background: color-mix(in srgb, var(--down) 28%, transparent); border-color: var(--down); }
.btn.success { background: color-mix(in srgb, var(--up) 16%, transparent); color: var(--up); border-color: color-mix(in srgb, var(--up) 50%, transparent); }
.btn.success:hover { background: color-mix(in srgb, var(--up) 28%, transparent); border-color: var(--up); }
.btn.ghost { background: transparent; border-color: var(--border-hi); color: var(--dim); }
.btn.ghost:hover { color: var(--text); border-color: var(--teal); background: var(--surface-2); }
.btn.sm { padding: 5px 11px; font-size: 12.5px; }
.btn-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
/* Square button holding an icon and nothing else. */
.btn.icon { padding: 6px; }
.btn.icon .ico { width: 16px; height: 16px; }
.btn.danger.icon:hover .ico { color: var(--down); }

/* Separates a destructive action from the ordinary form above it. */
.rule { border: 0; border-top: 1px solid var(--border); margin: 18px 0; }

/* ── Hover help ──────────────────────────────────────────────────────────── */

/* The "i" next to a field label. Explanations live in here so the form itself
   stays a clean column of inputs. */
.info-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; margin-left: 6px; padding: 0;
  border-radius: 50%; border: 1px solid var(--border-hi);
  background: transparent; color: var(--mute);
  font-family: inherit; font-size: 10px; font-weight: 700; line-height: 1;
  cursor: help; vertical-align: middle; flex: 0 0 auto;
  transition: color .14s ease, border-color .14s ease;
}
.info-btn:hover, .info-btn:focus-visible { color: var(--teal); border-color: var(--teal); outline: none; }

.tip {
  position: fixed; z-index: 200; max-width: 280px;
  padding: 8px 11px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-hi); background: var(--surface-3);
  color: var(--text); font-size: 12.5px; line-height: 1.45;
  box-shadow: var(--shadow); pointer-events: none;
}
.tip[hidden] { display: none; }

/* The What / Why / Example / Outcome explainer. Wider than a plain tip because
   four labelled lines wrapped into a 280px column is the wall this replaces. */
.tip.tip-struct { max-width: 330px; padding: 10px 12px; }
.tip .tip-k {
  display: block; margin-top: 9px;
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .09em;
  font-weight: var(--fw-bold); color: var(--teal);
}
.tip .tip-k:first-child { margin-top: 0; }
.tip .tip-p { margin: 2px 0 0; }

/* ── Forms ───────────────────────────────────────────────────────────────── */

label.field { display: block; margin-bottom: 12px; }
label.field > span {
  display: block; margin-bottom: 5px;
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .1em;
  font-weight: var(--fw-bold); color: var(--dim);
}
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=time], input[type=search], select, textarea {
  width: 100%;
  padding: 9px 12px;
  background: #0A0D14;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit; font-size: var(--fs-sm); font-weight: var(--fw-med);
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--teal); box-shadow: var(--glow-teal);
}
input::placeholder { color: var(--mute); }
select option { background: #0A0D14; color: var(--text); }
textarea { min-height: 76px; resize: vertical; }
.help { font-size: 12.5px; color: var(--dim); margin-top: 4px; }

/* Date fields. The calendar button stays its native size — stretching it over
   the whole input tiles its glyph across the box — and clicking anywhere in
   the field opens the picker through showPicker() in base.html instead. */
input[type=date] { cursor: pointer; }
input[type=date]::-webkit-calendar-picker-indicator {
  cursor: pointer; opacity: .75;
  /* Chrome's glyph is near-black; this lifts it onto the dark field. */
  filter: invert(72%) sepia(12%) saturate(700%) hue-rotate(190deg) brightness(105%);
}
input[type=date]:hover::-webkit-calendar-picker-indicator,
input[type=date]:focus::-webkit-calendar-picker-indicator {
  opacity: 1;
  filter: invert(78%) sepia(60%) saturate(1200%) hue-rotate(120deg) brightness(105%);
}

/* Time fields, the same treatment. Left out of the list above until now, which
   is why a trading window drew as two white browser-default boxes in the
   middle of a dark card. The digits are mono so 09:15 and 15:30 line up. */
input[type=time] { font-family: var(--mono); cursor: pointer; }
input[type=time]::-webkit-calendar-picker-indicator {
  cursor: pointer; opacity: .75;
  filter: invert(72%) sepia(12%) saturate(700%) hue-rotate(190deg) brightness(105%);
}
input[type=time]:hover::-webkit-calendar-picker-indicator,
input[type=time]:focus::-webkit-calendar-picker-indicator {
  opacity: 1;
  filter: invert(78%) sepia(60%) saturate(1200%) hue-rotate(120deg) brightness(105%);
}

/* ── Combobox: one field that searches, one list that appears ────────────── */
/* Replaces a search input sitting above an always-open <select size=n>. Those
   two drew as identical boxes, so the card looked like it was asking the same
   question twice and neither box explained which one you were meant to use.
   Here there is a single control: the list exists only while you are choosing,
   and once you have chosen it is gone and the consequences are on screen. */

.combo { position: relative; }
.combo-control { position: relative; }
/* Room for the clear button and the caret, so a long broker name cannot run
   underneath either of them. */
.combo-control input { padding-right: 62px; }
.combo-control input::-webkit-search-cancel-button { display: none; }

.combo-caret {
  position: absolute; right: 11px; top: 50%; transform: translateY(-50%);
  color: var(--mute); pointer-events: none;
  transition: transform .16s ease, color .16s ease;
}
.combo.open .combo-caret { transform: translateY(-50%) rotate(180deg); color: var(--teal); }

.combo-clear {
  position: absolute; right: 34px; top: 50%; transform: translateY(-50%);
  display: none; align-items: center; justify-content: center;
  width: 19px; height: 19px; padding: 0;
  border: 0; border-radius: 50%;
  background: var(--surface-3); color: var(--dim);
  font-size: 12px; line-height: 1; cursor: pointer;
}
.combo.picked .combo-clear { display: flex; }
.combo-clear:hover { background: var(--down); color: #fff; }

.combo-pop {
  position: absolute; z-index: 40;
  left: 0; right: 0; top: calc(100% + 6px);
  padding: 5px;
  background: var(--surface-2);
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  /* Height is set in script from the measured row height, so the cut always
     lands between rows rather than through the middle of one. */
  overflow-y: auto; overscroll-behavior: contain;
}
.combo-pop[hidden] { display: none; }

.combo-opt {
  display: block; width: 100%;
  padding: 7px 10px; border-radius: var(--radius-sm);
  border: 1px solid transparent; background: none;
  color: var(--text); font: inherit; text-align: left; cursor: pointer;
}
.combo-opt + .combo-opt { margin-top: 2px; }
.combo-opt:hover, .combo-opt.active {
  background: var(--surface-3); border-color: var(--border-hi);
}
.combo-opt .row1 {
  display: flex; align-items: baseline; gap: 8px;
  font-size: var(--fs-sm); font-weight: var(--fw-med);
}
.combo-opt .row2 {
  font-size: 12px; color: var(--mute); font-weight: var(--fw-body);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The matched run of characters, so it is obvious why a row is in the list. */
.combo-opt mark { background: none; color: var(--teal); font-weight: var(--fw-bold); }
.combo-empty { padding: 12px 10px; color: var(--mute); font-size: var(--fs-sm); }

/* The native control the picker was built over. Kept in the DOM because it is
   still what the form submits — and still what a browser with scripts blocked
   shows, which is why it is hidden by the attribute rather than removed. */
select.combo-source[hidden] { display: none; }

/* A terminal picker needs room for a name and a broker under it; the other
   filters on the same bar do not. */
.filterbar label.field.wide { min-width: 240px; }
.filterbar .combo-opt { white-space: normal; }

/* ── Naming a terminal ─────────────────────────────────────────────────────
   Two lines in a table cell: what it is called, and which login and broker
   that is. Nobody recognises a terminal by its number, and two people can
   easily name theirs the same thing — so neither line is optional. */

/* The name and the login share the top line; the broker has the second to
   itself. The login moved up because it is four characters wider than nothing
   and the broker's name is twenty — together on one line they set the column's
   width to the sum of the two, and the sum was the widest non-numeric thing in
   the trades table. */
.term-top { display: flex; align-items: baseline; gap: 8px; }
.term-name { font-weight: var(--fw-med); color: var(--text); }
a.term-name { text-decoration: none; }
a.term-name:hover { text-decoration: underline; }
.term-login { font-size: 11.5px; color: var(--dim); }
.term-where {
  margin-top: 1px; font-size: 11.5px; color: var(--mute);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Two ends of a trade in one cell ───────────────────────────────────────
   Opened above closed, or the entry price above the exit. See clock_pair and
   price_pair in _macros.html: two columns each became one so that Net stopped
   living behind a horizontal scroll.

   The key is a fixed width so the two values line up under each other. Without
   it "in" and "out" are different widths and every pair in the table is a
   half-character stagger. */
.pair { display: flex; flex-direction: column; gap: 1px; }
.pair-row { display: flex; align-items: baseline; gap: 7px; white-space: nowrap; }
.pair.num .pair-row { justify-content: flex-end; }
.pair-k {
  flex: 0 0 auto; min-width: 20px;
  font-size: 10px; font-weight: var(--fw-bold); letter-spacing: .09em;
  text-transform: uppercase; color: var(--mute);
}
.pair.num .pair-k { order: -1; margin-right: auto; }
.pair-v { color: var(--dim); }
/* Inside a pair the broker's clock sits beside its own time rather than under
   it — the pair is already two lines and stacking again would make it four. */
.pair .ts-broker { display: inline; font-size: 11.5px; }

/* Placeholder in the space the review panel will occupy. Without it the card
   is a tall empty box next to a full one, which reads as something failing to
   load rather than as a step you have not taken yet. */
.combo-hint {
  margin-top: 14px; padding: 18px 16px;
  border: 1px dashed var(--border); border-radius: var(--radius);
  color: var(--mute); font-size: var(--fs-sm); text-align: center;
}

.filterbar {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
  padding: 14px 16px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  margin-bottom: 18px;
}
.filterbar label.field { margin-bottom: 0; min-width: 150px; }

/* ── Sortable headers, pager, modal ──────────────────────────────────────── */

.sortlink {
  display: inline-flex; align-items: center; gap: 5px;
  color: inherit; text-decoration: none; white-space: nowrap;
}
.sortlink:hover { color: var(--teal); text-decoration: none; }
.sortlink.on { color: var(--teal); }
.sortlink .caret { font-size: 9px; line-height: 1; opacity: .85; }

/* Under a frozen-header box the pager is the one control you need *after*
   reading the rows, and it was the one thing the box pushed off the bottom of
   the screen: the rows stopped scrolling and there was still a page-scroll to
   do to reach it, and another back up afterwards. freeze.js sizes the box so it
   normally lands on screen by itself.

   Sticky is the backstop for when it cannot — a short window, a filter bar that
   has wrapped to three rows — where the box hits its floor and the page has to
   scroll after all. Then this pins to the bottom of the screen instead of
   sitting at the end of a scroll nobody wants to do. It is inert whenever the
   card already fits, which is the normal case: an element at its natural
   position is what sticky does nothing to.

   It needs its own background for that. Pinned, it is drawn over the rows
   passing underneath, and the pager is not readable — nor is anything under
   it — through a transparent strip. */
.pager {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 12px 16px; border-top: 1px solid var(--border);
  position: sticky; bottom: 0; z-index: 4;
  background: var(--surface-2);
}
.btn.disabled, .btn[disabled] { opacity: .4; pointer-events: none; }

dialog.modal {
  width: min(520px, calc(100vw - 32px));
  padding: 0; border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  background: var(--surface); color: var(--text);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}
dialog.modal::backdrop { background: rgba(4, 7, 18, .72); }
/* A trade is six field grids side by side. A 520px dialog would wrap every one
   of them into a column an inch wide, which is the layout this popup exists to
   avoid — so it takes the width it needs and scrolls in itself, not the page. */
dialog.modal.wide {
  width: min(1240px, calc(100vw - 32px));
  max-height: min(880px, calc(100vh - 48px));
}
/* Only when open: a bare `display: flex` here would beat the browser's own
   `dialog:not([open]) { display: none }` and leave the popup on the page. */
dialog.modal.wide[open] { display: flex; flex-direction: column; }
dialog.modal.wide .modal-body { flex: 1 1 auto; overflow: auto; padding: 0; }
.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.modal-head h2 { margin: 0; }
.modal-body { padding: 16px; }
.modal-foot {
  display: flex; justify-content: flex-end; gap: 8px;
  padding: 13px 16px; border-top: 1px solid var(--border);
  background: var(--surface-2);
}

.inline-form { display: inline; }

/* ── Tabs ────────────────────────────────────────────────────────────────── */

.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
.tab {
  padding: 7px 15px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--dim); font-weight: var(--fw-bold); font-size: 13.5px;
}
.tab:hover { color: var(--text); border-color: var(--border-hi); text-decoration: none; }
.tab.active {
  background: var(--accent-grad); color: #05121A; border-color: transparent;
  box-shadow: 0 3px 14px color-mix(in srgb, var(--teal) 20%, transparent);
}

/* Badge on a tab. Not a .pill: the translucent pill backgrounds are built for
   a dark surface and vanish against the active tab's bright gradient. These
   are solid in both states — amber on the resting tab, and on the active one
   the same dark ink as the label sitting on a light chip. */
.tab .count {
  display: inline-block; margin-left: 7px; padding: 1px 8px;
  border-radius: 999px; font-size: 11px; font-weight: 800; line-height: 1.6;
  background: var(--amber); color: #1A1200; vertical-align: 1px;
}
.tab.active .count { background: rgba(5,18,26,.82); color: #FFF; }

/* ── Heartbeat countdown ─────────────────────────────────────────────────── */

.hb { display: inline-flex; align-items: center; gap: 10px; white-space: nowrap; }
.hb-ring { width: 36px; height: 36px; flex: 0 0 auto; transform: rotate(-90deg); }
.hb-lg .hb-ring { width: 54px; height: 54px; }
.hb-ring circle { fill: none; stroke-width: 4; }
.hb-track { stroke: var(--surface-3); }
.hb-fill {
  stroke: var(--teal); stroke-linecap: round;
  /* 2πr for r=15.5, so dashoffset can be driven as a plain 0–1 fraction. */
  stroke-dasharray: 97.4; stroke-dashoffset: 0;
  transition: stroke-dashoffset .95s linear, stroke .25s ease;
}
.hb-text { display: flex; flex-direction: column; line-height: 1.25; }
.hb-next { font-size: 13.5px; font-weight: var(--fw-bold); color: var(--dim); }
.hb-lg .hb-next { font-size: 17px; }

/* Due, and the beat is in flight. Amber for a second or two is honest; it is
   not an alarm, so the ring breathes rather than sitting there accusingly. */
.hb.due .hb-fill { stroke: var(--amber); animation: hb-due 1.1s ease-in-out infinite; }
.hb.due .hb-next { color: var(--amber); }
@keyframes hb-due { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
/* Past the grace window — the EA has stopped opening trades on its own. */
.hb.dead .hb-fill { stroke: var(--down); }
.hb.dead .hb-next { color: var(--down); }
.hb.never .hb-fill { stroke: var(--mute); }
.hb.never .hb-next { color: var(--mute); }
/* Not on the clock at all: awaiting approval. A full, flat, grey ring — no
   countdown is running, and it should not look like one has stalled. */
.hb.wait .hb-fill { stroke: var(--surface-3); }
.hb.wait .hb-next { color: var(--mute); font-weight: var(--fw-med); }
/* The beat just landed: a single pulse, so a live terminal is visible from
   across the room without reading the number. */
.hb.beat .hb-fill { transition: none; animation: hb-pulse .9s ease; }
@keyframes hb-pulse {
  0%   { stroke: #FFFFFF; stroke-width: 8; }
  55%  { stroke: #FFFFFF; stroke-width: 6; }
  100% { stroke: var(--teal); stroke-width: 4; }
}

/* ── "Next update" meter ─────────────────────────────────────────────────────
   The same ring as a heartbeat, because it is counting to the same event: a
   terminal calling in. Nothing on these pages is pushed, so the honest answer
   to "when does this table change?" is "when the soonest terminal reports",
   and that is what the ring drains towards. Muted, so a table's own heartbeat
   rings stay the brighter thing on a page that has both. */
.hb.upd .hb-fill { stroke: var(--violet); }
.hb.upd .hb-next { color: var(--dim); font-weight: var(--fw-med); }
.hb.upd.due .hb-fill { stroke: var(--teal); }
.hb.upd.due .hb-next { color: var(--teal); }
.hb.upd.dead .hb-fill, .hb.upd.never .hb-fill { stroke: var(--surface-3); }
.hb.upd.dead .hb-next, .hb.upd.never .hb-next { color: var(--mute); }
/* The moment fresh rows land, so a table that changed under the reader says so
   rather than silently redrawing. */
.hb.upd.beat .hb-fill { transition: none; animation: hb-pulse .9s ease; }

/* Raw output — a parser error, a stack line. Scrolls rather than reflows. */
.pre {
  margin: 0; padding: 11px 13px; border-radius: var(--radius-sm);
  background: #0A0D14; border: 1px solid var(--border);
  color: var(--dim); font-family: var(--mono); font-size: 12.5px;
  overflow-x: auto; white-space: pre-wrap; word-break: break-word;
}

/* ── Flash ───────────────────────────────────────────────────────────────── */

.flashes { margin-bottom: 16px; display: grid; gap: 8px; }
.flash {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 11px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border-hi); background: var(--surface-2);
  border-left-width: 3px;
  /* Animated on the way out only — max-height collapses the gap so the rest
     of the page does not jump when one clears. */
  overflow: hidden;
  transition: opacity .3s ease, max-height .3s ease, margin .3s ease,
              padding .3s ease, border-width .3s ease;
  max-height: 300px;
}
.flash.out {
  opacity: 0; max-height: 0;
  margin-top: -8px; padding-top: 0; padding-bottom: 0; border-top-width: 0; border-bottom-width: 0;
}
.flash-msg { flex: 1 1 auto; }
.flash-x {
  flex: 0 0 auto; margin: -2px -4px 0 0; padding: 0 4px;
  background: none; border: 0; cursor: pointer;
  color: inherit; opacity: .55; font-size: 13px; font-family: inherit;
  transition: opacity .14s ease;
}
.flash-x:hover, .flash-x:focus-visible { opacity: 1; outline: none; }
.flash.success { border-left-color: var(--up);     color: var(--up); }
.flash.danger  { border-left-color: var(--down);   color: var(--down); }
.flash.warning { border-left-color: var(--amber);  color: var(--amber); }
.flash.info    { border-left-color: var(--violet); color: #C7B0FF; }

/* ── Logs ────────────────────────────────────────────────────────────────── */

/* Five columns, and every row on the page uses the same five, so the journal
   reads as columns rather than as a stack of differently-shaped rows.
   Clock · level · terminal · what kind of line · what it says.

   The terminal used to share a column with the phase and the event code. At
   132px that fits none of the three combinations — a terminal name and a phase
   pill wrapped onto two lines, so half the rows were two lines tall and their
   parts lined up with nothing above or below. Giving it a column of its own
   costs 116px and buys a page whose left edge is straight all the way down. */
.logline {
  /* The clock column holds two stacked — yours and the broker's — so it is
     wider than a single timestamp needs. */
  display: grid; grid-template-columns: 150px 62px 116px 108px 1fr;
  gap: 10px; padding: 8px 14px;
  border-bottom: 1px solid var(--border);
  font-family: var(--mono); font-size: 14.5px;
}
/* Never wraps. A name too long for the column is cut with an ellipsis and the
   full thing is in the tooltip — one row one line is the whole point here, and
   a terminal name is not worth breaking it for. */
.term-col { min-width: 0; }
.logline .logline-term {
  display: block; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.logline:hover { background: var(--surface-2); }
.logline .ts { color: var(--text); }
/* The broker's clock under your own. Dimmer and smaller — it is not when this
   happened to you, it is where to find the candle — but always present, because
   a journal you cannot line up against a chart is a journal you cannot debug
   from. Its own line: side by side, one of the two always got clipped. */
.ts-broker {
  display: block; color: var(--dim); font-size: 12.5px; letter-spacing: -.01em;
  white-space: nowrap; cursor: help;
}
/* A table cell holding the same instant twice. Local reads as the cell's own
   value; the broker's clock is the second line, in MT5's format, and is the one
   a chart is searched by. */
.clocks { display: flex; flex-direction: column; gap: 1px; }
.clock-local { color: var(--dim); }
/* Worked out from the account's offset rather than reported by the terminal.
   The "≈" is in the text, so it survives being pasted somewhere else. */
.ts-broker.derived { color: var(--mute); font-style: italic; }

/* A record's head is a wide flex row rather than a narrow column, so there the
   two clocks sit on one line with a dot between them. */
.rec-head .ts { display: inline-flex; align-items: baseline; gap: 7px; }
.rec-head .ts-broker { display: inline; font-size: 12.5px; }
.rec-head .ts-broker::before { content: '·'; margin-right: 7px; color: var(--border-hi); }
/* The longest codes — scalp.spread.refused, scalp.settle.partial — are wider
   than the column, and a grid cell does not clip, it overlaps the message. */
.logline .code { color: #C0A6FF; font-weight: 600; overflow-wrap: anywhere; }
.logline .msg { color: var(--text); word-break: break-word; }
.logline.error .msg { color: var(--down); }
.logline.warn  .msg { color: var(--amber); }
.logline.debug .msg { color: var(--dim); }
/* The step column exists on every line, empty on the terminal's own. Six
   columns down the whole page rather than five on some rows and six on others,
   because a column that comes and goes shifts everything after it and the page
   ends up looking like it was set by hand. */
.logline.has-step { grid-template-columns: 44px 150px 62px 116px 108px 1fr; }
/* Run, when the list has one — before the clock, because it is the coarser
   grouping and the eye runs down it looking for where one sitting ends. Narrow:
   it holds "#12" and a live dot and will never hold more. */
.logline.has-run          { grid-template-columns: 52px 150px 62px 116px 108px 1fr; }
.logline.has-step.has-run { grid-template-columns: 44px 52px 150px 62px 116px 108px 1fr; }
@media (max-width: 1240px) {
  .logline                    { grid-template-columns: 132px 58px 96px 96px 1fr; }
  .logline.has-step           { grid-template-columns: 40px 132px 58px 96px 96px 1fr; }
  .logline.has-run            { grid-template-columns: 46px 132px 58px 96px 96px 1fr; }
  .logline.has-step.has-run   { grid-template-columns: 40px 46px 132px 58px 96px 96px 1fr; }
}
/* The cell itself. Dim by default — it is an index and it repeats down the
   whole run, so it must not compete with the message beside it. */
.run-col { min-width: 0; overflow: hidden; }
.run-col .runtag { font-size: 12.5px; }
@media (max-width: 860px) {
  .logline, .logline.has-step { grid-template-columns: 1fr; gap: 2px; }
}

/* ── A journal that keeps its header ───────────────────────────────────────
   The same bargain the trades table makes, for a page that is not a table.

   Fifty journal lines is four screenfuls, and the pager, the filter bar and
   the view switch are all at the top of them — so reading a page meant
   scrolling to the bottom and then all the way back up to do anything about
   what you read. The lines scroll in here instead and the page stays put.

   The wrap has to own the vertical scroll for the sticky header to have
   anything to stick to: `sticky` sticks to the nearest scrolling ancestor, and
   without a capped height that ancestor is the page, where the header would
   ride up with everything else and look like it did nothing. */
.logscroll.freeze {
  overflow: auto;
  max-height: var(--freeze-h);
  overscroll-behavior: contain;      /* stop at the ends; do not scroll the page on */
}

/* The header itself. A `.logline` first — it inherits the grid, which is the
   only reason it lines up with the rows under it — and this second, for the
   things a header does not share with a line: it is not monospace, it does not
   light up under the pointer, and it does not carry a row's bottom hairline. */
.loghead {
  position: sticky; top: 0; z-index: 3;
  background: var(--surface-2);
  color: var(--dim); font-family: var(--sans);
  font-size: var(--fs-label); font-weight: 800;
  text-transform: uppercase; letter-spacing: .11em;
  white-space: nowrap;
  padding-top: 11px; padding-bottom: 11px;
  /* Drawn as an inset shadow for the same reason the trades header is: a border
     on a sticky element that sits over scrolling content renders under the
     first row's own background at some scroll offsets. */
  border-bottom: none;
  box-shadow: inset 0 -1px 0 var(--border-hi);
}
.loghead:hover { background: var(--surface-2); }
/* A run divider is the first thing on the page when a terminal is picked, and
   its top margin would push a gap between the header and the lines. */
.logscroll > .runbreak.first { margin-top: 10px; }
/* Below 860px every row collapses to a single column, so there are no columns
   left to head — and the box stops capping its height, because a phone screen
   is already shorter than the cap and an inner scroll on top of the page's own
   is two scrollbars fighting over one gesture. */
@media (max-width: 860px) {
  .loghead { display: none; }
  .logscroll.freeze { max-height: none; overflow: visible; }
}

/* ── The journal wall ──────────────────────────────────────────────────────
   The journal with nothing else on the screen — a tab left open on a second
   monitor. See journal_wall.html.

   It owns the viewport rather than living in the flow of a page: one bar at the
   top that never moves, and everything under it scrolling in a single box. That
   is what makes freeze.js irrelevant here — there is no card to fit above a
   fold and no pager to keep on screen, because the list does not page. */
.wall { display: flex; flex-direction: column; height: 100vh; min-height: 0; }

.wall-bar {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  padding: 9px 18px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.wall-id { display: flex; align-items: baseline; gap: 10px; }
.wall-name { font-weight: var(--fw-bold); font-size: 15px; }
.wall-count { font-size: 12.5px; color: var(--dim); font-family: var(--mono); }

/* Takes the slack, so the tools stay hard right however many chips there are. */
.wall-state { flex: 1; min-width: 0; }
/* The chip row is built to sit under a filter bar as a block of its own. In
   here it is one item in a row, so it gives up its margins and wraps with
   everything else. */
.wall-state .activefilters { margin: 0; }

/* Unfiltered is a state worth naming on this screen, because it is the one the
   tab is meant to be left in — "everything, as it arrives". Everywhere else on
   the site an empty chip row says the same thing by saying nothing, which is
   right there and too quiet here. */
.wall-live {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--fs-label); font-weight: var(--fw-bold);
  letter-spacing: .1em; text-transform: uppercase; color: var(--dim);
}
.wall-live-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--teal);
  animation: wall-pulse 2.6s ease-out infinite;
}
@keyframes wall-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--teal) 50%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .wall-live-dot { animation: none; }
}

.wall-tools { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }

/* `min-height: 0` on both, and it is load-bearing: a flex child's default
   minimum is its content, so without it the scroller grows to the length of the
   journal and takes the page with it — the bar scrolls away and the endless
   list becomes an endless page. */
.wall-body { flex: 1; min-height: 0; display: flex; }
.wall-scroll {
  flex: 1; min-height: 0; overflow: auto;
  overscroll-behavior: contain;
  padding: 0 6px 0 0;
}
/* Same reason as the paged journal's: a divider is the first thing under the
   header, and its top margin would push a gap between the two. */
.wall-list > .wall-chunk:first-child > .runbreak.first { margin-top: 10px; }

/* ── The overview rail ───────────────────────────────────────────────────────
   The journal itself down the right-hand edge, drawn word by word at about a
   sixteenth size in the colours these rules give it. See the note over the
   drawing code in wall.js: it is a picture of the journal, not a position in
   it.

   Its own element for the viewport window, over a canvas for the lines — on a
   journal short enough to fit the rail the picture stands still and only the
   window moves, which is one style write instead of a redraw. */
.wall-map {
  flex: 0 0 116px; position: relative;
  border-left: 1px solid var(--border);
  background: var(--surface);
  cursor: pointer; touch-action: none;
  overflow: hidden;
}
.wall-map-canvas { display: block; }
/* What is on screen, as a lifted pane rather than an outline: an outline on top
   of bars this small is two lines of noise where the bars are, and the eye
   wants the block, not its edges. */
/* No transition on the top, deliberately. The window and the picture under it
   both move on a scroll now — the picture slides when the journal is longer
   than the rail — and easing one of the two is the window sliding off the lines
   it is supposed to be around. */
.wall-map-view {
  position: absolute; left: 0; right: 0;
  background: rgba(255, 255, 255, .085);
  border-top: 1px solid var(--border-hi);
  border-bottom: 1px solid var(--border-hi);
  pointer-events: none;
}
.wall-map.dragging .wall-map-view { background: rgba(255, 255, 255, .14); }
.wall-map:hover .wall-map-view { background: rgba(255, 255, 255, .12); }
/* Below this there is not room for a rail and a readable line beside it. */
@media (max-width: 1100px) {
  .wall-map { display: none; }
}

/* With the rail there, the native scrollbar is the same fact twice. Kept, but
   thin — it is still the thing a trackpad and a keyboard drive. */
.wall-scroll { scrollbar-width: thin; }
.wall-scroll::-webkit-scrollbar { width: 9px; }
.wall-scroll::-webkit-scrollbar-thumb {
  background: var(--surface-3); border-radius: 5px;
}
.wall-scroll::-webkit-scrollbar-track { background: transparent; }

/* The foot of the list. One line, centred, and it says which of three things is
   true — more to come, fetching it, or that was all of it. */
.wall-more {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  padding: 22px 16px 40px;
}
.wall-end, .wall-spin, .wall-fail {
  font-size: 12.5px; color: var(--mute); letter-spacing: .04em;
}
.wall-fail { color: var(--amber); }
.wall-spin { color: var(--dim); }

/* A trade opened over the wall rather than navigated to. The dialog is the
   site's own wide modal; all this adds is a cap on how much of the screen it
   takes, because the point of the popup is that the journal is still behind
   it. */
.wall-modal .modal-body { padding: 0 18px 18px; }
.wall-modal .modal-body .card { border: none; background: transparent; }

/* ── What kind of line this is ─────────────────────────────────────────────
   One word — entry, trail, exit, summary — and it is the fastest thing to read
   in a block, so it is the thing that gets the colour.

   It used to be one grey chip for all seven, on a row of grey monospace, next
   to a violet event code that says the same thing in machine words. Six steps
   of a trade looked identical until you read them. Now the phase is the label
   that carries the colour: an entry is blue, a trail teal, an exit violet, the
   summary lime, and anything that went wrong is amber or red. The rail on the
   block stays green-or-red for the money — these say *what happened*, and the
   two never compete because they are answering different questions. */
.phase {
  display: inline-block; margin-left: 6px; padding: 1px 8px;
  border-radius: 999px; background: var(--surface-3);
  border: 1px solid var(--border-hi); color: var(--dim);
  font-size: 11px; font-weight: var(--fw-bold); letter-spacing: .07em;
  text-transform: uppercase; cursor: help; white-space: nowrap;
}
.ph-entry {
  background: color-mix(in srgb, var(--info) 16%, transparent); border-color: color-mix(in srgb, var(--info) 55%, transparent); color: #8FC0FF;
}
.ph-trail {
  background: color-mix(in srgb, var(--teal) 14%, transparent); border-color: color-mix(in srgb, var(--teal) 50%, transparent); color: #5CF0E4;
}
.ph-exit {
  background: color-mix(in srgb, var(--violet) 18%, transparent); border-color: color-mix(in srgb, var(--violet) 55%, transparent); color: #C4A6FF;
}
.ph-summary {
  background: color-mix(in srgb, var(--lime) 15%, transparent); border-color: color-mix(in srgb, var(--lime) 50%, transparent); color: #D2F97E;
}
.ph-refused, .ph-abandoned {
  background: color-mix(in srgb, var(--amber) 16%, transparent); border-color: color-mix(in srgb, var(--amber) 55%, transparent); color: #FFCE6B;
}
.ph-problem {
  background: color-mix(in srgb, var(--down) 16%, transparent); border-color: color-mix(in srgb, var(--down) 55%, transparent); color: #FF93A6;
}
.logline .trade-mark { margin-right: 4px; font-size: 15px; }
/* Whose line this is. Clipped rather than wrapped: the code under it is the
   longer string of the two and sets the column's width. */
.logline .logline-term {
  display: block; max-width: 100%;
  color: var(--teal); font-weight: var(--fw-med); text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.logline .logline-term:hover { text-decoration: underline; }

/* ── A trade, from open to close ───────────────────────────────────────────
   Four to forty lines that belong to one position, boxed so the story is
   readable as one thing instead of being found line by line. */

/* The `.trade-block` / `.trade-head` rules that were here are gone with the
   <details> box and the popup they dressed. A trade in the journal is a run of
   ordinary lines against a coloured rail now (.trade-run above), and one trade
   on its own is templates/trade_journal.html. */
.trade-emoji { font-size: 20px; line-height: 1; }
.trade-no { font-weight: 700; color: var(--text); letter-spacing: .01em; }
.trade-what { color: var(--dim); }
/* When it started, on the broker's clock. On the closed header on purpose: it
   is what you take to the chart, and needing to open a block to read it was the
   whole friction. */
.trade-when {
  padding: 1px 7px; border-radius: 4px; background: var(--surface);
  border: 1px solid var(--border); color: var(--mute); font-size: 12px;
  cursor: help;
}
/* A refusal inside. Amber, and only this — never the rail, which has one job. */
.trade-warn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%;
  background: color-mix(in srgb, var(--amber) 14%, transparent); border: 1px solid var(--amber);
  color: var(--amber); font-size: 12px; line-height: 1; cursor: help;
}
.trade-cont {
  padding: 1px 7px; border-radius: 4px; border: 1px dashed var(--border-hi);
  color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .04em;
}
.trade-spacer { flex: 1 1 auto; }
.trade-stat { color: var(--dim); font-size: 13.5px; }
.trade-stat.up { color: var(--up); font-weight: 700; }
.trade-stat.down { color: var(--down); font-weight: 700; }
.trade-stat.open { color: var(--violet); font-weight: 700; }
.trade-stat.warn { color: var(--amber); font-weight: 700; }


/* The ticket. MT5 calls this number "Position" in its History tab and it is the
   only string that is the same there and here, which is why it is drawn as a
   thing rather than left as loose digits.

   It comes in two shapes and they are deliberately not the same. In the
   journal it is a label — a fact to read or copy, with nothing that happens
   when you press it, because the way through to the trade is the link on the
   closing line and two doors to one room is one door too many. In the trades
   table it *is* the way through, so there it is a link and looks like one. */
.ticket {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 8px; border-radius: 5px;
  border: 1px solid var(--border-hi); background: var(--surface);
  color: #C0A6FF; font-family: var(--mono); font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  transition: border-color .12s ease, color .12s ease, background .12s ease;
}
/* A label: no pointer, no hover lift. The help cursor because the tooltip on it
   is worth finding — it carries the MT5 comment. */
span.ticket { cursor: help; }
a.ticket { cursor: pointer; text-decoration: none; }
a.ticket:hover {
  border-color: var(--violet); color: #DCCBFF; background: var(--surface-3);
  text-decoration: none;
}
a.ticket:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }
.ticket .ico { color: var(--mute); }
a.ticket:hover .ico { color: var(--violet); }
/* Tied to its trade by the clock rather than by a number either side wrote
   down. Dashed, and prefixed "≈", because a guess that looks like a fact is
   worse than no ticket at all. */
.ticket.guessed { border-style: dashed; color: var(--dim); }
.ticket.guessed:hover { color: var(--text); }

/* Which step of the trade this is. The number is the point: a block is a run
   of records that only mean anything in order, and "3/7" says both where you
   are and how much is left without reading a word of the record itself. */
.step {
  display: inline-flex; align-items: baseline; gap: 1px;
  min-width: 30px; padding: 1px 5px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--border-hi);
  font-family: var(--mono); line-height: 1.5;
}
.step > b { color: var(--text); font-size: 12.5px; font-weight: var(--fw-bold); }
.step > i { color: var(--mute); font-size: 10.5px; font-style: normal; }
/* A prose line is a grid, and a grid item stretches to its row unless told not
   to — so on a three-line `info` between two records the pill grew into a tall
   oval drawn around a two-digit number, while the same pill inside a record
   (a flex row, aligned to the baseline) stayed a badge. It is a badge here too,
   parked on the first line of the row however many lines the sentence runs to.
   One rule, so the journal page and the popup that reuses these blocks cannot
   drift apart. */
.logline > .step { align-self: start; justify-self: start; }

/* What comes next. One arrow between records, pointing the one way a trade can
   be read — it is the answer to "where do I start" that a wall of equal-weight
   grids could never give. */
.rec-then {
  display: flex; align-items: center; justify-content: center;
  height: 22px; margin: 0; color: var(--border-hi);
  font-size: 17px; line-height: 1; user-select: none;
  background: var(--surface-2);
}

@media (max-width: 860px) {
  .trade-spacer { flex-basis: 100%; }
}

/* ── A trade in the journal stream ─────────────────────────────────────────
   What the journal draws instead of the box above.

   The box was the problem. Shut, every trade cost a header strip carrying
   eleven chips before a single line of content; open, one trade was six field
   grids and a screen and a half. Twenty trades was the wall.

   A run costs exactly what it is: one line per thing the trade did, left in
   the stream among the terminal's own lines — which is what makes "the spread
   warning at 09:51 is why nothing was taken at 09:52" legible at all. All that
   holds it together is this rail, and the rail is the money: green made, red
   lost, violet still running, grey never got off the ground. */
.trade-run {
  position: relative;
  border-left: 3px solid var(--border-hi);
  background: linear-gradient(90deg, rgba(255, 255, 255, .012), transparent 420px);
}
.trade-run.up    { border-left-color: var(--up); }
.trade-run.down  { border-left-color: var(--down); }
.trade-run.flat  { border-left-color: var(--mute); }
.trade-run.open  { border-left-color: var(--violet); }
.trade-run.stale { border-left-color: var(--border-hi); }
/* The lines inside sit against the rail rather than at the card's own margin,
   so the run reads as one thing without needing a border round it. */
.trade-run > .logline { padding-left: 11px; }

/* The line that closes a run, and the only place its result is stated.
   Normally this *is* the EA's own summary record, drawn as a line at the
   moment it happened — which is why the old header strip is gone rather than
   moved: it was these same facts a second time, printed above the record they
   were copied from.

   It is a `.logline` first and this second, so it inherits the same five
   columns as every other row: the step where steps go, the clock where clocks
   go, a pill where info and warn go, the terminal in the terminal column. Only
   the tint and the contents of the last column are its own. It used to be a
   flex row of its own, which is why it sat crooked under the lines above it. */
.trade-close.up    { background: linear-gradient(90deg, color-mix(in srgb, var(--up) 7.5%, transparent), transparent 460px); }
.trade-close.down  { background: linear-gradient(90deg, color-mix(in srgb, var(--down) 7.5%, transparent), transparent 460px); }
.trade-close.open  { background: linear-gradient(90deg, color-mix(in srgb, var(--violet) 7.5%, transparent), transparent 460px); }
.trade-close.stale { background: linear-gradient(90deg, rgba(255, 255, 255, .035), transparent 460px); }
.trade-close.flat  { background: linear-gradient(90deg, rgba(255, 255, 255, .035), transparent 460px); }

/* Everything specific to the trade, in the message column — so it starts on
   the same left edge as every sentence on the page. */
.trade-facts {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  min-width: 0;
}
.trade-facts .trade-spacer { flex: 1 1 auto; }

/* Reading TRADE where the others read info or warn. Same shape, same column,
   so the eye finds "what kind of line is this" in one place down the page. */
.pill.trade-pill {
  background: color-mix(in srgb, var(--teal) 14%, transparent); border: 1px solid color-mix(in srgb, var(--teal) 50%, transparent);
  color: var(--teal); cursor: help;
}
/* Closed off from what the block knows rather than from a record the EA wrote:
   the trade is still running, or its summary fell on another page. Never lets
   it pass for an event that happened. */
.trade-close.partial .pill.trade-pill {
  background: var(--surface-3); border-color: var(--border-hi); color: var(--mute);
}
.trade-close.partial .trade-no { color: var(--dim); }
.ph-partial {
  background: var(--surface-3); border-color: var(--border-hi); color: var(--mute);
}

/* Where the detail went. Every number a run no longer prints is one click
   away, and this is that click — on every trade, in the same place. */
.trade-open {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 10px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--teal) 45%, transparent); background: color-mix(in srgb, var(--teal) 10%, transparent);
  color: var(--teal); font-family: var(--sans); font-size: 12px;
  font-weight: var(--fw-bold); letter-spacing: .03em; white-space: nowrap;
  text-decoration: none; transition: background .12s ease, border-color .12s ease;
}
.trade-open:hover {
  background: color-mix(in srgb, var(--teal) 20%, transparent); border-color: var(--teal);
  color: #6BFFF0; text-decoration: none;
}
.trade-open:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

@media (max-width: 860px) {
  .trade-facts { gap: 6px; }
  .trade-facts .trade-spacer { flex-basis: 100%; }
}

/* ── Which half of the journal to read ─────────────────────────────────────
   All of it, the trades, or everything that is not a trade. Three links rather
   than a select: this is which of two questions the page is answering, not a
   filter dialled in among others, and the third state has to be visible for it
   to be discovered at all. It replaced Expand all, which had nothing left to
   expand once trades stopped being boxes. */
.seg {
  display: inline-flex; border: 1px solid var(--border-hi);
  border-radius: var(--radius-sm); overflow: hidden;
}
.seg-b {
  padding: 5px 13px; border-right: 1px solid var(--border-hi);
  background: var(--surface-2); color: var(--mute);
  font-size: 13px; font-weight: var(--fw-med); text-decoration: none;
  white-space: nowrap; transition: background .12s ease, color .12s ease;
}
.seg-b:last-child { border-right: none; }
.seg-b:hover { background: var(--surface-3); color: var(--dim); text-decoration: none; }
.seg-b.on { background: color-mix(in srgb, var(--teal) 14%, transparent); color: var(--teal); }
.seg-b:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* ── A switch ──────────────────────────────────────────────────────────────
   Two states of one thing, thrown rather than chosen. The segmented control
   above is for three-and-more, where each option has to be readable before it
   is picked; this is for on and off, where the only thing worth showing is
   which of the two it is currently in.

   Driven by a class on <html> rather than by [aria-pressed], because that
   class is written before first paint and the button is not parsed until
   later — hanging the paint off the attribute would show every page in the
   other state for a frame. The attribute is still set, for anything reading
   the page rather than looking at it. */
.swtch {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 0; border: none; background: none;
  color: var(--mute); font-family: inherit; font-size: 13px;
  font-weight: var(--fw-med); line-height: 1; cursor: pointer;
  transition: color .12s ease;
}
.swtch:hover { color: var(--dim); }
.swtch-track {
  position: relative; flex: 0 0 auto; width: 40px; height: 23px;
  border-radius: 999px; background: var(--surface-3);
  border: 1px solid var(--border-hi);
  transition: background .18s ease, border-color .18s ease;
}
.swtch-knob {
  position: absolute; top: 2px; left: 2px; width: 17px; height: 17px;
  border-radius: 50%; background: var(--dim);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .55);
  /* Overshoots very slightly and settles, which is the whole of why a switch
     reads as a switch and not as a box that changed colour. */
  transition: transform .2s cubic-bezier(.3, .8, .4, 1.2), background .18s ease;
}
.swtch:focus-visible { outline: none; }
.swtch:focus-visible .swtch-track { outline: 2px solid var(--teal); outline-offset: 2px; }
html.jf-sans .swtch { color: var(--teal); }
html.jf-sans .swtch-track { background: color-mix(in srgb, var(--teal) 28%, transparent); border-color: var(--teal); }
html.jf-sans .swtch-knob { transform: translateX(17px); background: var(--teal); }
@media (prefers-reduced-motion: reduce) {
  .swtch-track, .swtch-knob { transition: none; }
}

/* ── The journal in the dashboard's own typeface ────────────────────────────
   Everything a terminal writes is set in mono, because a journal is a log and
   a log's columns have to line up under each other. That is the right default
   and it is tiring to sit in for an hour, so the switch moves the journal onto
   the face the rest of the dashboard uses.

   All of it, not the sentences alone. This started as a rule over the prose,
   on the grounds that clocks and field values are read by lining them up
   rather than by reading them — which is true, and beside the point: half a
   journal in one face and half in another is a page that looks broken, and the
   timestamp beside a sentence is part of reading the sentence. A line, a
   record and a trade page each go over whole.

   Every container the journal draws into is named below, and the change is
   made on their descendants rather than on them — `.mono` is set on a good
   number of the spans inside, and a rule on the container alone would be
   inherited straight past. It is scoped to those containers for the same
   reason it is not on <body>: the trades table, the fleet tiles and the P&L
   figures are columns of numbers that were never prose, and none of them is
   what anybody is sitting and reading. */
html.jf-sans .logline, html.jf-sans .logline *,
html.jf-sans .rec, html.jf-sans .rec *,
html.jf-sans .tj, html.jf-sans .tj *,
html.jf-sans .tj-head, html.jf-sans .tj-head *,
html.jf-sans .tj-facts, html.jf-sans .tj-facts *,
html.jf-sans .tj-fallback, html.jf-sans .tj-fallback *,
html.jf-sans .trade-run, html.jf-sans .trade-run *,
html.jf-sans .trade-facts, html.jf-sans .trade-facts * {
  font-family: var(--sans);
}
/* Sans at a given pixel size reads smaller than mono at the same one, so the
   prose comes up to the body size the rest of the site is set in. Half the
   point of the switch is the size; the face is the other half. Only the
   sentences move — a field value that grew would push its neighbours out of
   the grid they are lined up in, which is the one thing the mono was buying
   and the one thing worth keeping. */
/* Not the line that closes a trade run: that one is a `.msg` by position but a
   row of chips and figures by content, and growing it only pushes them apart. */
html.jf-sans .logline .msg:not(.trade-facts) { font-size: var(--fs-body); line-height: 1.5; }
html.jf-sans .tj-say { font-size: var(--fs-sm); line-height: 1.6; }
html.jf-sans .rec-say { font-size: var(--fs-sm); line-height: 1.6; }
html.jf-sans .retcode .retcode-what,
html.jf-sans .retcode .retcode-do { font-size: var(--fs-sm); }

/* ── The bridge to MT5 ─────────────────────────────────────────────────────
   The position number and the comment the EA wrote onto the order — the only
   two strings that are the same in the terminal's History tab and on this
   site, which is the whole reason they are drawn like this rather than left as
   ordinary text.

   These were the popup's head. The popup is gone; the trade journal page keeps
   the three pieces of it that were about MT5 rather than about being a dialog,
   and the rest went with it. */

.journey-ticket {
  padding: 2px 9px; border-radius: 6px;
  border: 1px solid var(--border-hi); background: var(--surface);
  color: #C0A6FF; font-size: 12.5px;
}
.journey-guess {
  padding: 1px 8px; border-radius: 5px; border: 1px dashed var(--border-hi);
  color: var(--amber); font-size: 10.5px; font-weight: var(--fw-bold);
  letter-spacing: .04em; text-transform: uppercase;
}
.jf { display: inline-flex; align-items: baseline; gap: 8px; font-size: 13px; }
.jf > b {
  color: var(--dim); font-size: 10.5px; font-weight: var(--fw-bold);
  letter-spacing: .1em; text-transform: uppercase;
}
.jf .up { color: var(--up); font-weight: var(--fw-bold); }
.jf .down { color: var(--down); font-weight: var(--fw-bold); }
.journey-cmt-what { flex: 1 1 320px; min-width: 0; color: var(--dim); line-height: 1.55; }

/* ── The trade journal ─────────────────────────────────────────────────────
   One trade, whole, on a page of its own — the screen the journal's dropdown
   was trying and failing to be.

   Stacked, six records of six groups each is a screen and a half of scrolling
   with no order of importance in it. Here the steps are a list down one side
   and exactly one of them is open on the other: the same information, a sixth
   of the height, and the reader chooses what they are looking at.

   The left column is the real win. Read on its own it is the whole trade in
   six short lines — got in, cut the risk, went risk-free, locked 62 points,
   trailed out, made 3.40 — which is the story the journal could never show,
   because showing it meant showing all hundred and fifty numbers at once. */

.tj-card { overflow: hidden; }
.tj-head { gap: 10px; }
.tj-head h2 { font-size: 1.05rem; }
.tj-head .trade-emoji { font-size: 22px; }

/* What the trades table knows and a journal record does not: what the price
   did, whose strategy this was, and the string in the MT5 History tab. */
.tj-facts {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 22px;
  padding: 10px 16px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.tj-facts .jf.wide { flex: 1 1 340px; min-width: 0; }
.tj-fallback {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 22px;
  padding: 0 16px 16px;
}

.tj { display: grid; grid-template-columns: 320px minmax(0, 1fr); align-items: start; }

/* The spine. Sticky, because the panel beside it is taller than it is and the
   list is the thing you navigate by — losing it off the top of the screen is
   losing the table of contents. */
.tj-spine {
  position: sticky; top: 0; align-self: start;
  max-height: 100vh; overflow-y: auto;
  border-right: 1px solid var(--border); background: var(--surface);
}
/* The step count, and the switch for the face the steps under it are set in —
   which belongs here rather than in the page header, because this column is
   the page's prose and the switch is about reading it. Wrapping, because at
   320px the two of them together are a squeeze and a switch overhanging the
   spine's edge is worse than one on a second line. */
.tj-spine-head {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 5px 10px;
  padding: 9px 14px; border-bottom: 1px solid var(--border);
  color: var(--mute); font-size: 11px; font-weight: var(--fw-bold);
  letter-spacing: .1em; text-transform: uppercase;
}
.tj-spine-head .swtch { font-size: 11px; letter-spacing: 0; text-transform: none; }
.tj-step {
  display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 10px;
  padding: 9px 14px; border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent; color: var(--dim);
  font-weight: var(--fw-body); text-decoration: none;
  transition: background .12s ease, border-color .12s ease;
}
.tj-step:hover { background: var(--surface-2); text-decoration: none; color: var(--dim); }
.tj-step.on { background: var(--surface-2); border-left-color: var(--teal); }
.tj-step:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.tj-n {
  padding-top: 1px; color: var(--mute);
  font-family: var(--mono); font-size: 12.5px; font-variant-numeric: tabular-nums;
}
.tj-step.on .tj-n { color: var(--teal); font-weight: var(--fw-bold); }
.tj-t { min-width: 0; }
.tj-top { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 2px; }
/* The phase pill carries a left margin everywhere else, where it trails a code
   in a line of prose. Here it starts the row. */
.tj-top .phase { margin-left: 0; cursor: default; }
.tj-clock { color: var(--mute); font-size: 11.5px; }
.tj-say {
  display: block; min-width: 0; color: var(--dim);
  font-family: var(--mono); font-size: 12.5px; line-height: 1.5;
  overflow-wrap: anywhere;
}

.tj-panel { min-width: 0; background: var(--surface-2); }
/* Every pane is rendered; the script shows one. The base state is all of them
   visible, so a browser running no scripts gets the page as every step in
   order — a worse read than the panel, and a perfectly good one, with the
   spine's anchors still jumping into it. `.js` goes on <html> in the head,
   before first paint, so there is no moment where the rest flash past. */
.tj-pane { border-bottom: 1px solid var(--border); }
.tj-pane:last-child { border-bottom: none; }
.js .tj-pane { display: none; border-bottom: none; }
.js .tj-pane.on { display: block; }
/* The record inside a pane is the whole of it — no second border round it, and
   nothing to separate it from since only one is ever on screen. */
.tj-pane > .rec, .tj-pane > .logline { border-bottom: none; }

@media (max-width: 900px) {
  .tj { grid-template-columns: 1fr; }
  .tj-spine {
    position: static; max-height: 320px;
    border-right: none; border-bottom: 1px solid var(--border);
  }
}

/* ── Updates you are not being interrupted by ──────────────────────────────
   A beat landed while you were scrolled into the page. Nothing moved; this
   says there is something to see and loads it when you ask. */

/* The floating "new activity" button that used to live here is gone — see the
   note where it was in base.html. A live page that offers to scroll you
   somewhere is a live page that will eventually do it at the wrong moment. */

/* ── One trade record ──────────────────────────────────────────────────────
   An entry, a stop move, an exit, a summary — or one of the endings that is
   not an exit. Drawn as a field grid rather than a sentence, because a trade
   is fifteen numbers and a paragraph is the wrong container for fifteen
   numbers.

   The layout rule the whole thing hangs on: --cols is set per record from how
   many groups it actually has, so the groups always fill the width exactly.
   Nothing is ever cut off and the page never scrolls sideways — at narrow
   widths the grid steps down to three columns, then two, then one. */

.rec {
  margin: 0; border-bottom: 1px solid var(--border);
  background: var(--surface-2); font-family: var(--mono);
}
.rec:last-child { border-bottom: none; }

.rec-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 9px;
  padding: 9px 14px 7px; font-size: 14.5px; color: var(--dim);
}
.rec-head .ts { color: var(--dim); }
.rec-head .code { color: #C0A6FF; font-weight: var(--fw-med); }
.rec-head .trade-mark { font-size: 15px; }
.rec-spacer { flex: 1 1 auto; }
/* The one-line version of the record — the same sentence the EA writes into
   the terminal's own Experts tab. Second billing to the grid, but present, so
   the two logs can be read against each other. */
.rec-say {
  flex: 1 1 340px; min-width: 0; text-align: right;
  color: var(--dim); font-size: 13px; line-height: 1.55;
}
.rec.lvl-warn  .rec-say { color: var(--amber); }
.rec.lvl-error .rec-say { color: var(--down); }

/* ── The few a record is actually read for ─────────────────────────────────
   The grid below holds everything a record carries, which for a stop move is
   twenty-six values at one size. Twenty-six values at one size is none: there
   is nothing for the eye to land on, so the grid gets skipped whole and the
   record reads as noise. That — not the amount of data — is what made a record
   unreadable, and hiding it behind a dropdown never touched it.

   So the handful that answer the question the record exists to answer are
   drawn large, above it. Which handful is services/journal_fields.HEADLINE, so
   the choice is made once and in words rather than here in a stylesheet. */
.rec-lead {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  gap: 1px; background: var(--border); border-top: 1px solid var(--border);
}
.rec-lead .hl { background: var(--surface-2); padding: 11px 14px 12px; min-width: 0; }
.rec-lead .hl > b {
  display: flex; align-items: center; gap: 2px; margin-bottom: 2px;
  color: var(--mute); font-family: var(--sans); font-size: 10.5px;
  font-weight: var(--fw-bold); letter-spacing: .1em; text-transform: uppercase;
}
.rec-lead .hl > span {
  display: block; color: var(--text); font-size: 19px; font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums; letter-spacing: -.01em;
  line-height: 1.25; overflow-wrap: anywhere;
}
.rec-lead .hl > span.up   { color: var(--up); }
.rec-lead .hl > span.down { color: var(--down); }
/* The "i" is a control sized for a body line; against a 10.5px label it has to
   come down with it or the label sits crooked around it. */
.rec-lead .hl > b .info-btn { width: 13px; height: 13px; font-size: 9px; }

/* The rest of the record, and how to ask for it. "There is more" and "there
   are twenty-two more" are different invitations, so the count is on the
   button. */
.rec-more {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 14px; border: none; border-top: 1px solid var(--border);
  background: var(--surface); color: var(--mute);
  font-family: var(--sans); font-size: 12.5px; font-weight: var(--fw-med);
  letter-spacing: .03em; text-align: left; cursor: pointer;
  transition: color .12s ease, background .12s ease;
}
.rec-more:hover { color: var(--teal); background: var(--surface-3); }
.rec-more:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }
.rec-more .caret { font-size: 10px; transition: transform .15s ease; }
.rec-more[aria-expanded="true"] .caret { transform: rotate(180deg); }

/* Folded on `.js` only. A browser running no scripts would never get the
   button above to work, so it is served the grid open rather than a fold it
   cannot undo — see the head of base.html, which sets that class. */
.js .rec-grid.folded { display: none; }

/* One row per group, reading across — not a column per group reading down.
   Six columns on a laptop is about 150px each, and "Locked in +619 pts" does
   not fit in 150px. So a third of the values broke over two and three lines,
   "+619" stacked above "pts", "4384.2" above "80", and a grid meant to be
   scannable came out as ragged confetti.

   Every value on its own is short; it is the label-and-value *pair* that is
   long. Laid out as pairs flowing along a line, a wrap falls between two pairs
   instead of through the middle of a number. */
.rec-grid { border-top: 1px solid var(--border); }
.rec-row {
  display: grid; grid-template-columns: 104px minmax(0, 1fr);
  gap: 12px; align-items: baseline;
  padding: 7px 14px; border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.rec-row:last-child { border-bottom: none; }
.rec-cap {
  font-family: var(--sans); font-size: 10.5px; font-weight: var(--fw-bold);
  letter-spacing: .1em; text-transform: uppercase; color: var(--mute);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* column-gap does the separating, so no bullet character has to be rendered
   and none is ever left stranded at the end of a wrapped line. */
.rec-fields {
  display: flex; flex-wrap: wrap; gap: 4px 22px; min-width: 0;
}
/* The pair. It is a flex item, so when the row runs out of room the *whole*
   pair moves to the next line — which is what stops a value being torn off its
   label, and stops "+619" and "pts" landing on separate lines. Text inside is
   left free to wrap, because a few of these fields are really sentences (Rule,
   Reason, Outcome) and nowrap would push those off the side of the card. Short
   values never reach a wrap point: the row is the full width of the record. */
.rec-f {
  display: inline-flex; align-items: baseline; gap: 7px;
  font-size: 13.5px; line-height: 1.75; min-width: 0;
}
.rec-f > b {
  display: inline-flex; align-items: center; white-space: nowrap;
  color: var(--mute); font-weight: var(--fw-med);
}
.rec-f > span {
  color: var(--text); font-weight: var(--fw-med);
  font-variant-numeric: tabular-nums; min-width: 0;
}
.rec-f > span.up   { color: var(--up); font-weight: var(--fw-bold); }
.rec-f > span.down { color: var(--down); font-weight: var(--fw-bold); }

/* The sentences: why the trade was taken, what an ending means, the day's
   running total. Full width, because a reason squeezed into a 180px cell is a
   reason nobody reads. */
.rec-foot {
  display: flex; gap: 10px; padding: 6px 14px;
  border-top: 1px solid var(--border);
  font-size: 14px; line-height: 1.7; color: var(--dim);
}
.rec-foot > b {
  flex: 0 0 74px; display: inline-flex; align-items: center;
  color: var(--dim); font-size: 11px; font-weight: var(--fw-bold);
  letter-spacing: .1em; text-transform: uppercase; padding-top: 3px;
}
.rec-foot > span { min-width: 0; }
.rec-foot.notes { padding-bottom: 10px; }

/* The "i" is smaller inside a record than in a form: it sits on a 12px label
   in a dense grid and the form's 15px circle throws the baseline out. */
.rec-f .info-btn, .rec-foot .info-btn {
  width: 12px; height: 12px; margin-left: 5px; font-size: 8.5px;
}

/* Records used to carry a rail each, in a colour per phase: blue entry, teal
   trail, violet exit, lime summary, amber for the bad endings. Five more
   colours running down the inside of the block's own green-or-red rail, none
   of them saying anything the phase pill next to the step number does not say
   in words. Two stripes an inch apart, disagreeing.

   What is left is structure, not code: a dim rule that separates one record
   from the next, and the two severities worth spotting from a distance. */
.rec { box-shadow: inset 2px 0 0 var(--border); }
.rec.lvl-warn  { box-shadow: inset 2px 0 0 var(--amber); }
.rec.lvl-error { box-shadow: inset 2px 0 0 var(--down); }

/* The three column-count breakpoints that used to be here are gone with the
   columns. A row of pairs reflows on its own at any width — that is the whole
   advantage of it — so the only thing narrow screens still need is the group
   caption moving above its fields rather than beside them. */
@media (max-width: 620px) {
  .rec-row { grid-template-columns: 1fr; gap: 3px; }
}
@media (max-width: 480px)  {
  .rec-say { flex-basis: 100%; text-align: left; }
  .rec-foot { flex-direction: column; gap: 2px; }
}

/* ── What the broker's number means ────────────────────────────────────────
   MT5 gives an EA five words for a rejection and "10011 common error" is all
   of them. Whether that was a throttle, a stop too near the market or a
   read-only account is the only thing worth knowing, so it is printed under
   the line rather than left as a number to go and look up. */

.retcode {
  display: block; margin: 6px 0 2px; padding: 7px 10px;
  border-left: 2px solid var(--border-hi); border-radius: 0 6px 6px 0;
  background: var(--surface-3); color: var(--dim);
  font-size: 13px; line-height: 1.6; white-space: normal;
}
.retcode b { color: #C0A6FF; font-weight: 700; }
.retcode .retcode-what { display: block; color: var(--text); opacity: .82; }
.retcode .retcode-do { display: block; color: var(--dim); font-style: normal; }
.retcode .retcode-do::before { content: "→ "; color: var(--violet); font-weight: 700; }
.logline.error .retcode { border-left-color: var(--down); }
.logline.warn  .retcode { border-left-color: var(--amber); }

/* ── Charts ──────────────────────────────────────────────────────────────── */

.chart-wrap { position: relative; width: 100%; height: 260px; }
.chart-wrap canvas { width: 100%; height: 100%; display: block; }
/* A curve given the whole width deserves the height to match, or it is a wide
   flat line. 48 hours of scalping has its shape in the vertical. */
.chart-wrap.wide { height: 340px; }
@media (max-width: 860px) { .chart-wrap.wide { height: 260px; } }

/* An action row at the foot of a card: the buttons that act on what is above
   them, all one size, with the destructive one across the gap rather than
   hanging underneath in a smaller weight. */
.card-foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 10px;
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border);
}
.card-body.tight + .card-foot { margin-top: 0; padding: 14px 16px; }
.foot-gap { flex: 1 1 auto; }

/* Rows the terminal has not confirmed since it went quiet. Not wrong, not
   current — see the banner above the table. */
tr.stale > td { opacity: .62; }
.chart-legend { display: flex; gap: 16px; font-size: 12.5px; font-weight: var(--fw-med); color: var(--dim); margin-top: 8px; flex-wrap: wrap; align-items: center; }
.chart-legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; margin-right: 5px; }
.chart-legend .legend-note { margin-left: auto; font-weight: var(--fw-body); opacity: .8; }

/* The window the curve covers. A segmented control rather than a select: seven
   options, one of them always in force, and the answer to "what am I looking
   at" has to be readable without opening anything. */
.range-row { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px;
             background: var(--surface-2); border: 1px solid var(--border); }
.rangebtn {
  appearance: none; border: 0; cursor: pointer; background: transparent;
  color: var(--dim); font: inherit; font-size: 12px; font-weight: var(--fw-med);
  padding: 4px 10px; border-radius: 7px; line-height: 1.5;
}
.rangebtn:hover { color: var(--text); background: var(--surface-3); }
.rangebtn.on { color: var(--bg); background: var(--teal); }
.card.loading .rangebtn { cursor: progress; }
.card.loading .chart-wrap { opacity: .55; transition: opacity .12s; }

/* The crosshair readout. Follows the pointer inside the chart, so it is
   pointer-transparent — otherwise it would sit under the cursor and eat the
   very pointermove events that place it. */
.chart-tip {
  position: absolute; z-index: 3; pointer-events: none;
  min-width: 168px; padding: 8px 10px; border-radius: 10px;
  background: var(--surface-3); border: 1px solid var(--border-hi);
  box-shadow: 0 10px 26px rgba(0,0,0,.45);
  font-size: 12.5px; color: var(--text);
}
.chart-tip .tip-when { color: var(--dim); font-size: 11.5px; font-weight: var(--fw-med);
                       margin-bottom: 6px; white-space: nowrap; }
.chart-tip .tip-row { display: flex; align-items: center; gap: 6px; white-space: nowrap;
                      line-height: 1.9; }
.chart-tip .tip-row i { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 auto; }
.chart-tip .tip-row b { margin-left: auto; font-family: var(--mono); font-weight: 700; }
.chart-tip .tip-gap { color: var(--dim); margin-top: 4px; padding-top: 5px;
                      border-top: 1px solid var(--border); }
.chart-tip .up { color: var(--up); }
.chart-tip .down { color: var(--down); }

/* ── Login ───────────────────────────────────────────────────────────────── */

.login-page {
  min-height: 100vh; display: grid; place-items: center; padding: 20px;
}
.login-card {
  width: 100%; max-width: 400px;
  background: var(--surface);
  border: 1px solid var(--border-hi);
  border-radius: 16px;
  padding: 32px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .6);
  position: relative;
}
.login-card::before {
  content: ""; position: absolute; inset: -1px -1px auto; height: 3px;
  border-radius: 16px 16px 0 0; background: var(--accent-grad);
}
.login-logo {
  width: 48px; height: 48px; border-radius: 14px; margin: 0 auto 16px;
  background: var(--accent-grad); display: grid; place-items: center;
  font-weight: 800; color: #05121A; font-size: 18px;
  box-shadow: var(--glow-teal);
}

/* ── Misc ────────────────────────────────────────────────────────────────── */

.muted { color: var(--mute); }
.dim   { color: var(--dim); }
.small { font-size: 12.5px; }
.right { text-align: right; }
.nowrap { white-space: nowrap; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.stack { display: grid; gap: 4px; }

/* ── Section separator ───────────────────────────────────────────────────────
   A .grid already puts a visible gutter between two cards standing side by
   side. Stacked sections had nothing but their own borders, so a page read as
   one continuous strip of boxes with no telling where one subject ended and
   the next began. This is the horizontal twin of that gutter: a drawn rule in
   the gap, faded at both ends so it separates without boxing anything in.
   Every page puts one between its top-level sections, vertical or horizontal. */
hr.sep {
  border: 0; height: 1px; margin: 26px 0;
  background: linear-gradient(90deg, transparent 0%, var(--border-hi) 6%,
                              var(--border-hi) 94%, transparent 100%);
}
/* A card and a grid already carry a bottom margin of their own; without this
   the rule would sit nearer the section below it than the one above. */
.card + hr.sep, .grid + hr.sep, .filterbar + hr.sep, .tabs + hr.sep { margin-top: 8px; }
hr.sep.tight { margin: 16px 0; }
.card + hr.sep.tight, .grid + hr.sep.tight { margin-top: 2px; }
/* A rule that separates nothing is a rule floating at the foot of the page.
   Sections that stay in the DOM while empty — a terminal's queued-commands card,
   which has to exist before the first command can appear in it — take their
   separator with them. */
hr.sep:has(+ [hidden]) { display: none; }

.kv { display: grid; grid-template-columns: 160px 1fr; gap: 9px 14px; font-size: var(--fs-sm); }
.kv dt { color: var(--dim); font-weight: var(--fw-med); }
.kv dd { margin: 0; font-family: var(--mono); color: var(--text); }
/* The same list in a full-width card. One label-and-value pair per half, so the
   rows keep a readable measure instead of a 160px label followed by a value
   floating alone at the far end of the screen. Auto-placement fills left to
   right, which is the order the pairs are written in. */
@media (min-width: 1000px) {
  .kv.split { grid-template-columns: 160px minmax(0, 1fr) 160px minmax(0, 1fr); column-gap: 28px; }
}

.codeblock {
  background: #0A0D14; border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px 14px; font-family: var(--mono); font-size: 12.5px;
  color: var(--dim); overflow-x: auto; white-space: pre;
}
/* A command with a Copy on it. The button floats over the right-hand end and
   the block keeps room for it, so a long command scrolls under the button
   rather than being hidden by it. */
.codeblock-wrap { position: relative; }
.codeblock-wrap .codeblock { padding-right: 88px; }
.codeblock-copy { position: absolute; top: 7px; right: 7px; }
.codeblock-copy.ok { color: var(--up); border-color: var(--up); }

/* An action sitting in a fieldset's legend — "select all" beside the step it
   applies to, rather than a row of its own under the grid it acts on. */
.legend-act { margin-left: 12px; vertical-align: middle; text-transform: none; letter-spacing: 0; }

/* ── Date + time picker ──────────────────────────────────────────────────────
   One popover holding a calendar and a clock face. It is fixed-position and
   parented to <body> because .filterbar and .card both clip their overflow —
   drawn in flow it would be sliced off at the card's border, which is the same
   reason the tooltip lives there.

   The field it opens from is an ordinary text input carrying the app's one
   datetime format, so it can still be typed into, and what is on screen is
   exactly what gets submitted. See static/js/datetime.js. */

.dtfield { position: relative; }
.dtfield input { padding-right: 34px; cursor: pointer; }
.dtfield .dtfield-ico {
  position: absolute; right: 10px; bottom: 9px;
  width: 16px; height: 16px; color: var(--mute); pointer-events: none;
  transition: color .16s ease;
}
.dtfield input:focus ~ .dtfield-ico,
.dtfield input[aria-expanded="true"] ~ .dtfield-ico { color: var(--teal); }

.dtp {
  position: fixed; z-index: 210; width: 320px;
  background: var(--surface); border: 1px solid var(--border-hi);
  border-radius: var(--radius); box-shadow: var(--shadow);
  font-size: var(--fs-sm); user-select: none;
}
.dtp[hidden] { display: none; }

.dtp-tabs {
  display: flex; align-items: center; gap: 6px;
  padding: 9px 10px; border-bottom: 1px solid var(--border);
}
.dtp-tab {
  padding: 5px 12px; border-radius: 999px;
  border: 1px solid transparent; background: transparent;
  color: var(--mute); font: inherit; font-weight: var(--fw-bold); cursor: pointer;
}
.dtp-tab:hover { color: var(--text); }
.dtp-tab.on { background: var(--surface-3); border-color: var(--border-hi); color: var(--teal); }
/* The value as it will actually be submitted, always visible while picking —
   the point of a custom control over the native one is that this string is
   predictable, so it should not be a surprise on close. */
.dtp-read {
  margin-left: auto; font-family: var(--mono); font-size: 11.5px;
  color: var(--dim); white-space: nowrap;
}
.dtp-read.empty { color: var(--mute); font-style: italic; }

.dtp-pane { padding: 10px 12px 12px; }
.dtp-pane[hidden] { display: none; }

.dtp-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.dtp-arrow, .dtp-month {
  border: 1px solid transparent; background: transparent; color: var(--dim);
  font: inherit; cursor: pointer; border-radius: var(--radius-sm);
}
.dtp-arrow { width: 28px; height: 28px; font-size: 17px; line-height: 1; }
.dtp-month { padding: 4px 10px; font-weight: var(--fw-bold); color: var(--text); }
.dtp-arrow:hover, .dtp-month:hover { background: var(--surface-2); border-color: var(--border-hi); }

.dtp-dow, .dtp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dtp-dow {
  margin-bottom: 4px; text-align: center;
  font-size: var(--fs-label); letter-spacing: .06em; text-transform: uppercase;
  font-weight: var(--fw-bold); color: var(--mute);
}
.dtp-day {
  height: 34px; border: 1px solid transparent; border-radius: var(--radius-sm);
  background: transparent; color: var(--dim);
  font: inherit; font-weight: var(--fw-med); cursor: pointer;
}
.dtp-day:hover { background: var(--surface-2); border-color: var(--border-hi); color: var(--text); }
.dtp-day.out { cursor: default; }
/* Today is outlined, the chosen day is filled: two different facts about a
   cell, so they have to be able to show at once on the same cell. */
.dtp-day.today { border-color: var(--border-hi); color: var(--text); }
.dtp-day.on {
  background: var(--teal); border-color: var(--teal); color: #071022;
  font-weight: var(--fw-bold);
}

.dtp-digits { display: flex; align-items: center; justify-content: center; gap: 4px; margin-bottom: 10px; }
.dtp-digits > span { color: var(--mute); font-size: 22px; font-weight: var(--fw-bold); }
.dtp-unit {
  padding: 3px 9px; border-radius: var(--radius-sm);
  border: 1px solid transparent; background: transparent;
  color: var(--mute); font-family: var(--mono); font-size: 22px;
  font-weight: var(--fw-bold); cursor: pointer;
}
.dtp-unit:hover { color: var(--dim); }
/* Which half of the clock the needle is driving. Without it the face is
   ambiguous — the same twelve positions mean hours or minutes. */
.dtp-unit.on { background: var(--surface-3); border-color: var(--teal); color: var(--teal); }

.dtp-ampm { display: flex; flex-direction: column; gap: 2px; margin-left: 8px; }
.dtp-ampm button {
  padding: 2px 8px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: transparent;
  color: var(--mute); font: inherit; font-size: 11px; font-weight: var(--fw-bold);
  cursor: pointer; letter-spacing: .04em;
}
.dtp-ampm button:hover { color: var(--text); border-color: var(--border-hi); }
.dtp-ampm button.on { background: var(--violet); border-color: var(--violet); color: #0B0616; }

.dtp-face { display: block; width: 220px; height: 220px; margin: 0 auto; touch-action: none; cursor: grab; }
.dtp-face:active { cursor: grabbing; }
.dtp-dial { fill: #0A0D14; stroke: var(--border); stroke-width: 1.5; }
.dtp-hand { stroke: var(--teal); stroke-width: 2.5; stroke-linecap: round; }
.dtp-knob { fill: color-mix(in srgb, var(--teal) 22%, transparent); stroke: var(--teal); stroke-width: 2; }
.dtp-pin { fill: var(--teal); }
.dtp-num {
  fill: var(--dim); font-family: var(--sans); font-size: 15px;
  font-weight: var(--fw-med); pointer-events: none;
}
.dtp-num.on { fill: #071022; font-weight: var(--fw-bold); }
/* Invisible, and the only thing on the dial that takes a click: the glyphs are
   too small to aim at, so each one gets a target the size of the knob. */
.dtp-hit { fill: transparent; }

.dtp-hint { margin: 8px 0 0; text-align: center; font-size: 12px; color: var(--mute); }

.dtp-foot {
  display: flex; gap: 8px; justify-content: flex-end;
  padding: 10px 12px; border-top: 1px solid var(--border);
}
.dtp-foot [data-act="clear"] { margin-right: auto; }

/* A range is one control that happens to have two ends, so it is drawn as one
   block with a single label rather than as two unrelated fields that the reader
   has to work out are a pair. */
.dtrange { display: flex; align-items: flex-end; gap: 8px; flex: 1 1 520px; }
.dtrange > .field { flex: 1 1 190px; min-width: 150px; margin-bottom: 0; }
.dtrange .dtrange-sep { padding-bottom: 10px; color: var(--mute); font-weight: var(--fw-bold); }

/* The windows anyone actually asks for, sitting on the range they fill in.
   Small and quiet until hovered: they are a shortcut through the two fields
   beside them, not a second control competing with them. */
.dtpresets { display: flex; gap: 4px; padding-bottom: 4px; }
.dtpreset {
  padding: 5px 10px; border-radius: 7px;
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--mute); font: inherit; font-size: 12.5px; font-weight: var(--fw-bold);
  cursor: pointer; transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.dtpreset:hover {
  color: var(--teal); border-color: var(--teal); background: var(--surface-3);
}
.dtpreset:focus-visible { outline: 2px solid var(--teal); outline-offset: 1px; }

@media (max-width: 560px) {
  .dtp { width: calc(100vw - 16px); }
  .dtrange { flex-wrap: wrap; }
  .dtrange .dtrange-sep { display: none; }
  .dtpresets { flex-basis: 100%; }
}

/* ── Active filters ──────────────────────────────────────────────────────────
   What the filter bar above has actually done, as a row of removable chips.
   Only rendered when something is filtered, so an unfiltered page carries no
   empty strip — see the filter_chips macro. */

.activefilters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: -8px 0 16px;
}
.af-lead {
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .1em;
  font-weight: var(--fw-bold); color: var(--mute);
}
/* The only reset there is, now that the bars apply themselves — so it is drawn
   to be found rather than tucked in as a ghost button, and it only ever exists
   while there is something to clear. */
.af-clear {
  margin-left: 4px; font-weight: var(--fw-bold); letter-spacing: .01em;
}

.chip {
  display: inline-flex; align-items: center; gap: 0;
  border: 1px solid var(--border-hi); border-radius: 999px;
  background: var(--surface-2); overflow: hidden;
  font-size: 12.5px; line-height: 1;
}
/* Which filter, then what it is set to. The key is muted and the value is not,
   because the value is the part being scanned for. */
.chip-k {
  padding: 6px 8px 6px 11px; color: var(--mute);
  font-weight: var(--fw-bold); text-transform: uppercase;
  font-size: 10.5px; letter-spacing: .06em;
}
.chip-v {
  padding: 6px 4px 6px 0; color: var(--text);
  font-weight: var(--fw-med); font-family: var(--mono);
  max-width: 30ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chip-x {
  display: inline-flex; align-items: center; justify-content: center;
  align-self: stretch; padding: 0 9px;
  border-left: 1px solid var(--border);
  color: var(--mute); font-size: 11px; font-weight: var(--fw-bold);
  text-decoration: none;
  transition: background .14s ease, color .14s ease;
}
.chip-x:hover {
  background: color-mix(in srgb, var(--down) 22%, transparent); color: var(--down); text-decoration: none;
}

/* The schema default under a parameter field. Muted when the value above it
   still matches the file, and lit when it does not — that difference is the
   whole point of the line, and is what makes writing schema.json visible on a
   form whose own values deliberately did not move. */
.fieldhint {
  display: block; margin-top: 4px;
  font-size: 11.5px; font-family: var(--mono); color: var(--mute);
}
.fieldhint.differs { color: var(--amber); }
.fieldhint em { font-style: normal; color: var(--dim); }

/* ── Trading windows ───────────────────────────────────────────────────────
   Two clocks in one table. The IST columns are what you set; the columns after
   them are those same hours on the broker's clock — the half nobody can work
   out in their head, and the half that decides whether the window means what
   it looks like.

   A row reads rather than edits: times are text, and only the row being
   changed holds inputs. Six time boxes stacked down a table is a form that has
   to be scanned to find what moved. */
.sessions td.mono { font-family: var(--mono); white-space: nowrap; }
.sessions th.act, .sessions td.act { text-align: right; width: 1%; white-space: nowrap; }
.rowacts { display: inline-flex; gap: 6px; justify-content: flex-end; }
.sessions input[type=time] { width: auto; min-width: 128px; }
/* The one row that is a form. Lit rather than merely different, because its
   Save is the only button on the page that does not save anything to the
   server — it puts the window back in the table, and the reader has to be able
   to tell which state the table is in. */
.sessions tr.editing td { background: var(--surface-2); }
.sessions tr.editing td:first-child { box-shadow: inset 2px 0 0 var(--teal); }
.sessions tr.editing:hover td { background: var(--surface-2); }
.sessions .pill.sm { margin-left: 6px; vertical-align: middle; }

/* The draft — one window's worth of controls, kept out of the table until it
   is a real window, so what the table shows is only what will be pushed. */
.session-draft {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px;
  margin-top: 12px; padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border-hi); border-radius: var(--radius);
}
.session-draft[hidden] { display: none; }
.session-draft .field { margin-bottom: 0; flex: 0 0 auto; }
.session-draft .btn-row { margin: 0; margin-left: auto; }
/* The conversion, live, while the hour is still being chosen. Full width so a
   second trade server wraps under the first rather than squeezing the fields. */
.session-draft-say {
  flex: 1 1 100%; order: 3;
  font-family: var(--mono); font-size: 12px; color: var(--dim);
}
.session-say {
  margin-top: 10px; font-size: 12.5px; line-height: 1.6; color: var(--dim);
}
.session-say b { color: var(--text); font-family: var(--mono); font-weight: var(--fw-med); }

@media (max-width: 640px) {
  .session-draft .field { flex: 1 1 140px; }
  .session-draft .btn-row { margin-left: 0; flex: 1 1 100%; }
}

/* ── What one save changed ─────────────────────────────────────────────────
   A cell of "label: was → is" lines, one per parameter that moved. A save that
   moved four numbers is four lines rather than one run-on sentence, because
   the question being asked of this table is "which one was it". */
.changelist { margin: 0; padding: 0; list-style: none; }
.changelist li { display: flex; flex-wrap: wrap; gap: 6px; align-items: baseline; padding: 1px 0; }
.changelist .ck { color: var(--dim); min-width: 150px; }
.changelist .mono { font-family: var(--mono); font-size: 12.5px; }
/* The line beside Save & push after a history row is applied: the fields have
   moved and nothing has been written, which is a state worth stating. */
[data-history-note] { color: var(--amber); }
[data-history-note][hidden] { display: none; }

/* ══════════════════════════════════════════════════════════════════════════
   Data — what the database is holding, and taking some of it out
   ═══════════════════════════════════════════════════════════════════════════ */

/* A row count that stopped being exact because counting the rest would have
   cost a full table read. The "+" is the entire difference, so it is drawn as
   something to notice rather than as punctuation. */
.capped {
  color: var(--amber); font-weight: 800;
  cursor: help; margin-left: 1px;
}

/* ── Retention buckets ────────────────────────────────────────────────────
   Five cutoffs per dataset, side by side. Read as a shape, not as numbers:
   a table whose bars are all dark has nothing old in it, and one that fills
   up from the left is where a purge would actually pay. */
.buckets { display: flex; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
.bucket {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 4px 7px; min-width: 58px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: help;
}
.bucket .bk-d {
  font-size: 9.5px; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--mute);
}
.bucket .bk-v { font-family: var(--mono); font-size: 11.5px; color: var(--teal); font-weight: 700; }
.bucket.zero { opacity: .38; }
.bucket.zero .bk-v { color: var(--mute); }

.datatbl tbody td { vertical-align: top; }
.strong { font-weight: 800; color: var(--text); }

/* ── The purge builder ────────────────────────────────────────────────────
   Three numbered steps, then the consequence. The consequence is not a
   confirmation dialog that appears after you commit — it is on screen the
   whole time, changing as the steps change. */
.purge-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.purge-step.wide { grid-column: 1 / -1; }
@media (max-width: 900px) { .purge-grid { grid-template-columns: 1fr; } }

fieldset.purge-step {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 14px 16px 6px;
  margin: 0; min-width: 0;
}
fieldset.purge-step legend {
  display: flex; align-items: center; gap: 8px;
  padding: 0 8px; margin-left: -4px;
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .11em;
  font-weight: 800; color: var(--dim);
}
.stepno {
  display: inline-flex; align-items: center; justify-content: center;
  width: 19px; height: 19px; border-radius: 50%;
  background: var(--accent-grad); color: #071022;
  font-size: 11px; font-weight: 800; letter-spacing: 0;
}

/* ── Date presets ───────────────────────────────────────────────────────── */
.presets { display: grid; gap: 6px; margin-bottom: 12px; }
.preset {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "radio label" ". why";
  gap: 1px 9px;
  padding: 8px 11px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}
.preset:hover { border-color: var(--border-hi); background: var(--surface-3); }
.preset input { grid-area: radio; margin: 3px 0 0; accent-color: var(--teal); }
.preset .preset-label { grid-area: label; font-weight: var(--fw-bold); color: var(--text); }
.preset .preset-why { grid-area: why; font-size: 12px; color: var(--mute); line-height: 1.45; }
.preset:has(input:checked) {
  border-color: var(--teal); background: var(--surface-3);
  box-shadow: inset 3px 0 0 var(--teal);
}
/* "Everything, no date limit" removes the entire history for the scope. It is
   a legitimate thing to want and it should not look like the other five. */
.preset.danger:has(input:checked) {
  border-color: var(--down); box-shadow: inset 3px 0 0 var(--down);
}
.preset.danger:has(input:checked) .preset-label { color: var(--down); }

/* ── Dataset checkboxes ─────────────────────────────────────────────────── */
.dsgrid {
  display: grid; gap: 8px; margin-bottom: 12px;
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
}
.dscheck {
  display: grid; grid-template-columns: auto 1fr; gap: 10px;
  padding: 11px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
}
.dscheck:hover { border-color: var(--border-hi); background: var(--surface-3); }
/* Drawn rather than left to the browser. A native checkbox is a grid item here,
   and a grid item stretches: with no size of its own it grew to the full height
   of the card beside it and rendered as a tall empty rectangle with a tick
   floating in the middle of it. Explicit box, explicit tick, `align-self: start`
   so it sits with the title rather than centring itself against a paragraph. */
.dscheck input {
  appearance: none; -webkit-appearance: none;
  width: 18px; height: 18px; margin: 2px 0 0; align-self: start;
  display: grid; place-content: center;
  border: 1.5px solid var(--border-hi); border-radius: 5px;
  background: var(--surface-2); cursor: pointer;
  transition: background .12s ease, border-color .12s ease;
}
.dscheck input::before {
  content: "✓"; font-size: 12px; font-weight: var(--fw-bold); line-height: 1;
  color: #FFFFFF; transform: scale(0); transition: transform .12s ease;
}
.dscheck input:checked { background: var(--down); border-color: var(--down); }
.dscheck input:checked::before { transform: scale(1); }
.dscheck input:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.dscheck input:disabled { cursor: not-allowed; opacity: .5; }
.dscheck:has(input:checked) {
  border-color: var(--down); background: color-mix(in srgb, var(--down) 8%, transparent);
}
.dscheck.disabled { opacity: .45; cursor: not-allowed; }
.ds-body { display: grid; gap: 3px; min-width: 0; }
.ds-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.ds-name { font-weight: var(--fw-bold); color: var(--text); }
.ds-size { font-size: 12px; color: var(--dim); }
.ds-blurb { font-size: 12px; color: var(--mute); line-height: 1.45; }
/* The cascade, stated on the control that triggers it rather than only in the
   plan below — by the time it is in the plan it can read as a surprise. */
.ds-pulls {
  font-size: 11.5px; font-weight: var(--fw-bold);
  color: var(--amber); letter-spacing: .01em;
}
.ds-pulls::before { content: "↳ "; }

/* ── The plan ───────────────────────────────────────────────────────────── */
.planbox {
  border: 1px solid var(--border-hi);
  border-radius: var(--radius);
  background: var(--surface-2);
  overflow: hidden;
  transition: opacity .15s ease;
}
.planbox.working { opacity: .5; }
.plan-empty { padding: 22px 16px; text-align: center; color: var(--mute); font-weight: var(--fw-med); }
.plan-added {
  padding: 11px 14px;
  background: color-mix(in srgb, var(--amber) 10%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--amber) 35%, transparent);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: var(--fs-sm); color: var(--amber);
}
.plan-added .chip { background: var(--surface); border-color: color-mix(in srgb, var(--amber) 40%, transparent); }
table.plan-tbl tfoot td {
  padding: 11px 14px;
  border-top: 1px solid var(--border-hi);
  background: var(--surface-3);
}
.plan-tbl .rebuild-row td { color: var(--dim); }
.approx {
  display: inline-block; margin-left: 6px; padding: 1px 6px;
  border-radius: 999px; background: var(--surface-3); border: 1px solid var(--border-hi);
  font-size: 10px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--amber); cursor: help;
}

/* ── The commit bar ─────────────────────────────────────────────────────── */
.dangerbar {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  margin-top: 14px; padding: 14px 16px;
  border: 1px solid color-mix(in srgb, var(--down) 50%, transparent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--down) 9%, transparent);
}
.dangerbar[hidden] { display: none; }
.danger-text { flex: 1 1 320px; font-size: var(--fs-sm); color: var(--dim); }
.danger-text strong { color: var(--down); }
.danger-text code, .plan-tbl code {
  font-family: var(--mono); font-size: 12px;
  padding: 1px 5px; border-radius: 4px;
  background: var(--surface-3); color: var(--down); font-weight: 700;
}
.confirm-field { margin-bottom: 0; width: 150px; }
.confirm-field input { text-align: center; font-family: var(--mono); font-weight: 700; letter-spacing: .14em; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── The runner ─────────────────────────────────────────────────────────── */
.runbox { display: grid; gap: 11px; }
.progress {
  height: 9px; border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--border);
  overflow: hidden;
}
.progress-fill {
  height: 100%; width: 0;
  background: var(--accent-grad);
  transition: width .3s ease;
}
.runbox.finished .progress-fill { background: linear-gradient(90deg, var(--up), #00A874); }
.runbox.cancelled .progress-fill,
.runbox.paused .progress-fill { background: linear-gradient(90deg, var(--amber), #C97D00); }
.progress-what { color: var(--dim); font-weight: var(--fw-med); }
.progress-count { color: var(--mute); }

.stagelist {
  display: grid; gap: 2px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-2); padding: 8px 10px;
}
.stage {
  display: grid; grid-template-columns: 10px 1fr auto;
  align-items: center; gap: 9px;
  font-size: var(--fs-sm); color: var(--mute);
}
.stage-dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--surface-3); border: 1px solid var(--border-hi);
}
.stage.running .stage-dot { background: var(--teal); border-color: var(--teal); animation: pulse 1.4s infinite; }
.stage.running { color: var(--text); font-weight: var(--fw-bold); }
.stage.done .stage-dot { background: var(--up); border-color: var(--up); }
.stage.done { color: var(--dim); }
.stage-num { color: var(--dim); font-size: 12.5px; }

.runerr {
  padding: 9px 12px; border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--down) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--down) 45%, transparent);
  color: var(--down); font-family: var(--mono); font-size: 12px;
  word-break: break-word;
}
.runerr[hidden] { display: none; }

/* ── Client management ─────────────────────────────────────────────────────
   The pieces the Clients screens added: the credentials handed over once, the
   label/value grid that replaced a two-column table nobody was reading as a
   table, and the presence dot. */

/* A required field says so on the label rather than only by refusing to submit.
   Three of the four fields on the create dialog are mandatory now, so the one
   worth marking is arguably the optional one — but "required" is the word
   people scan for, and an empty marker on Notes would be a puzzle. */
.req {
  font-style: normal; font-size: 10.5px; font-weight: var(--fw-bold);
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--amber); margin-left: 4px;
}

/* Cards stacked in one column of the two-column grid. */
.stack { display: flex; flex-direction: column; gap: 16px; }
.stack > .card { margin: 0; }

/* Label/value pairs. A <table> for seven facts about one thing is a table with
   no second row — this reads the same and takes two thirds of the height. */
.kv {
  display: grid; grid-template-columns: minmax(96px, 34%) 1fr;
  gap: 7px 14px; align-items: baseline;
}
.kv.tight { gap: 4px 12px; font-size: var(--fs-sm); }
.kv-k { color: var(--mute); font-size: var(--fs-sm); }
.kv-v { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.kv-v code { background: var(--surface-3); padding: 1px 6px; border-radius: 5px; }
.kv-v .btn { margin-left: 6px; vertical-align: baseline; }

/* Everything on the map card sits closer together: it is one field until a
   terminal is picked, and it shares a column with the access card. */
.card.compact .card-body { padding-top: 12px; padding-bottom: 12px; }
.card.compact .combo-hint { margin: 10px 0 0; font-size: var(--fs-sm); }
.card.compact #map-detail { margin-top: 12px; }

/* The credentials, once. Deliberately loud — it is the one thing on the page
   that cannot be recovered by reloading it. */
.creds {
  border: 1px solid color-mix(in srgb, var(--teal) 45%, transparent);
  background: color-mix(in srgb, var(--teal) 7%, transparent);
  border-radius: var(--radius-sm);
  padding: 14px 16px; margin-bottom: 16px;
}
.creds-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.creds-once {
  font-size: 10.5px; font-weight: var(--fw-bold); text-transform: uppercase;
  letter-spacing: .06em; color: var(--amber);
  border: 1px solid color-mix(in srgb, var(--amber) 45%, transparent); background: color-mix(in srgb, var(--amber) 12%, transparent);
  border-radius: 999px; padding: 2px 8px;
}
.creds-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 5px 0;
}
.creds-k { color: var(--mute); font-size: var(--fs-sm); min-width: 74px; }
.creds-v {
  font-family: var(--mono); font-size: 14px; color: var(--text);
  background: var(--surface-3); border: 1px solid var(--border-hi);
  border-radius: 6px; padding: 4px 10px; user-select: all;
  overflow-wrap: anywhere;
}
.creds-foot {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--border);
}

/* The generated password and its two buttons, on one line. */
.pwfield { display: flex; align-items: center; gap: 8px; }
.pwfield input { flex: 1 1 auto; min-width: 0; }

/* Presence. The dot alone was ambiguous — green next to a name reads as
   "healthy" as easily as "here" — so it carries the word. */
.online {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--fs-sm); color: var(--text); white-space: nowrap;
}
.online.off { color: var(--mute); }

/* ── Password rules ────────────────────────────────────────────────────────
   The requirements, on screen before anything is typed, each going green as it
   is met. Neutral until the field has something in it: a form that greets you
   in red is telling you off for not having started. */
.pwrules {
  list-style: none; margin: 10px 0 4px; padding: 10px 12px;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  display: grid; gap: 5px; font-size: var(--fs-sm); color: var(--mute);
}
.pwrules li { display: flex; align-items: baseline; gap: 9px; transition: color .14s ease; }

/* The marker is drawn rather than typed so the three states are one element:
   an empty ring while nothing is known, a tick when the rule is met, a cross
   when it is not and the field is no longer empty. */
.pwrules li > i {
  flex: none; width: 15px; height: 15px; margin-top: 2px;
  border-radius: 50%; border: 1.5px solid var(--border-hi);
  position: relative; transition: all .14s ease;
}
.pwrules li.ok  { color: var(--up); }
.pwrules li.ok > i { background: var(--up); border-color: var(--up); }
.pwrules li.ok > i::after {
  content: ""; position: absolute; left: 4.5px; top: 1.5px;
  width: 3.5px; height: 7px; border: solid #06231A;
  border-width: 0 1.9px 1.9px 0; transform: rotate(42deg);
}
.pwrules li.bad { color: var(--mute); }
.pwrules li.bad > i { border-color: var(--down); }
.pwrules li.bad > i::after {
  content: ""; position: absolute; inset: 3.5px;
  background: var(--down); border-radius: 50%;
}
.pwrules .muted { font-weight: 400; }

/* Still signed in, but nothing from them in a while — they closed the tab
   rather than signing out. Not green (they are not there) and not the flat grey
   of somebody who left deliberately. */
.online.idle { color: var(--amber); }
.online.idle .dot.off { background: var(--amber); box-shadow: 0 0 0 3px color-mix(in srgb, var(--amber) 16%, transparent); }

/* ── Audit ledger ─────────────────────────────────────────────────────────────
   The one screen that confirms a push reached a terminal. Read-only, scanned
   rather than read, so state is carried in form as well as in words: a row that
   is still owed something is tinted, and the five delivery states each get a
   colour that means the same thing here as it does anywhere else on the site —
   amber is "needs you", red is "it went wrong", grey is "nothing to do".
   See templates/audit.html and services/ledger.py. */

/* Reuses .pill's shape and only restates the colour, so a delivery pill and a
   status pill two columns away are the same object at the same size. */
.pill.ledger-done    { background: color-mix(in srgb, var(--up) 14%, transparent);  color: var(--up);     border-color: color-mix(in srgb, var(--up) 42%, transparent); }
.pill.ledger-waiting { background: color-mix(in srgb, var(--teal) 13%, transparent);  color: var(--teal);   border-color: color-mix(in srgb, var(--teal) 42%, transparent); }
.pill.ledger-part    { background: color-mix(in srgb, var(--amber) 14%, transparent); color: var(--amber);  border-color: color-mix(in srgb, var(--amber) 45%, transparent); }
.pill.ledger-fail    { background: color-mix(in srgb, var(--down) 14%, transparent); color: var(--down);   border-color: color-mix(in srgb, var(--down) 45%, transparent); }
.pill.ledger-rec     { background: var(--surface-3);     color: var(--mute);   border-color: var(--border-hi); }

/* The action itself, in mono: these are machine names — `license.suspend`,
   `config.save` — and reading them as prose is what made the old table a wall.
   Monospace is what makes the family obvious at a glance. */
.pill.act {
  background: color-mix(in srgb, var(--violet) 12%, transparent); color: var(--violet);
  border-color: color-mix(in srgb, var(--violet) 38%, transparent);
  font-family: var(--mono); font-size: 11px; letter-spacing: 0;
}

/* Outstanding actions, above the table. Only drawn when there are some — a
   banner that permanently reads zero is a thing people learn not to see. */
.ledger-strip {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 11px 16px; margin-bottom: 16px;
  border: 1px solid color-mix(in srgb, var(--teal) 32%, transparent); border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--teal) 6%, transparent);
}
.ledger-strip strong { color: var(--teal); font-size: 17px; }
.ledger-strip .muted { display: block; margin-top: 2px; }

.ledger-tbl td { vertical-align: top; }
/* Still owed a beat. Faint on purpose: it marks the rows worth watching without
   turning a page of recent activity into a wall of colour. */
.ledger-tbl tr.open > td { background: color-mix(in srgb, var(--teal) 3.5%, transparent); }

.ledger-target { display: block; color: var(--text); font-weight: var(--fw-med); }
/* What the operator changed, spelled out — the raw detail JSON used to be
   printed here as a Python dict. */
.ledger-detail {
  display: block; margin-top: 2px;
  font-size: 12.5px; color: var(--mute); line-height: 1.45;
}

.ledger-delivery { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }

/* The expander. A count rather than a caret alone, so "4 terminals" is legible
   without opening it. */
.ledger-more {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 1px 7px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--border-hi); background: var(--surface-2);
  color: var(--mute); font: inherit; font-size: 11.5px; font-weight: var(--fw-bold);
  transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.ledger-more:hover { color: var(--text); border-color: var(--border-hi); background: var(--surface-3); }
.ledger-more:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.ledger-more.on { color: var(--teal); border-color: color-mix(in srgb, var(--teal) 45%, transparent); background: color-mix(in srgb, var(--teal) 10%, transparent); }
.ledger-chev { display: inline-block; transition: transform .16s ease; }
.ledger-more.on .ledger-chev { transform: rotate(180deg); }
@media (prefers-reduced-motion: reduce) {
  .ledger-chev { transition: none; }
}

/* Why an amber row is amber. The one state that is genuinely ambiguous — a
   suspend that never reached the terminal is still in force, because the
   session was revoked server-side — so it says so rather than leaving somebody
   to work it out from a colour. */
.ledger-why {
  margin-top: 6px; max-width: 46ch;
  font-size: 12.5px; line-height: 1.45; color: var(--amber);
  border-left: 2px solid color-mix(in srgb, var(--amber) 40%, transparent); padding-left: 9px;
}

/* The per-terminal breakdown, revealed by the expander above. */
.ledger-lines > td { background: var(--surface-2); padding: 4px 14px 8px; }
.ledger-line {
  display: grid; grid-template-columns: minmax(180px, 1fr) minmax(0, 2fr) auto;
  gap: 14px; align-items: baseline;
  padding: 8px 0; border-bottom: 1px dashed var(--border);
}
.ledger-line:last-child { border-bottom: 0; }
.ledger-line-who { display: flex; flex-direction: column; gap: 1px; }
.ledger-line-what { line-height: 1.45; }

@media (max-width: 720px) {
  .ledger-line { grid-template-columns: 1fr; gap: 4px; }
}

/* The ledger's Showing filter: a segmented control in the filter bar, made of
   links rather than inputs so each state is a plain address that can be
   bookmarked and shared. `.field` styling is scoped to `label.field` and this
   is a div — it holds links, not a control — so its label needs saying here. */
.filterbar .statefield { margin-bottom: 0; }
.filterbar .statefield > span {
  display: block; margin-bottom: 5px;
  font-size: var(--fs-label); text-transform: uppercase; letter-spacing: .1em;
  font-weight: var(--fw-bold); color: var(--dim);
}
/* The shared button rule sets padding and colour; these two are all an anchor
   needs on top of it. */
a.rangebtn { text-decoration: none; display: inline-block; }
a.rangebtn:hover, a.rangebtn.on { text-decoration: none; }

/* The ledger keeps its header while the rows scroll, and its expanded
   breakdown scrolls with the row it belongs to — so the sticky header has to
   sit above both. `.tbl thead th` already carries the surface colour; this is
   only about which of the two wins where they overlap. */
#ledger .table-wrap.freeze thead th { z-index: 4; }
#ledger .ledger-lines > td { position: relative; z-index: 1; }

/* ── Symbols ─────────────────────────────────────────────────────────────────

   The one screen on this site where an operator waits. Everything else queues a
   command that is collected and acked inside a single heartbeat; a symbol scan
   is a question the terminal answers minutes later, after pulling candle
   history it does not hold yet. A pill saying "measuring" cannot tell that
   apart from a pill saying "stuck", so the row carries a bar and a step. */
.scanbar {
  display: block; width: 120px; max-width: 100%; height: 4px;
  border-radius: 3px; background: var(--surface-3); overflow: hidden;
}
.scanbar > span {
  display: block; height: 100%; background: var(--amber);
  border-radius: 3px; transition: width .4s ease;
}

/* ── The hour-of-day strip ────────────────────────────────────────────────────

   Twenty-four numbers are a wall; the same twenty-four shaded by size are a
   shape, and the shape is the answer to "when does this thing move". The tint
   is set per cell from the value, so the scale belongs to the symbol being read
   rather than to a threshold chosen here.

   THE GEOMETRY IS THE LOAD-BEARING PART. A session ribbon sits above the cells
   and has to line up with them to the pixel — a band labelled London pointing
   at the wrong column is worse than no band at all. So the grid has NO GAP: a
   gap of 2px across twenty-four cells accumulates to nearly a whole hour of
   drift by the right-hand edge, because the ribbon is positioned in percentages
   of the same box and percentages do not know about gaps. The cells are
   separated by an inset shadow instead, which paints inside the box and moves
   nothing. */
.tod { position: relative; }

.tod-ribbon { position: relative; height: 50px; margin-bottom: 5px; }
/* One lane per market rather than one row for all three. The sessions overlap —
   that is the single most useful thing about them — and stacked bars at the
   same x say so at a glance, where three bars fighting for one row would just
   paint over each other. */
.tod-band {
  position: absolute; height: 14px; border-radius: 3px;
  display: flex; align-items: center; padding: 0 5px;
  font-size: 9.5px; font-weight: var(--fw-bold); letter-spacing: .07em;
  text-transform: uppercase; white-space: nowrap; overflow: hidden;
  color: var(--bg); cursor: help;
}
/* Only the colour lives here. Which lane a band sits in is set inline from the
   band's own lane index, because the overlaps span two lanes each and WHICH two
   differs between them — Asia/London brackets the top pair, London/New York the
   bottom pair. A single hardcoded height could only ever draw one of them. */
.tod-band.tod-asia    { background: color-mix(in srgb, var(--violet) 74%, transparent); }
.tod-band.tod-london  { background: color-mix(in srgb, var(--teal) 76%, transparent); }
.tod-band.tod-newyork { background: color-mix(in srgb, var(--amber) 74%, transparent); }
/* An overlap is not a fourth session, so it is not a fourth bar. It brackets
   the two it is made of — an outline with no fill, which cannot muddy the
   colours underneath it and cannot be mistaken for a market of its own. */
.tod-band.tod-overlap {
  background: transparent;
  border: 1px dashed var(--text); border-radius: 4px; opacity: .5;
}

/* A market opening: an instant, drawn as a line rather than as a band. The hit
   area is deliberately wider than the line so the tooltip is catchable. */
.tod-tick { position: absolute; top: -3px; bottom: -3px; width: 9px; margin-left: -4px; cursor: help; }
.tod-tick::before {
  content: ""; position: absolute; left: 4px; top: 0; bottom: 0;
  border-left: 1px dashed var(--text); opacity: .55;
}

.tod-grid { position: relative; display: grid; grid-template-columns: repeat(24, 1fr); }
.tod-cell {
  position: relative; min-height: 54px; padding: 4px 1px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  font-size: 10.5px; line-height: 1.2; color: var(--dim);
  box-shadow: inset -1px 0 0 var(--bg);
}
.tod-cell:first-of-type { border-radius: 3px 0 0 3px; }
.tod-cell:last-of-type  { border-radius: 0 3px 3px 0; box-shadow: none; }

/* Three lines: IST, the same hour on the broker's clock, and the reading. The
   two clocks are deliberately weighted differently rather than sitting at the
   same size — one is the clock you plan in and the other is the clock the chart
   is stamped in, and a cell where they look identical is a cell you have to
   read twice to know which is which. */
.tod-ist { font-weight: var(--fw-bold); color: var(--text); white-space: nowrap; }
/* AM and PM in full, set two points down so the pair still fits a column an
   hour wide. The strip used to abbreviate to "6:30p", and a lone letter is not
   a word anybody reads as a meridiem at a glance — "12:30a" was read as a typo
   often enough to be worth the six pixels. */
.tod-ist i { font-size: 8.5px; font-style: normal; font-weight: var(--fw-med, 500); margin-left: 1px; }
.tod-brk { font-size: 9.5px; color: var(--mute); letter-spacing: -.01em; }
.tod-val { margin-top: 2px; font-variant-numeric: tabular-nums; }

/* The busy end of the ramp, where the cell is bright teal and white type
   vanishes into it. Ink goes dark instead, the way it does on every other
   filled-teal control here. See the note in symbol_detail.html for why the ramp
   jumps rather than sliding into this. */
.tod-cell.hot { color: var(--bg); }
.tod-cell.hot .tod-ist { color: var(--bg); }
.tod-cell.hot .tod-brk { color: color-mix(in srgb, var(--bg) 68%, transparent); }
.tod-cell.hot .tod-val { color: var(--bg); font-weight: var(--fw-bold); }

/* An hour inside the best window. Inset rather than outset, so marking a cell
   cannot push the grid around by a pixel and break the alignment the ribbon
   depends on. */
.tod-cell.on { outline: 2px solid var(--teal); outline-offset: -2px; z-index: 1; }
.tod-cell.hot.on { outline-color: var(--bg); }

/* The spread takes as much as the median candle delivers — the rollover, and
   the three hours around a thin Sunday open. One dot: the numbers that prove it
   are in the tooltip, and a second figure per cell turns the strip back into
   the table it exists to replace. */
.tod-toll {
  position: absolute; top: 3px; right: 3px;
  width: 5px; height: 5px; border-radius: 50%; background: var(--amber);
}
.tod-toll.static { position: static; display: inline-block; margin-right: 5px; }

/* Where "now" falls, placed by the browser from a UTC minute the server stamped
   on the strip. Non-interactive: it is orientation, not a control. */
.tod-now {
  position: absolute; top: -4px; bottom: -4px; width: 2px; margin-left: -1px;
  background: var(--text); opacity: .8; pointer-events: none; z-index: 2;
  border-radius: 2px;
}

.tod-legend {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px;
  margin-top: 10px; font-size: 12.5px; color: var(--dim);
}
.tod-legend .tod-ramp {
  width: 90px; height: 8px; border-radius: 4px;
  background: linear-gradient(90deg, var(--surface-3), var(--teal));
}
.tod-key { display: inline-flex; align-items: center; gap: 5px; }
.tod-swatch { width: 11px; height: 11px; border-radius: 2px; display: inline-block; }
.tod-swatch.sw-asia    { background: color-mix(in srgb, var(--violet) 74%, transparent); }
.tod-swatch.sw-london  { background: color-mix(in srgb, var(--teal) 76%, transparent); }
.tod-swatch.sw-newyork { background: color-mix(in srgb, var(--amber) 74%, transparent); }
.tod-swatch.sw-overlap { background: transparent; border: 1px dashed var(--text); }
.tod-swatch.sw-now     { width: 3px; background: var(--text); border-radius: 2px; }

/* Twenty-four columns do not fit a phone, and re-flowing them to twelve would
   put half the day on a second row — which the ribbon, being one continuous
   axis, cannot follow. It scrolls sideways instead, ribbon and cells together,
   so the two can never disagree. The legend is outside this box for the same
   reason: a legend that scrolls away is a legend you have to go and find. */
@media (max-width: 1180px) {
  .tod, .spd { overflow-x: auto; padding-bottom: 4px; }
  .tod-ribbon, .tod-grid, .spd-grid, .spd-axis { min-width: 1010px; }
}

/* ── How fast it moves ─────────────────────────────────────────────────────── */

/* Same twenty-four column geometry as the strip above and in the same rotated
   order, so the two cards read down the page as one picture: a column here is
   the same hour as the cell directly above it. */
.spd { position: relative; }
.spd-grid {
  position: relative; display: grid; grid-template-columns: repeat(24, 1fr);
  align-items: end; height: 160px;
  border-bottom: 1px solid var(--border);
}
.spd-col {
  position: relative; height: 100%; cursor: help;
  display: flex; align-items: flex-end; justify-content: center;
}
/* Hollow bar, solid core. Total height is pace — ground covered per minute —
   and the solid part is drift, drawn as a share of the bar it sits inside
   rather than of the chart. That makes the empty top of a bar mean something on
   sight: it is the ground price covered and gave back. */
.spd-bar {
  position: relative; width: 62%; min-height: 2px; border-radius: 3px 3px 0 0;
  background: color-mix(in srgb, var(--teal) 24%, var(--surface-2));
}
.spd-net {
  position: absolute; left: 0; right: 0; bottom: 0; min-height: 1px;
  border-radius: 3px 3px 0 0; background: var(--teal);
}
.spd-col.best .spd-bar { background: color-mix(in srgb, var(--teal) 42%, var(--surface-2)); }
.spd-col.best .spd-bar::after {
  content: ""; position: absolute; inset: -2px -2px 0;
  border: 2px solid var(--teal); border-bottom: 0; border-radius: 4px 4px 0 0;
}
.spd-col:hover .spd-bar { filter: brightness(1.15); }
/* Too few candles in the bucket for its median to be a habit. Faded rather than
   dropped: an hour the market is thin in is a fact about the hour, and a gap in
   the chart would read as no data at all. */
.spd-col.thin .spd-bar { opacity: .4; }
.spd-col.thin .spd-bar { background-image: repeating-linear-gradient(
  45deg, transparent 0 3px, color-mix(in srgb, var(--bg) 45%, transparent) 3px 6px); }

/* The day's own middle. Without it every bar is only readable against the
   tallest one, and "half the best hour" is a weaker fact than "under the day's
   median". */
.spd-median {
  position: absolute; left: 0; right: 0; height: 0; z-index: 1; cursor: help;
  border-top: 1px dashed var(--amber); opacity: .7; pointer-events: auto;
}

.spd-axis {
  display: grid; grid-template-columns: repeat(24, 1fr);
  margin-top: 6px; font-size: 10px; color: var(--mute);
}
.spd-axis span { text-align: center; white-space: nowrap; }
.spd-axis i { font-size: 8.5px; font-style: normal; margin-left: 1px; }

.spd-swatch { width: 11px; height: 11px; border-radius: 2px; display: inline-block; }
.spd-swatch.sw-pace   { background: color-mix(in srgb, var(--teal) 24%, var(--surface-2)); }
.spd-swatch.sw-net    { background: var(--teal); }
.spd-swatch.sw-median { height: 0; border-top: 1px dashed var(--amber); border-radius: 0; }
.spd-swatch.sw-thin   { background: color-mix(in srgb, var(--teal) 24%, var(--surface-2)); opacity: .4;
  background-image: repeating-linear-gradient(
    45deg, transparent 0 3px, color-mix(in srgb, var(--bg) 45%, transparent) 3px 6px); }

/* ── The weekday strip ─────────────────────────────────────────────────────── */
.dow { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 6px; }
.dow-cell {
  border-radius: var(--radius-sm, 4px); padding: 10px 8px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  border: 1px solid var(--border); font-size: 11px; color: var(--dim);
}
.dow-name { font-weight: var(--fw-bold); color: var(--text); letter-spacing: .04em; }
.dow-val { font-size: 17px; font-variant-numeric: tabular-nums; color: var(--text); }
.dow-sub { font-size: 10.5px; color: var(--mute); }
.dow-cell.hot { color: var(--bg); border-color: transparent; }
.dow-cell.hot .dow-name, .dow-cell.hot .dow-val { color: var(--bg); }
.dow-cell.hot .dow-sub { color: color-mix(in srgb, var(--bg) 68%, transparent); }
.dow-cell.on { outline: 2px solid var(--violet); outline-offset: -2px; }
.dow-cell.hot.on { outline-color: var(--bg); }

/* A finding the reader can act on, inside a paragraph of ones they cannot.
   Amber rather than red: nothing is broken, there is just a setting on their
   machine costing them history they could have. */
.warn-text { color: var(--amber); }
/* The same slot when the answer is the good one — "Unlimited", "nothing is
   truncating this". Teal rather than green: green in this theme means profit
   and would read as a number rather than as a state. */
.ok-text { color: var(--teal); }

/* A percentile the sample was too thin to support. Not hidden and not zero —
   both would read as a measurement. An em dash that says why on hover. */
.thin { color: var(--mute); }

/* A figure the terminal re-cut from a finer series rather than measuring on the
   timeframe's own candles. Violet, which is this theme's colour for "worked out
   here" wherever else it appears — not amber, because nothing is wrong: the
   number is real, it was just arrived at a second way and the reader is owed
   that in the same glance as the number. */
.rolled {
  color: var(--violet); font-weight: var(--fw-bold);
  font-size: 10px; text-transform: uppercase; letter-spacing: .07em;
  white-space: nowrap;
}

/* A whole ROW built on a thin sample, which is a different statement from a
   thin CELL and needs a different instrument.

   It used to share the class above, and so it greyed the row out. That was
   wrong twice. "Weigh this less" is not what a greyed row says — it says "this
   is disabled" — and on an account whose every slice is thin, which is every
   new account, it turned the whole P&L report to unreadable grey. Runs,
   Symbols, Terminals, Buy-against-sell and the weekday table were all one
   class away from white and every one of them was grey.

   A separate class rather than an override of `.thin`, deliberately. The
   override worked, on specificity, and left the greying rule sitting there
   still trying — one careless selector away from winning again. The rows do
   not carry a colour class at all now, so there is nothing left to beat.

   The type is therefore whatever the table says: white, or the up/down colour
   on a figure that has one. The thin signal moves to the row's leading edge —
   amber, one line, findable while scanning a column and impossible to mistake
   for a value. */
table.tbl tbody tr.row-thin > td:first-child { box-shadow: inset 3px 0 0 var(--amber); }

/* The movement matrix.

   Eight numeric columns spanning four orders of magnitude — tens of points on
   M1, hundreds of thousands on MN1 — which as bare digits is read as a list
   rather than as a shape. Each cell carries a --w set from its size against the
   biggest value in the same column, and paints that as a tint behind the
   number. The scale therefore belongs to the symbol being looked at rather than
   to a threshold chosen here, which is the same rule the hour heat map follows.

   Deliberately a background and not a bar: a bar inside a table cell competes
   with the digits for the same few pixels, and the digits are the thing being
   read. */
table.tbl.matrix tbody td.mx {
  position: relative;
  background: color-mix(in srgb, var(--teal) calc(var(--w, 0) * 22%), transparent);
}
table.tbl.matrix tbody tr:hover td.mx {
  background: color-mix(in srgb, var(--teal) calc(var(--w, 0) * 30%), var(--surface-2));
}
/* Measured off fewer candles than the column normally wants. Printed, because
   an em dash down the whole MN1 row tells the reader nothing — but printed in
   the way a provisional number should be, with a dotted underline that invites
   the tooltip explaining the sample it came from. */
table.tbl.matrix tbody td.mx-thin {
  color: var(--mute);
  text-decoration: underline dotted var(--border-hi);
  text-underline-offset: 3px;
}

/* The group header, which is what makes this a matrix and not ten columns. */
table.tbl.matrix thead tr.mx-group th {
  padding-bottom: 2px; border-bottom: none;
  font-size: 12px; letter-spacing: .02em; color: var(--text);
}
table.tbl.matrix thead tr.mx-group th.mx-body  { border-bottom: 2px solid var(--violet); }
table.tbl.matrix thead tr.mx-group th.mx-range { border-bottom: 2px solid var(--teal); }
table.tbl.matrix thead tr.mx-group th span { font-weight: var(--fw-med); }
table.tbl.matrix thead th.mx-step { font-size: 11px; color: var(--mute); padding-top: 6px; }
/* A hairline where one group ends and the next begins, on every row. Without it
   the eye has to count five columns to know which half it is in. Column 8 and
   not 7: two label columns, then four quantiles and a tail per group. */
table.tbl.matrix thead th.mx-edge,
table.tbl.matrix tbody td:nth-child(8) { border-left: 1px solid var(--border); }

/* The tail column. Dimmed and set apart by a hairline, because P99 is read for
   a different reason from the four beside it — what has to be survivable, never
   what to aim at — and a column that looks like the others gets used like the
   others. */
table.tbl.matrix th.mx-tail,
table.tbl.matrix td.mx-tail {
  border-left: 1px dotted var(--border);
  color: var(--mute); font-size: 12px;
}
table.tbl.matrix tbody td.mx-tail { background: none; }

/* A section that is worth keeping and not worth reading twice — the history
   depth breakdown under the matrix being the case this was written for. Folded
   shut by default: it answers "why is this row short", which is a question
   asked once and then never again, and left open it pushes the part of the page
   people came for off the screen.

   A plain <details>, so it works with JavaScript off and the browser handles
   the state. The marker is drawn here rather than left to the default triangle,
   which is a different shape and colour in every engine. */
details.fold { border-top: 1px solid var(--border); }
details.fold > summary {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 14px; cursor: pointer;
  font-size: var(--fs-sm); color: var(--mute);
  list-style: none;
}
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::before {
  content: "›"; display: inline-block;
  font-size: 15px; line-height: 1; color: var(--mute);
  transition: transform .15s ease;
}
details.fold[open] > summary::before { transform: rotate(90deg); }
details.fold > summary:hover,
details.fold[open] > summary { color: var(--text); }

/* The reading guide at the foot of a card whose body is `tight`, so it carries
   its own padding. Set apart by a rule rather than by a second card: it is a
   footnote to the table above it and a card of its own would read as another
   measurement. */
.legend { border-top: 1px solid var(--border); padding: 14px 14px 4px; }
/* Inside a fold the summary already draws the rule and owns the top gap, so the
   legend drops both rather than printing a second line under the heading. */
details.fold > .legend { border-top: 0; padding: 0 14px 10px; }
.legend h3 {
  margin: 0 0 8px; font-size: var(--fs-sm); font-weight: var(--fw-med);
  color: var(--text); letter-spacing: .02em;
}
.legend p { margin: 10px 0 0; line-height: 1.6; }
.legend .table-wrap { margin: 10px 0 2px; }
.legend table.tbl td { vertical-align: top; }

/* The currency or unit trailing a stat's value. Same line, smaller and dimmer:
   "0.01 USD" reads as one fact, "0.01" over "USD per point per lot" reads as
   two and the second one is the part nobody finishes. */
.stat .value .unit {
  font-size: 13px; font-weight: var(--fw-med); color: var(--mute);
  margin-left: 3px; letter-spacing: 0;
}

/* The ledger's "what happened" column, and the config footprint under it.

   Both exist because the trail used to be columns of primary keys: `account
   #140`, `licence 88`, and a config version nobody could look up. A ledger is
   read to cross-check something, so every fact it points at has to be on the
   same screen — a row that sends you to four other pages to find out who #140
   is has not recorded anything you can use. */
.ledger-story { display: block; line-height: 1.5; }
.ledger-story strong, .ledger-story .mono { color: var(--text); }

.conf-head {
  padding: 8px 0 6px; font-size: 12.5px; color: var(--dim);
}
.conf-head code { font-size: 12px; }
.conf-tbl { margin-bottom: 6px; }
.conf-tbl td, .conf-tbl th { padding: 4px 10px; }
/* Only the rows that moved. Everything else is context — the set as a whole is
   what was written, and greying the unchanged rows is what makes the two or
   three that differ findable without reading twenty. */
.conf-tbl tbody tr { color: var(--mute); }
.conf-tbl tr.conf-moved { color: var(--text); }
.conf-tbl tr.conf-moved td:last-child { color: var(--teal); font-weight: var(--fw-bold); }

/* ══════════════════════════════════════════════════════════════════════════
   The P&L report
   ═══════════════════════════════════════════════════════════════════════════

   Seventeen sections on one page, which is a long page on purpose: the sections
   only mean anything against each other — a weekday total is read next to the
   symbol that produced it — and one filter bar scoping all of them at once is
   the point of the screen. What a long page needs instead is two things that
   never leave: the filters, and a way to know where you are. Both are sticky
   here and nothing else is.
   ═══════════════════════════════════════════════════════════════════════════ */

/* How far down the viewport the app's own topbar reaches. Everything sticky on
   this page pins under it rather than to the top of the window, or it slides
   beneath the bar and is read through frosted glass. */
:root { --topbar-h: 55px; }

/* ── The page frame ──────────────────────────────────────────────────────── */

/* Report and rail. The app's nav already owns the left, so the section rail
   takes the right — two rails on the left would leave the report itself in a
   trench. Below 1280px the rail becomes the horizontal chip strip further
   down, which is the same navigation in the shape a narrow screen can hold. */
.pnl { display: grid; grid-template-columns: minmax(0, 1fr) 236px; gap: 30px; align-items: start; }
@media (max-width: 1279px) { .pnl { grid-template-columns: minmax(0, 1fr); gap: 0; } }

/* How tall the filter bar above actually is, measured by pnl.js and written
   here. Everything that pins or scrolls-into-view on this page is positioned
   off it.

   This is not a nicety. The rail and the filter bar are both sticky, and until
   the rail knew the bar's height it pinned at the topbar and spent the whole
   page sliding underneath the bar — which looked exactly like the rail
   scrolling away, because the top of it was being covered a row at a time. The
   fallback is a sane bar height for the moment before the script measures. */
:root { --pnl-bar-h: 168px; }

/* Room under the last section, so the rail can finish.

   A section is highlighted once its head scrolls up into a reading band near
   the top of the viewport, and that means every head has to be ABLE to get
   there. The last one usually cannot: the document runs out of scroll while it
   is still halfway down the screen, so the last stretch of the report has to
   share whatever scroll is left — and when the report ends on short sections
   there is so little left that a single wheel notch jumps clean over one. That
   is the second-to-last link never lighting up, going straight from 13 to 15.

   It hit clients and not operators, which made it look like a permissions bug:
   an operator's report ends on section 17, so 15 and 16 have a screenful below
   them and reach the band the ordinary way. A client does not get 17, so their
   last two are the ones with nothing underneath.

   One rule fixes it for every reader. Give the last section enough height that
   its own head can reach the band, and every head above it can too — so each
   section is current for a scroll distance equal to its own height, and the
   tail resolver in pnl.js goes back to being a fallback nothing normally needs.

   min-height and not padding: a section already this tall costs nothing, which
   is the operator's section 17 and most windows. The blank space only appears
   where it is the difference between a working rail and a broken one.

   The subtraction has to match triggerPx() in pnl.js exactly — bar height plus
   76, measured from the top of the VIEWPORT, not from under the topbar. Taking
   the topbar off as well looks right and is not: it would shorten this by the
   topbar's height, which is the amount by which the last head then fails to
   reach the band, and the bug comes back on tall windows only. */
.pnl main > section.pnl-sec:last-of-type {
  min-height: calc(100vh - var(--pnl-bar-h) - 76px);
}

.pnl-rail {
  position: relative;
  top: calc(var(--topbar-h) + var(--pnl-bar-h) + 20px);
  position: sticky;
  /* Sized to fit the viewport outright rather than to scroll inside it.

     It used to have its own scrollbar and the scrollspy nudged the highlighted
     link into view — which meant the reader had to hover the rail and scroll it
     to see either end, and the top of it (the first group) or the bottom of it
     (the last section) was always hidden. A map you have to scroll is not a
     map. There is one scrolling thing on this page and it is the report.

     The overflow rule below is a floor, not a design: on a viewport too short
     to hold seventeen links at any spacing it degrades to a scroller instead of
     silently cutting the last one off. Every ordinary window fits. */
  max-height: calc(100vh - var(--topbar-h) - var(--pnl-bar-h) - 34px);
  overflow-y: auto; overscroll-behavior: contain;
  padding: 2px 2px 2px 0;
  scrollbar-width: thin;
}
@media (max-width: 1279px) { .pnl-rail { display: none; } }

/* The spine, and how far down the report you are — one element doing both.

   A separate progress bar under the rail cost a row of height the rail could
   not spare. This is the same information drawn on the line that was already
   there: the track is the whole report, the filled part is how much of it is
   behind you. */
.pnl-rail::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0;
  width: 2px; background: var(--border); border-radius: 2px;
}
.pnl-rail .rail-progress {
  position: absolute; left: 0; top: 0; width: 2px; height: 0;
  background: var(--accent-grad); border-radius: 2px;
  transition: height .18s ease;
}
@media (prefers-reduced-motion: reduce) { .pnl-rail .rail-progress { transition: none; } }

/* Grouped by the question each run of sections answers — seventeen flat links
   is a list nobody reads to the end of, and the groups make the rail double as
   the argument the report is making.

   Coloured, not grey. These were one step off the page background and vanished
   into it; they are the only structure the rail has, so they are the last thing
   that should be quiet. Violet rather than teal: teal means "you are here" a few
   pixels away, and a heading that wears the highlight colour reads as selected. */
.pnl-rail .rail-g {
  font-size: 9.5px; text-transform: uppercase; letter-spacing: .16em;
  color: var(--violet); font-weight: 800;
  padding: 11px 0 4px 14px; line-height: 1.1;
}
.pnl-rail .rail-g:first-child { padding-top: 0; }

.pnl-rail a {
  position: relative;
  display: grid; grid-template-columns: 19px minmax(0, 1fr); gap: 8px;
  align-items: baseline;
  padding: 5px 8px 5px 14px;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--dim); font-size: 13px; font-weight: var(--fw-body);
  line-height: 1.25;
  transition: color .13s ease, background .13s ease;
}
.pnl-rail a:hover { color: var(--text); text-decoration: none; background: var(--surface-2); }
.pnl-rail a:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* Where you are. Nothing in the rail moves — only this does, which is the whole
   contract of a fixed map with a dot on it. */
.pnl-rail a.on {
  color: var(--teal); font-weight: var(--fw-med);
  background: linear-gradient(90deg, color-mix(in srgb, var(--teal) 14%, transparent), transparent 80%);
}
.pnl-rail a.on::after {
  content: ""; position: absolute; left: -4px; top: 50%; margin-top: -3px;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--teal); box-shadow: 0 0 0 3px var(--bg);
}

.pnl-rail a i {
  font-family: var(--mono); font-style: normal; font-size: 10px;
  font-weight: var(--fw-bold); color: var(--border-hi); text-align: right;
  transition: color .13s ease;
}
.pnl-rail a:hover i { color: var(--mute); }
.pnl-rail a.on i { color: var(--teal); }

/* ── The filter bar that never leaves ────────────────────────────────────── */

/* Pinned, and always the same height. It used to shrink once the page moved —
   which changed the height of a sticky element, which moved the content under
   it, which changed the scroll position, which re-evaluated the shrink. That
   loop is what the flicker was. It does not shrink any more, and the sections
   below are positioned off its measured height. */
.pnl-sticky {
  position: sticky; top: var(--topbar-h); z-index: 15;
  margin: 0 -24px 18px; padding: 12px 24px 0;
  background: rgba(7, 10, 19, .9);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.pnl-sticky .filterbar { margin-bottom: 10px; }
.pnl-sticky .tabs { margin-bottom: 10px; }
.pnl-sticky .activefilters { margin-bottom: 10px; }

/* The horizontal section nav, for screens with no room for the rail. */
.pnl-chips {
  display: none; gap: 6px; overflow-x: auto; padding-bottom: 10px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.pnl-chips::-webkit-scrollbar { display: none; }
@media (max-width: 1279px) { .pnl-chips { display: flex; } }
.pnl-chips a {
  flex: 0 0 auto; padding: 5px 11px; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--border); background: var(--surface-2);
  color: var(--mute); font-size: 12.5px; font-weight: var(--fw-med);
}
.pnl-chips a.on { border-color: var(--teal); color: var(--teal); background: color-mix(in srgb, var(--teal) 10%, transparent); }
.pnl-chips a:hover { text-decoration: none; color: var(--text); }

/* ── Section heads ───────────────────────────────────────────────────────── */

/* Numbered because the numbers are true: they are the order the sections are
   in, which is the order the report is meant to be read in, and the rail uses
   the same ones so "I am in 7" means something in both places. */
/* Off the bar's measured height, so a click in the rail lands the section head
   just under the filter bar rather than behind it. */
.pnl-sec { scroll-margin-top: calc(var(--topbar-h) + var(--pnl-bar-h) + 24px); margin-bottom: 38px; }
.pnl-head { display: flex; gap: 13px; align-items: baseline; margin: 0 0 14px; }
.pnl-head .n {
  font-family: var(--mono); font-size: 12px; font-weight: var(--fw-bold);
  color: var(--teal); background: color-mix(in srgb, var(--teal) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--teal) 34%, transparent); border-radius: 6px;
  padding: 2px 7px; flex: 0 0 auto;
}
.pnl-head h2 { margin: 0; font-size: 1.22rem; letter-spacing: -.015em; }
.pnl-head .sub { margin: 3px 0 0; font-size: 13.5px; color: var(--mute); }
.pnl-head > div { min-width: 0; }

/* ── Sample size, said in form rather than colour ────────────────────────── */

/* A figure computed on too few trades. Muted and marked, never hidden: hiding
   teaches the reader the data does not exist, marking teaches them it is not
   trustworthy yet — and only one of those is true. */
.thin-mark {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 10.5px; font-weight: var(--fw-bold); letter-spacing: .06em;
  text-transform: uppercase; color: var(--amber);
  border: 1px solid color-mix(in srgb, var(--amber) 40%, transparent); background: color-mix(in srgb, var(--amber) 10%, transparent);
  border-radius: 999px; padding: 1px 7px; white-space: nowrap;
}
tr.thin td, .thin-row { opacity: .55; }
tr.thin td .thin-mark { opacity: 1; }

/* ── Charts ──────────────────────────────────────────────────────────────── */

.pnl-chart { position: relative; width: 100%; }
/* Clipped to its own box, deliberately. Every chart here allocates its axis and
   label margins inside the viewBox, so nothing legitimate is cut — and the one
   time geometry escaped (a waterfall whose scale did not cover a negative
   gross) it drew bars straight down through the table in the card below it.
   A chart is not allowed to paint outside the chart. */
.pnl-chart svg { display: block; width: 100%; height: auto; overflow: hidden; }
.pnl-chart .grid-line { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }
.pnl-chart .zero-line { stroke: var(--border-hi); stroke-width: 1; shape-rendering: crispEdges; }
.pnl-chart .tick { font: 600 10.5px var(--mono); fill: var(--mute); }
.pnl-chart .tick-strong { font: var(--fw-med) 12px var(--sans); fill: var(--dim); }
.pnl-chart .band { font: 700 10px var(--sans); letter-spacing: .1em; }
.pnl-chart .val { font: 700 11px var(--mono); }
.pnl-chart .hit { fill: transparent; cursor: pointer; }
.pnl-chart .hit:focus-visible { outline: 2px solid var(--teal); outline-offset: -2px; }

/* The readout. Same shape as the equity curve's on the terminal page, so the
   two charts are read the same way. */
.pnl-tip {
  position: absolute; z-index: 12; pointer-events: none; min-width: 158px;
  background: var(--surface-3); border: 1px solid var(--border-hi);
  border-radius: 9px; box-shadow: var(--shadow); padding: 9px 11px; font-size: 12.5px;
}
.pnl-tip[hidden] { display: none; }
.pnl-tip .w {
  color: var(--mute); font-size: 10.5px; font-weight: var(--fw-bold);
  letter-spacing: .07em; text-transform: uppercase; margin-bottom: 5px;
}
.pnl-tip .r { display: flex; gap: 14px; justify-content: space-between; line-height: 1.55; }
.pnl-tip .r span { color: var(--dim); }
.pnl-tip .r b { margin-left: auto; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.pnl-tip .r.warn b { color: var(--amber); }

/* ── The weekday × hour grid ─────────────────────────────────────────────── */

.hm-scroll { overflow-x: auto; padding-bottom: 4px; }
.hm {
  display: grid; grid-template-columns: 42px repeat(24, minmax(24px, 1fr));
  gap: 2px; min-width: 660px;
}
.hm-col { font: var(--fw-med) 9.5px var(--mono); color: var(--mute); text-align: center; height: 14px; }
.hm-day { font: var(--fw-bold) 10.5px var(--mono); color: var(--mute); display: flex; align-items: center; height: 26px; }
.hm-cell {
  height: 26px; border-radius: 3px; border: 0; padding: 0; width: 100%;
  display: block; position: relative; cursor: pointer;
  background: var(--surface-3);
}
.hm-cell:hover, .hm-cell:focus-visible { outline: 1.5px solid var(--teal); outline-offset: 1px; z-index: 2; }
/* Under the sample floor. A dot, not a lighter colour — encoding "uncertain"
   as opacity alone makes it look like "nearly zero", which is the opposite
   thing to say. */
.hm-cell.thin::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 3px; height: 3px; border-radius: 50%; background: var(--border-hi);
}
.hm-cell.empty { opacity: .4; }
.hm-legend { display: flex; align-items: center; gap: 9px; margin-top: 12px; font-size: 12.5px; color: var(--dim); flex-wrap: wrap; }
/* Diverging: loss ↔ nothing ↔ profit, with a neutral middle. The middle has to
   read as "nothing happened" and the two ends as opposites, which is why there
   is no hue at the centre. */
.hm-ramp {
  width: 132px; height: 9px; border-radius: 5px;
  background: linear-gradient(90deg, var(--down), #2A2330 46%, #1C2A2B 54%, var(--up));
}

/* ── Horizontal bars, for exit reasons ───────────────────────────────────── */

.hbars { display: grid; gap: 9px; }
.hbar { display: grid; grid-template-columns: 190px minmax(0, 1fr) auto; gap: 12px; align-items: center; font-size: 13.5px; }
.hbar > .k { color: var(--dim); }
.hbar > .track { background: var(--surface-3); border-radius: 5px; height: 20px; position: relative; overflow: hidden; }
.hbar > .track > i { display: block; height: 100%; border-radius: 5px; }
.hbar > .v { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12.5px; white-space: nowrap; }
@media (max-width: 700px) { .hbar { grid-template-columns: 1fr; gap: 3px; } }

/* ── Findings ────────────────────────────────────────────────────────────── */

.finding {
  border: 1px solid var(--border); border-left: 3px solid var(--tint, var(--teal));
  background: var(--surface); border-radius: var(--radius); padding: 14px 16px;
}
.finding.bad { --tint: var(--down); }
.finding.warn { --tint: var(--amber); }
.finding.watch { --tint: var(--violet); }
.finding.good { --tint: var(--up); }
.finding h3 {
  margin: 0 0 6px; font-size: 1rem; text-transform: none; letter-spacing: -.01em;
  color: var(--text); font-weight: var(--fw-bold);
  display: flex; gap: 9px; align-items: baseline; flex-wrap: wrap;
}
.finding p { margin: 0; color: var(--dim); font-size: 14px; line-height: 1.55; }
.finding .rule {
  display: block; margin-top: 7px; border: 0; padding: 0;
  font-size: 11.5px; color: var(--mute); font-family: var(--mono);
}
.finding .stake { font-size: 11px; }

/* ── Two-panel charts share one x-axis and never one y-axis ──────────────── */

.panel-label {
  font: var(--fw-bold) 10px var(--sans); letter-spacing: .12em;
  fill: var(--mute); text-transform: uppercase;
}

/* ── Compare rows (buy vs sell, in / out of window) ──────────────────────── */

.compare { display: grid; gap: 10px; }
.compare-row {
  display: grid; grid-template-columns: 120px repeat(auto-fit, minmax(96px, 1fr));
  gap: 10px; padding: 11px 13px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-2); align-items: center;
}
.compare-row .k { font-weight: var(--fw-bold); }
.compare-row .m { min-width: 0; }
.compare-row .m .l {
  font-size: 10px; text-transform: uppercase; letter-spacing: .1em;
  color: var(--mute); font-weight: var(--fw-bold);
}
.compare-row .m .v { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 15px; font-weight: var(--fw-bold); }
@media (max-width: 700px) { .compare-row { grid-template-columns: 1fr 1fr; } }

/* ── Currency switch ─────────────────────────────────────────────────────── */

/* Only drawn when the scope holds more than one. Adding USD to INR is not a
   quantity, so the report shows one currency at a time and this is how the
   others are reached — never a total across them. */
.ccy-switch { display: flex; gap: 7px; align-items: center; flex-wrap: wrap; font-size: 12.5px; }
.ccy-switch a, .ccy-switch span.on {
  padding: 3px 10px; border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--mute); font-weight: var(--fw-med);
}
.ccy-switch span.on { border-color: var(--teal); color: var(--teal); background: color-mix(in srgb, var(--teal) 10%, transparent); }

/* ── Small helpers ───────────────────────────────────────────────────────── */

.stat .label .info-btn { margin-left: 4px; }
.metric-note { font-size: 12.5px; color: var(--mute); margin: 9px 0 0; }
.kv { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; font-size: 13.5px; }
.kv dt { color: var(--mute); white-space: nowrap; }
.kv dd { margin: 0; color: var(--dim); }
