/* TradingOps portal — shared design system
   Light and dark themes. Set data-theme="light|dark" on <html>. */

:root,
html[data-theme="light"]{
  --page:#f7f7f5;
  --surface:#ffffff;
  --surface-2:#fcfcfb;
  --sunken:#f4f3f0;
  --ink:#191917;
  --ink-2:#5f5e59;
  --ink-3:#8d8c85;
  --line:#e7e6e1;
  --line-2:#d8d7d1;
  --row-hover:#fbfbfa;
  --up:#0f7a45;
  --up-bg:#e9f4ec;
  --up-line:#bfe0cb;
  --down:#b0271d;
  --down-bg:#fbeceb;
  --down-line:#eec9c6;
  --warn:#8a5a06;
  --warn-bg:#fdf3e1;
  --warn-line:#ecd8ac;
  --info:#1f5c8f;
  --info-bg:#e9f1f8;
  --info-line:#c3d8ea;
  --neutral-bg:#f1f0ec;
  --neutral-ink:#57564f;
  --shadow:0 6px 24px rgba(25,25,23,.10);
  --scrim:rgba(25,25,23,.38);
}

html[data-theme="dark"]{
  --page:#111110;
  --surface:#191918;
  --surface-2:#1e1e1c;
  --sunken:#232321;
  --ink:#eceae4;
  --ink-2:#a5a49c;
  --ink-3:#7b7a73;
  --line:#2b2b29;
  --line-2:#3a3a36;
  --row-hover:#1e1e1c;
  --up:#54c184;
  --up-bg:rgba(84,193,132,.13);
  --up-line:rgba(84,193,132,.32);
  --down:#e8796b;
  --down-bg:rgba(232,121,107,.13);
  --down-line:rgba(232,121,107,.34);
  --warn:#d9a95c;
  --warn-bg:rgba(217,169,92,.13);
  --warn-line:rgba(217,169,92,.32);
  --info:#6fa8d8;
  --info-bg:rgba(111,168,216,.13);
  --info-line:rgba(111,168,216,.32);
  --neutral-bg:#262624;
  --neutral-ink:#a5a49c;
  --shadow:0 6px 24px rgba(0,0,0,.5);
  --scrim:rgba(0,0,0,.62);
}

:root{
  --font-serif:"Hedvig Letters Serif",Georgia,serif;
  --font-sans:"Hedvig Letters Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
  --font-mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  --rail:70px;
  --topbar:58px;
}

*{box-sizing:border-box;border-radius:0}
html,body{height:100%}
body{
  margin:0;background:var(--page);color:var(--ink);
  font-family:var(--font-sans);font-weight:400;font-size:14px;
  -webkit-font-smoothing:antialiased;
}
strong,b,th{font-weight:400}
a{color:inherit}
::selection{background:var(--ink);color:var(--surface)}

/* ---------- shell ---------- */
.app{min-height:100%;display:flex;flex-direction:column;background:var(--surface)}
.top{
  display:flex;align-items:center;gap:14px;height:var(--topbar);
  padding:0 24px 0 0;border-bottom:1px solid var(--line);
  position:sticky;top:0;z-index:40;background:var(--surface);
}
.logo{
  width:var(--rail);height:100%;flex:none;display:flex;align-items:center;justify-content:center;
  border-right:1px solid var(--line);color:var(--ink);
}
.top .spacer{flex:1}
.main{flex:1;display:flex;align-items:stretch;min-height:0}
.rail{
  width:var(--rail);flex:none;border-right:1px solid var(--line);
  padding:16px 0;display:flex;flex-direction:column;align-items:center;gap:8px;
  position:sticky;top:var(--topbar);align-self:flex-start;height:calc(100vh - var(--topbar));
}
.rail a,.rail button{
  width:38px;height:38px;display:flex;align-items:center;justify-content:center;
  color:var(--ink);border:1px solid transparent;text-decoration:none;
  background:none;padding:0;cursor:pointer;font:inherit;
}
.rail a:hover,.rail button:hover{background:var(--row-hover)}
.rail a.on{background:var(--sunken);border-color:var(--line-2)}
.rail .sep{width:26px;height:1px;background:var(--line);margin:4px 0}
.body{flex:1;min-width:0;padding:22px 24px 48px}
.body.narrow{max-width:1060px}

/* ---------- header controls ---------- */
.pill{
  display:inline-flex;align-items:center;gap:8px;height:32px;padding:0 12px;
  border:1px solid var(--line-2);font-size:13px;color:var(--ink);
  background:var(--surface);cursor:pointer;font-family:inherit;text-decoration:none;
}
.pill:hover{background:var(--row-hover)}
.pill.system{border-color:transparent}
.pill.danger{border-color:var(--down-line);color:var(--down)}
.pill.static{cursor:default;background:var(--info-bg);border-color:var(--info-line);color:var(--info)}
.pill.static:hover{background:var(--info-bg)}
.pill.danger:hover{background:var(--down-bg)}
/* The header clock: an ambient READING wearing the health pill's own box (`.pill.system` —
   same metrics, same padding, transparent border; owner call 2026-08-04 evening, replacing
   the earlier padding-less bare-text form). This modifier keeps ONLY what `.pill` must not
   give a thing that is not pressable: no pointer cursor, no hover tint — the health pill
   beside it is a link and earns both — plus tabular figures because the digits change under
   the reader. */
.pill.clock{cursor:default;font-variant-numeric:tabular-nums}
.pill.clock:hover{background:var(--surface)}
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;flex:none}
.dot.ok{background:var(--up)}
.dot.unk{background:var(--ink-3)}
.dot.warn{background:var(--warn)}
.dot.bad{background:var(--down)}
.dot.info{background:var(--info)}
.avatar{
  width:30px;height:30px;border-radius:50%;background:var(--ink);color:var(--surface);
  font-size:11px;display:flex;align-items:center;justify-content:center;letter-spacing:.04em;
  cursor:pointer;border:0;font-family:inherit;
}

/* ---------- dropdowns ---------- */
.dd{position:relative}
.dd-menu{
  display:none;position:absolute;z-index:60;min-width:236px;
  background:var(--surface);border:1px solid var(--line-2);box-shadow:var(--shadow);padding:4px;
}
.dd.open>.dd-menu{display:block}
.dd-menu.right{top:calc(100% + 6px);right:0}
.dd-menu.flyout{left:calc(100% + 9px);top:-4px}
.dd-menu a,.dd-menu button{
  display:flex;align-items:center;gap:9px;width:100%;padding:7px 10px;
  font-size:13px;color:var(--ink);text-decoration:none;background:none;border:0;
  cursor:pointer;font-family:inherit;text-align:left;
}
.dd-menu a:hover,.dd-menu button:hover{background:var(--row-hover)}
.dd-menu .head{padding:8px 10px 6px;font-size:11.5px;color:var(--ink-3)}
.dd-menu .note{padding:2px 10px 8px;font-size:11.5px;color:var(--ink-3);line-height:1.5}
.dd-menu .div{height:1px;background:var(--line);margin:4px 0}
.dd-menu .danger{color:var(--down)}
/* A menu row that holds a PILL rather than something pressable — since docs/18 §90 the
   read-only account/mode chip is a reading inside the account menu. The pill keeps its own
   box; the row only lends it the menu's horizontal rhythm, so it lines up with the head above
   it and still reads as a statement rather than as an item you can click. */
.dd-menu .pill-row{padding:2px 10px 8px}
.ext{margin-left:auto;color:var(--ink-3);font-size:11px}

/* ---------- typography ---------- */
h1,h2,h3,h4{font-weight:400;margin:0}
.page-title{font-size:22px;letter-spacing:-.01em}
.page-sub{font-size:13px;color:var(--ink-2);line-height:1.6;margin-top:8px;max-width:86ch}
/* A page title with its CONTROLS on the same line — today only `/debug`, whose command-id
   lookup sits beside the *Signals* title (docs/18 §72 item 3, replacing the §71 magnifying-
   glass toggle that folded it away). The same shape `.sec` already gives a section heading and
   its trailing link, at page-title scale: the title takes the room and the controls are pushed
   to the far edge.

   The box is ALWAYS visible now, so it is sized here rather than by an inline style: 400px is
   the width the owner's mockup asks for, `flex:0 1` lets it give that width back on a narrow
   screen instead of overflowing, and `flex-wrap` drops the pair onto its own line before the
   input gets uselessly short. `.input` is width:100%, which is why the basis and the max both
   have to be stated. The trailing margin is this page's own breathing room — the mockup shows
   a generous gap before the filter bar, and it COLLAPSES with the margin-top the `.alert` /
   `.empty` blocks below state for themselves, so the two can never add up. */
.page-head{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:38px}
.page-head .page-title{margin-right:auto}
/* …and a TAB STRIP as those controls (docs/18 §134, `/activity`): the same push `.sec > .tabs`
   already gives a strip riding on a section heading, at page-title scale. Only the strip's own
   vertical margins have to go — the underline treatment, the active marking and the panel
   wiring are untouched, and the title's `margin-right:auto` is what does the pushing. */
.page-head > .tabs{margin:0}
.page-head .input{flex:0 1 400px;max-width:400px;min-width:140px}
.page-head .btn{flex:none}
/* …and 20px of it on `/debug` (docs/18 §73 item 5, the owner's call): the filter bar under the
   lookup box is itself a row of controls, and 38px between two control rows read as a break in
   a page that has none. Scoped to the page rather than changed at the base, because the gap is
   a judgement about what follows the title HERE — a page whose title is followed by prose still
   wants the wider one. `[data-portal]` is the page's own hook, which every converted page
   states inside `<main>`. */
[data-portal="debug"] .page-head{margin-bottom:20px}
/* …and the SAME 20px on `/activity` (docs/18 §139.3, the operator's call after a live look).
   Its head carries the tab strip (§134), and what follows the strip is the first TABLE — so
   the 38px base put a gap under the title bigger than the one between the strip and the
   section it labels, which read as the page having lost its first block. 20px is `/debug`'s
   number for `/debug`'s reason, and the same reason holds here: the title row is a control
   row, and the thing under it is the content those controls select. It is a per-page
   judgement rather than a base change for the reason §73 item 5 already gave — a page whose
   title is followed by PROSE still wants the wider gap. */
[data-portal="positions"] .page-head{margin-bottom:20px}
.sec{margin:26px 0 10px;display:flex;align-items:baseline;gap:12px;flex-wrap:wrap}
/* `/health` IS THE ONE PAGE WITH NO `.page-head` (docs/18 §141, the operator's live look):
   its first visible block is the *System status* heading itself, so the 26px every OTHER page
   spends between two sections was landing on top of `.body`'s own 22px of padding and reading
   as a page that had lost its title. The margin is a gap BETWEEN sections; at the top of the
   page there is nothing above to be apart from.

   Scoped to the FIRST child so it can only ever be the top of the page, and to the direct
   child so a `.sec` nested in some later block is untouched. When the engine's stale-snapshot
   alert is present it is the first child instead — it carries no top margin of its own, and the
   heading under it keeps the ordinary 26px, which is the spacing that block wants. */
[data-portal="health"] > .sec:first-child{margin-top:0}
.sec h2{font-size:18px;letter-spacing:-.01em}
.sec .right{margin-left:auto;font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.foot{margin-top:8px;font-size:11.5px;color:var(--ink-3)}
.mono{font-family:var(--font-mono)}

/* ---------- metric cards ---------- */
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:22px}
/* FIVE up (docs/18 §133) — `/activity`'s row, since the maintenance margin and the cushion
   became a card each. A modifier rather than a second grid: same gap, same margin, same
   card, one more column. It degrades in two steps below — 1180px → three columns, and at
   720px it stops being a grid at all and becomes a swipeable row (docs/18 §138) — so a
   narrow screen never asks five serif headlines to share a phone's width. */
.cards.five{grid-template-columns:repeat(5,1fr)}
.card{background:var(--surface);border:1px solid var(--line);padding:14px 16px}
.card .k{font-size:12.5px;color:var(--ink-2);letter-spacing:.01em}
.card .v{
  font-family:var(--font-serif);font-size:27px;line-height:1.15;margin:7px 0 4px;
  font-variant-numeric:tabular-nums lining-nums;
}
.card .s{font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.card .s b{color:var(--ink-2)}
/* A card value in the DANGER tone (docs/18 §30) — today the Broker-reconciliation card's
   "2 breaks". Deliberately its own class rather than the money `.down` it shares an ink
   with: a red verdict and a red loss are different claims, and one of them must be able to
   change colour without the other following. Grey (`.mute`) needs no rule here — the design
   system's tertiary-ink class already applies, since `.card .v` sets no colour of its own. */
.card .v.bad{color:var(--down)}

/* ---------- system checks: the verdict card and the list (docs/18 §142) ----------
   ⚠️ This replaced the six-across CHIP GRID (`.status` / `.chips` / `.chip`) on 2026-08-18,
   the operator's call. A grid of sixteen small boxes made the reader hunt for the one that
   was not green and wrapped into a wall on a phone; a list reads top to bottom at every
   width, and the verdict is stated once above it.

   NO NEW COLOURS: every tone below is one of the existing state tokens, the same three
   `.b`/`.dot` already use, and neither the tint nor the icon is ever the only carrier of a
   state — the row says it in words twice (its detail and its verdict). */
.sysverdict{
  display:flex;align-items:flex-start;gap:11px;padding:14px 16px;margin-bottom:14px;
  border:1px solid var(--line);background:var(--surface);
}
.sysverdict .i{display:flex;flex:none;margin-top:1px;color:var(--ink-3)}
.sysverdict .h{font-size:15px;line-height:1.3}
.sysverdict .s{font-size:12px;color:var(--ink-3);margin-top:4px;font-variant-numeric:tabular-nums}
.sysverdict.ok{background:var(--up-bg);border-color:var(--up-line)}
.sysverdict.ok .i{color:var(--up)}
.sysverdict.warn{background:var(--warn-bg);border-color:var(--warn-line)}
.sysverdict.warn .i{color:var(--warn)}
.sysverdict.bad{background:var(--down-bg);border-color:var(--down-line)}
.sysverdict.bad .i{color:var(--down)}
/* `unk` keeps the plain surface and the tertiary ink it inherits: an unreadable set states
   nothing, so it is tinted as nothing — and above all not green. */

/* The list. ⚠️ **Each ROW is a `.set` CARD since docs/18 §144** (the operator's call): the
   settings row's own box, so a check reads like everything stacked on `/settings` — which is
   also where the strategy cards went with §142. `.set` (below, in the settings block) supplies
   the border, the padding and the 10px of air between rows; what is left here is the row's own
   flex line and the state colours. The wrapper around them draws nothing and carries no class
   at all, so the `.checks` frame with its hairline between rows is gone with it. */
.check{
  display:flex;align-items:center;gap:10px;width:100%;
  font:inherit;color:inherit;text-align:left;
}
.check .i{display:flex;flex:none;color:var(--ink-3)}
.check .n{font-size:12.5px;flex:none}
/* The reading. It takes the slack and is the ONE thing that may be cut on a narrow screen —
   the name and the verdict either side of it are never elided. */
.check .d{
  font-size:11.5px;color:var(--ink-3);flex:1 1 auto;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  font-variant-numeric:tabular-nums;
}
/* …so the verdict still sits at the right edge on a row whose reading is empty. */
.check .w{font-size:11.5px;color:var(--ink-3);flex:none;margin-left:auto;white-space:nowrap}
.check[data-state="ok"] .i{color:var(--up)}
.check[data-state="warn"] .i{color:var(--warn)}
.check[data-state="warn"] .w{color:var(--warn)}
.check[data-state="bad"] .i{color:var(--down)}
.check[data-state="bad"] .w{color:var(--down)}
/* A row that HAS an explanation is rendered as a <button> (`macros/status.html`) and opens
   the modal with it; one without stays a <div>, so the pointer cursor is an honest promise.
   Button chrome only is reset — the padding and the surface above still make the row. */
button.check{cursor:pointer}
button.check:hover{background:var(--row-hover)}

/* ---------- data tables ---------- */
.tbl-wrap{border:1px solid var(--line);overflow-x:auto;overflow-y:hidden}
/* ⚠️ `border-collapse:SEPARATE` (docs/18 §72 item 1) — every horizontal rule on these tables is
   drawn by the CELL that sits above it, never by the row and never by the collapsing model.
   The reason is the pinned first column: a collapsed border belongs to the TABLE and is painted
   before a sticky cell paints its own opaque background over it, so the separator went missing
   under the pinned column exactly when that column was stuck. Per-cell borders travel with the
   cell, so the sticky and the scrolling halves of a row draw the identical 1px `--line` from
   the identical rule. `border-spacing:0` keeps the cells touching, so nothing about the grid's
   geometry changes. */
table.blotter{
  width:100%;min-width:max-content;border-collapse:separate;border-spacing:0;
  table-layout:auto;font-size:12.5px;
  font-variant-numeric:tabular-nums lining-nums;
}
/* nowrap keeps every row exactly one line tall, so the grid never reflows as
   prices tick; the wrapper scrolls instead of the cell truncating. */
table.blotter th,table.blotter td{
  white-space:nowrap;overflow:visible;text-overflow:clip;padding:0 12px;height:26px;text-align:left;
}
table.blotter thead th{
  height:30px;color:var(--ink-3);font-size:11.5px;letter-spacing:.03em;
  border-bottom:1px solid var(--line);background:var(--surface-2);
}
/* A table's title INSIDE its own frame (docs/18 §134, the operator's refined mockup) — the row
   `macros/blotter.html`'s `caption=` draws above the column headers, on `/activity`'s Trades
   tab where three tables stack under one page title. It is the section heading's ink and size,
   in the header's own band, so the table reads as one titled block rather than as a heading
   with a table under it. */
table.blotter thead tr.tcap td{
  height:32px;color:var(--ink-2);font-size:12.5px;letter-spacing:-.01em;
  border-bottom:1px solid var(--line);background:var(--surface-2);
}
/* …and the space BETWEEN two stacked tables (docs/18 §134, the operator's mockup): a panel
   drawing three of them reads as three tables rather than as one long grid. Only between —
   the first keeps the panel's own top edge — and the pager is deliberately excluded, since it
   is the bottom edge of the table above it and shares its border. */
.tabpanel > .tbl-wrap + .tbl-wrap{margin-top:22px}
/* The row separator: one border per CELL (see the `border-collapse` note above), suppressed
   under the last row so the wrapper's own frame is not doubled. The ledger's merged rows take
   two more rules of their own, further down with the rest of that table's shape.

   ⚠️ `tbody:last-child`, not a bare `tbody` (docs/18 §93). Every table `macros/blotter.html`
   draws has exactly ONE tbody, so for all of them this reads exactly as `tbody tr:last-child`
   always did — but the trade ledger now opens a tbody per TRADE, and "the last row" there has
   to mean the last row of the TABLE, not the last line of every trade. Unqualified, the rule
   between two trades disappeared.

   The HOVER is the ledger's one exception: a trade there is highlighted WHOLE by its own tbody
   rule further down, so the row-level tint is excluded here rather than left to stack on top of
   it — two tints over one row is the double grey §93 was asked to remove. */
table.blotter tbody td{border-bottom:1px solid var(--line)}
table.blotter tbody:last-child tr:last-child td{border-bottom:0}
table.blotter:not(.ledger) tbody tr:hover{background:var(--row-hover)}
/* A column header's explanation trigger (docs/18 §28): the small muted "i" that opens the
   page's shared dialog with what that column means. Sized in the header's own 11.5px type
   and vertically centred, so it can never change the 30px header height or push a `num`
   header off the column it labels. */
button.info{
  margin-left:5px;width:13px;height:13px;padding:0;border:1px solid var(--line-2);
  border-radius:50%;background:none;color:var(--ink-3);font:inherit;font-size:9px;
  line-height:11px;text-align:center;vertical-align:middle;display:inline-block;cursor:pointer;
}
button.info:hover{color:var(--ink);border-color:var(--ink-3)}
.num{text-align:right}
.sub{color:var(--ink-2)}
.mute{color:var(--ink-3)}
/* The DEAD-LEG strike (docs/18 §102, extending §87): on a trade-ledger line whose order never
   traded, the two cells naming that order — *Leg* and *Price* — are struck through as well as
   muted. Set per COLUMN (`col(..., strike=True)`) beside the row's `mute`, and the parent `td`'s
   line-through paints through the Leg cell's `.runbtn` anchor, so no anchor rule is needed. */
.strike{text-decoration:line-through}
.up{color:var(--up)}
.down{color:var(--down)}
/* The two ALERT inks, for a toned TEXT cell rather than a pill (docs/18 §52.6 — `/home`'s
   Risk used column, which the owner asked to be plain text). They carry the SAME tone names
   the pills use, so a cell builder's tone travels to either shape without a translation table
   in between, and they add no colour: `--warn` and `--down` are already the amber and the red
   of `.b.warn` / `.b.bad`. Every existing `.warn` / `.bad` element (a dot, a snackbar, a card
   value, a pill) sets its own colour in a two-class rule and so outranks these. */
.warn{color:var(--warn)}
.bad{color:var(--down)}
/* `.band` (a bracket or a collapsed run, set server-side) is a STRUCTURAL class only since
   2026-08-04 (owner call): the grey row tint it used to carry is gone — the expander and the
   ×N count already say "this line stands for several" — and band rows hover like any other. */
.warn-age{color:var(--warn)}
.side-buy{color:var(--up)}
.side-sell{color:var(--down)}

/* reference tables (prose pages) */
/* `border-collapse:separate` for the same reason the blotter has it (docs/18 §72 item 1) —
   this table pins its first column too. Its rules were already declared per CELL, so nothing
   below moved. */
table.ref{width:100%;min-width:660px;border-collapse:separate;border-spacing:0;font-size:13px}
table.ref td:first-child,table.ref th:first-child{white-space:nowrap}
table.ref th{
  text-align:left;color:var(--ink-3);font-size:11.5px;letter-spacing:.03em;
  padding:8px 12px;border-bottom:1px solid var(--line);background:var(--surface-2);vertical-align:bottom;
}
table.ref td{padding:11px 12px;border-bottom:1px solid var(--line);vertical-align:top;line-height:1.6}
table.ref tr:last-child td{border-bottom:0}
table.ref td p{margin:0 0 8px}
table.ref td p:last-child{margin-bottom:0}
.tbl-wrap.scroll{overflow-x:auto}

/* ---------- badges ----------
   The base carries the NEUTRAL look, so a tone class that does not exist still paints a
   visible pill rather than unboxed text — which is what the blotter macro's fallback claims
   happens. Every tone below overrides all three properties. */
.b{
  display:inline-flex;align-items:center;gap:5px;height:18px;padding:0 7px;
  font-size:11px;white-space:nowrap;
  background:var(--neutral-bg);color:var(--neutral-ink);border:1px solid var(--line-2);
}
.b.live{background:var(--up-bg);color:var(--up);border:1px solid var(--up-line)}
.b.neu{background:var(--neutral-bg);color:var(--neutral-ink);border:1px solid var(--line-2)}
.b.warn{background:var(--warn-bg);color:var(--warn);border:1px solid var(--warn-line)}
.b.bad{background:var(--down-bg);color:var(--down);border:1px solid var(--down-line)}
.b.info{background:var(--info-bg);color:var(--info);border:1px solid var(--info-line)}
/* The two QUIET tones (docs/18 §52.1). They used to render as bare toned text — the
   "quiet when fine, loud when not" rule — and are pills now like everything else in a badge
   column, so a reader never has to work out whether a cell is a chip or a sentence. They stay
   quiet by sitting on the SUNKEN ground with the hairline border and the secondary/tertiary
   ink, which is a whole step below `.neu`: a stated fact nobody needs to act on. */
.b.sub{background:var(--sunken);color:var(--ink-2);border:1px solid var(--line)}
.b.mute{background:var(--sunken);color:var(--ink-3);border:1px solid var(--line)}
/* A pill that CARRIES an explanation is a real <button> (`macros/blotter.html::_badge`) and
   opens it in a popover; one with nothing to say stays a <span> and gets none of this, so the
   pointer cursor is an honest promise. The hover moves the BORDER towards the pill's own ink —
   `currentColor`, so one rule serves all six tones and none of them gains a colour the tokens
   do not already give it — but only PART of the way there (docs/18 §52.6): the full-strength
   border read as a state change rather than as a hover, which in a table of chips is a lot of
   noise for a pointer that has to be somewhere. `color-mix` toward `transparent` keeps the
   step tone-agnostic and background-agnostic; a browser that does not support it drops the
   declaration and simply keeps the resting border, which is a quieter miss than a louder one.
   The FOCUS ring stays at full strength deliberately — it appears only on keyboard focus,
   where it is the only thing saying where you are. */
button.b{font-family:inherit;margin:0;text-align:left;cursor:pointer}
button.b:hover{border-color:color-mix(in srgb,currentColor 38%,transparent)}
button.b:focus-visible{outline:1px solid currentColor;outline-offset:1px}
/* The popover itself: ONE element, created by portal.js on first use and appended to <body>.
   It has to be `position:fixed` and placed from JS — a badge lives inside `.tbl-wrap`, which
   scrolls horizontally and hides its vertical overflow, so an absolutely-positioned box would
   be clipped at the table's edge exactly where the explanations matter most. Above the topbar
   and the drawers (70), below the confirm dialog (80) and the snackbar (90).

   `pre-line` since docs/18 §71 item 7, and it is what lets one box serve both of its callers.
   Every explanation on this portal — a pill's graded sentence, a column header's — is written
   as PARAGRAPHS separated by a blank line, and `normal` collapsed them into one run: the graded
   WORD ran straight into the sentence explaining it, and a composed column explanation read as
   a wall. It preserves the newlines and nothing else (runs of spaces still collapse), so no
   copy had to change for it. */
.pop{
  display:none;position:fixed;z-index:75;max-width:min(330px,calc(100vw - 24px));
  padding:9px 12px;background:var(--surface);color:var(--ink);
  border:1px solid var(--line-2);box-shadow:var(--shadow);
  font-size:12.5px;line-height:1.55;white-space:pre-line;
}
.pop.open{display:block}

/* A CLIPPED cell — `col(..., cls='clip')`, which puts this class on the `<td>`. For a column
   whose text is usually two words but can be a whole refused-at-the-ingress reason (the two
   signal tables. Result, docs/18 §35.7): it is cut at ~20 characters with an ellipsis, and the
   FULL text is already in the span's own `title`, so nothing is lost and one long row can no
   longer stretch the table or wrap the cell onto a second line.

   Clipped in CSS, never by truncating the string server-side: a shortened cell plus a full
   reading elsewhere would be two sources for one fact, and the FULL one has to stay whole. It
   was the cell's own `title` until docs/18 §144 took every hover tooltip off the portal; the
   one clipped column (`performance/ledger.py::status_cell`) sends a mapping WITH a title in
   every branch, which `_badge` draws as a click POPOVER, so the whole of it is still one tap
   away. A future clipped column that does not do that would be cut with nowhere to read it.

   `max-width` only ever SHRINKS — a short badge keeps its natural width and its usual look.
   The display swap is what makes the ellipsis possible at all: `.b` is an inline-flex box and
   `text-overflow` cannot act on a flex item's anonymous text, so a clipped badge becomes
   inline-block and gets back, as an explicit line-height, the centring it loses with
   `align-items` (18px box less its 1px borders). `vertical-align:bottom` keeps the box sitting
   on the row's own text line rather than on an overflow box's baseline, which is the bottom
   margin edge and would ride ~3px high. */
td.clip>span,td.clip>button{
  display:inline-block;max-width:20ch;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;vertical-align:bottom;
}
td.clip>.b{line-height:16px}

/* ---------- toggle ---------- */
.tog{
  width:32px;height:18px;border-radius:9px;background:var(--ink);position:relative;
  display:inline-block;vertical-align:-4px;border:0;padding:0;cursor:pointer;
}
.tog:after{content:"";position:absolute;top:2px;left:16px;width:14px;height:14px;border-radius:50%;background:var(--surface)}
.tog.off{background:var(--line-2)}
.tog.off:after{left:2px}

/* ---------- buttons + inputs ----------
   ⚠️ `text-decoration:none` and `white-space:nowrap` are docs/18 §144's, and both exist because
   this class is worn by `<a>` as often as by `<button>`:

   * an anchor arrives UNDERLINED from the browser, so `/debug`'s *Reset* sat beside *Apply*
     wearing the same box at a different weight of line — one control, drawn two ways;
   * a pager's `← Newer` broke between its arrow and its word on a phone, which reads as two
     dead half-buttons. A button's label is one thing and never wraps. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:7px;height:34px;padding:0 14px;
  border:1px solid var(--line-2);background:var(--surface);color:var(--ink);
  font-family:inherit;font-size:13px;cursor:pointer;
  text-decoration:none;white-space:nowrap;
}
.btn:hover{background:var(--row-hover)}
.btn.primary{background:var(--ink);border-color:var(--ink);color:var(--surface)}
.btn.primary:hover{opacity:.88}
.btn.xs{height:24px;padding:0 9px;font-size:11.5px}
.btn.ghost{border-color:transparent;color:var(--ink-2)}
.btn.block{width:100%}
.input{
  height:34px;padding:0 11px;border:1px solid var(--line-2);background:var(--surface);
  color:var(--ink);font-family:inherit;font-size:13px;width:100%;
}
.input:focus{outline:0;border-color:var(--ink-3)}
.input.mono{font-family:var(--font-mono);font-size:12.5px}
.input.sm{height:28px;font-size:12px}
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-size:12px;color:var(--ink-2)}

/* ---------- prose ---------- */
.prose{font-size:13.5px;line-height:1.65;color:var(--ink)}
.prose p{margin:0 0 10px;max-width:88ch}
.prose p:last-child{margin-bottom:0}
.prose ul,.prose ol{margin:0 0 10px;padding-left:20px;max-width:88ch}
.prose li{margin-bottom:7px}
.prose li > p{margin:0 0 7px}
.prose li > p:last-child{margin-bottom:0}
.prose li:last-child{margin-bottom:0}
.prose .muted{color:var(--ink-2)}
code{font-family:var(--font-mono);font-size:.88em;background:var(--sunken);padding:1px 5px;border:1px solid var(--line)}
pre{
  margin:0 0 10px;padding:12px 14px;background:var(--sunken);border:1px solid var(--line);
  overflow-x:auto;font-family:var(--font-mono);font-size:12px;line-height:1.65;color:var(--ink);
}
pre code{background:none;border:0;padding:0;font-size:inherit}
.codeblock{position:relative}
.codeblock .copy{position:absolute;top:7px;right:7px;opacity:0;transition:opacity .1s}
.codeblock:hover .copy{opacity:1}

/* pinned first column — stays put while the rest of the row scrolls.
   Sticky cells need their own opaque background, so the row band and hover
   tints are re-applied to them as an overlay rather than inherited.

   ⚠️ `:first-child` is the right hook for a table whose every row has exactly one cell per
   column — which is every table `macros/blotter.html` draws. It is the WRONG one for the trade
   ledger (docs/18 §70.4): a trade there is TWO rows with its own facts merged across them by
   `rowspan`, so the second row's first `<td>` is a DIFFERENT column (Type, not Trade #) and it
   was being pinned at left:0 — sliding under the merged cell the moment the table scrolled
   sideways, which on a phone it always does. So the ledger is excluded from the generic rule
   and pins the `pin` class its own macro puts on the Trade # column instead, header cell
   included. Nothing about any other table's pinning moved. */
table.blotter:not(.ledger) th:first-child,
table.blotter:not(.ledger) td:first-child,
table.ledger th.pin,
table.ledger td.pin,
table.ref th:first-child,
table.ref td:first-child{
  position:sticky;left:0;z-index:2;
  background-color:var(--surface);
  box-shadow:1px 0 0 var(--line);
}
table.blotter:not(.ledger) thead th:first-child,
table.ledger thead th.pin,
table.ref thead th:first-child{z-index:3;background-color:var(--surface-2)}
/* The hovered row's pinned cell re-applies the tint as an IMAGE over the opaque background its
   stickiness depends on — a row-level tint cannot reach a cell that paints its own. The ledger's
   half of this pair moved to that table's own block below (docs/18 §93), where the tint is
   scoped to the hovered TRADE rather than to one row; the mechanism is this one, unchanged. */
table.blotter:not(.ledger) tbody tr:hover td:first-child{background-image:linear-gradient(var(--row-hover),var(--row-hover))}
/* ⚠️ The ROW SEPARATOR under a pinned cell needs NO rule here (docs/18 §72 item 1). docs/18
   §71 item 2 added an `inset 0 -1px 0` shadow to redraw it, having read the missing line as a
   painting-order problem between the collapsed border and the sticky cell. It is — but only
   while the cell is actually stuck and composited, which on a desktop it usually is not. Every
   other time the collapsed border rendered as well and the shadow DOUBLED it, so the owner saw
   a 2px rule down the first column of every table. The fix is the border model itself: the
   separator is a per-cell `border-bottom` now (see `table.blotter tbody td` above), which the
   pinned cell carries like any other cell, in every state. Only the pinned column's own RIGHT
   EDGE is still a shadow, above — a border there would take width inside the sticky box. */

/* scrollbars sit inside the table frame, not under the page — ONE treatment for every box on
   the portal that scrolls sideways, which since docs/18 §138 is also the phone's card
   carousel (`.cards.five`, inert as a scroller above 720px, so this costs it nothing there). */
.tbl-wrap::-webkit-scrollbar,.cards.five::-webkit-scrollbar{height:9px}
.tbl-wrap::-webkit-scrollbar-track,.cards.five::-webkit-scrollbar-track{background:var(--surface-2)}
.tbl-wrap::-webkit-scrollbar-thumb,.cards.five::-webkit-scrollbar-thumb{background:var(--line-2)}
.tbl-wrap::-webkit-scrollbar-thumb:hover,.cards.five::-webkit-scrollbar-thumb:hover{background:var(--ink-3)}
.tbl-wrap,.cards.five{scrollbar-width:thin;scrollbar-color:var(--line-2) var(--surface-2)}

/* content cards on prose pages */
.panel{border:1px solid var(--line);background:var(--surface);padding:18px 20px;margin-bottom:12px}
.panel.sunken{background:var(--surface-2)}
.panel > h4{font-size:15px;margin-bottom:9px}
.panel > h3{font-size:15px;margin-bottom:9px}
.alert{
  display:flex;gap:9px;align-items:flex-start;padding:10px 13px;margin-bottom:12px;
  border:1px solid var(--warn-line);background:var(--warn-bg);color:var(--warn);
  font-size:13px;line-height:1.55;
}
.note{margin-top:18px;border:1px solid var(--line);background:var(--surface-2);padding:14px 16px}
.note h4{font-size:15px;margin:0 0 5px}
.note p{margin:0;font-size:12.5px;color:var(--ink-2);line-height:1.55;max-width:96ch}

/* ---------- two-column layout with sticky nav ---------- */
.split{display:flex;gap:32px;align-items:flex-start}
.split .col{flex:1;min-width:0}
.toc{
  width:214px;flex:none;position:sticky;top:calc(var(--topbar) + 22px);
  border-left:1px solid var(--line);padding-left:14px;
}
.toc .t{font-size:11.5px;color:var(--ink-3);letter-spacing:.03em;margin-bottom:8px}
.toc a{display:block;font-size:12.5px;color:var(--ink-2);text-decoration:none;padding:4px 0;line-height:1.4}
.toc a:hover{color:var(--ink)}
.toc a.on{color:var(--ink)}
.doc-sec{margin-bottom:34px;scroll-margin-top:calc(var(--topbar) + 20px)}
.doc-sec > h3{font-size:19px;margin-bottom:6px}
.doc-sec > .lead{font-size:13px;color:var(--ink-2);line-height:1.6;margin-bottom:14px;max-width:88ch}

/* ---------- settings rows ---------- */
.set{border:1px solid var(--line);background:var(--surface);padding:15px 18px;margin-bottom:10px}
.set .hd{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.set .key{font-family:var(--font-mono);font-size:13px}
.set .def{margin-left:auto;font-family:var(--font-mono);font-size:11.5px;color:var(--ink-3);white-space:nowrap}
.set .desc{font-size:12.5px;color:var(--ink-2);line-height:1.6;margin:7px 0 10px;max-width:92ch}
.set .val-row{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.set .val{
  font-family:var(--font-mono);font-size:12.5px;background:var(--sunken);
  border:1px solid var(--line);padding:3px 8px;word-break:break-all;
}
/* THE STRATEGY CARDS (docs/18 §142) — one `.set` per strategy on the General tab. They reuse
   the settings row whole; all this adds is the two body lines' label ink and a little air
   above the control row, so a card reads as name → what it is doing → what you can do to it.
   The tone classes on those lines are the design system's own (`.sub` / `.mute` / `.warn` /
   `.bad`), unchanged from when these were blotter cells. */
.set.strat .desc{margin:6px 0 0}
.set.strat .desc .lbl{color:var(--ink-3)}
.set.strat .val-row{margin-top:11px}
.grp{margin-bottom:30px;scroll-margin-top:calc(var(--topbar) + 20px)}
.grp > .hd{margin-bottom:12px}
.grp > .hd h3{font-size:19px}
.grp > .hd .blurb{font-size:13px;color:var(--ink-2);line-height:1.6;margin-top:6px;max-width:88ch}
.grp > .hd .readers{font-size:11.5px;color:var(--ink-3);margin-top:6px}
.tabs{display:flex;gap:14px;margin:16px 0 22px;flex-wrap:wrap}
/* No horizontal padding (docs/18 §94): the active tab's underline is exactly as long as its
   own word, and the space between tabs is the strip's `gap` rather than each tab's box. */
.tabs a{
  padding:9px 0 5px;font-size:13px;color:var(--ink-2);text-decoration:none;
  border-bottom:1px solid transparent;margin-bottom:-1px;
}
.tabs a:hover{color:var(--ink)}
.tabs a.on{color:var(--ink);border-bottom-color:var(--ink)}
/* The IN-PAGE half of the strip (docs/18 §69, `/positions`). `[hidden]` is the UA's own
   display:none and is what makes the no-JS case show exactly one whole panel; it is restated
   here so a future `display` rule on `.tabpanel` cannot quietly outrank it. The two
   scroll-margins are for deep links (`#closed`, `#orders`): portal.js scrolls the STRIP into
   view, and since docs/18 §92 the heading rides ON that row, so one clearance covers both. */
.tabpanel[hidden]{display:none}
.tabs[data-tabs]{scroll-margin-top:calc(var(--topbar) + 20px)}
.tabpanel{scroll-margin-top:calc(var(--topbar) + 20px)}
/* A strip INSIDE its section heading (docs/18 §92, the owner's call) — `/positions`' Positions
   and Orders, which cost two rows and ~60px of height each while the tabs sat on a line of
   their own. `.sec` is already a baseline-aligned flex row whose trailing item is pushed out
   by `margin-left:auto`, so the strip only has to drop its own vertical margins and take that
   same push: the underline treatment, the active marking and the panel wiring are untouched.
   The push applies at EVERY width (docs/18 §94, the owner's call): below 720px the row wraps
   and the strip keeps hugging the far edge, on the title's line while it fits and under it
   when it does not — there is deliberately no breakpoint override. */
.sec > .tabs{margin:0 0 0 auto}

/* ---------- login ---------- */
body.centered{background:var(--surface)}
/* Two `min-height` declarations on purpose (docs/18 §140): on mobile `100vh` is the viewport
   with the address bar COLLAPSED, so while the bar is showing the page is taller than what is
   visible and offers a pointless little scroll. `100svh` is that visible height; the `100vh`
   line stays first as the fallback for browsers without the small-viewport unit. */
.login-wrap{min-height:100vh;min-height:100svh;display:flex;align-items:center;justify-content:center;padding:40px 16px}
.login{width:100%;max-width:312px;display:flex;flex-direction:column;align-items:center;gap:22px}
.login .brand{color:var(--ink)}
.login h1{font-family:var(--font-serif);font-size:21px;text-align:center}
.login form{width:100%;display:flex;flex-direction:column;gap:14px}
.login .stack{display:flex;flex-direction:column;gap:10px}
.login .hint{font-size:11.5px;color:var(--ink-3);text-align:center;line-height:1.55}

/* ---------- the public document page (/schema) ----------
   A shell-LESS document like `/login` above, but a long READ rather than a form. It paints
   itself on the white document SURFACE — the same ground `.app` gives every page inside the
   shell — rather than the app's grey `--page`, exactly as `body.centered` does. That is what
   makes the reference tables read: their header row is `--surface-2` and their pinned first
   column is `--surface`, both painted for that ground, so on any other one the sticky column
   shows as a band down the table.

   Everything else here is the ordinary design system — `.body.narrow` for the reading column,
   `.doc-sec`, `.prose`, `table.ref` in a scrolling `.tbl-wrap`, `.panel`, `.alert`, `.note`,
   `pre` — including the 720px padding, so there is no second layout to keep in step. Three
   overrides and no more: centre the column (`.body` is normally a flex child and gets its
   position from `.main`), drop the anchor offset that exists to clear a sticky topbar this
   document does not have, and un-box a code span inside a section heading, where the inline
   chip's fill and border are too loud at 19px. */
body.doc{background:var(--surface)}
body.doc .body{margin-left:auto;margin-right:auto}
body.doc .doc-sec{scroll-margin-top:18px}
body.doc .doc-sec > h3 code{background:none;border:0;padding:0}

/* ---------- confirm modal + snackbar ----------
   Not in the mockups: every destructive operator action is confirmed before it fires, and
   a refused one has to say so. Built from the same tokens — no radius, no bold, --shadow
   for elevation, --scrim for the backdrop. Behaviour is data-attribute driven (portal.js):
   [data-modal="id"] opens, [data-modal-close] / a backdrop click / Escape closes. */
.modal-backdrop{
  display:none;position:fixed;inset:0;z-index:80;background:var(--scrim);
  align-items:flex-start;justify-content:center;padding:12vh 16px 16px;
}
.modal-backdrop.open{display:flex}
.modal{
  width:100%;max-width:452px;background:var(--surface);border:1px solid var(--line-2);
  box-shadow:var(--shadow);padding:18px 20px;
}
/* A dialog holding a TABLE rather than a paragraph — today only a collapsed run's members
   (docs/18 §72 item 7). Seven columns do not read at the confirm dialog's width; the table
   inside keeps its own `.tbl-wrap` scroll, so this never makes the page scroll sideways. */
.modal.wide{max-width:min(880px,calc(100vw - 32px))}
.modal.wide .tbl-wrap{margin-top:12px}
/* A dialog headed like a CHART CARD (docs/18 §153): the card vocabulary's own parts — the
   caption naming what is plotted, the serif headline stating the reading, the sub-line naming
   which trade, then the key — so this dialog is titled the way every chart on the portal is
   titled rather than in a dialect of its own.

   ⚠️ THE TYPE IS THE CARD'S OWN SINCE docs/18 §155, not a near-miss of it (the operator's call).
   `.k` was 11px uppercase on `--ink-3` with wide tracking and `.v` was 26px — an eyebrow of a
   different design — beside `/home`'s 12.5px sentence-case `--ink-2` caption over a 27px
   headline. The values below are `.card .k` / `.card .v` / `.card .s` restated for this box
   rather than inherited, because a `.modal` is not inside a `.card`; keep the two blocks equal.
   Only the BOX is this rule's own, because a dialog has no card frame around it. */
.modal .chart-head{margin-bottom:14px}
.modal .chart-head .k{font-size:12.5px;color:var(--ink-2);letter-spacing:.01em}
.modal .chart-head .v{font-family:var(--font-serif);font-size:27px;line-height:1.15;margin:7px 0 4px;font-variant-numeric:tabular-nums lining-nums}
.modal .chart-head .s{font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums}
/* THE KEY STANDS OFF THE LINE ABOVE IT (docs/18 §158, the operator's call). It was 5px — the
   spacing of two sentences in one paragraph — and the key is not another sentence: it is the
   legend for the picture below, and at 5px it read as a third line of the sub-line. 9px is the
   air the card's own headline already takes above its caption, so nothing new is invented. */
.modal .chart-head .s + .s{margin-top:9px}
/* …and the plot itself. A FIXED aspect ratio rather than the cards' width-driven one: this box
   is inside a dialog whose width is the viewport's, so a ratio is what keeps the chart from
   growing taller than a phone screen. `min-height:0` is what lets it shrink at all inside the
   dialog's flex column — without it the canvas's own intrinsic size wins and the PAGE scrolls
   sideways instead of the chart fitting. */
.modal .chart-box{position:relative;aspect-ratio:1.9;min-height:0;margin-top:12px}
@media (max-width:720px){.modal .chart-box{aspect-ratio:1.25}}
.modal h3{font-size:16px;margin-bottom:8px}
.modal p{margin:0 0 10px;font-size:13px;color:var(--ink-2);line-height:1.6}
.modal p:last-of-type{margin-bottom:0}
.modal .actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:16px}
.snackbar{
  display:none;position:fixed;left:50%;bottom:22px;transform:translateX(-50%);z-index:90;
  align-items:center;gap:9px;max-width:min(560px,calc(100vw - 32px));padding:10px 14px;
  font-size:13px;line-height:1.5;background:var(--surface);color:var(--ink);
  border:1px solid var(--line-2);box-shadow:var(--shadow);
}
.snackbar.show{display:flex}
.snackbar.ok{background:var(--up-bg);border-color:var(--up-line);color:var(--up)}
.snackbar.warn{background:var(--warn-bg);border-color:var(--warn-line);color:var(--warn)}
.snackbar.bad{background:var(--down-bg);border-color:var(--down-line);color:var(--down)}

/* ---------- misc ---------- */
.kv{display:grid;grid-template-columns:180px 1fr;gap:0;border:1px solid var(--line);font-size:13px}
.kv > div{padding:9px 13px;border-bottom:1px solid var(--line)}
.kv > div:nth-child(2n){border-left:1px solid var(--line)}
.kv > div:nth-last-child(-n+2){border-bottom:0}
.kv .k{color:var(--ink-2)}
.empty{
  border:1px solid var(--line);background:var(--surface-2);padding:10px 20px;
  text-align:center;color:var(--ink-3);font-size:13px;line-height:1.6;
}

/* ---------- mobile chrome, inert above 720px ----------
   Four things carry the whole small-screen layout, and every one of them is a no-op here:
   the two mobile-only triggers and the shared scrim are display:none, `.hdr` is
   display:contents (so the header's controls stay direct flex items of `.top` and render
   exactly as they always did), and the rail's labels are hidden so the icon rail is
   unchanged. That is the mechanism: below the breakpoint the SAME DOM is restyled into two
   drawers, so no stateful control — no `data-admin-*` button, no toggle, no switch — is ever
   rendered twice. `.drawer-scrim` and the panels' `.open` class are driven by portal.js. */
.burger,.drawer-trigger{display:none}
.hdr{display:contents}
.rail .label{display:none}
.drawer-scrim{display:none;position:fixed;inset:var(--topbar) 0 0;z-index:60;background:var(--scrim)}
.drawer-scrim.open{display:block}
body.drawer-open{overflow:hidden}

@media (max-width:1180px){
  .cards{grid-template-columns:repeat(2,1fr)}
  /* Three, not two: five cards over two columns leaves a lone card on a third row, and a
     3 + 2 pair of rows reads as one block (docs/18 §133). */
  .cards.five{grid-template-columns:repeat(3,1fr)}
  .toc{display:none}
}

/* ---------- the 720px breakpoint: two drawers over one document ----------
   Left: the rail itself slides in as a LABELLED nav (same links, same order, same active
   marking — it is the same <nav>). Right: everything after the spacer in the topbar slides
   in as the account drawer, with the two dropdowns already expanded, because a drawer is
   already a menu and a nested tap-to-open would be a second interaction for nothing.

   Stacking, in one place because it is the easy thing to get wrong: the scrim is 60, the nav
   drawer 65, and `.top` is raised to 70 so the account drawer — which is a CHILD of the
   header, and therefore painted inside the header's stacking context — sits above the scrim
   too. The confirm dialog (80) and the snackbar (90) are body-level and stay above all of
   it, which is what lets an operator action complete from inside a drawer. */
@media (max-width:720px){
  .top{z-index:70;gap:10px;padding-right:12px}
  .logo{display:none}
  .burger{
    display:flex;align-items:center;justify-content:center;flex:none;width:52px;height:100%;
    padding:0;border:0;border-right:1px solid var(--line);background:none;color:var(--ink);
    font:inherit;cursor:pointer;
  }
  /* The health pill stays in the header, beside the hamburger — the one reading worth a
     permanent slot. It may shrink and clip rather than push the avatar off the screen. */
  .top .pill.system{min-width:0;overflow:hidden;white-space:nowrap;padding:0 8px}
  /* …and the clock goes: a phone already has one, and the header has no width to spare. */
  .top .pill.clock{display:none}
  .drawer-trigger{display:flex;flex:none}
  .body{padding:16px 14px 40px}

  /* --- left: the rail, as a labelled drawer --- */
  .rail{
    position:fixed;top:var(--topbar);left:0;bottom:0;z-index:65;
    width:min(272px,82vw);height:auto;padding:10px 0;gap:2px;
    align-items:stretch;background:var(--surface);overflow-y:auto;
    transform:translateX(-100%);transition:transform .16s ease-out;
  }
  .rail.open{transform:translateX(0)}
  .rail a,.rail button{width:100%;height:auto;min-height:42px;justify-content:flex-start;gap:12px;padding:0 15px}
  .rail .label{display:inline;font-size:13.5px}
  /* The More flyout becomes an inline section: its own "External tools" head labels it, so
     the trigger button would be a tap that reveals what is already there. Same nodes, same
     config gating — an unset URL still drops its entry, and both unset still renders none. */
  .rail > .dd{position:static}
  .rail > .dd > button{display:none}
  .rail .dd-menu.flyout{
    display:block;position:static;min-width:0;margin-top:6px;
    border:0;border-top:1px solid var(--line);box-shadow:none;padding:6px 0 0;
  }
  .rail .dd-menu.flyout a{min-height:38px;padding:0 15px;font-size:13.5px}
  .rail .dd-menu .head{padding:6px 15px}

  /* --- right: the topbar's controls, as the account drawer --- */
  .hdr{
    display:flex;flex-direction:column;align-items:stretch;gap:8px;z-index:1;
    position:fixed;top:var(--topbar);right:0;bottom:0;width:min(320px,88vw);
    background:var(--surface);border-left:1px solid var(--line);padding:12px;
    overflow-y:auto;transform:translateX(100%);transition:transform .16s ease-out;
  }
  .hdr.open{transform:translateX(0)}
  .hdr .pill{width:100%;height:36px}
  /* The account menu's own avatar is the trigger for a menu that is already open here — the
     header's `.drawer-trigger` named the reader on the way in. Its ITEMS stay. */
  .hdr .avatar{display:none}
  .hdr .dd{position:static}
  .hdr .dd-menu{
    display:block;position:static;min-width:0;
    background:none;border:0;box-shadow:none;padding:0;
  }
  .hdr .dd-menu a,.hdr .dd-menu button{min-height:38px}
  /* One rule between the drawer's sections — the chip and the halt link keep their own
     borders, so only the expanded menus need separating. */
  .hdr > .dd{border-top:1px solid var(--line);padding-top:8px}
  .hdr > .dd:first-child{border-top:0;padding-top:0}
  .hdr > .pill.danger{height:auto;min-height:44px;justify-content:center}

  .cards{grid-template-columns:repeat(2,1fr)}
  /* …but `/activity`'s FIVE stop being a grid here and become a SWIPEABLE ROW (docs/18 §138,
     the operator's call of 2026-08-18) — a 3 + 2 stack of half-width cards was five serif
     numbers on two crowded rows, and the phone reads one card at a time anyway.

     Flex rather than grid because the row has to scroll and the cards have to keep ONE width
     whatever the count. The basis is deliberately NOT half the row: at `/2.2` the third card
     is a third visible at the right edge, and that PEEK is the whole affordance — nothing
     says "there is more, swipe" as plainly as a card cut off mid-air. `flex:0 0` (no grow)
     is what keeps it: a stretching card would swallow the peek at every width.

     The overflow lives on THIS box only, inside `.body`'s own padding — the page never
     scrolls sideways, exactly as `.tbl-wrap` does it for a wide table, and the scrollbar
     treatment is shared with it above. Nothing here is focusable, and nothing is hidden:
     `overflow-y:hidden` only stops the scroller from offering a vertical axis it has no
     content for. Snapping is `proximity`, not `mandatory`: it tidies a swipe that lands
     near a card edge but never fights an operator who wants to rest between two.

     `align-items:flex-start` is the one thing a flex row has to say that the grid did not
     (docs/18 §141, the operator's live look): flex stretches its children to the tallest by
     default, so a two-line card gave every one-line card beside it a blank line under its
     figure — a strip of dead air at the bottom of the swipe row that read as a rendering
     fault. Each card sizes to its OWN content instead; the row's height is still the tallest
     card's, because that is what the tallest card needs. */
  .cards.five{
    display:flex;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;
    scroll-snap-type:x proximity;align-items:flex-start;
  }
  .cards.five > .card{flex:0 0 calc((100% - 8px)/2.2);scroll-snap-align:start}
  /* The checks list needs no column override (docs/18 §142 — it is a list, not a grid); what
     it needs at this width is for a long reading to stop pushing the verdict off the row, so
     the READING drops out and the name, the icon and the verdict stay. The full reading is
     one tap away in the row's own dialog, which is the whole reason that dialog exists. */
  .check .d{display:none}

  /* THE TAB STRIP SCROLLS RATHER THAN WRAPPING (docs/18 §144, the operator's call). `/settings`'
     six tabs took two lines on a phone and pushed the page down by a whole row; one scrolling
     line keeps the strip exactly where the eye expects it with every tab still reachable. The
     scrollbar treatment is the one `.tbl-wrap` and `.cards.five` already share. `padding-bottom`
     gives back the 1px the active tab's underline sits outside the content box by
     (`margin-bottom:-1px`), which the overflow would otherwise clip. */
  .tabs{
    flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;max-width:100%;padding-bottom:1px;
    scrollbar-width:thin;scrollbar-color:var(--line-2) var(--surface-2);
  }
  .tabs::-webkit-scrollbar{height:6px}
  .tabs::-webkit-scrollbar-track{background:var(--surface-2)}
  .tabs::-webkit-scrollbar-thumb{background:var(--line-2)}
  .tabs a{flex:none;white-space:nowrap}

  /* `/debug`'s LOOKUP BOX (docs/18 §144, the operator's call; the basis is §150's correction).
     `.page-head` wraps, and at this width the 400px input plus *Look up* would not fit beside
     the title — so the BUTTON dropped to a line of its own, which is the one arrangement that
     reads as broken: a box and the button that acts on it, on different rows. The title takes
     the first line outright and the pair share the second, the input taking whatever the button
     leaves. Scoped to `/debug` because it is the only page whose head carries an input.

     THE INPUT'S BASIS HAS TO BE 0, NOT `auto` (docs/18 §150) — with `auto` this block did not
     achieve the arrangement it describes, and the button still took a third line. A flex
     container breaks its lines from the items' HYPOTHETICAL main sizes, worked out BEFORE any
     growing or shrinking is resolved; a basis of `auto` falls back to `width`, and `.input` is
     `width:100%`, so the input asked for the whole line at exactly the moment the browser was
     deciding what fits beside it. `min-width:0` cannot undo that — it is a floor, not a
     ceiling. A basis of 0 asks for nothing while the lines are being drawn, so the pair always
     land together, and `flex-grow` then hands the input every pixel *Look up* does not take,
     which is the width it visibly has. The base rule above never had this problem because its
     `flex:0 1 400px` states a basis outright. */
  [data-portal="debug"] .page-head .page-title{flex:1 0 100%;margin-right:0}
  [data-portal="debug"] .page-head .input{flex:1 1 0;min-width:0;max-width:none}
  /* A section heading and its tab strip (docs/18 §92) share one row while there is room for
     both; when there is not, `.sec` wraps and the strip takes its own line. There is NO
     `.sec > .tabs` override here on purpose (docs/18 §94): the strip stays right-aligned at
     every width — one shape to learn, and the tabs sit where the eye left them. */
}

/* ---------- filter bar, pager, collapsed runs (signals browse, docs/18 §36) ---------- */
.filters{
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
  padding:10px 12px;border:1px solid var(--line);background:var(--surface-2);margin-bottom:-1px;
}
.filters .grow{flex:1;min-width:150px}
.filters .sep{margin-left:auto}
/* The bar fills its width: the selects share what the buttons do not take, and the buttons
   keep their natural width and the selects' 28px height. */
.filters select.input{flex:1 1 auto;min-width:120px}
/* The one TEXT box in that bar — the Trade # filter (docs/18 §71 item 9d). `.input` is
   width:100%, so without a basis of its own it would take the whole line; it is sized to the
   short strings it holds and matches the selects' height and type rather than the page's
   full-size input. */
.filters input.input{flex:0 1 130px;min-width:96px;height:28px;font-size:12px}
.filters .btn{height:28px;padding:0 12px;font-size:12px}
select.input{
  height:28px;padding:0 26px 0 9px;font-size:12px;width:auto;cursor:pointer;
  appearance:none;
  background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);
  background-position:calc(100% - 13px) 12px,calc(100% - 9px) 12px;
  background-size:4px 4px,4px 4px;background-repeat:no-repeat;
}
/* (⚠️ A SECOND `.check` rule stood here until docs/18 §144 — the filter bar's own checkbox
   LABEL, whose two controls §36.2 removed. It outlived its markup and, sharing a class name
   with `/health`'s check rows, went on restyling THOSE instead: it is why a row was
   `inline-flex` with a nowrap line and a secondary-ink name, none of which the system-checks
   block above asks for. Deleted with the §144 restyle; the checks list has exactly one rule
   now, and it is the one up there.) */
.pager{
  display:flex;align-items:center;gap:10px;padding:9px 12px;
  border:1px solid var(--line);border-top:0;background:var(--surface-2);font-size:12px;color:var(--ink-3);
}
.pager .btns{margin-left:auto;display:flex;gap:6px}
.btn[disabled]{opacity:.4;cursor:not-allowed}
.btn[disabled]:hover{background:var(--surface)}
/* A collapsed run's trigger. The band's OWN TEXT is inside this button (docs/18 §42.3), so
   the button must not restyle it: `font:inherit` + `color:inherit` leave it reading exactly
   like the cell it sits in, and no padding keeps it flush at the cell's left edge.

   ⚠️ Since docs/18 §73 item 4 the trigger is the band's `Protection ×N` COUNTER, on the Type
   cell, and the `.runmore` ellipsis that marked the Time cell is gone: a counter that stands
   for several rows is the affordance, and a glyph beside a timestamp was a second one saying
   the same thing. What tells the reader it is pressable is the same understated bottom rule
   `a.ref` uses for a reference link — one idiom for "clickable text" on this portal, not two.

   `.lead-in` STAYS: the trade ledger uses the same spacer for a missing value
   (`macros/ledger.html`), and it never had anything to do with runs. */
.runbtn{
  background:none;border:0;border-bottom:1px solid var(--line-2);padding:0;margin:0;
  cursor:pointer;color:inherit;font:inherit;text-align:left;
}
/* …and the SAME idiom on an anchor (docs/18 §84): the trade ledger's *Alert* and *Leg* cells
   are links that keep their href for a reader with no script, and they show words and a
   timestamp rather than an identifier — so they take this rule instead of `a.ref`'s monospace.
   The one declaration an anchor needs on top is losing the browser's own underline, which would
   otherwise sit beside the bottom rule as a second, louder line. */
a.runbtn{text-decoration:none}
/* AN ICON-ONLY CONTROL INSIDE A ROW (docs/18 §153) — the trade ledger's trailing chart button,
   and so far its only user. It is deliberately NOT `.runbtn`: that idiom is for clickable TEXT
   and says so with an underline, which under a 14px glyph would read as a rule rather than as
   an affordance.

   ⚠️ IT IS A BARE GLYPH SINCE docs/18 §155 (the operator's call). It had a `--line-2` border and
   a `--row-hover` tint behind it from §153, on the argument that a bordered square is the
   smallest thing that says "pressable"; in a dense row of readings that square read as a cell of
   its own. The affordance is the INK instead: the glyph rests in the secondary ink every other
   quiet mark in a row wears and steps up to the primary ink under the cursor, which is a change
   the reader sees on the thing they are pointing at. No hue and no state colour — it is a way
   onward, exactly like the *Signals* link, and a coloured icon in a row of readings would read
   as a STATE. */
.iconbtn{
  display:inline-flex;align-items:center;justify-content:center;
  width:22px;height:22px;padding:0;margin:0;
  background:none;border:0;color:var(--ink-2);
  cursor:pointer;font:inherit;vertical-align:middle;
}
.iconbtn:hover{color:var(--ink)}
.iconbtn svg{display:block}
/* `.xn` — the muted `×N` counter that rode after such a link (docs/18 §84) — came OUT here on
   2026-08-12 with its one user, the trade ledger's Leg cell (§87, the owner's call). The signal
   tables' own counter is INSIDE a `.runbtn` and never used this rule. */
td .lead-in{display:inline-block;width:14px}
/* The trade ledger's two shapes (docs/18 §63, §64, narrowed to one by §70), and nothing else
   was needed for it: a merged cell sits vertically centred already, because `vertical-align`
   on a table cell defaults to `middle`.

   `tr.tied` is any line of a trade that is not its LAST — the rule under it is suppressed so a
   trade's lines read as one block rather than as rows that happen to be adjacent.

   ⚠️ Since docs/18 §72 item 1 the separator is a per-cell border, so the suppression has to
   spare the MERGED cells: a `rowspan` cell's own bottom edge is the bottom of the trade's LAST
   line, which is where the rule between two trades belongs. So the tied rows silence their
   ordinary cells and let the spanning ones draw — and the FINAL trade's spanning cells are
   silenced in turn (`tbody:last-child td[rowspan]`), because their edge lands on the table's
   last row, whose own cells are already bare so the wrapper's frame is not doubled. Net result
   is what the collapsing model drew: exactly one line between trades and none inside one.

   (That last rule read `tr.tied:nth-last-child(2)` until docs/18 §93 — "the tied row two from
   the end", which named the final trade only while a trade was exactly two lines. It has been
   as many lines as the trade had ORDERS since §84, so the final trade of a three-line table
   drew a rule the ones above it did not. Per-trade tbodies name it directly.)

   ⚠️ **THE HOVER IS THE TRADE'S, NOT THE LINE'S** (docs/18 §93, the owner's call). Each trade
   is its own `<tbody>`, so hovering any of its lines tints every cell of the whole group at
   once — the merged cells included, at the same one strength. Painting the `<tr>` could not do
   that: a merged cell belongs to the trade's FIRST row, so hovering that row tinted the group's
   full-height column AND the row band over it (two greys, nested), while hovering any later
   line left the merged cells untinted. The CELLS are painted rather than the tbody because the
   pinned *Trade #* cell paints its own opaque background — a group-level tint cannot reach it —
   so it keeps the gradient overlay it has always used, and `:not(.pin)` keeps the plain rule
   from taking away the `--surface` its stickiness depends on.

   `.stacked` is a cell holding two lines: today only the contract count with what those
   contracts were worth under it. The block children are what break the line inside a `nowrap`
   cell, and the second line is the page's own sub type, so it is quieter than the figure above
   it without being a new size.

   ⚠️ `.beside` and `.after` came OUT here on 2026-08-08 (docs/18 §70.3) with their one user:
   the trade's direction, which shared the number's cell from §64 and is a *Side* column of its
   own now, rendering as the same toned cell every other Side on the portal does. The
   `.under.side-*` exception went with them — the only thing that ever stacked under a figure
   here is a dollar value, and it carries no direction. Nothing was added in their place. */
table.ledger tbody tr.tied td:not([rowspan]){border-bottom:0}
table.ledger tbody:last-child td[rowspan]{border-bottom:0}
table.ledger tbody:hover td:not(.pin){background:var(--row-hover)}
table.ledger tbody:hover td.pin{background-image:linear-gradient(var(--row-hover),var(--row-hover))}
td .stacked{display:block;line-height:1.3}
td .stacked>*{display:block}
td .stacked .under{font-size:11px;color:var(--ink-3)}
/* (`td.glyph .b` — the bare-icon rule the ledger's *Webhook* and *Engine* columns wore from
   docs/18 §70.6 — went with those columns in §80, when the three verdicts became the ONE word
   pill *Status*. No column carries `cls='glyph'` any more, and a rule nothing can select is a
   rule that quietly comes back wrong; the shared pill renders every graded answer on every
   table again.) */
a.ref{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-2);text-decoration:none;border-bottom:1px solid var(--line-2)}
a.ref:hover{color:var(--ink)}

/* ---------- the performance overview's charts (docs/18 §108) ----------
   `/home`'s own vocabulary, and the design system's FIRST chart rules — the page draws
   eleven Chart.js canvases and `portal.css` carried no chart language at all. Everything
   here is either pure layout or a reference to a token this file already defines; no
   colour is invented, and the canvases themselves read the same tokens at runtime
   (`static/performance.js`), because a canvas cannot carry a class.

   The page's header is the design system's OWN `.page-head` / `.page-title` pair rather
   than the mockup's `.sec` copy — the live system wins where the two differ — so the only
   thing this page changes about it is the gap under it, scoped by the page hook exactly
   as `/debug` scopes its own.

   THE CARD COUNT IN A ROW PICKS THE RATIO. On the DESKTOP layout a row of THREE cards is
   the owner's golden rectangle: the whole card — header and chart together — at
   width : height = 1.618. A row of exactly TWO takes 1.77, wider because each card is
   half the row rather than a third of it, and at one shared ratio a two-up card would stand
   half again as tall as a three-up one purely for being wider. Both card families say it —
   the plain `.chart-pair` and the strategy row's `.cards3.two` — so "exactly two cards"
   reads the same wherever the page draws it.

   The wider ratio SHORTENS that difference without closing it, deliberately: a two-up card
   is still the taller one, and the hero row still reads taller than the rows beneath it,
   because it carries the two readings the page is for.

   The height is WIDTH-driven, which is what ends the resize loop a content-driven chart
   box causes: `aspect-ratio` fixes the card's height before its contents are laid out, so
   nothing the canvas does can move it and the box inside can simply flex-fill what the
   header leaves. `min-height:0` is load-bearing twice — it lets a flex item shrink below
   its content, and it keeps the box from ever being the thing that decides the height.

   The collapse query at 1180px (the breakpoint the card grid already uses) has to name
   EVERY two-class selector it overrides, and there are now four of them: `.chart-pair.three`
   and `.cards3.two` for the columns, `.chart-pair.three > .chart-card` and `.cards3.two
   .card` for the ratio. Each beats its one-class sibling on specificity and would otherwise
   leave a phone with a multi-column row or a desktop ratio. The query restates the gap in
   both axes — the desktop rule spends its gap horizontally only — so a collapsed page is
   one stack at one spacing.

   COLLAPSED, THE RATIO IS 1.33 for EVERY card at EVERY count — squarer, and stated for both
   card families because every card is full-width there and a full-width golden rectangle is
   most of a phone viewport for one chart. (A collapsed page has no two-card ROW to widen, so
   1.77 has nothing to say here.) There is NO height cap (owner's call, docs/18 §112): a cap would break the
   one property the ratio is here for — that height is width-driven and settled before the
   contents are laid out — and reintroduce the letterboxed card the ratio replaced. Squarer
   is how the collapsed page keeps a chart readable without one. */
[data-portal="home"] .page-head{margin-bottom:12px}
/* THE TITLE AND THE RANGE'S DATES (docs/18 §136). One line on a desktop — "Overview  Aug 3 –
   Aug 18" — with the dates on the title's own BASELINE (`align-items:baseline`, so 13px and
   22px type sit on one line rather than being centred against each other). The dates are in
   the page's FULL ink since docs/18 §141 (the operator's call): they were muted on the
   argument that they QUALIFY the title rather than being a second title, and at 13px beside
   22px the SIZE already says that — the grey only made the one fact that says which period
   every figure below belongs to the hardest thing on the page to read. Size carries the
   hierarchy; colour is not asked to carry it twice.

   The `margin-right:auto` moves onto the wrapper: `.page-head`'s own rule puts it on
   `.page-title`, and here the title is inside a block, so without this the range control would
   sit beside the dates instead of at the far edge. It is the same push `/calendar` states for
   its own two-line heading, from the other side.

   NARROW: the dates STACK UNDER the title — `/calendar`'s two-line heading shape, which is
   what the operator asked for (docs/18 §136) — at the page's phone breakpoint rather than at
   the 1180px chart collapse, because the pair fits a tablet width comfortably and only a phone
   makes the row wrap on its own. `display:block` is what does it: the wrapper stops being a
   flex row, so the `<h2>` and the `<span>` take their own lines with no reordering. */
[data-portal="home"] .page-head > .title-dates{
  display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-right:auto;
}
[data-portal="home"] .title-dates .dates{
  font-size:13px;color:var(--ink);font-variant-numeric:tabular-nums;
}
@media (max-width:720px){
  [data-portal="home"] .page-head > .title-dates{display:block}
  [data-portal="home"] .title-dates .dates{display:block;margin-top:3px;font-size:12px}
}
/* The active row of a dropdown menu. No new colour — `--sunken` is the same ground
   `.rail a.on` uses to mark the current page. */
.dd-menu a[aria-current="page"]{background:var(--sunken)}
/* A leading glyph inside a pill. `.pill` is already an inline-flex row with 8px gaps and
   centred items, so the icon needs no alignment of its own — only a guard against flex
   shrinking it. It paints in `currentColor`, i.e. the pill's own ink, in both themes. */
.pill .ico{flex:none;display:block}
.chart-card{margin-bottom:0;display:flex;flex-direction:column;overflow:hidden}
.chart-pair > .chart-card{aspect-ratio:1.77}
.chart-pair.three > .chart-card{aspect-ratio:1.618}
.chart-box{position:relative;flex:1 1 0;min-height:0;margin-top:10px}
.chart-box canvas{display:block;width:100%;height:100%}
.chart-pair{display:grid;grid-template-columns:1fr 1fr;gap:0 12px;margin-top:12px}
.chart-pair.three{grid-template-columns:repeat(3,1fr)}
@media (max-width:1180px){
  .chart-pair,.chart-pair.three{grid-template-columns:1fr;gap:12px}
  .chart-pair > .chart-card,.chart-pair.three > .chart-card{aspect-ratio:1.33}
}
/* The per-strategy card row: three equal columns at the page's one 12px gap, so this block
   spends that gap in BOTH axes. It collapses to ONE column at the same breakpoint — not
   two, which would leave the third card orphaned on a row of its own. The cards are not
   `.chart-pair` children, so they state the ratio themselves.

   `.two` is the row with exactly TWO cards to draw (docs/18 §122): three columns would leave a
   third of the row empty beside two cards that are the whole answer, so the pair takes the
   width. The template adds the class only at that count — at one card a full-width card is not
   what this row is, and at three or more the default already fills the row. Like
   `.chart-pair.three` it is TWO classes, so the collapse query has to NAME it — twice, since it
   carries the two-card RATIO (1.77, see above) as well as the two columns — or it would win on
   specificity and leave a phone with two columns at a desktop ratio. */
.cards3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:12px}
.cards3.two{grid-template-columns:repeat(2,1fr)}
.cards3 .card{display:flex;flex-direction:column;aspect-ratio:1.618;overflow:hidden}
.cards3.two .card{aspect-ratio:1.77}
@media (max-width:1180px){
  .cards3,.cards3.two{grid-template-columns:1fr}
  .cards3 .card,.cards3.two .card{aspect-ratio:1.33}
}
/* A card sub-line carrying MORE THAN ONE reading. The middot is CSS rather than a text
   node, and that is the whole reason it is a rule: a loose "·" between two spans would be
   a flex ITEM, take a gap on both sides and be free to wrap onto a line by itself. As a
   `::before` on every span after the first it is part of the reading it introduces and
   cannot be separated from it. The 8px column gap plus the 8px margin puts equal air on
   either side of the dot. (`+` is a DOM selector, not a LINE one, so a row that really
   wraps opens its second line with a middot; CSS has no "first on this line" selector and
   the honest alternatives are JS or giving up the wrap.) */
.stats{display:flex;flex-wrap:wrap;gap:4px 8px}
.stats > span + span::before{content:'·';margin-right:8px}
/* The swatch legends — the cards whose canvas draws MORE than one thing, which is the only
   place a legend has anything to say (everywhere else one series sits in a card whose `.k`
   names it). They ride the card vocabulary's own `.s` line, so the type is the design
   system's and `.leg` adds layout only. The swatch classes are named after the TOKENS
   (`ink1` = `--ink`, `line2` = `--line-2`), never after a reading, so two cards spending
   one token on two different quantities share one class and invent no vocabulary.

   ⚠️ THERE WAS A `--series-2` TOKEN HERE FOR ONE DAY (docs/18 §136.4, REVERTED by §139.1 on
   2026-08-18, the operator's call after a live look): a series-only grey, `--line-2` in light
   mode and a lighter one in dark, meant to pull the *Trades filled* key's two swatches apart
   in dark mode. It introduced other issues on the live pages and the whole of it came out —
   the token from both themes, the class, and the bar segment in `performance.js`. The second
   series is `--line-2` again, and the dark-mode indistinguishability §136.4 named is a KNOWN,
   ACCEPTED cost until someone re-opens it. Do not re-cut this token without that decision.

   ⚠️ The ring's swatches are ROUND, and that is a deliberate, owner-chosen exception to the
   global `*{border-radius:0}` above. It is scoped to `.leg .sw` and opted out of by the
   legends that want the square default back (`.leg.sq`, beside the charts made of
   rectangles), so no third shape enters. Since docs/18 §123 `.ink3-slashes` opts BACK IN
   inside a `.leg.sq` key — see its own note below — so the exception now has two members and
   still no third shape. */
.leg{display:flex;flex-wrap:wrap;gap:4px 12px;align-items:center}
.leg > span{display:inline-flex;align-items:center;gap:6px}
.leg .sw{width:8px;height:8px;flex:none;border-radius:50%}
.leg.sq .sw{border-radius:0}
.leg .sw.ink1{background:var(--ink)}
.leg .sw.ink2{background:var(--ink-2)}
.leg .sw.ink3{background:var(--ink-3)}
.leg .sw.line2{background:var(--line-2)}
.leg .sw.dash{width:16px;height:2px;background:repeating-linear-gradient(to right,var(--ink-3) 0 4px,transparent 2px 6px)}
/* ONE of the per-strategy cards' three marks (docs/18 §120, §123); the other two are `.ink1`
   and `.dash` above, reused as they are. ⚠️ There used to be a second class here,
   `.leg .sw.ink1-line` — a 16×2 solid `--ink` rule, on the argument that a LINE of a token is
   not the BLOCK of it `.ink1` draws. §123 took it out: `IBKR` wears the same plain black square
   on this card that it wears on *Trades filled*, so one word carries one mark across the page
   and this file keeps one fewer class. Nothing was named after a reading then and nothing is
   now — `ink3-slashes` states the token AND the form, the same reason the dashed one has always
   been `.dash` rather than `.ink3`.

   THE SLASHES stand for the chart's hatched band (docs/18 §122, re-cut by §123, owner's call
   both times): short thick strokes with generous transparent gaps and NO ground behind them — a
   mark, not the filled `--line-2` block it was until §122 — now inside a CIRCLE, with the lean
   flipped to 60° so it falls the other way from the §122 glyph. Two deliberate divergences from
   the canvas pattern it names, both because a swatch is twelve pixels and a chart is not: the
   strokes are STEEPER than the canvas hatch's 45°, and they are `--ink-3` rather than the
   `--line-2` the canvas tile is built from. `--line-2` is the lightest line token in the
   palette; at two-pixel strokes with air around them it is not a visible mark in either theme,
   and an invisible swatch is a hole in the key. The chart's own hatch is unchanged.

   ⚠️ THE ROUND CORNER IS DELIBERATE AND IT RE-ROUNDS INSIDE A `.leg.sq` KEY. `.leg.sq .sw` and
   `.leg .sw.ink3-slashes` are both three classes, so SOURCE ORDER decides and this rule — being
   below it — wins. That makes this the second sanctioned radius on the page, joining the ring's
   dots under the same owner's-exception note above: the mark stands for a soft, unaligned
   texture, and a square of strokes read as a fourth rectangle in a row of rectangles. No third
   shape enters. */
.leg .sw.ink3-slashes{width:11px;height:11px;border-radius:50%;background:repeating-linear-gradient(60deg,var(--ink-3) 0 2px,transparent 2px 5px)}
/* (⚠️ THE PER-TRADE CHART DIALOG HAD TWO CLASSES OF ITS OWN HERE and has none since docs/18
   §158, the operator's call. `ink2-diamond` named an `--ink-2` diamond for the TradingView marks
   from §153, and `ink3-dots` a dotted grey rule for both protective levels from §155 — which was
   itself a re-cut of §154's hued `down-dash` / `up-dash` pair. §158 took that dialog onto the
   swatches `/home` already had: the TradingView series wears `.dash` (the same 1.5px `[4, 3]`
   `--ink-3` line the per-strategy cards' alert-price curve wears), and the now-SOLID level rules
   wear the plain `.ink3` block, the way `.ink1` names `/home`'s own solid P&L curve. One
   quantity, one mark, on both pages — and this file keeps two fewer classes.) */
/* The dotted BACKDROP behind the exposure ring. CSS on the chart BOX, not anything on the
   canvas: Chart.js is given no canvas background anywhere on this page, so the canvas is
   transparent and the box's own ground shows through — which keeps it token-pure and
   theme-native with no script. `transparent` here is COMPOSITING (the gap between dots,
   where the card's `--surface` shows through), not a palette value. It is a backdrop and
   not a grid: nothing on the ring is measured against it, which is why it is furniture in
   CSS rather than a plugin drawing decoration into a plot area. */
.chart-box.dotgrid{
  background-image:radial-gradient(var(--line) 1px, transparent 1px);
  background-size:8px 8px;
}

/* ---------- the monthly P&L calendar (docs/18 §109) ----------
   `/calendar`'s own vocabulary, ported from the owner-approved mockup
   `design/portal/performance-calendar-v2.html`. Everything here is either pure layout or a
   reference to a token this file already defines: no colour is invented, and the whole page
   follows the theme toggle with no script at all, because HTML inherits the theme.

   The page HEADER is the design system's own `.page-head` / `.page-title` pair rather than
   the mockup's `.sec` copy — the live system wins where the two differ — so the only things
   scoped to this page are the gap under it (this page's own 12px, the same single gap the
   grid and the section heading are spaced by), the sub-line's type, and the push that sends
   the stepper to the far edge. That push is a rule of its own because `.page-head`'s usual
   `margin-right:auto` sits on `.page-title`, and here the title is inside a two-line block.

   THE STEPPER — ‹ August › — wears `.pill`, so its border, height, ground and type are the
   design system's. What is local is that the pill is NOT the pressable thing: the two
   CHEVRONS are, and the month between them is a READING. That is exactly the shape
   `.pill.clock` already carries a modifier for, so this follows it rather than inventing a
   second answer — no pointer cursor and no hover tint on the box, and the hover moves INSIDE,
   onto the two controls that really do something. The padding drops to zero so each chevron's
   own 26px box reaches the pill's edge and is a comfortable target; `min-width` on the label
   stops the control changing width as the month name does ("May" and "September" would
   otherwise move both chevrons). `.dead` is the forward chevron when there is no next month
   to step to: the same box, muted, with the reason in its hover.

   THE CARD keeps its GROUND and nothing else. The calendar's colour argument is built on two
   neighbouring tokens — an ordinary cell keeps `--surface` and a losing day takes
   `--surface-2` — and `--surface` is the CARD's ground rather than the page's, so a table
   dropped straight onto `--page` would be reading its two steps off a third one. The frame
   and the inset come off, because the table draws its own 1px `--line` lattice and a card
   border 16px outside that lattice is a second frame around a thing that already has one.

   THE GRID is a real `<table>`: the data IS tabular, so the day-name row keeps its
   `<th scope="col">` semantics for free, and `table-layout:fixed` is what pins the EIGHT
   columns at every width — a calendar that reflows to fewer DAY columns has stopped being a
   calendar. Eight because the grid is a FULL week, Mon–Sun, plus the muted Week total; all
   eight are equal, because the widest thing any of them holds is one money figure. (On a
   PHONE the Week total is dropped — docs/18 §136, see the rule at the foot of this block. It
   is the one column that is not a day, so the week itself is still whole.) The
   headers are CELLS OF THAT LATTICE at a shorter height (36px against the data cells' 108px),
   because a day name is one line and a day is three.

   TWO grounds and one TEXTURE, and that is the page's entire colour vocabulary — no `.up` /
   `.down` anywhere. A losing day takes `--surface-2`, the card ground's own second step, so
   the cell reads as a SHADING of the card rather than as a hole punched in it; the test that
   token passes is not "the loudest quiet ground" but the quietest mark that is still a mark,
   because the sign is already carried by the `+` / `−` in the number. Marking the LOSS rather
   than the gain is deliberate: a good month is mostly gains, so marking the exception is less
   ink than marking the rule. The texture is the hatch on a CLOSED day — a weekend, or a
   filler day from the month either side — one repeating gradient in `--line`; the
   `transparent` beside it is COMPOSITING (the cell's own ground showing between strokes), not
   a palette value.

   `.cd` carries a MIN-HEIGHT so an EMPTY date div still occupies its line, which is what puts
   the Week column's total on the same baseline as the day nets beside it with no magic
   offset. `.cn` — the trade count, the cell's third line — is muted small print in the page's
   own sans rather than the date's mono: it is a phrase ("4 trades"), not a tick label.

   NARROW WIDTHS: the media rule trims row height, padding and the money size — layout only,
   no reflow; the phone rule below it drops the Week column and nothing else. The HEADER
   height does not scale: 36px is one line of 12px type plus its
   padding, and there is nothing in it to give back. A figure that still will not fit its
   column is truncated rather than wrapped, because a wrapped money figure reads as two
   numbers, which is worse than a visibly cut one.

   The Strategies table below takes NO rule at all: `.tbl-wrap` + `table.blotter` + `.num` are
   the design system's own table vocabulary, complete. */
[data-portal="calendar"] .page-head{margin-bottom:12px}
[data-portal="calendar"] .page-head > .pill{margin-left:auto}
/* The month's net, under the month name. FULL ink since docs/18 §141, for `/home`'s dates'
   reason and by the same call: it is the heading's own money figure, and a money figure the
   reader has to hunt for is the one thing a page head must not make small AND grey. The 12px
   size still ranks it below the title it sits under. */
[data-portal="calendar"] .page-head .net{
  margin-top:3px;font-size:12px;color:var(--ink);font-variant-numeric:tabular-nums;
}
.pill.stepper{cursor:default;padding:0;gap:0}
.pill.stepper:hover{background:var(--surface)}
.pill.stepper a,.pill.stepper .dead{
  width:26px;align-self:stretch;display:flex;align-items:center;justify-content:center;
  color:inherit;text-decoration:none;
}
.pill.stepper .dead{color:var(--ink-3)}
.pill.stepper a:hover{background:var(--row-hover)}
.pill.stepper .m{min-width:72px;text-align:center;font-variant-numeric:tabular-nums}
.cal-card{padding:0;border:0}
table.cal{width:100%;table-layout:fixed;border-collapse:collapse}
table.cal th{
  height:36px;padding:0 12px;border:1px solid var(--line);
  text-align:left;font-weight:400;vertical-align:middle;
  font-family:var(--font-mono);font-size:12px;letter-spacing:.03em;color:var(--ink-3);
}
table.cal td{
  height:108px;padding:10px 12px;border:1px solid var(--line);
  vertical-align:top;font-variant-numeric:tabular-nums;
}
table.cal td.neg{background:var(--surface-2)}
table.cal td.off{
  background-image:repeating-linear-gradient(45deg,var(--line) 0 1px,transparent 1px 7px);
}
table.cal td.wk{color:var(--ink-3)}
table.cal .cd{
  min-height:17px;line-height:17px;
  font-family:var(--font-mono);font-size:13px;color:var(--ink-3);
}
table.cal .cm{margin-top:8px;font-size:18px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
table.cal .cn{
  margin-top:5px;line-height:16px;font-size:12px;color:var(--ink-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
@media (max-width:1180px){
  table.cal th{padding:0 9px}
  table.cal td{height:84px;padding:8px 9px}
  table.cal .cd{min-height:16px;line-height:16px;font-size:12px}
  table.cal .cm{margin-top:6px;font-size:15px}
  table.cal .cn{margin-top:4px;font-size:11px}
}
/* ON A PHONE THE WEEK COLUMN GOES (docs/18 §136, the operator's call). It is the one column
   that is not a DAY, and at this width the eighth of the row it takes is the difference
   between a day cell that can hold its money figure and one that truncates it. Seven columns
   is still a calendar — the whole Mon–Sun week is intact — where six would not be, which is
   why this is the only column that may be dropped.

   `display:none` on the cells rather than a column width: with `table-layout:fixed` the
   remaining seven share the width evenly with no rule of their own. The header goes with the
   cells (`th:last-child`), or the row would carry a name for a column nobody draws. The week
   totals are a SUM of the days beside them, so nothing is lost that the row does not
   still state. */
@media (max-width:720px){
  table.cal th:last-child,table.cal td.wk{display:none}
}
