/* Investflick — design system.
 *
 * Layout is master–detail, not a grid. The workflow ends in approve/reject on
 * each candidate, which is a review queue: you look at one name, decide, move
 * on. A 20-column table optimises for comparing rows; this optimises for
 * deciding on one. Rotated column headers were the tell that the grid was
 * fighting the content.
 *
 * No build step, no framework, no CDN.
 */

:root {
  --bg-0: #0b0e13;  --bg-1: #12161d;  --bg-2: #1a1f28;  --bg-3: #242a35;
  --border: #262c37;  --border-hi: #38404e;

  --fg-0: #e8eef6;  --fg-1: #98a2b3;  --fg-2: #6b7484;

  --accent: #5b9cf8;
  --pass: #45c26a;  --pass-bg: #10251a;
  --fail: #f2564c;  --fail-bg: #2a1315;
  --warn: #dba32a;  --warn-bg: #2a2009;
  --info: #b18cf5;

  --mono: ui-monospace, 'SF Mono', 'JetBrains Mono', monospace;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Helvetica, sans-serif;
  --r: 7px;  --r-lg: 11px;
  --nav-h: 50px;
}

*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; }

/* `hidden` must actually hide.
 *
 * The browser's own stylesheet says `[hidden] { display: none }`, but ANY
 * author rule that sets `display` beats it — author styles outrank the UA
 * sheet regardless of specificity. So `.tag { display: inline-block }` silently
 * un-hid every `.tag[hidden]`: the stub-prices badge kept its background and
 * border while holding no text, and rendered as a 14x2px amber dash in the nav.
 *
 * Global rather than per-component: any element given `hidden` should vanish,
 * and the next component that sets `display` should not reintroduce this. */
[hidden] { display: none !important; }
body {
  margin: 0; background: var(--bg-0); color: var(--fg-0);
  font-family: var(--sans); font-size: 13px; line-height: 1.5;
  -webkit-font-smoothing: antialiased;

  /* App-shell layout: the viewport itself never scrolls, exactly one region
     does — the table. Before this the page scrolled AND the table scrolled, so
     the toolbar and filters slid away while you were reading rows, and a wheel
     gesture did different things depending on where the pointer happened to be. */
  height: 100vh; overflow: hidden;
  display: flex; flex-direction: column;
}

/* ------------------------------------------------------------ scrollbars */
/* macOS hides overlay scrollbars until you move, which on a dense table leaves
   no signal that there is more below and nothing to grab. These stay visible
   and occupy real layout width.
 *
 * ⚠ EVERY scroll region belongs in this list. The app shell does not scroll
 * (`body` is `height: 100vh; overflow: hidden`), so each page supplies its own
 * — and one that is not named here gets the macOS overlay bar instead: zero
 * layout width, invisible until moved, which reads as "this page does not
 * scroll". Reported on the detail page, and `.dash-scroll` had it too.
 *
 * ⚠ `scrollbar-width` / `scrollbar-color` are deliberately NOT set here.
 * Chromium honours the standard properties in preference to the legacy
 * pseudo-elements — setting either one silently disables every
 * `::-webkit-scrollbar` rule below, which is why the first attempt still
 * rendered a zero-width overlay bar. They are applied only in the @supports
 * block at the end, for engines that lack the pseudo-element. */
.tablewrap::-webkit-scrollbar, .drawer::-webkit-scrollbar,
.dash-scroll::-webkit-scrollbar, #page-root::-webkit-scrollbar,
.hovercard__body::-webkit-scrollbar { width: 13px; height: 13px; }
.tablewrap::-webkit-scrollbar-track, .drawer::-webkit-scrollbar-track,
.dash-scroll::-webkit-scrollbar-track, #page-root::-webkit-scrollbar-track,
.hovercard__body::-webkit-scrollbar-track { background: var(--bg-0); }
.tablewrap::-webkit-scrollbar-thumb, .drawer::-webkit-scrollbar-thumb,
.dash-scroll::-webkit-scrollbar-thumb, #page-root::-webkit-scrollbar-thumb,
.hovercard__body::-webkit-scrollbar-thumb {
  background: #4a525d; border-radius: 7px;
  /* Transparent border + background-clip keeps the thumb visually slim while
     the hit area stays a full 13px — easy to grab, quiet to look at. */
  border: 3px solid transparent; background-clip: content-box;
}
.tablewrap::-webkit-scrollbar-thumb:hover, .drawer::-webkit-scrollbar-thumb:hover,
.dash-scroll::-webkit-scrollbar-thumb:hover, #page-root::-webkit-scrollbar-thumb:hover,
.hovercard__body::-webkit-scrollbar-thumb:hover {
  background: #626b78; background-clip: content-box;
}
.tablewrap::-webkit-scrollbar-thumb:active { background: var(--accent); background-clip: content-box; }
.tablewrap::-webkit-scrollbar-corner { background: var(--bg-0); }

/* Firefox and anything else without the pseudo-element. Scoped so it cannot
   suppress the rules above on Chromium. */
@supports not selector(::-webkit-scrollbar) {
  .tablewrap, .drawer, .dash-scroll, #page-root {
    scrollbar-width: thin; scrollbar-color: #4a525d var(--bg-0);
  }
}
h1,h2,h3 { margin: 0; font-weight: 600; }
button, input, select { font: inherit; color: inherit; }
a { color: var(--accent); text-decoration: none; }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------------ nav */
.nav {
  height: var(--nav-h); display: flex; align-items: center; gap: 18px;
  padding: 0 16px; background: var(--bg-1);
  border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 30;
}
.nav__brand { font-weight: 650; letter-spacing: -0.2px; flex: none; }
.nav__brand span { color: var(--fg-2); font-weight: 400; }
.nav__links { display: flex; gap: 2px; }
.nav__link { padding: 6px 11px; border-radius: var(--r); color: var(--fg-1); font-size: 12.5px; }
.nav__link:hover { background: var(--bg-2); color: var(--fg-0); }
.nav__link.is-active { background: var(--bg-3); color: var(--fg-0); }
.nav__spacer { flex: 1; }

/* ---- the nav has to FIT, and for a long time it did not -------------------
   Measured 2026-09-09 at an 820px viewport, on every page:

       /watchlist  /instrument   document.scrollWidth 1135   client 820
       /scanner    /universe                          1184

   The number stayed pinned as the viewport shrank, which is the tell: two of
   the nav's children could not shrink AT ALL.

   ⚠ **`min-width: auto` IS THE WHOLE BUG, AND IT IS THE SAME ONE THIS FILE
   ALREADY DOCUMENTS FOR `min-height` ON FLEX COLUMNS** — *"the rule that makes
   the layout work is the one that looks like it does nothing."* A flex item's
   `min-width` defaults to `auto`, which resolves to its MIN-CONTENT width, and
   `.nav__links` and `#acct-switch` are a `<div>` and a `<span>` whose content
   is `white-space: nowrap`. So their min-content was their full width — 455px
   of links and a 291px account pill — and the browser could not take a single
   pixel from either. The whole 315px of overflow was pushed onto the one item
   that COULD shrink: `.gsearch` collapsed from 340px to **22px**, which is a
   search box you cannot read what you typed into. A control that is present
   and useless is the failure this codebase deletes on sight, and here the
   layout produced one on its own.

   ⚠ **THE NAV MAY NOT WRAP TO A SECOND ROW**, which is the other obvious fix.
   `--nav-h` is not decoration: three rules compute a scroll region from
   `calc(100vh - var(--nav-h))`, so a nav that grows silently makes each of
   them 50px too tall — a page that scrolls its last row out of reach with
   nothing on screen saying why. One row, and it fits.

   The order things give way in is NOT invented here. `.feedbadge` already
   wrote it down: *"This badge outranks the other nav chrome for space:
   'bars current' and the env label are context, this is whether what you are
   looking at is real."* So:

       1. the search shrinks, to a FLOOR                  (it is how you reach
                                                           any instrument)
       2. the account pill clips, keeping its broker chip (paper vs live is the
                                                           load-bearing half)
       3. the links SCROLL rather than disappear          (navigation is never
                                                           hidden)
       4. the context tags go                             (their own tier, per
                                                           the comment above)
       5. the wordmark goes                               (identity, not
                                                           function)

   The feed badge's own 13ch floor is never touched. */

/* ⚠ **THE LINKS SCROLL; THEY ARE NEVER HIDDEN.** Six destinations is
   navigation, and `features/watchlist.md` states the rule this follows —
   *"navigation you cannot see is not navigation."* Scrolling keeps every one
   reachable at any width, which dropping them into a menu would not. The
   hidden scrollbar is `.ipage__tabs`'s idiom, already in this file: a 2px
   scrollbar inside a 50px nav is chrome nobody asked for. */
/* ⚠ **AND A CLIPPED EDGE HAS TO SAY IT IS CLIPPED.** With the scrollbar
   hidden, "Dashboard Scanner Strategies U" reads as a nav that ENDS at
   Universe rather than one you can scroll — `features/watchlist.md`'s
   *"navigation you cannot see is not navigation"*, arriving through a scroll
   container instead of through a dropdown. A fade says "there is more" in the
   width of a gradient.

   ⚠ **THE MATCHING `padding-right` IS WHAT LETS THIS BE UNCONDITIONAL.**
   `box-sizing` is `border-box` here, so when the links FIT the trailing 20px
   is padding rather than a word — nothing visibly fades and `Journal` does not
   render as `Journa▒`. Only once the strip is actually scrolling does the
   gradient land on real text. The mask paints the element box rather than the
   scrolled content, so it stays pinned to the visible right edge. */
.nav__links {
  min-width: 0; overflow-x: auto; scrollbar-width: none;
  padding-right: 20px;
  -webkit-mask-image:
    linear-gradient(to right, #000 calc(100% - 20px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
}
.nav__links::-webkit-scrollbar { display: none; }
.nav__link { flex: none; }

/* The account pill: shrinkable now, and it degrades in the right order —
   `.feedbadge`'s `overflow: hidden` clips the account NAME while the `paper` /
   `live` chip in front of it survives, which is the half that decides whether
   the numbers on the page are real money. Its own `min-width: 13ch` is the
   floor.

   ⚠ **THE SHRINK FACTORS ARE THE PRIORITY ORDER, EXPRESSED.** Flex distributes
   a deficit in proportion to `flex-shrink × basis`, so leaving all three at 1
   takes a slice off the LINKS on the very first pixel of pressure — measured
   at 1400px, where `Journal` lost 18px to a hard clip on a screen with room to
   spare. Weighting the search and the pill means they give first and the links
   are only touched once both have hit their own floors, which is the order
   `.feedbadge`'s comment already states. */
/* ⚠ **A FLOOR AND A CLIP, and `min-width: 0` alone gave neither.** Unpinned,
   this span shrank to **40px at an 820px viewport** while the button inside it
   still measured its own `13ch` — so the pill was not shrinking, it was
   OVERFLOWING its container, with nothing clipping it and the feed badge
   underneath. And 40px answers nothing: this is in the nav because *which
   account* is a question the page must always be answering.

   `9ch` is the `paper` / `live` chip plus the button's own padding, which is
   the half that decides whether the numbers on screen are real money. The
   account NAME is what goes. */
#acct-switch {
  min-width: 9ch; display: flex; flex-shrink: 4;
}
/* ⚠ **THE CLIPPING GOES ON THE BUTTON, NEVER ON `#acct-switch`.**
   `overflow: hidden` lived here and fixed the shrink above — and silently broke
   the switcher, because this same element is the dropdown's positioning context
   (`position: relative`, in the account-switcher block further down). The menu
   is `position: absolute; top: 100%`, so it rendered 184px BELOW a 25px-tall
   box that clipped it: present in the DOM, invisible on screen, and
   `elementFromPoint` over it returned the page rather than the menu. Clicking
   the account switcher did nothing at all, which is how it was reported.

   `min-width: 0` is the half that looks like it does nothing: without it the
   button's flex-basis resolves to its min-content — the same `min-width: auto`
   trap this file documents for the nav itself — and it overflows the host
   instead of clipping inside it. */
#acct-btn { min-width: 0; max-width: 100%; overflow: hidden; }
/* ⚠ The search's own factor is in its `flex` SHORTHAND further down, not
   here. Written here first, it did nothing — the later shorthand resets
   `flex-shrink` to 1 at the same specificity, so the search gave 30px where it
   owed 110 and the links paid the difference. */

/* ⚠ **THE TWO LOWEST-PRIORITY TAGS GO FIRST, and the ranking is quoted rather
   than invented** — see `.feedbadge` above. `#priced-badge` says which session
   the prices are from and `#env-badge` says the environment and broker; both
   are context, and the broker half survives on the account chip beside them.
   1400px is where they would otherwise start forcing the links to scroll on a
   screen wide enough not to need it. */
@media (max-width: 1400px) {
  .nav #priced-badge, .nav #env-badge { display: none; }
}

/* Below this the wordmark goes too. It is identity, not function, and every
   pixel it holds is one the links cannot use. */
@media (max-width: 1150px) {
  .nav__brand { display: none; }
}

/* Fills whatever the nav leaves. min-height:0 is load-bearing: without it a
   flex child refuses to shrink below its content, the panel grows past the
   viewport, and the table's overflow never engages — the classic reason a
   "scrollable" flex pane scrolls the page instead of itself. */
.page { padding: 12px 16px; flex: 1; min-height: 0; display: flex; flex-direction: column; }

/* ---- the page's own scroll region ---------------------------------------
   ⚠ **EVERY `#page-root` PAGE WAS SILENTLY CUTTING ITS OWN CONTENT OFF**, and
   the rest of this file already assumed otherwise. `.tablewrap--auto` says a
   dashboard panel "sizes to its rows and THE PAGE SCROLLS AS A WHOLE" — but the
   page never scrolled. The shell is `height:100vh; overflow:hidden` by design,
   so each page must supply its own scroller the way `dashboard.html` supplies
   `.dash-scroll`; `.page` correctly claims whatever the nav leaves; and then
   `#page-root` had NO RULE AT ALL. It sized to its content, overflowed `.page`,
   and `body` clipped the remainder with no scrollbar to admit it.

   Measured at 1440x900 before this landed:

       /journal            main 850px tall holding 968px  ->  118px gone,
                           taking the last rows of the trades table with it
       /position-sizing    the Save bar and most of the bottom panel, unreachable

   `flex:1; min-height:0` is the same pair `.page` carries, for the reason its
   own comment gives: without `min-height:0` a flex child refuses to shrink
   below its content, so the scroller never engages and the overflow lands on an
   ancestor that is hidden.

   ONE region per page, not two. Tables keep `.tablewrap--auto`'s `max-height:
   46vh` cap, which only bites on a long list — that nesting is deliberate and
   predates this. `scanner.html` and the dashboard are untouched: neither routes
   its content through `#page-root`. */
#page-root { flex: 1; min-height: 0; overflow-y: auto; }

/* --------------------------------------------------------------- panel */
.panel {
  background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--r-lg);
  overflow: hidden;
  flex: 1; min-height: 0; display: flex; flex-direction: column;
}
/* Toolbar, stats, chips and pager keep their natural height; the table eats
   the rest. */
.toolbar, .stats, .chips, .pager { flex: none; }

.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 11px 13px; border-bottom: 1px solid var(--border); }
.field { display: flex; align-items: center; gap: 7px; }
.field__label { color: var(--fg-2); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; }

.input, .select {
  background: var(--bg-0); border: 1px solid var(--border);
  border-radius: var(--r); padding: 6px 9px; font-size: 12.5px;
}
.input:focus, .select:focus { outline: none; border-color: var(--accent); }
.input { font-family: var(--mono); }
.input--search { width: 180px; }
.select { max-width: 300px; }

/* ---------------------------------------------------------- custom select */
/* Replaces native <select>. The scope picker holds 131 indices; a native popup
   is drawn by the OS, so it cannot be styled, searched, or given the app's
   scrollbar. This one is ours end to end. */
.sel { position: relative; display: inline-block; }

.sel__btn {
  display: flex; align-items: center; gap: 8px;
  min-width: 150px; max-width: 300px;
  background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--r);
  padding: 6px 9px; font-size: 12.5px; cursor: pointer; text-align: left;
}
.sel__btn:hover { border-color: var(--border-hi); }
.sel.is-open .sel__btn { border-color: var(--accent); }
.sel__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sel__label.is-empty { color: var(--fg-2); }
.sel__chev { color: var(--fg-2); font-size: 10px; transition: transform 0.12s; }
.sel.is-open .sel__chev { transform: rotate(180deg); }

.sel__pop {
  position: absolute; z-index: 60; top: calc(100% + 4px); left: 0;
  min-width: 100%; width: max-content; max-width: 380px;
  background: var(--bg-2); border: 1px solid var(--border-hi);
  border-radius: var(--r-lg); box-shadow: 0 12px 34px rgba(0,0,0,0.55);
  display: none; overflow: hidden;
}
.sel.is-open .sel__pop { display: block; }
/* Flip above when the trigger sits near the bottom of the viewport. */
.sel--up .sel__pop { top: auto; bottom: calc(100% + 4px); }

.sel__search { padding: 7px; border-bottom: 1px solid var(--border); }
.sel__input {
  width: 100%; background: var(--bg-0); border: 1px solid var(--border);
  border-radius: var(--r); padding: 5px 8px; font-size: 12.5px; font-family: var(--mono);
}
.sel__input:focus { outline: none; border-color: var(--accent); }

/* The scroll region we actually control. */
.sel__list { max-height: 290px; overflow-y: auto; padding: 5px; overscroll-behavior: contain; }
.sel__list::-webkit-scrollbar { width: 11px; }
.sel__list::-webkit-scrollbar-track { background: transparent; }
.sel__list::-webkit-scrollbar-thumb {
  background: #4a525d; border-radius: 6px;
  border: 3px solid transparent; background-clip: content-box;
}
.sel__list::-webkit-scrollbar-thumb:hover { background: #626b78; background-clip: content-box; }

/* Sticky so you always know which category you are scrolling through — the
   thing a native grouped <select> gives up as soon as the list is long. */
.sel__group {
  position: sticky; top: -5px; z-index: 1;
  background: var(--bg-2);
  padding: 7px 9px 4px; margin: 0 -5px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.7px; text-transform: uppercase;
  color: var(--fg-2);
}

.sel__opt {
  display: flex; align-items: center; gap: 9px;
  padding: 6px 9px; border-radius: var(--r); cursor: pointer; font-size: 12.5px;
}
.sel__optlabel { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sel__meta { font-family: var(--mono); font-size: 11px; color: var(--fg-2); }
.sel__tick { color: var(--accent); font-size: 11px; visibility: hidden; }
.sel__opt.is-sel .sel__tick { visibility: visible; }
.sel__opt.is-sel { color: var(--fg-0); font-weight: 600; }
/* Hover and keyboard share one highlight, so the cursor never splits in two. */
.sel__opt.is-active { background: var(--bg-3); }
.sel__opt.is-active .sel__meta { color: var(--fg-1); }

.sel__none { padding: 16px 10px; text-align: center; color: var(--fg-2); font-size: 12px; }

.btn {
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--r); padding: 6px 12px; font-size: 12.5px; cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.btn:hover { background: var(--bg-3); border-color: var(--border-hi); }
.btn--primary { background: #2563eb; border-color: #3b82f6; color: #fff; }
.btn--primary:hover { background: #3b82f6; }
.btn--pass { background: var(--pass-bg); border-color: #1f6b3a; color: #6fdc8c; }
.btn--pass:hover { background: #16351f; border-color: var(--pass); }
.btn--fail { background: var(--fail-bg); border-color: #7a2b28; color: #ff8b84; }
.btn--fail:hover { background: #3a1a1c; border-color: var(--fail); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; }
.btn__k { font-family: var(--mono); font-size: 10px; opacity: 0.65; margin-left: 5px; }

/* --------------------------------------------------------------- stats */
.stats { display: flex; gap: 22px; padding: 11px 13px; border-bottom: 1px solid var(--border); }
.stat__label { color: var(--fg-2); font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; }
.stat__value { font-family: var(--mono); font-size: 17px; font-weight: 650; line-height: 1.25; }
.stat__value--pass { color: var(--pass); }
.stat__value--muted { color: var(--fg-1); }

/* --------------------------------------------------------------- table */
/* Table is the default view. Every column is horizontal and readable — the
   per-clause matrix lives in the drawer, opened per row, rather than as ten
   rotated columns competing with the data. */
/* No max-height calc. That approach guesses the height of everything above it
   and breaks the moment the chips wrap to a second line — which they do as soon
   as a strategy has more reject reasons. flex:1 measures instead of guessing. */
.tablewrap { overflow: auto; flex: 1; min-height: 0; overscroll-behavior: contain; }
.tbl { width: 100%; border-collapse: separate; border-spacing: 0; }
.tbl th, .tbl td {
  padding: 0 10px; height: 34px; white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
.tbl thead th {
  position: sticky; top: 0; z-index: 10; background: var(--bg-2);
  font-size: 10px; font-weight: 650; text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--fg-2); text-align: left; cursor: pointer; user-select: none;
}
.tbl thead th:hover { color: var(--fg-0); }
.tbl thead th.r { text-align: right; }
/* Must live HERE, beside its sibling, not on `.th-act`. `.tbl thead th` sets
   text-align:left and outranks a single class, so the Actions LABEL stayed
   left while the buttons under it centred — the column looked misaligned even
   though every cell box and every button was already on the same centre. */
.tbl thead th.th-act { text-align: center; }
.tbl thead th.sorted::after { content: ' ▼'; font-size: 8px; }
.tbl thead th.sorted.asc::after { content: ' ▲'; }

/* A qualifier that rides IN the header label, for a sort that cannot reach the
   whole result set. Lowercase and un-tracked against the uppercase, letter-
   spaced header so it reads as a note about the column rather than part of its
   name — the column is still called Price. */

/* ---- the portfolio panel's summary strip -------------------------------
   Invested, Market value and Open P&L, between the panel head and the column
   headers — the three figures that came out of the account strip at the top
   of the page. See `positionsSummary` in trade.js for why they moved.

   ⚠ **ABOVE THE ROWS, NOT BELOW THEM.** A totals row under the table was the
   first shape, and it put the answer at the bottom of a scrolling list. This
   reads before the rows, like the panel title.

   ⚠ **LABEL SMALL AND FLAT, NUMBER LARGE AND MONO.** Same idiom as `.stat`,
   so the strip reads as a summary rather than as a first data row — which is
   exactly what it would be mistaken for sitting directly on the header. */
.tsum {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 26px;
  padding: 11px 14px; border-bottom: 1px solid var(--border);
  background: var(--bg-1);
}
.tsum__item { display: inline-flex; align-items: baseline; gap: 8px; }
.tsum__label {
  font-size: 10px; font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.6px; color: var(--fg-2);
}
.tsum__val {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  font-size: 15px; font-weight: 650; color: var(--fg-0);
}
/* Signed by `setTotal` on every price tick, the same pos/neg pair the cells
   below use — so the total and its column agree on what green means. */
.tsum__val.pos { color: var(--pass); }
.tsum__val.neg { color: var(--fail); }
/* The percentage rides inside the number, quieter, exactly as it does in the
   P&L cells — `cellMoneyPct` renders both, so the idiom is already shared. */
.tsum__val .cellpct { font-size: 12px; }

/* The pending count, subordinate to the open one in the same cell — the same
   rank `.cellpct` gives a percentage beside its rupees. "2 open" is the figure;
   "· 1 pending" is what qualifies it. */
.tsum__aside {
  margin-left: 6px; font-size: 12px; font-weight: 400; color: var(--fg-2);
}

/* On a narrow pane the three wrap rather than compress: a summary that
   ellipsises a rupee figure is worse than one on two lines. */
@media (max-width: 720px) {
  .tsum { gap: 6px 18px; padding: 9px 12px; }
  .tsum__val { font-size: 13.5px; }
}

.tbl tbody tr { background: var(--bg-1); }
.tbl tbody tr:hover { background: var(--bg-2); }
.tbl tbody tr.is-sel { background: var(--bg-3); box-shadow: inset 2px 0 0 var(--accent); }
.tbl td.r { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.td-sym { font-family: var(--mono); font-weight: 650; }

/* Symbol is the click target for the clause drawer — underlined so it reads as
   one, the same affordance the reference UI uses. */
.td-sym a { color: var(--fg-0); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--fg-2); cursor: pointer; }
.td-sym a:hover { text-decoration-color: var(--accent); color: var(--accent); }

/* Actions pinned right so they stay reachable while the numbers scroll. */
/* CENTRED, not right-aligned. The cell holds either one button or a chip plus
   a button, and right-aligning made the lone Approve sit hard against the edge
   while the two-element rows started further left — the column had no common
   axis and read as ragged. Centring gives every state the same centre line. */
.td-act, .th-act { position: sticky; right: 0; background: inherit; text-align: center; width: 178px; }
.tbl thead th.th-act { background: var(--bg-2); z-index: 12; cursor: default; }
.tbl tbody tr:hover .td-act { background: var(--bg-2); }
.tbl tbody tr.is-sel .td-act { background: var(--bg-3); }
.acts { display: inline-flex; gap: 6px; align-items: center; justify-content: center; }

/* The base sets a NEUTRAL look, not just geometry. It used to declare only
   size and radius, so a bare `.abtn` — every modifier here overrides both —
   fell through to the browser's default button chrome: a white box whose
   ButtonText was overridden to a light colour elsewhere, rendering as an empty
   white rectangle with the label invisible inside it. That is a trap for the
   next person who reaches for the class without knowing it is abstract. */
.abtn {
  padding: 4px 14px; border-radius: 5px; cursor: pointer;
  font-size: 12px; font-weight: 600; line-height: 1.5; white-space: nowrap;
  background: var(--bg-2); color: var(--fg-1);
  border: 1px solid var(--border); transition: filter 0.12s, background 0.12s;
}
/* Scoped to the UNMODIFIED button. A plain `.abtn:hover` here would also set
   `color` on `.abtn--approve`, whose own hover rule only touches `filter` —
   turning the green button's dark label light on hover. */
.abtn:not([class*="abtn--"]):hover { border-color: var(--border-hi); color: var(--fg-0); }
/* Solid green / outlined red — approving is the affirmative action and reads
   heavier; rejecting is available but not visually urging you toward it. */
/* ⚠ **`.btn--buy` SHARES THIS DECLARATION RATHER THAN COPYING IT.** The
   drawer's header Buy is a `.btn` (that header's shape and sizing) and the
   watchlist's is an `.abtn`, but they are the SAME affirmative action on the
   same instrument and must be the same green — the owner asked for the
   drawer's to be "the green one", meaning this one. Listed together so a
   change to the colour cannot reach one and miss the other.

   ⚠ And it is DEFINED, which the last button class was not: `abtn--buy` was
   invented at a call site, existed in no stylesheet, and rendered as the
   bare base class beside this rule's own green for a whole session
   (`contract_row_check` now fails the build on that). */
.abtn--approve, .btn--buy { background: #3fb950; border-color: #3fb950; color: #06210d; }
.abtn--approve:hover, .btn--buy:hover { filter: brightness(1.12); }
/* ⚠ `.btn--short` JOINS THIS ONE for the reason `.btn--buy` joins the green
   above: the drawer's header Short and the watchlist's are the same action on
   the same instrument, differing only in the button SHAPE each header uses.
   Listed together so a colour change cannot reach one and miss the other —
   and, more to the point, so the class EXISTS. `abtn--buy` was invented at a
   call site, matched no stylesheet, and rendered as the bare base class
   beside a solid green twin for a whole session. */
.abtn--reject, .btn--short { background: transparent; border-color: #7a2b28; color: #ff8b84; }
.abtn--reject:hover, .btn--short:hover { background: var(--fail-bg); border-color: var(--fail); }

/* A disabled action must READ as disabled.
   There was no rule here at all, so a Buy button disabled because the stock is
   already held rendered in exactly the same solid green as a live one — it
   simply stopped responding, with nothing on screen saying why. A browser's
   own dimming does almost nothing once background and border are set
   explicitly, which is why this needs stating.
   :disabled beats the :hover rules below it by order, so the hover lift goes
   too — half the "is this clickable?" signal is the mouse response. */
.abtn:disabled,
.abtn[disabled] {
  cursor: not-allowed;
  opacity: 0.42;
  filter: grayscale(0.65);
  box-shadow: none;
}
.abtn:disabled:hover,
.abtn[disabled]:hover { filter: grayscale(0.65); background: inherit; }
/* Solid green greyed out still reads as a button; this reads as a state. */
.abtn--approve:disabled, .abtn--approve[disabled] {
  background: var(--bg-3); border-color: var(--border); color: var(--fg-2);
  opacity: 0.75; filter: none;
}
.abtn--approve:disabled:hover, .abtn--approve[disabled]:hover {
  background: var(--bg-3); filter: none;
}
.chipbtn:disabled, .chipbtn[disabled] { cursor: not-allowed; opacity: 0.45; }
.btn:disabled, .btn[disabled] { cursor: not-allowed; opacity: 0.5; filter: grayscale(0.6); }

/* Decided state: the chosen side stays lit, the other recedes. */
/* A STATE, not an action. An approved row is on the watchlist and has nothing
   left to approve, so it stops offering a button that would only un-approve
   it — which is exactly what the old Approve-as-toggle did by accident. */
.statechip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 11px; border-radius: 5px; white-space: nowrap;
  font-size: 12px; font-weight: 600; line-height: 1.5;
  border: 1px solid transparent;
}
.statechip--watch { background: var(--pass-bg); color: #6fdc8c; border-color: #1f6b3a; }
/* Deliberately quiet: undoing is available, not encouraged. */
.abtn--undo { background: transparent; border-color: var(--border); color: var(--fg-2); }
.abtn--undo:hover { border-color: var(--fg-2); color: var(--fg-1); }

/* Compact pass-strength — horizontal, in its own column. */
.pips { display: inline-flex; gap: 2px; vertical-align: -1px; }
.pip { width: 5px; height: 10px; border-radius: 1px; background: var(--fail); opacity: 0.4; }
.pip.is-on { background: var(--pass); opacity: 0.9; }
.score { font-family: var(--mono); font-size: 11px; color: var(--fg-2); margin-left: 6px; }

/* --------------------------------------------------------------- drawer */
.scrim {
  position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 40;
  opacity: 0; pointer-events: none; transition: opacity 0.15s;
}
.scrim.is-open { opacity: 1; pointer-events: auto; }
.drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(620px, 94vw);
  background: var(--bg-1); border-left: 1px solid var(--border-hi);
  z-index: 50; overflow-y: auto;
  transform: translateX(100%); transition: transform 0.18s ease-out;
  box-shadow: -14px 0 40px rgba(0,0,0,0.5);
}
.drawer.is-open { transform: none; }
.drawer { overflow-x: hidden; }

/* Stacked drawers. `--stack-depth` is set per level by `ensure()`; each sits
   a little left of the one below so the edges show. The scrim stays at 50 —
   it is the page's dimming, not a per-level backdrop. */
.drawer { --stack-depth: 0; z-index: calc(50 + var(--stack-depth)); }
.drawer.is-open {
  transform: translateX(calc(var(--stack-depth) * -22px));
}
.drawer.is-stacked { box-shadow: -14px 0 40px rgba(0,0,0,0.5),
                                 -1px 0 0 0 var(--border-hi); }
.drawer__head {
  display: flex; align-items: center; gap: 12px; padding: 15px 20px;
  border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg-1); z-index: 2;
  flex-wrap: wrap;
}
.drawer__head > div:first-child { min-width: 0; flex: 1; }
/* ⚠ **BUY AND SHORT TOUCHED, because they share ONE flex item.** The head's
   own `gap: 12px` separates its CHILDREN, and both buttons live inside the
   single `[data-dd="acts"]` slot — so the gap applied around the pair and not
   between them, and the two rendered as one welded control. Reported by the
   owner. The slot carries its own gap now; a `<span>` filled by innerHTML is
   still the right shape (the verdict lands after the head is written), it
   just has to space what it holds. */
.drawer__acts { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.drawer__close { margin-left: auto; flex: none; }
/* 3 across in a 620px drawer reads better than 5 squeezed. */
.drawer .metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex: none; }
.dot--pass { background: var(--pass); box-shadow: 0 0 7px rgba(69,194,106,0.55); }
.dot--fail { background: var(--fail); opacity: 0.5; }
.dot--warm { background: var(--fg-2); }

.tag { display: inline-block; padding: 0 6px; border-radius: 4px; font-size: 10px; font-family: var(--mono); background: var(--bg-3); color: var(--fg-1); }
.tag--fno { background: #14294a; color: #7fb2ff; }
/* The F&O tag is a `<button>` — these rules make it sit identically to the
   ASM and GSM spans beside it. Only the pointer and hover differ. */
button.tag {
  border: 0; font-family: var(--mono); line-height: inherit;
  appearance: none; vertical-align: baseline;
}
button.tag--fno { cursor: pointer; }
button.tag--fno:hover { background: #1b3763; color: #a8ccff; }
button.tag--fno:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tag--asm { background: var(--warn-bg); color: #e0b040; }
.tag--gsm { background: var(--fail-bg); color: #ff8b84; }
.tag--ok  { background: var(--pass-bg); color: #6fdc8c; }

/* -------------------------------------------------------------- detail */
.detail { padding: 16px 20px 24px; }
.detail__head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 4px; }
.detail__sym { font-family: var(--mono); font-size: 22px; font-weight: 650; letter-spacing: -0.4px; }
/* ⚠ ONE LINE, ALWAYS. A contract's ref is long — `OPT:NSE:BAJAJFINSV:
   2026-09-29:1980:CE` is 37 characters — and with a kind chip in front of it
   this wrapped to 138px of two lines, breaking the ref mid-token. An identity
   line that reflows is a layout that moves under the reader as the drawer
   fills in. Clipped, with the full text on `title` instead.

   ⚠ NOT `display: flex`. `text-overflow` only applies to a block container's
   own inline content — under flex the ref becomes an anonymous flex item and
   the ellipsis silently does nothing. Left as a block, with the chip inline
   inside it, which is what `.ik { display: inline-block }` is for. */
.detail__isin {
  color: var(--fg-2); font-family: var(--mono); font-size: 11.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.detail__spacer { flex: 1; }
.verdictbadge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 12px; border-radius: 999px; font-size: 12px; font-weight: 600;
}
.verdictbadge--pass { background: var(--pass-bg); color: #6fdc8c; border: 1px solid #1f6b3a; }
.verdictbadge--fail { background: var(--fail-bg); color: #ff8b84; border: 1px solid #7a2b28; }
.verdictbadge--warm { background: var(--bg-2); color: var(--fg-1); border: 1px solid var(--border); }

.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; margin: 16px 0 20px; }
.metric { background: var(--bg-2); padding: 10px 12px; }
.metric__label { color: var(--fg-2); font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; }
.metric__value { font-family: var(--mono); font-size: 16px; font-weight: 600; margin-top: 3px; }
.metric__note { color: var(--fg-2); font-size: 10.5px; font-family: var(--mono); }

.sectitle { font-size: 11px; text-transform: uppercase; letter-spacing: 0.7px; color: var(--fg-2); font-weight: 600; margin: 0 0 9px; }

/* The clause breakdown. Vertical and readable — this is what the rotated
   column headers were trying and failing to be. */
.clauses { display: flex; flex-direction: column; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
/* minmax(0, 1fr), not 1fr: a `1fr` track refuses to shrink below its content's
   min-content width, and these expressions are `white-space: nowrap`. That
   blows the grid wider than its container and pushes the drawer's close button
   off-screen. minmax(0, …) lets it actually clip. */
/* Stacked, not three columns. The clause is now stated three ways — id, then
   English, then the DSL it was written as — and the English is a sentence, not
   a cell: forcing it into a fixed column either truncated it or squeezed the
   expression into an ellipsis, which is the one part that has to be exact. */
.clause { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: 9px;
  align-items: start; padding: 8px 12px 9px; background: var(--bg-2); }
.clause__body { min-width: 0; }
.clause--fail { background: #1c1416; }
.clause__mark { font-family: var(--mono); font-weight: 700; text-align: center; line-height: 1.45; }
.clause--pass .clause__mark { color: var(--pass); }
.clause--fail .clause__mark { color: var(--fail); }
/* Wraps rather than ellipsing. `! (sma(20) < sma(50))@{0..20}` cut off at the
   edge of a column is worse than useless — it is the half that verifies the
   sentence above it. */
.clause__expr { font-family: var(--mono); font-size: 10px; color: var(--fg-2); margin-top: 4px; line-height: 1.5; overflow-wrap: anywhere; }
.clause__plain { font-size: 12px; color: var(--fg-1); line-height: 1.45; }
.clause__plain--raw { font-family: var(--mono); color: var(--fg-2); }
.clause--fail .clause__plain { color: #ffb8b3; }

.actions { display: flex; align-items: center; gap: 9px; margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--border); }
.actions .hint { margin-left: auto; }

.decided { display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px; border-radius: var(--r); font-size: 12px; }
.decided--approved { background: var(--pass-bg); color: #6fdc8c; }
.decided--rejected { background: var(--fail-bg); color: #ff8b84; }

/* -------------------------------------------------------------- states */
.empty, .loading { padding: 44px 16px; text-align: center; color: var(--fg-2); }
.spinner { width: 15px; height: 15px; display: inline-block; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: -3px; }
@keyframes spin { to { transform: rotate(360deg); } }

.chips { display: flex; flex-wrap: wrap; gap: 5px; padding: 9px 13px; border-bottom: 1px solid var(--border); align-items: center; }
.chip { display: inline-flex; align-items: center; gap: 5px; background: var(--bg-2); border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px; font-size: 11.5px; cursor: pointer; font-family: var(--mono); }
.chip:hover { border-color: var(--border-hi); }
.chip.is-on { background: var(--fail-bg); border-color: var(--fail); color: #ff8b84; }
.chip__n { color: var(--fg-2); }
.chip.is-on .chip__n { color: #ff8b84; }

.pager { display: flex; align-items: center; gap: 9px; padding: 9px 13px; border-top: 1px solid var(--border); color: var(--fg-1); font-size: 12px; }
.pager__info { font-family: var(--mono); }
.pager__spacer { flex: 1; }

.hint { color: var(--fg-2); font-size: 11.5px; }
kbd { font-family: var(--mono); font-size: 10px; background: var(--bg-3); border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 4px; padding: 1px 5px; }

/* ⚠ **ONE TOAST BLOCK. `.toast` WAS DECLARED TWICE**, here and again at the
   old `.toasts` rule below, because two implementations rendered into two
   containers pinned to the same corner. The second declaration won for every
   shared property, so the two systems looked alike and the split was
   undetectable by eye — see `M.toast`. Padding leaves room for the ✕. */
.toast-host { position: fixed; right: 14px; bottom: 14px; z-index: 100; display: flex; flex-direction: column; gap: 8px; }
.toast { background: var(--bg-2); border: 1px solid var(--border-hi); border-left: 3px solid var(--accent); border-radius: var(--r); padding: 9px 30px 9px 13px; font-size: 12px; position: relative; max-width: 340px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
.toast--err { border-left-color: var(--fail); }
.toast--ok { border-left-color: var(--pass); }
.toast__x { position: absolute; right: 6px; top: 5px; background: none; border: none; color: var(--fg-2); cursor: pointer; font-size: 13px; line-height: 1; }
.toast__x:hover { color: var(--fg-0); }

@media (max-width: 980px) {
  .split { grid-template-columns: 1fr; }
  .split__list { border-right: none; border-bottom: 1px solid var(--border); }
  .list { max-height: 300px; }
  .clause { grid-template-columns: 20px 1fr; }
  .clause__expr { display: none; }
}

/* Stub-data badge — removed with the ingestor. */
.tag--stub { background: var(--warn-bg, #2b2109); color: #e3b341; border: 1px solid #6b5310; }

/* ---------------------------------------- select: stars, badges, collapse */
.sel__group {
  display: flex; align-items: center; gap: 7px;
  cursor: pointer; user-select: none;
}
.sel__group:hover { color: var(--fg-1); }
.sel__caret { font-size: 9px; transition: transform 0.12s; display: inline-block; }
.sel__group.is-collapsed .sel__caret { transform: rotate(-90deg); }
.sel__grouplabel { flex: 1; }
.sel__groupn {
  font-family: var(--mono); font-size: 10px; font-weight: 400;
  background: var(--bg-3); border-radius: 999px; padding: 0 6px; color: var(--fg-2);
}

.sel__star {
  font-size: 13px; line-height: 1; color: var(--fg-2);
  cursor: pointer; width: 15px; text-align: center; flex: none;
}
.sel__star:hover { color: var(--warn); }
.sel__star.is-on { color: var(--warn); }

/* Exchange badge — NSE and BSE both publish a "100" and a "500". */
.sel__badge {
  font-family: var(--mono); font-size: 9.5px; font-weight: 600;
  border-radius: 3px; padding: 1px 4px; flex: none;
  background: var(--bg-3); color: var(--fg-2); letter-spacing: 0.3px;
}
.sel__opt.is-active .sel__badge { background: var(--border-hi); color: var(--fg-1); }

/* =========================================================== scope picker */
/* Purpose-built for the 192-index catalogue (js/scope_picker.js). Collapse,
   search and star are CSS state on a DOM built once — never a re-render. */
.sp { position: relative; display: inline-block; }

.sp__btn {
  display: flex; align-items: center; gap: 8px;
  min-width: 190px; max-width: 320px;
  background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--r);
  padding: 6px 9px; font-size: 12.5px; cursor: pointer; text-align: left;
}
.sp__btn:hover { border-color: var(--border-hi); }
.sp.is-open .sp__btn { border-color: var(--accent); }
.sp__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sp__chev { color: var(--fg-2); font-size: 10px; transition: transform 0.12s; }
.sp.is-open .sp__chev { transform: rotate(180deg); }

.sp__pop {
  position: absolute; z-index: 60; top: calc(100% + 5px); left: 0;
  width: 420px; max-width: 92vw;
  background: var(--bg-2); border: 1px solid var(--border-hi);
  border-radius: var(--r-lg); box-shadow: 0 16px 44px rgba(0,0,0,0.6);
  display: none; overflow: hidden;
}
.sp.is-open .sp__pop { display: flex; flex-direction: column; }
.sp--up .sp__pop { top: auto; bottom: calc(100% + 5px); }

.sp__head { padding: 9px; border-bottom: 1px solid var(--border); flex: none; }
.sp__search {
  width: 100%; background: var(--bg-0); border: 1px solid var(--border);
  border-radius: var(--r); padding: 7px 10px; font-size: 12.5px; font-family: var(--mono);
}
.sp__search:focus { outline: none; border-color: var(--accent); }

/* Taller than the old 290px — with 192 indices the extra rows matter. */
.sp__body { max-height: 480px; overflow-y: auto; padding: 6px; overscroll-behavior: contain; }
.sp__body::-webkit-scrollbar { width: 11px; }
.sp__body::-webkit-scrollbar-track { background: transparent; }
.sp__body::-webkit-scrollbar-thumb {
  background: #4a525d; border-radius: 6px;
  border: 3px solid transparent; background-clip: content-box;
}
.sp__body::-webkit-scrollbar-thumb:hover { background: #626b78; background-clip: content-box; }

.sp__grp { margin-bottom: 2px; }
.sp__grp.is-empty { display: none; }
.sp__grphead {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 9px; border-radius: var(--r); cursor: pointer; user-select: none;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.6px;
  text-transform: uppercase; color: var(--fg-2);
}
.sp__grphead:hover { background: var(--bg-3); color: var(--fg-1); }
.sp__caret { font-size: 9px; transition: transform 0.12s; display: inline-block; }
.sp__grp.is-open .sp__caret { transform: rotate(90deg); }
.sp__grpname { flex: 1; }
.sp__grpn {
  font-family: var(--mono); font-weight: 400; font-size: 10px;
  background: var(--bg-3); border-radius: 999px; padding: 0 6px;
}
/* Collapse is one class on the group — no node is removed, so nothing the
   click handler is standing on can be detached mid-event. */
.sp__grpitems { display: none; }
.sp__grp.is-open .sp__grpitems { display: block; }

.sp__opt {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 9px; border-radius: var(--r); cursor: pointer; font-size: 12.5px;
}
.sp__opt.is-hidden { display: none; }
.sp__opt:hover { background: var(--bg-3); }
.sp__opt.is-sel { background: var(--bg-3); font-weight: 600; box-shadow: inset 2px 0 0 var(--accent); }
.sp__star { color: var(--fg-2); font-size: 13px; width: 15px; text-align: center; flex: none; }
.sp__star:hover { color: var(--warn); }
.sp__star.is-on { color: var(--warn); }
.sp__ex {
  font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.3px;
  border-radius: 3px; padding: 1px 4px; flex: none; width: 30px; text-align: center;
}
.sp__ex--NSE { background: #16304d; color: #79c0ff; }
.sp__ex--BSE { background: #3a2a12; color: #e0a44a; }
.sp__name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sp__n { font-family: var(--mono); font-size: 11px; color: var(--fg-2); }

.sp__foot {
  display: flex; align-items: center; justify-content: space-between;
  padding: 7px 11px; border-top: 1px solid var(--border); flex: none;
  font-size: 11px; color: var(--fg-2);
}
.sp__count { font-family: var(--mono); }
.sp__expand {
  background: none; border: none; color: var(--accent);
  font-size: 11px; cursor: pointer; padding: 0;
}
.sp__expand:hover { text-decoration: underline; }

/* ------------------------------------------------------- ingest progress */
.prog { padding: 11px 13px; border-bottom: 1px solid var(--border); }
.prog__bar {
  height: 6px; background: var(--bg-0); border-radius: 999px; overflow: hidden;
  border: 1px solid var(--border);
}
.prog__fill {
  height: 100%; width: 0; border-radius: 999px;
  background: linear-gradient(90deg, #1f6feb, #58a6ff);
  transition: width 0.35s ease-out;
}
.prog__meta {
  display: flex; align-items: center; gap: 10px;
  margin-top: 7px; font-size: 12px; color: var(--fg-1);
}
.prog__stat { font-family: var(--mono); font-size: 11.5px; }
.prog__recent { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; }
.prog__pill {
  font-family: var(--mono); font-size: 10px;
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 1px 7px; color: var(--fg-2);
}
.prog__pill.is-bad { border-color: #7a2b28; color: #ff8b84; }

/* Progress pills: fixed slot width and a reserved row, so the panel does not
   reflow as index names of different lengths shuffle through it. */
.prog__recent { min-height: 38px; align-content: flex-start; }
.prog__pill {
  min-width: 108px; max-width: 108px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center;
}
.prog__pill.is-empty { background: transparent; border-color: transparent; }
.prog__meta { min-height: 20px; }
#prog-text { font-family: var(--mono); font-size: 11.5px; }

/* A scope with members but nothing scannable — BSE SME IPO is the live case. */
.sp__opt.is-empty .sp__name { color: var(--fg-2); }
.sp__n--none {
  color: var(--warn); background: var(--warn-bg);
  border-radius: 3px; padding: 0 5px; font-size: 10.5px; letter-spacing: 0.2px;
}

/* ------------------------------------------------- execution layer pages */
.stats--wrap { flex-wrap: wrap; }
.stat__value--fail { color: var(--fail); }
.tpage { display: flex; flex-direction: column; gap: 14px; padding: 14px; }
.tpanel { border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg-1); }
/* ⚠ 14px, NOT 10. The head carries a title, a broker badge and a position
   count, then three controls, and at 10px the badge sat close enough to the
   word "Portfolio" to read as part of the title. The summary strip now sits
   directly under this row, so the two need a visible gap or they read as one
   three-line header. */
.tpanel__head { position: relative; display: flex; align-items: center; gap: 14px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
.tpanel__title { font-size: 13px; font-weight: 650; }

/* The way out to the full page — see `panelOpen`. It SAYS where it goes, in
   words, without being hovered: an arrow that only appears when you happen to
   be over the heading is a control nobody finds. Quiet at rest so the panel
   still reads as a table, and the whole PANEL is the hover target, because
   that is the thing you are already pointing at when you want more of it. */
.tpanel__open {
  font-size: 11px; color: var(--fg-2); text-decoration: none;
  display: inline-flex; align-items: baseline; gap: 3px; flex: none;
  border-bottom: 1px dashed var(--border-hi); line-height: 1.4;
  transition: color 0.12s, border-color 0.12s;
}
.tpanel:hover .tpanel__open { color: var(--accent); border-bottom-color: var(--accent); }
.tpanel__open:hover, .tpanel__open:focus-visible {
  color: var(--accent); border-bottom-style: solid;
}
.tpanel__go { font-size: 10px; }

/* Named for screen readers, absent from the layout. Two panels each offering
   "Open full page" need to differ in a list of links read out of context. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.tpanel__meta { color: var(--fg-2); font-size: 11px; }
.tpanel__spacer { flex: 1; }

/* ⚠ **THE POSITION COUNT IS NOT HERE ANY MORE.** It was centred in this row
   (`.tpanel__count`, absolutely positioned at 50%), and centred it read as a
   caption on the panel rather than as one of the four figures it belongs with.
   It is the first item in `.tsum` now — see `positionsSummary`. */
/* Panels host the SCANNER's table component (.tablewrap/.tbl). --auto opts
   out of the scanner's flex:1 fill, since a dashboard panel sizes to its rows
   and the page scrolls as a whole; the cap only engages on long lists. */
.tpanel .tablewrap--auto { flex: none; max-height: 46vh; }
.dash-scroll { display: flex; flex-direction: column; }
.tpanel .tbl thead th { background: var(--bg-1); }
.tpanel .tbl tbody tr:last-child td { border-bottom: none; }
/* 150px was sized for the widest pair of buttons; "Buy"+"Remove" and
   "Sell" need less, and on a dashboard panel every pixel here is one the
   numbers do not get. */
.tpanel .td-act, .tpanel .th-act { width: 118px; }

/* ---- prose columns in a table of numbers --------------------------------
   `Company` and `Sector` are the only free-text columns these tables have, and
   they are long: "Adani Energy Solutions Limited", "Automobile and Auto
   Components". Left to size themselves they pushed the watchlist table to
   1,511px inside a panel that is nowhere near that wide, so turning either on
   scrolled every NUMBER off the right-hand side.

   Capped and ellipsised instead, with the full value on the cell's title. A
   name you can read most of beats a table you have to scroll to read at all. */
.tbl td.td-text { max-width: 170px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.tpanel .tbl td.td-text { max-width: 140px; }
.pos { color: var(--pass); } .neg { color: var(--fail); }
.rowbadge { font-size: 9px; padding: 1px 6px; border-radius: 8px; border: 1px solid var(--border); color: var(--fg-2); margin-left: 6px; text-transform: uppercase; }
.rowbadge--warn { color: var(--warn); border-color: var(--warn); }
/* Held is money on the table. Yellow, and the same yellow wherever it
   appears — the scanner's symbol column and the watchlist's. */
.rowbadge--held { color: #e3b341; border-color: #6b5310; background: var(--warn-bg); }
.rowbadge--watch { color: #6fdc8c; border-color: #1f6b3a; background: var(--pass-bg); }
.chipbtn { font: 12px var(--sans); padding: 3px 10px; border-radius: 6px; border: 1px solid var(--border-hi); background: var(--bg-2); color: var(--fg-0); cursor: pointer; }
.chipbtn--buy { border-color: var(--pass); color: var(--pass); }
.chipbtn--sell, .chipbtn--danger { border-color: var(--fail); color: var(--fail); }
.chipbtn:disabled { opacity: .45; cursor: default; }
.tmodal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 60; }
/* ⚠ **CAPPED AND SCROLLABLE, WITH THE NAME AND THE COMMIT PINNED.** This had
   neither a max-height nor an overflow: the dialog simply grew, and on a
   short viewport it grew past both edges at once — the heading above the top
   and Confirm below the bottom, with no scrollbar, so the one control the
   dialog exists for could not be reached at all.

   Latent on the equity dialog, which is short enough to fit. The CONTRACT buy
   modal is not: it shows SPAN + exposure, notional, leverage, the sizing
   working and up to four warnings, and it overflowed a 450px window on its
   first real render.

   Sticky rather than a fixed header/footer, because that needs no wrapper
   element and therefore no markup change in three dialogs. The negative
   offsets cancel the padding so the pinned rows sit flush against the edges
   they stick to; the background is opaque so the working scrolls UNDER them
   rather than through them. */
.tmodal { width: 430px; max-width: 92vw; background: var(--bg-1); border: 1px solid var(--border-hi); border-radius: var(--r-lg); padding: 16px;
  max-height: 90vh; overflow-y: auto; }
.tmodal h3 { margin: 0 0 10px; font-size: 14px;
  position: sticky; top: -16px; z-index: 2;
  background: var(--bg-1); padding: 16px 0 8px; margin-top: -16px; }
/* The price beside the name. Every figure in the dialog is derived from it,
   and it was the one number not on screen. Mono so it lines up with the
   working below, dimmer than the symbol so it reads as an attribute of it. */
.tmodal h3 .bd__price { font: 12px var(--mono); color: var(--fg-2); margin-left: 6px; }
.tmodal .frow { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 7px 0; font-size: 12px; }
.tmodal .frow input, .tmodal .frow select { font: 12px var(--mono); background: var(--bg-0); color: var(--fg-0); border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px; width: 130px; text-align: right; }
/* ---- the stop, in either unit ------------------------------------------
   Two boxes for one decision — a percent and a price, each writing the other.
   Buy asked only for the percent and Move stop only for the price, so a trader
   who thinks in the other unit did the arithmetic by hand. See `stopPairHtml`.

   ⚠ **THE PAIR OVERRIDES `.tmodal .frow input`'s 130px.** That width is for one
   box on a row; two of them plus the units would overflow the modal.

   ⚠ **THE TWO BOXES ARE NOT THE SAME WIDTH, and that is measured rather than
   guessed.** A percent is at most five characters ("12.34"); an Indian price is
   routinely eight ("11674.00") and the number ends in the decimals that matter.
   At a shared 7ch the price box clipped ULTRACEMCO's stop to "107" — a
   truncated price is worse than no price, because it still looks like one. */
/* ⚠ **ONE BORDER AROUND BOTH HALVES, hairline between.** This was two bordered
   boxes joined by an "=", which read as two settings that happen to be related.
   The border is the claim that they are ONE value with two spellings — the
   shape a broker's own SL Trigger field uses, and the one traders already know.

   The wrapper owns the border, the focus ring and the error state; the inputs
   inside are stripped bare. `is-on` is set by the component on focus rather
   than by `:focus-within`, so one mechanism drives it. */
.stopfield {
  display: inline-flex; align-items: stretch;
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--bg-0); overflow: hidden;
}
.stopfield.is-on { border-color: var(--accent); }
.stopfield.is-bad { border-color: var(--fail); }
.stopfield__cell { display: flex; align-items: center; gap: 3px; padding: 5px 8px; }
/* The percent half is narrower and right-hung, so the two numbers do not drift
   apart as the price grows a digit. */
.stopfield__cell--pct { padding-left: 6px; }
.stopfield__div { width: 1px; background: var(--border); }
.stopfield.is-bad .stopfield__div { background: var(--fail); opacity: 0.45; }
.stopfield__unit { color: var(--fg-2); font-size: 11px; font-family: var(--mono); }

/* ⚠ **BORDERLESS AND TRANSPARENT — the wrapper draws the box now.** Without
   this they inherit `.tmodal .frow input`'s own border and 130px width, which
   put a second rectangle inside the first. */
.tmodal .frow--stop .stopfield input {
  border: none; background: none; border-radius: 0; padding: 0;
  font: 12.5px var(--mono); color: var(--fg-0); text-align: right;
}
.tmodal .frow--stop .stopfield input:focus { outline: none; }
/* ⚠ **KEYED ON THE ARIA LABEL, NOT ON THE ID — and it was ids until a THIRD
   dialog arrived.** These read `#bd-price, #sm-price` / `#bd-pct, #sm-pct`,
   one pair of ids per dialog, so the contract buy modal's `cb-` prefix
   matched neither and both of its boxes silently fell back to
   `.tmodal .frow input`'s 130px. Measured: 68px/41px in the equity dialog
   against 130px/130px in the contract one — the two halves the same width,
   the ₹ box filling the row, and the % box pushed out of sight. The control
   was the shared component the whole time; only its SIZING was per-dialog.

   `stopPairHtml` writes these labels on every instance, so a fourth dialog
   gets the sizing by existing rather than by remembering to add its prefix
   here. That is the same reason the component itself is shared. */
.tmodal .frow--stop input[aria-label="Stop-loss price"] { width: 9ch; }
.tmodal .frow--stop input[aria-label="Stop-loss percent"] { width: 5.5ch; }
/* ⚠ THE SPINNER WAS EATING THE WIDTH. A number input reserves room for its
   step arrows even when they are only drawn on hover, and that reservation is
   what clipped "10740.05". These fields are typed, not stepped — a stop is not
   a value you nudge one tick at a time. */
.tmodal .frow--stop input::-webkit-outer-spin-button,
.tmodal .frow--stop input::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}
.tmodal .frow--stop input { -moz-appearance: textfield; appearance: textfield; }
/* The reference price the percent is OF, on the label. Without it the percent
   is unreadable — 8% of what? */
.stoppair__ref { color: var(--fg-2); font-size: 10.5px; margin-left: 6px;
                 font-family: var(--mono); }
/* The distance from the LIVE price, which is not the field's percent and is the
   figure a trailing stop is judged by — or, when the stop is invalid, the one
   sentence saying why. Right-aligned under the control it describes, and it
   RESERVES its line: a message that appears and disappears would otherwise
   shift every row beneath it as you type. */
.stoppair__note { color: var(--fg-2); font-size: 10.5px; text-align: right;
                  margin: -2px 0 8px; min-height: 15px; line-height: 1.35; }
.stoppair__warn { color: var(--fail); }

.tmodal .sizes { display: flex; gap: 6px; }
.tmodal .sizes .chipbtn.is-on { border-color: var(--accent); color: var(--accent); }
.tmodal .preview { border-top: 1px solid var(--border); margin-top: 10px; padding-top: 8px; color: var(--fg-2); font-size: 12px; }
.tmodal .preview .frow { margin: 3px 0; }

/* A working-out row whose right-hand side needs two lines.
   
   The stop-distance line read
   "₹74.55 − ₹68.55 = ₹6.00 (8.05%, widened from 8.00% by tick rounding)"
   on one line, which pushed the dialog wide and buried the number that
   matters — ₹6.00 — in the middle of a sentence. The arithmetic keeps the
   first line; the caveat drops beneath it, dimmer, where it reads as a
   footnote rather than part of the sum. */
.tmodal .preview .frow--calc { align-items: flex-start; }
/* The label keeps its own line. "Risk on this trade" wrapped to two lines the
   moment the working beside it got longer, which made the row that matters most
   look like the untidiest thing on the dialog. */
.tmodal .preview .frow--calc > span:first-child { flex: none; white-space: nowrap; }
.tmodal .preview .frow--calc > span:last-child { text-align: right; min-width: 0; }
.tmodal .preview .calcnote {
  display: block; font-size: 11px; color: var(--fg-2);
  margin-top: 2px; white-space: normal; line-height: 1.35;
}
/* Pinned to the bottom of the scroll box — see `.tmodal`. A Confirm button
   that scrolls out of reach is a dialog with no way to say yes. */
.tmodal .actions { display: flex; gap: 8px; justify-content: flex-end;
  margin-top: 12px; position: sticky; bottom: -16px; z-index: 2;
  background: var(--bg-1); padding: 10px 0 16px; border-top: 1px solid var(--border); }
.tmodal .err { color: var(--fail); font-size: 12px; margin-top: 8px; min-height: 15px; }
/* ⚠ `.toasts`, `.toast`'s SECOND declaration, `.toast--error` and `.toast__x`
   stood here — the styling for `trade.js`'s own toast box. That implementation
   is gone (it now delegates to `M.toast`), and the surviving rules moved up
   beside `.toast-host` so there is one block rather than two that silently
   overrode each other. `.toast--error` is not carried across: `M.toast`
   normalises both spellings of the kind to the `--err` class. */
.alert-item { padding: 8px 12px; border-top: 1px solid var(--border); border-left: 3px solid var(--fg-2); cursor: pointer; font-size: 12px; }
.alert-item--warning { border-left-color: var(--warn); }
.alert-item--critical { border-left-color: var(--fail); }
.alert-item .when { color: var(--fg-2); font-size: 10px; margin-left: 6px; }
.split { display: grid; grid-template-columns: 1fr 300px; gap: 14px; align-items: start; }
@media (max-width: 1000px) { .split { grid-template-columns: 1fr; } }
/* ------------------------------------------------------------- settings */
/* Grouped rows rather than a bare key/value grid: a threshold on a trading
   system needs its unit and its consequence next to it, not just a number. */
.setgroup + .setgroup { margin-top: 6px; }
.setgroup__head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding: 13px 16px 9px; border-top: 1px solid var(--border); }
.setgroup:first-child .setgroup__head { border-top: none; }
.setgroup__title { font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.7px; color: var(--fg-1); }
.setgroup__note { font-size: 11.5px; color: var(--fg-2); }

.setrow { display: grid; grid-template-columns: minmax(0, 1fr) 200px 190px;
  gap: 16px; align-items: center; padding: 9px 16px; border-left: 2px solid transparent; }
.setrow + .setrow { border-top: 1px solid var(--border); }
.setrow:hover { background: var(--bg-2); }
.setrow.is-dirty { border-left-color: var(--accent); background: rgba(91,156,248,0.06); }
.setrow__label { font-size: 12.5px; font-weight: 600; }
.setrow__help { font-size: 11.5px; color: var(--fg-2); margin-top: 2px; line-height: 1.45; }
.setrow__state { font-size: 11px; color: var(--fg-2); }
.setrow.is-dirty .setrow__state::before { content: 'unsaved'; color: var(--accent); font-weight: 600; }
.setrow.is-dirty .rowbadge { display: none; }

.inputwrap { display: flex; align-items: center; gap: 0;
  background: var(--bg-0); border: 1px solid var(--border-hi); border-radius: var(--r);
  padding: 0 9px; }
.inputwrap:focus-within { border-color: var(--accent); }
.setinput { font: 13px var(--mono); font-variant-numeric: tabular-nums;
  background: none; border: none; color: var(--fg-0); padding: 7px 0; width: 100%;
  text-align: right; outline: none; }
.setinput::-webkit-outer-spin-button, .setinput::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0; }
.unit { font-size: 11.5px; color: var(--fg-2); flex: none; padding-left: 7px; }
.unit--pre { padding: 0 7px 0 0; }

.linkbtn { background: none; border: none; padding: 0 0 0 6px; cursor: pointer;
  color: var(--accent); font: 11px var(--sans); text-decoration: underline; }
.savehint { font-size: 11.5px; color: var(--fg-2); }
.muted { color: var(--fg-2); }

/* ---------------------------------------------- dashboard (momentum-style) */
.dash { display: flex; flex-direction: column; gap: 14px; padding: 14px; }
.dash__statbar { display: flex; gap: 30px; flex-wrap: wrap; align-items: baseline; padding: 12px 16px; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg-1); }
/* The rail holds stat cards and alert lines, not tables — 260 fits them and
   hands 30px back to the column that has columns to fit. */
.dash__grid { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 14px; align-items: start; }
@media (max-width: 1100px) { .dash__grid { grid-template-columns: 1fr; } }
.dash__col { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.scanbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.scanbar select { font: 12px var(--sans); background: var(--bg-0); color: var(--fg-0); border: 1px solid var(--border-hi); border-radius: 6px; padding: 5px 8px; max-width: 190px; }
.loglist { min-height: 0; }
.logrow { display: flex; gap: 8px; padding: 6px 12px; border-top: 1px solid var(--border); font-size: 11px; align-items: baseline; }
.logrow .ev { font-family: var(--mono); color: var(--fg-1); min-width: 92px; }
.logrow .sym { font-weight: 600; }
.logrow .when { color: var(--fg-2); margin-left: auto; white-space: nowrap; }
.abtn2 { font: 11px var(--sans); padding: 2px 9px; border-radius: 6px; cursor: pointer; }
.abtn2--ok { border: 1px solid var(--pass); background: var(--pass-bg); color: var(--pass); }
.abtn2--no { border: 1px solid var(--border-hi); background: var(--bg-2); color: var(--fg-1); }
.abtn2.is-on { outline: 1px solid currentColor; }

/* Dashboard hosts the real scanner panel below the JS-rendered panels. The
   page scrolls as a whole; the embedded scanner keeps a fixed height so its
   own table scroll engages like on /scanner. */
.dash-scroll { flex: 1; min-height: 0; overflow-y: auto; }
.dash-scroll .dash { padding-bottom: 0; }
/* Both columns split their height the SAME way: two panels, equal flex, equal
   gap. Because the grid stretches both columns to one height, that makes the
   boundaries line up — Alerts ends where Portfolio ends, Trade Log where
   Watchlist does. Sizing them by content instead left a short Alerts box
   beside a tall ledger.

   Each panel then hands its overflow to the scroller inside it; min-height:0
   is what lets a flex child shrink below its content so that engages at all. */
.dash__grid { align-items: stretch; }
/* The grid takes whatever height is left after the stat bar, and the two
   columns stretch into it — so Watchlist and Trade Log end at the bottom of the
   window rather than at an arbitrary 72vh with dead space beneath.

   `min-height: 100%` on `.dash` is what gives the grid something to fill: the
   scroller's height is definite, so the percentage resolves. Content taller
   than the window still scrolls, because 100% is a FLOOR. */
.dash-scroll > .dash { min-height: 100%; }
.dash__grid { flex: 1 1 auto; }
#dash-left, #dash-right {
  display: flex; flex-direction: column; gap: 14px;
  /* A FLOOR, not the height. `.dash__grid { flex: 1 }` above makes the grid
     take whatever the window leaves, so on a normal screen the panels stretch
     to the bottom; this only stops them collapsing on a short one. Dropping it
     to a pixel value made every panel SHORTER on a narrow viewport, where the
     grid stacks and the two columns become two rows. */
  min-height: 72vh;
}
#dash-left > .tpanel, #dash-right > .tpanel {
  display: flex; flex-direction: column; flex: 1 1 0; min-height: 150px;
}
/* ⚠ **THE TWO ROWS ARE EQUAL — 50/50, from the `flex: 1 1 0` above.** They were
   0.85/1.15, on the argument that the top row is a summary of short lists and
   the bottom row is the working surface. That was wrong once the portfolio grew
   its own summary strip and a 20-column picker: the top panel spends a header,
   a strip and a column header row before its first position, so 42% of the
   window left it showing four rows of a book that has more, while the watchlist
   underneath had room it was not using. Equal halves give the portfolio the
   rows it needs and cost the watchlist about one.

   THE RATIOS MUST MATCH ACROSS THE TWO COLUMNS, whatever they are. That is what
   keeps Alerts ending where Portfolio ends and Trade Log where Watchlist does;
   the grid stretches both columns to one height, so equal ratios line the
   boundaries up and unequal ones stagger them. Nothing here overrides the base
   rule for one column only. */
#dash-left > .tpanel > .tablewrap--auto,
#dash-right > .tpanel > .scrolly {
  flex: 1 1 auto; max-height: none; min-height: 0; overflow-y: auto;
}

/* ---- chart (drawer) ------------------------------------------------------
   Height is fixed rather than aspect-based: Lightweight Charts measures its
   host when it is created, and a host still collapsing to its content renders
   a zero-height canvas that never recovers. */
.chart {
  position: relative;
  min-height: 240px;
  margin: 0 0 16px;
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: 8px;
  background: var(--bg-1);
}
.chart__credit {
  position: absolute;
  right: 12px;
  bottom: 8px;
  font-size: 10px;
  color: var(--fg-2);
  letter-spacing: .02em;
}
.chart__credit a { color: var(--fg-2); text-decoration: underline; }
.chart__credit a:hover { color: var(--fg-0); }

/* ---- hover panel ---------------------------------------------------------
   DOCKED, not anchored to the pointer, and that is a measurement rather than a
   taste. At this size, against all 26 symbol cells on the dashboard at
   1280×720: fits horizontally beside 26, vertically below NONE — and 610px
   does not fit above a table row either, so flipping does not rescue it. There
   is no offset at which a pointer-anchored panel this large lands on screen.
   `place()` in hovercard.js docks it opposite the pointer, vertically centred.

   `fixed`, so scrolling the table underneath a pinned panel is just reading —
   which is what retired the old hide-on-scroll rule. That rule existed because
   the card was anchored to a row, and scrolling made the anchoring a lie.

   Height is capped against the viewport rather than fixed: on a short window a
   610px panel would hang off both ends, and the chart host is a flex child so
   it gives the space back instead of overflowing. */
.hovercard {
  position: fixed;
  z-index: 60;
  width: 740px;
  height: 610px;
  max-width: calc(100vw - 24px);
  max-height: calc(100vh - 24px);
  display: none;
  flex-direction: column;
  padding: 14px 16px 12px;
  border: 1px solid var(--border-hi);
  border-radius: var(--r-lg);
  background: var(--bg-2);
  box-shadow: 0 24px 64px rgba(0,0,0,.6);
}
.hovercard.is-open { display: flex; }
/* The grip. `resize` needs `overflow` to be anything but visible, and the
   floor stops a stray drag collapsing the panel to a sliver you then cannot
   grab. Width follows on its own through the chart's ResizeObserver; height is
   pushed by hovercard.js, because a chart's height IS its host's height and
   reading it back would be circular. */
.hovercard { resize: both; overflow: hidden; min-width: 420px; min-height: 320px; }
/* Pinned is a MODE, and a mode with no visible difference is a mode users do
   not believe in. The accent border is the whole tell. */
.hovercard.is-pinned { border-color: var(--accent); }

.hovercard__head { display: flex; align-items: baseline; gap: 8px; }
.hovercard__spacer { flex: 1; }
.hovercard__sym { font: 600 15px var(--mono); color: var(--fg-0); letter-spacing: .02em; }
.hovercard__ex { font-size: 10px; color: var(--fg-2); }
.hovercard__btn {
  padding: 4px 10px; font: 500 10px var(--mono);
  color: var(--fg-1); background: var(--bg-1);
  border: 1px solid var(--border-hi); border-radius: var(--r);
  cursor: pointer; align-self: center;
  /* `--tv` is an <a>, so it needs both of these; the others are buttons and
     are unaffected by them. One rule, three controls that look alike. */
  text-decoration: none; display: inline-block; white-space: nowrap;
}
.hovercard__btn:hover { color: var(--fg-0); border-color: var(--accent); }
.hovercard__btn--close { padding: 4px 8px; }
/* The one control that leaves the app. Tinted so "this goes somewhere else" is
   visible before you read it — it sits beside two controls that do not. */
.hovercard__btn--tv { color: var(--accent); border-color: var(--border-hi); }
.hovercard__btn--tv:hover { color: var(--accent); border-color: var(--accent); }

/* THE BODY IS A StockView HOST, which is why it also carries `.stock` — the
   component styles its own markup and the host is what its rules hang off, the
   same way `#dr-price` is `.stock .stock--indrawer` in the drawer.

   `min-height: 0` because a flex child otherwise refuses to shrink below its
   content, which pushes the stats grid out of the panel. `overflow-y: auto` is
   the forgiving half: if a short viewport or a narrower grid makes the content
   taller than the space, it scrolls rather than clipping. */
/* `overflow-x: hidden` is a BACKSTOP, not the fix. What actually produced a
   horizontal bar here was `follow()` sizing the canvas to `clientWidth`, which
   includes `.chart`'s 8px padding — corrected in charts.js. This stays because
   a chart is meant to BE the width of its container: if one ever measures long
   again, clipping it is right and scrolling it sideways is not. Vertical stays
   `auto` as the safety valve for a viewport short enough to cap the panel. */
.hovercard__body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; }

/* ⚠ `.stock--inpanel` lives DOWN BESIDE `.stock--indrawer`, not here. Both are
   one class, so they tie with `.stock` on specificity and source order decides
   — and `.stock` is declared after this block. Written here, its `margin: 0
   auto` and `padding: 20px 0 48px` both won: the body rendered 445px wide
   inside a 706px panel, centred by 130px of auto margin, with 68px of vertical
   padding pushing the stats grid out of view. */

.hovercard__foot {
  margin-top: 10px; padding-top: 7px; border-top: 1px solid var(--border);
  font-size: 10px; color: var(--fg-2);
}
.td-sym[data-isin] { cursor: default; }

/* ---- one instrument, rendered into a host ---------------------------------
   `.stock` is StockView's own wrapper. It is mounted in exactly two places now
   — the drawer and the hover panel — and both override the page-shaped defaults
   below via `.stock--indrawer` / `.stock--inpanel`, which sit further down so
   they win on source order. The defaults are kept rather than folded in because
   a third host would otherwise inherit nothing at all. */
.stock { max-width: 1100px; margin: 0 auto; padding: 20px 0 48px; }

/* ---- instrument facts, at the foot of the drawer --------------------------
   Venues, index membership and corporate actions. These were the detail page's
   rail until that page was deleted (2026-08-15); the classes were never scoped
   to it, so the markup moved into `stockdrawer.js` without the stylesheet
   changing. `.drawer__facts` is the only new rule the move needed. */
.drawer__facts { margin-top: 20px; padding-top: 4px; border-top: 1px solid var(--border); }

.rail__sec {
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: var(--r); padding: 13px 15px 15px;
}
.rail__title {
  display: flex; align-items: baseline; gap: 8px; margin: 0 0 10px;
  font: 600 11px var(--mono); letter-spacing: .09em; text-transform: uppercase;
  color: var(--fg-2);
}
.rail__note { margin-left: auto; font-weight: 400; letter-spacing: 0; text-transform: none; }
.rail__verdict { display: flex; align-items: center; gap: 9px; margin-bottom: 11px; }
.rail__when { font: 400 11px var(--mono); color: var(--fg-2); }
.rail__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 14px; margin: 7px 0 0; }
.rail__row { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }
.rail__row dt { color: var(--fg-2); margin: 0; }
.rail__row dd { color: var(--fg-0); margin: 0; font-family: var(--mono); }

/* 3 across is right in a 620px drawer; the 380px rail fits 2. */
.metrics--rail { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 0; }
.sectitle__count { margin-left: 8px; font-weight: 400; color: var(--fg-2); }

.venues { display: flex; flex-direction: column; gap: 9px; }
.venue { border: 1px solid var(--border); border-radius: var(--r); padding: 9px 11px; }
.venue.is-here { border-color: var(--accent); background: rgba(41,98,255,.06); }
.venue__head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.venue__ex { font: 600 11px var(--mono); color: var(--fg-2); }
.venue__sym { font: 600 14px var(--mono); color: var(--fg-0); }
.venue__series { font: 400 10px var(--mono); color: var(--fg-2); border: 1px solid var(--border); border-radius: 3px; padding: 0 4px; }
.venue__here { margin-left: auto; font: 400 10px var(--mono); color: var(--accent); }
.venue__flags { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 7px; }
.chip { font: 500 10px var(--mono); border-radius: 3px; padding: 1px 6px; border: 1px solid transparent; }
.chip--on { background: var(--pass-bg); color: #6fdc8c; border-color: #1f6b3a; }
.chip--off { background: var(--bg-2); color: var(--fg-2); border-color: var(--border); }
/* A pledge or an encumbrance is the one ownership figure that changes a buy
   decision, so it reads as a caution rather than as another grey row. */
.chip--warn { background: var(--fail-bg, #3a1f1f); color: #f0a868; border-color: #6b4a1f; }

/* A figure we do not hold, said out loud. Absent is NOT zero and NOT an empty
   cell: a 0 would be a lie, and a blank reads as a rendering fault. Coverage is
   permanently partial (ADR-0044), so this is a normal state, not an error. */
/* ---- who owns it, as a composition ---------------------------------------
   ⚠ **THE SEGMENTS ARE ONE LEVEL OF A TWO-LEVEL SPLIT.** Promoter and public
   are the whole of it; FII, DII and retail are subsets of PUBLIC with a real
   unattributed remainder; MF and insurance are subsets of DII and are
   therefore TEXT on the DII key rather than segments of their own — a bar
   containing all of them would sum past 100. `ownershipBar` refuses to draw
   at all where the filed figures disagree, which is why these colours never
   describe a composition that does not add up.

   Promoter takes the accent because it is the one segment the reader is
   looking for; the remainder is `--fg-2` at low opacity because it is an
   absence of attribution rather than a holder. */
.ownbar {
  display: flex; height: 10px; margin: 4px 0 9px;
  border-radius: 3px; overflow: hidden; background: var(--bg-3);
}
.ownbar__seg { display: block; min-width: 0; }
.ownbar__seg--prom   { background: var(--accent); }
.ownbar__seg--fii    { background: var(--info); }
.ownbar__seg--dii    { background: var(--pass); }
.ownbar__seg--retail { background: var(--warn); }
.ownbar__seg--rest   { background: var(--bg-3); box-shadow: inset 0 0 0 1px var(--border-hi); }

.ownkey { display: flex; flex-wrap: wrap; gap: 3px 14px; margin-bottom: 4px; }
.ownkey__item {
  display: inline-flex; align-items: baseline; gap: 5px;
  font-size: 11.5px; color: var(--fg-1);
}
.ownkey__item b { font-family: var(--mono); font-weight: 600; color: var(--fg-0); }
.ownkey__dot {
  width: 7px; height: 7px; border-radius: 2px; flex: none;
  align-self: center;
}
.ownkey__dot--prom   { background: var(--accent); }
.ownkey__dot--fii    { background: var(--info); }
.ownkey__dot--dii    { background: var(--pass); }
.ownkey__dot--retail { background: var(--warn); }
.ownkey__dot--rest   { background: var(--bg-3); box-shadow: inset 0 0 0 1px var(--border-hi); }
/* MF and insurance sit INSIDE DII, so they read as a qualification of that
   key rather than as two more holders. */
.ownkey__sub { color: var(--fg-2); font-family: var(--mono); font-size: 10.5px; }

.rail__none { color: var(--fg-2); font-style: italic; font-family: var(--sans, inherit); }

/* The as-of line under every fundamentals block. Not decoration — the whole
   join is on the date the market LEARNED a figure, and a number without its
   date invites a stale reading. */
.rail__asof {
  margin-top: 6px; font-size: 10px; color: var(--fg-2);
  letter-spacing: .02em;
}

.rail__empty { font-size: 12px; color: var(--fg-2); font-style: italic; }

.ixwrap { display: flex; flex-wrap: wrap; gap: 5px; }
.ixchip {
  font: 500 10px var(--mono); background: var(--bg-2); color: var(--fg-1);
  border: 1px solid var(--border); border-radius: 3px; padding: 2px 6px; cursor: help;
}
.ixmore { width: 100%; margin-top: 7px; }
.ixmore summary { font: 400 11px var(--mono); color: var(--fg-2); cursor: pointer; }
.ixmore .ixwrap { margin-top: 7px; }

.ca { width: 100%; border-collapse: collapse; font-size: 12px; }
.ca td { padding: 3px 0; border-bottom: 1px solid var(--border); }
.ca__date { font-family: var(--mono); color: var(--fg-1); }
.ca__type { color: var(--fg-2); }
.ca__factor { font-family: var(--mono); text-align: right; color: var(--fg-0); }
.warn { color: #ffb454; font-size: 12px; }

/* ---- indicator panes ----------------------------------------------------
   One strip per clause input, under the price. The HEADER is the part that
   answers the question — the number, the gate it is tested against, and which
   clause reads it. The strip only shows how long it has been that way. */
.studies { margin: 18px 0 6px; display: flex; flex-direction: column; gap: 10px; }
.studies__head { display: flex; align-items: baseline; gap: 9px; font-size: 11px; color: var(--fg-2); }
/* The way back. Hiding the strips leaves this line in place, because the
   preference persists — collapsing to nothing would make "nothing" permanent. */
.studies__toggle {
  font: 500 11px var(--mono); background: var(--bg-2); color: var(--fg-1);
  border: 1px solid var(--border); border-radius: var(--r);
  padding: 3px 9px; cursor: pointer; flex: none;
}
.studies__toggle:hover { border-color: var(--accent); color: var(--fg-0); }

/* ---- the indicator picker ------------------------------------------------
   Which lines exist at all, overlays and panes alike. Distinct from the Hide
   toggle: that collapses the strips section, this decides what is drawn — and
   the moving averages live on the price chart, where there is no strip to
   collapse.

   `margin-left: auto` puts it at the far end of the range row, which is the
   chart's top corner, and outside the chart host so a repaint cannot wipe it. */
.studypick { position: relative; margin-left: auto; }
.studypick__btn {
  font: 500 11px var(--mono); background: var(--bg-2); color: var(--fg-1);
  border: 1px solid var(--border); border-radius: var(--r);
  padding: 4px 10px; cursor: pointer;
}
.studypick__btn:hover { border-color: var(--accent); color: var(--fg-0); }
.studypick__menu {
  position: absolute; right: 0; top: calc(100% + 5px); z-index: 30;
  min-width: 210px; padding: 6px;
  background: var(--bg-1); border: 1px solid var(--border-hi);
  border-radius: var(--r); box-shadow: 0 10px 28px rgba(0,0,0,.45);
}
.studypick__group {
  font: 600 10px var(--mono); letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-2); padding: 7px 8px 4px;
}
.studypick__row {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px; border-radius: 4px; cursor: pointer;
  font: 400 12px var(--mono); color: var(--fg-1);
}
.studypick__row:hover { background: var(--bg-2); color: var(--fg-0); }
.studypick__row input { margin: 0; cursor: pointer; accent-color: var(--accent); }
.studypick__foot {
  display: flex; gap: 5px; padding: 7px 6px 3px; margin-top: 5px;
  border-top: 1px solid var(--border);
}
.studypick__foot button {
  flex: 1; font: 500 10px var(--mono); background: var(--bg-2); color: var(--fg-2);
  border: 1px solid var(--border); border-radius: 4px; padding: 4px 0; cursor: pointer;
}
.studypick__foot button:hover { border-color: var(--accent); color: var(--fg-0); }
.study { border: 1px solid var(--border); border-radius: var(--r); background: var(--bg-1); }
.study--fail { border-color: #7a2b28; }
.study--pass { border-color: var(--border); }
/* Neutral on purpose: this study is one side of an `or`, so the gate failing
   does NOT mean the clause failed. Red here contradicted the verdict badge. */
.study--part { border-color: var(--border); }
.study__head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 7px 11px; border-bottom: 1px solid var(--border);
}
.study__mark { font: 600 12px var(--mono); }
.study--pass .study__mark { color: #6fdc8c; }
.study--fail .study__mark { color: #ff8b84; }
.study--part .study__mark { color: #8b9dc3; }
.study__label { font: 600 12px var(--mono); color: var(--fg-0); }
.study__value { font: 600 13px var(--mono); color: var(--fg-0); }
.study--fail .study__value { color: #ff8b84; }
.study__today { font: 400 11px var(--mono); color: var(--fg-2); }
.study__gate { font: 400 11px var(--mono); color: var(--fg-2); }
/* Full width on its own line: a clause sentence is 80-140 characters and
   will not sit beside the value on a 1,166px chart, let alone a narrow one. */
.study__clause { flex-basis: 100%; font: 400 11px var(--sans, inherit); color: var(--fg-2); line-height: 1.4; }
.study__chart { padding: 2px 4px 4px; }
.study__none { padding: 9px 11px; font-size: 11px; color: var(--fg-2); }
/* Distinct from .warn: a demerger with no factor is a known limit of the
   adjustment, not a broken row. Same colour for both would make the one
   that needs fixing indistinguishable from the one that does not. */
.note { color: var(--fg-2); font-size: 12px; }
.ca__factor .note, .ca__factor .muted { font-size: 11px; }
.stock__head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; }
/* In the drawer there is no symbol beside it — the drawer's own head has that —
   so the price sits left rather than being pushed to the right of nothing. */
.stock__head--priceonly { justify-content: flex-start; margin-bottom: 8px; }
.stock__head--priceonly .stock__price { text-align: left; }
/* A STACK, not a row. `.stock__last` and `.stock__asof` are `display: block`,
   so making this element a flex row lays the headline out horizontally — which
   is what it briefly was, while the TradingView link sat inside it. The link is
   a sibling in `.stock__head` now, so this is three text lines and nothing
   else, and the flex row that positions the link is the HEADER around it. */
.stock__price { text-align: right; }
.stock__last { display: block; font: 600 26px var(--mono); color: var(--fg-0); }
.stock__chg { font: 500 13px var(--mono); }
.stock__chg.is-up { color: var(--pass); }
.stock__chg.is-dn { color: var(--fail); }

/* AS OF WHEN — the line that stops a stored close being read as a live price.
   Its own row, so it cannot be mistaken for part of the change figure. */
.stock__asof {
  display: block; margin-top: 3px;
  font: 500 11px var(--mono); color: var(--fg-2); letter-spacing: .02em;
}
.stock__asof.is-live { color: var(--pass); }
.stock__asof .livedot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--pass); margin-right: 5px; vertical-align: middle;
  animation: feedpulse 2s ease-in-out infinite;
}

/* ⚠ **IT WRAPS, because the row outgrew the drawer.** Six ranges, three zoom
   buttons and a WORD is 24px wider than the drawer's chart column at a 800px
   viewport — measured, not guessed — and the TradingView label was the half
   hanging over the edge. `margin-left: auto` on `.chartzoom` still pushes the
   controls right on one line when there is room, and carries them (with the
   link behind it) onto a second, right-aligned line when there is not. */
.ranges { display: flex; flex-wrap: wrap; gap: 4px; row-gap: 6px; margin: 18px 0 10px; }
.range {
  padding: 5px 14px; font: 500 12px var(--mono);
  color: var(--fg-1); background: var(--bg-2);
  border: 1px solid var(--border); border-radius: var(--r); cursor: pointer;
}
.range:hover { border-color: var(--border-hi); color: var(--fg-0); }
.range.is-on { background: var(--accent); border-color: var(--accent); color: #08111f; }

.chart--full { min-height: 420px; }
/* The lean pane's chart (IX/FUT/OPT). Same `.chart` base as every other
   chart in the app — which is what anchors the OHLC legend — at the
   drawer's height rather than the full page's. */
.chart--indrawer { min-height: 280px; }

/* The OHLC legend, top-left INSIDE the chart. `.chart` is already
   `position: relative`, so this anchors to it without extra scaffolding.

   `pointer-events: none` is load-bearing: the legend sits over the plot area
   and would otherwise swallow the click and drag that pan the chart underneath
   it. `white-space: nowrap` because wrapping mid-number reads as two prices. */
.chart__ohlc {
  position: absolute; top: 6px; left: 10px; z-index: 3;
  pointer-events: none; white-space: nowrap;
  font: 500 11px var(--mono); letter-spacing: .01em;
}
.chart__ohlc b { font-weight: 400; color: var(--fg-2); margin-right: 2px; }
.chart__ohlc-v { margin-right: 8px; }
.chart__ohlc-v.is-up { color: var(--pass); }
.chart__ohlc-v.is-dn { color: var(--fail); }
.chart__ohlc-chg.is-up { color: var(--pass); }
.chart__ohlc-chg.is-dn { color: var(--fail); }

/* Zoom, beside the ranges. `margin-left: auto` pushes it to the far end of the
   row so the range buttons stay grouped at the left — they are the primary
   control and these are an adjustment to whatever they picked. */
/* The interval picker, beside the ranges. Reuses `.studypick`'s chrome so the
   two dropdowns in this row open and dismiss alike. */
.ivpick { position: relative; align-self: center; margin-left: 8px; }

/* DISABLED, NOT HIDDEN. A control that vanishes when the interval changes makes
   the row jump under the pointer and leaves no way to learn why the choice went
   away — so an unusable pair dims and keeps its tooltip. `pointer-events` stays
   ON, or the `title` explaining it would never show. */
.range.is-void, .studypick__row.is-void {
  opacity: .32; cursor: not-allowed;
}
.range.is-void:hover { border-color: var(--border); color: var(--fg-1); }
.studypick__row.is-void input { cursor: not-allowed; }

.chartzoom { display: inline-flex; gap: 4px; margin-left: auto; align-self: center; }
.chartzoom__btn {
  width: 26px; height: 26px; line-height: 1;
  font: 500 14px var(--mono); color: var(--fg-1); background: var(--bg-2);
  border: 1px solid var(--border); border-radius: var(--r); cursor: pointer;
}
.chartzoom__btn:hover { color: var(--fg-0); border-color: var(--border-hi); }

/* "Open on TradingView", at the TOP-RIGHT of the StockView header and nowhere
   else — one control, one call site (`StockView.shell`), on every surface that
   draws a chart. Tinted so the one control here that LEAVES the app is
   distinguishable from everything around it that does not.

   `.dd-tv` and `.hovercard__btn--tv` are kept: they were the drawer's and the
   panel's own copies, and a modifier that no longer renders is harmless while
   a modifier that renders as its base class is the `abtn--buy` defect. Removed
   when the next reader is sure nothing writes them. */
.dd-tv, .btn--tv { color: var(--accent); text-decoration: none; }
.dd-tv:hover, .btn--tv:hover { border-color: var(--accent); }
/* `margin-left: auto` against a `flex-start` header, so it goes to the far
   right whatever the price is — the same right edge the zoom group sits on.

   ⚠ `align-self: FLEX-END`, not flex-start. The price block is three lines
   tall, so pinning this to the top of it left the height of a headline, a
   change line and an as-of line between this button and the zoom buttons it
   pairs with — a gap the owner asked twice to close. At the END it sits on the
   as-of line, directly above them, and what remains is only the header's own
   8px and the ranges row's top margin. */
.stock__tv {
  display: inline-flex; align-items: center;
  margin-left: auto; align-self: flex-end; flex: none;
}
/* Matched to `.chartzoom__btn`'s 26px box — the two are read as a pair down the
   right edge, and a taller one would not line up with the ranges row.

   ⚠ `white-space: nowrap` IS LOAD-BEARING. It is the only worded control on
   this side, and in the drawer "TradingView ↗" broke after the word and
   rendered as a two-line button. */
.stock__tv .btn {
  height: 26px; display: inline-flex; align-items: center;
  padding: 0 9px; font-size: 11px; white-space: nowrap;
}


.oneday { padding: 28px 16px; text-align: center; }
.oneday__row { display: flex; justify-content: center; gap: 34px; margin-bottom: 12px; }
.oneday__row span { display: flex; flex-direction: column; gap: 3px; }
.oneday__row em { font-style: normal; font-size: 10px; color: var(--fg-2); }
.oneday__row span { font: 600 17px var(--mono); color: var(--fg-0); }

.stock__grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 8px 20px; margin: 18px 0 0;
}
.stock__grid div {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 7px 0; border-bottom: 1px solid var(--border);
}
.stock__grid dt { font-size: 11px; color: var(--fg-2); margin: 0; }
.stock__grid dd { font: 500 12px var(--mono); color: var(--fg-0); margin: 0; }
.stock__note { margin-top: 22px; font-size: 11px; color: var(--fg-2); line-height: 1.6; }

/* The expand affordance moved INTO the panel head (`.hovercard__btn`), beside
   ✕. It used to float over the chart's top-right corner, revealed on hover —
   which is now exactly where a chart's own controls belong, and where the
   TradingView link lands in phase 4. A control that appears on hover, over a
   canvas you are trying to read, is two problems rather than one. */

/* ---- the detail drawer ---------------------------------------------------
   One drawer, every table (stockdrawer.js). Wider than it used to be: it now
   carries a full candlestick chart with a range picker and a market-data grid,
   and at 420px the chart was unreadable and the stats grid collapsed to one
   column. */
.drawer { width: min(760px, 96vw); }
@media (min-width: 1500px) { .drawer { width: min(880px, 70vw); } }
.drawer__head .dd-full { text-decoration: none; margin-left: auto; }
.stock--indrawer { max-width: none; padding: 0; margin: 0 0 18px; }
.stock--indrawer .stock__note { margin-top: 14px; }
.stock--indrawer .ranges { margin-top: 0; }

/* The same component again, sized for the 740×610 hover panel rather than a
   page. It sits here rather than up in the hover-panel block because `.stock`
   is declared above and these tie with it on specificity — source order is the
   only thing separating them, which cost a debugging round: `margin: 0 auto`
   and `padding: 20px 0 48px` both won, rendering the body 445px wide inside a
   706px panel with the stats grid pushed out of sight.

   `.chart--full`'s 420px floor is a PAGE measurement. Inside the panel it is
   taller than the chart's whole allowance, so it has to be released — the real
   height comes from hovercard.js, derived from the panel. */
.stock--inpanel { max-width: none; margin: 0; padding: 0; }
.stock--inpanel .stock__head--priceonly { margin-bottom: 0; }
.stock--inpanel .stock__last { font-size: 22px; }
.stock--inpanel .ranges { margin: 10px 0 8px; }
.stock--inpanel .chart--full { min-height: 0; }
.stock--inpanel .stock__grid { margin: 10px 0 0; gap: 2px 18px; }
.stock--inpanel .stock__grid div { padding: 4px 0; }
/* The adjusted-prices note is four lines of prose. It earns its place on the
   page and in the drawer; in a preview it would cost the chart 60px to say
   something you can read one click away. */
.stock--inpanel .stock__note { display: none; }

/* ---- the instrument PAGE -------------------------------------------------
   ADR-0061. The third `StockView` host, and the only one that is a page.

   ⚠ **`.stock`'s OWN DEFAULTS HAVE TO BE RELEASED, and this is the third time
   that has been true.** `.stock { max-width: 1100px; margin: 0 auto; padding:
   20px 0 48px }` was written for the page deleted in 2026-08-15 and is still
   the base every host ties with on specificity — `.stock--indrawer` and
   `.stock--inpanel` both sit below it for that reason, and the panel's own
   comment records the debugging round it cost when it did not. `--inpage`
   goes here, after both, for the same reason.

   1,100px was generous when the drawer was 688; it is now a CEILING on the
   one thing this page exists to give the chart. The page's own container
   carries the width instead, so the cap is released rather than raised. */
.stock--inpage { max-width: none; margin: 0; padding: 0; }
.stock--inpage .ranges { margin-top: 0; }

/* ⚠ **1,600px, NOT `none`.** A chart is read across, and past about 1,600 the
   crosshair stops being usable — the eye cannot carry a value from the candle
   to an axis two feet away. This is a working width, not a claim on the
   monitor. ADR-0040 measured the deleted page's chart at 1,166px and the
   drawer's at 688; this clears both without becoming a stunt. */
.ipage { max-width: 1600px; margin: 0 auto; padding: 4px 0 40px; }

.ipage__head {
  display: flex; align-items: center; gap: 12px;
  padding: 0 0 13px; margin-bottom: 14px;
  border-bottom: 1px solid var(--border);
}
/* `min-width: 0` on the identity block, or a long contract ref refuses to
   shrink and pushes the action buttons off the right edge — the flex default
   that `.drawer__head > div:first-child` already carries for the same pane. */
.ipage__id { min-width: 0; flex: 1; }
.ipage__id .detail__sym { font-size: 26px; }
.ipage__id .detail__isin { margin-top: 3px; font-size: 12px; }
.ipage__head .drawer__acts { margin-left: auto; }

/* ⚠ **THE RAIL IS THE LAST COLUMN AND IT COLLAPSES FIRST.** `features/
   watchlist.md` rejected a rail on `/watchlist` because "200px off 1,350 is
   two columns" — true there, where a 14-column table needs every pixel, and
   not true here. Below 1,240px it goes under the chart rather than squeezing
   it, because the chart is what the page is for. */
.ipage__body { display: flex; flex-direction: column; gap: 22px; min-width: 0; }
/* ⚠ **`min-width: 0`, AND WITHOUT IT `flex: 0 0 260px` RENDERS AT 748.** A
   flex item's automatic minimum size is its MIN-CONTENT width, and these rows
   are `white-space: nowrap` — so "BAJAJFINSV 1980 CE 29Sep26" set the floor
   and the rail took half the page while its computed `flex-basis` still read
   260px, which is what makes this so hard to see. Measured: rail 748px,
   chart 785px, on a page whose whole purpose is the chart.
   It is the same rule `features/strategy-dsl.md` records for the builder's
   layout, one axis over: *"the rule that makes the layout work is the one
   that looks like it does nothing."* */
/* ⚠ **340px, UP FROM 260, AND THE OLD NUMBER WAS MEASURED AS TOO SMALL.** At
   260 the list strip needed **746px and got 258** — 8 lists, 2 visible, 65% of
   the navigation off-screen — and a row had room for a symbol and a price and
   nothing else. 340 leaves ~1,190px of chart, still 1.7x the drawer's 688.
   The number is not a compromise, because the rail FOLDS: 340 is right for
   working a list and wrong for reading a chart, and the reader picks. */
@media (min-width: 1240px) {
  .ipage__body { flex-direction: row; align-items: flex-start; }
  .ipage__rail { flex: 0 0 340px; min-width: 0; position: sticky; top: 4px; }
  .ipage__rail.is-collapsed { flex: 0 0 auto; }
}
.ipage__main { display: flex; flex-direction: column; min-width: 0; flex: 1; }

/* Folded: a spine you can read and press. Vertical text rather than an icon,
   because an icon at the edge of a page is a thing people do not find. */
.wlrail__peek {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 12px 7px; cursor: pointer;
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: var(--r-lg); color: var(--fg-2);
}
.wlrail__peek:hover { color: var(--fg-0); border-color: var(--border-hi); }
.wlrail__peektext {
  writing-mode: vertical-rl; font: 600 10.5px var(--sans, inherit);
  letter-spacing: .1em; text-transform: uppercase;
}
.wlrail__peekn { font: 500 10px var(--mono); color: var(--fg-1); }

.wlrail {
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--bg-1); overflow: hidden;
}
.wlrail__head {
  display: flex; align-items: center; gap: 8px;
  padding: 9px 8px 8px 12px; font: 600 11px var(--sans, inherit);
  letter-spacing: .06em; text-transform: uppercase; color: var(--fg-2);
  border-bottom: 1px solid var(--border);
}
/* The full table is one click away and the rail says so, rather than a reader
   wondering whether this IS the watchlist. */
.wlrail__all {
  margin-left: auto; font: 500 10px var(--mono); letter-spacing: 0;
  text-transform: none; color: var(--accent); text-decoration: none;
}
.wlrail__all:hover { text-decoration: underline; }
.wlrail__fold {
  flex: none; width: 20px; height: 20px; padding: 0; cursor: pointer;
  background: none; border: 0; color: var(--fg-2);
  font: 400 15px var(--mono); line-height: 1;
}
.wlrail__fold:hover { color: var(--fg-0); }

/* ⚠ **`flex: none` ON THE STRIP, or a short rail crushes it.** Measured on
   the watchlist at **11px against a natural 38**, with 20px tabs clipped
   through the middle — a flex item shrinks by default and the panel took the
   space out of navigation. The rows scroll; navigation does not give up its
   height.
   ⚠ **AND IT WRAPS HERE, WHICH IT DOES NOT ON THE PAGE.** A strip that shows
   2 of 8 does not show the lists exist either, which is the whole property
   `features/watchlist.md` chose a strip over a dropdown FOR. Its argument was
   made about 1,350px; at 340 the premise fails and the fix is room, not a
   different control. Three rows of pills, every list visible, one navigation. */
.wlrail .wltabs { flex: none; margin: 0; flex-wrap: wrap; overflow: visible; }
.wlrail .wltab { padding: 7px 9px; }

.wlrail__filter { padding: 8px 10px; border-bottom: 1px solid var(--border); }
.wlrail__filter .input { width: 100%; font-size: 11.5px; padding: 5px 8px; }

/* 56vh, not 46: the rail is the thing you work down, and eight more rows
   before a scroll is eight fewer scrolls. */
.wlrail__rows { display: flex; flex-direction: column; max-height: 56vh; overflow-y: auto; }

/* ⚠ **THE ROW IS A WRAPPER, NOT THE LINK.** It carries a link AND a button,
   and a `<button>` inside an `<a>` is invalid markup that browsers drop —
   which is how a remove control ends up navigating instead of removing. */
.wlrow {
  display: flex; align-items: center; min-width: 0;
  border-bottom: 1px solid var(--border);
}
.wlrow:last-child { border-bottom: 0; }
.wlrow:hover { background: var(--bg-2); }
/* The one on screen. An accent BAR rather than a background, so it reads as
   "you are here" instead of as a hover that will not clear. */
.wlrow.is-here { background: var(--bg-2); box-shadow: inset 2px 0 0 0 var(--accent); }

/* ⚠ **TWO LINES, BECAUSE ONE WAS MEASURED AT 95px FOR A NAME NEEDING 158.**
   The kind chip is 67px of a 340px rail and it keeps its full `<segment>
   <family>` label — no short form, the rule `instrument_kind_check` pins — so
   the row grows rather than the label shrinking. Name and price lead; the
   family and the move sit under them.
   `min-width: 0` on the link and on both lines, or the ellipsis on the symbol
   never engages and the cell simply grows instead. */
.wlrow__go {
  display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1;
  padding: 8px 4px 8px 12px; text-decoration: none; color: var(--fg-1);
}
.wlrow__top, .wlrow__bot {
  display: flex; align-items: baseline; gap: 8px; min-width: 0;
}
.wlrow:hover .wlrow__go, .wlrow.is-here .wlrow__go { color: var(--fg-0); }
/* The family, at a size that does not compete with the name. A mixed list is
   the norm here — 17 rows held stocks, futures and options — and `.ik` is the
   same chip the three tables use. */
.wlrow__kind { flex: 1; min-width: 0; }
.wlrow__kind .ik { font-size: 8.5px; padding: 1px 4px; }
.wlrow__sym {
  font: 600 12.5px var(--mono); flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wlrow__px { font: 500 12px var(--mono); color: var(--fg-0); flex: none; }
/* Fixed width, or seventeen rows of different-length percentages make a ragged
   right edge that reads as noise. */
.wlrow__chg { font: 400 10.5px var(--mono); flex: none; width: 7ch; text-align: right; }

/* Quiet until pointed at — the treatment the watchlist's own ✕ uses, and for
   the same reason: seventeen always-lit destructive glyphs down a column is
   noise, and the control is only wanted on the row you are on. */
.wlrow__act { flex: none; display: flex; align-items: center; padding-right: 8px; }
.wlrow__x, .wlrow__star {
  width: 22px; height: 22px; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 0; border-radius: var(--r);
  color: var(--fg-2); font: 400 12px var(--mono);
  opacity: 0; transition: opacity .12s ease-out;
}
.wlrow:hover .wlrow__x, .wlrow__x:focus-visible,
.wlrow:hover .wlrow__star, .wlrow__star:focus-visible { opacity: 1; }
/* The destructive colour arrives when you are actually on it. */
.wlrow__x:hover { color: var(--fail); background: var(--fail-bg); }
.wlrow__star { opacity: .55; color: var(--accent); }
.wlrow:hover .wlrow__star { opacity: 1; }
.wlrow__star .wicon { width: 13px; height: 13px; }

.rail-note { padding: 10px 12px; font-size: 11.5px; line-height: 1.5; margin: 0; }

/* ---- the section tabs ----------------------------------------------------
   ⚠ **THE STRIP GETS ITS OWN GROUND AND TWO EDGES.** `features/watchlist.md`
   records both, paid for in three separate reports on the watchlist's own
   strip: without a background of its own it read as part of whatever sat
   under it, and with only a `border-bottom` it ran into the block above. The
   idiom is the journal's underline tabs, which is what a reader here has
   already been taught.

   ⚠ **AND `flex: none`.** The watchlist's strip was measured CRUSHED to 11px
   against a natural 38 because a flex child shrinks by default and the panel
   ran short. Navigation does not give up its height; the section below it
   scrolls. */
.ipage__tabs {
  display: flex; align-items: stretch; gap: 2px; flex: none;
  margin: 18px 0 0; padding: 0 2px;
  background: var(--bg-0);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  overflow-x: auto; scrollbar-width: none;
}
.ipage__tabs::-webkit-scrollbar { display: none; }
.itab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 10px 15px; flex: none;
  font: 500 12px/1 var(--sans, inherit); letter-spacing: .01em;
  /* ⚠ `--fg-1`, NOT `--fg-2`. At #6b7484 an inactive tab was the EXACT value
     of the muted text beside it, which is what made the watchlist's strip
     read as a band of grey words rather than as controls. */
  color: var(--fg-1); background: none; border: 0;
  border-bottom: 2px solid transparent; cursor: pointer;
  white-space: nowrap;
}
.itab:hover { color: var(--fg-0); }
/* The folder-tab shape: square, flush, taking the section's own ground —
   deliberately not the rounded floating capsule that was rejected on the
   watchlist. */
.itab.is-active {
  color: var(--fg-0); background: var(--bg-1);
  border-bottom-color: var(--accent);
}
/* SMALLER AND DIMMER, because a same-size number runs into the label — the
   exact failure a list called "test 2" showing "test 2 1" produced. */
.itab__n { font: 400 10.5px var(--mono); color: var(--fg-2); }
.itab.is-active .itab__n { color: var(--fg-1); }

.ipage__section { padding: 18px 2px 0; min-width: 0; }
/* The facts sections were written for a 380px rail and then a 760px drawer.
   Given a page they lay out side by side rather than growing a venue card to
   1,600px — `auto-fill` rather than `auto-fit`, so two sections do not stretch
   to fill a row built for four.
   ⚠ **SCOPED TO THE TWO SECTIONS THAT WANT IT.** On `.ipage__section` alone
   it also caught Strategy, whose panel is ONE child — so the whole thing
   rendered in a single 320px track on a 1,600px page. A section names itself
   in a class because CSS cannot ask what is inside it. */
.ipage__section .rail__sec { break-inside: avoid; }
@media (min-width: 1100px) {
  .ipage__section--overview,
  .ipage__section--fundamentals {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 0 26px; align-items: start;
  }
}
.ipage__section .lean-facts { display: flex; flex-wrap: wrap; gap: 6px 22px; }

/* ---- Fundamentals, at page scale ----------------------------------------
   ⚠ **THE SECTIONS NAME THEMSELVES SO THE PAGE CAN WEIGHT THEM; THE DRAWER
   HAS NO RULE FOR ANY OF IT AND IS UNCHANGED.** One renderer, two layouts —
   which is the whole of ADR-0061, and the reason `section()` grew an optional
   modifier rather than the page growing a second `fundamentalsHtml`.

   "How big is this" is the first question a page you opened to STUDY a company
   answers, and in a 380px rail it was a 12px mono row sharing a column with a
   rank. Here it spans the row and reads as the number it is. */
.ipage__section--fundamentals .rail__sec--size { grid-column: 1 / -1; }
.ipage__section--fundamentals .rail__sec--size .rail__row {
  display: inline-flex; flex-direction: column; align-items: flex-start;
  gap: 1px; margin-right: 44px; vertical-align: top;
}
.ipage__section--fundamentals .rail__sec--size dd {
  font-size: 21px; font-weight: 600; color: var(--fg-0);
}
/* Ownership carries a composition bar and a five-item key; the other two are
   four rows each. Giving it two tracks stops the bar being squeezed to 320px
   while Trend sits half empty beside it. */
@media (min-width: 1400px) {
  .ipage__section--fundamentals .rail__sec--own { grid-column: span 2; }
}

/* ---- Derivatives, with the room a seven-column ladder wants -------------
   The futures strip runs across the page instead of wrapping in a 760px
   column, and the chain sits beside it above 1,400px — which is the layout
   the drawer's own `⚡ F&O chain` button exists to compensate for, since in a
   scroll the ladder begins 4,034px down.

   ⚠ **THE LADDER KEEPS A `max-width`.** A seven-column table stretched to
   1,550px puts a premium and its ΔOI two feet apart, and the whole point of
   a chain is reading ACROSS a strike. Wider is not better past the point the
   eye can carry a row. */
.ipage__section--fno .rail__sec { margin: 0; }
.ipage__section--fno .fno-strip { display: flex; flex-wrap: wrap; gap: 8px; }
.ipage__section--fno .chain__t { max-width: 980px; }
@media (min-width: 1400px) {
  .ipage__section--fno .rail__sec > :first-child { grid-column: 1 / -1; }
}

/* ---- the market-data grid, once it moved into Overview -------------------
   ⚠ **IT SPANS EVERY COLUMN.** The Overview section is an auto-fill grid of
   ~320px tracks, so without this the thirteen figures land in ONE of them and
   wrap to six rows beside two half-empty neighbours. It is a full-width band
   over the cards, which is also how it reads: the numbers you came for, then
   the reference you consult. */
.ipage__stats { grid-column: 1 / -1; min-width: 0; }
.ipage__stats .stock__grid { margin: 0 0 6px; }
/* The caveat travels with the grid (see `PRICE_NOTE`) and is prose, not data —
   it does not want the grid's mono column widths. */
.ipage__stats .stock__note { margin: 0 0 18px; }

/* ---- Strategy, at a width the drawer never had --------------------------
   The strip, then the five stored studies across the page, then the clause
   list in TWO columns. That last one is the thing a wider drawer could not
   buy: eleven clauses in a 760px column is a scroll, and beside their gate
   lines it is a table you read. */
.istrategy .strategypick { margin-top: 0; }
/* ⚠ **A GRID, NOT `columns`.** `.clauses` is a flex container whose 1px `gap`
   over a `--border` background IS the hairline between rows — multicol does
   not apply to a flex container at all, so `columns: 2` would silently do
   nothing. A two-track grid keeps the same gap trick and the same rule. */
@media (min-width: 1100px) {
  .istrategy .clauses {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ---- the door to an instrument's own page --------------------------------
   ADR-0061 phase 2. ONE control, two scales — `M.instrumentLink` — which is
   the shape `M.instrumentActions` already uses for Buy · Sell · ★. The head
   variant also carries `.dd-full`, whose rule up in the drawer block predates
   this control by weeks and had zero usages until now. */
.ilink { display: inline-flex; align-items: center; gap: 6px; }
.ilink__i { width: 13px; height: 13px; flex: none; }

/* ---- the head variant: an icon, and nothing else -------------------------
   ⚠ **NO WORD ON IT.** `Full page` beside Close was heavier than the act
   deserves and competed with Buy and Sell, which are the DECISIONS on that
   header. Asked for directly. `margin-left: auto` moves here from the old
   `.drawer__head .dd-full` rule, which the label carried. */
.ilink--head {
  position: relative; margin-left: auto; flex: none;
  width: 28px; height: 28px; justify-content: center;
  color: var(--fg-2); text-decoration: none;
  border: 1px solid var(--border); border-radius: var(--r);
}
.ilink--head:hover, .ilink--head:focus-visible {
  color: var(--fg-0); border-color: var(--border-hi); background: var(--bg-2);
}
.ilink--head .ilink__i { width: 15px; height: 15px; }

/* ⚠ **AN ELEMENT, NOT `title=`, BECAUSE IT CARRIES THE ICON.** A native
   tooltip is text and cannot hold an SVG — and an icon-only control whose
   meaning lives in a string the icon never appears in teaches nothing the
   second time you meet it. Repeating the glyph beside the words is what makes
   the two learnable together. Asked for.
   ⚠ **ANCHORED RIGHT.** The drawer is `overflow-x: hidden`, so a tip growing
   rightward from a control at the header's right edge is clipped in half. */
.ilink__tip {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 60;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 9px; white-space: nowrap; pointer-events: none;
  font: 500 11px var(--sans, inherit); color: var(--fg-0);
  background: var(--bg-3); border: 1px solid var(--border-hi);
  border-radius: var(--r); box-shadow: 0 4px 14px rgba(0,0,0,.4);
  opacity: 0; transform: translateY(-3px);
  transition: opacity .12s ease-out, transform .12s ease-out;
}
.ilink--head:hover .ilink__tip,
.ilink--head:focus-visible .ilink__tip { opacity: 1; transform: none; }
.ilink__tipi { width: 13px; height: 13px; flex: none; color: var(--accent); }

/* ⚠ **QUIET UNTIL POINTED AT.** A hundred identical icons down a column is
   noise, and this control is only wanted on the row you are actually on —
   the reasoning the watchlist's ✕ already records for the same shape.
   `:focus-visible` reveals it too, or it is a control no keyboard can find. */
/* ⚠ **THE HIT AREA IS 20px; THE GLYPH IS 12.** Padding plus a negative margin
   grows the target without moving the icon or the symbol beside it — a 12px
   square is a tap target you miss, and this one sits inches from a link that
   does something else (opens the drawer). Measured: the first attempt to click
   it in the browser landed on nothing. */
.ilink--row {
  margin: -4px 0 -4px 3px; padding: 4px; vertical-align: middle;
  color: var(--fg-2); text-decoration: none;
  opacity: 0; transition: opacity .12s ease-out;
}
tr:hover .ilink--row,
.ilink--row:focus-visible { opacity: 1; }
.ilink--row:hover { color: var(--accent); }
.ilink--row .ilink__i { width: 12px; height: 12px; }

.ipage__empty { padding: 60px 20px; max-width: 640px; margin: 0 auto; }
.ipage__empty h2 { margin: 0 0 10px; font-size: 18px; }
.ipage__empty code {
  font: 400 12px var(--mono); background: var(--bg-2);
  border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px;
}

/* ---- feed badge ----------------------------------------------------------
   Lives in the nav on every page: "is this live?" is not a question that
   should require navigating somewhere to answer. */
.feedbadge {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 11px; margin-right: 10px;
  font: 500 11px/1 var(--mono);
  color: var(--fg-1); background: var(--bg-2);
  border: 1px solid var(--border); border-radius: 999px;
  cursor: pointer; letter-spacing: .02em;
  /* One line, always. The label grew from "live" to "market closed · last
     price 07 Aug, 15:59" — enough words to be unambiguous — and a pill that
     wraps turns into a four-line blob overlapping the nav. It shrinks by
     ellipsis instead; the full text is in the title. */
  white-space: nowrap; flex: 0 1 auto; max-width: 50ch;
  overflow: hidden;
  /* A FLOOR, not just a ceiling. `min-width: 0` let the nav's flex squeeze the
     pill to 70px at an 820px window — narrow enough that even the compact
     "live 4s" was ellipsised, so the badge reported the market state and hid
     the one number that proves it. 13ch holds the compact form of every state.
     This badge outranks the other nav chrome for space: "bars current" and the
     env label are context, this is whether what you are looking at is real. */
  min-width: 13ch;
}
.feedbadge #feed-text { overflow: hidden; text-overflow: ellipsis; }
.feedbadge:hover { border-color: var(--border-hi); color: var(--fg-0); }
/* Closed and not running: the click is a no-op, so it must not look clickable.
   A pointer cursor on a button that deliberately does nothing reads as a bug. */
.feedbadge--inert { cursor: default; }
.feedbadge--inert:hover { border-color: var(--border); color: var(--fg-1); }
.feedbadge__dot {
  width: 7px; height: 7px; border-radius: 50%; background: var(--fg-2);
  flex: 0 0 auto;
}
.feedbadge__dot.is-live  { background: var(--pass); box-shadow: 0 0 0 3px rgba(69,194,106,.16); animation: feedpulse 2s ease-in-out infinite; }
.feedbadge__dot.is-stale { background: var(--warn); box-shadow: 0 0 0 3px rgba(219,163,42,.16); }
.feedbadge__dot.is-dead  { background: var(--fail); }
.feedbadge__dot.is-idle  { background: var(--fg-2); }

/* The pulse is not decoration: a still dot beside a number that has stopped
   moving is exactly what a dead feed looks like. */
@keyframes feedpulse { 0%,100% { opacity: 1 } 50% { opacity: .45 } }

/* --------------------------------------------- watchlist view menu (⚙) */
.iconbtn { display: inline-flex; align-items: center; gap: 6px; font: 11px var(--sans);
  padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border-hi);
  background: var(--bg-2); color: var(--fg-1); cursor: pointer; }
.iconbtn:hover { color: var(--fg-0); border-color: var(--fg-2); }
.iconbtn.is-on { color: var(--accent); border-color: var(--accent); }
.iconbtn__tag { font-family: var(--mono); font-size: 10px; }

.vmenu { position: fixed; z-index: 80; min-width: 190px; padding: 5px;
  background: var(--bg-2); border: 1px solid var(--border-hi);
  border-radius: var(--r-lg); box-shadow: 0 10px 30px rgba(0,0,0,.5); }
.vmenu__sect { padding: 7px 10px 4px; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .6px; color: var(--fg-2); }
.vmenu__item { display: flex; width: 100%; align-items: center; justify-content: space-between;
  gap: 10px; padding: 6px 10px; border: none; border-radius: 6px; background: none;
  color: var(--fg-0); font: 12px var(--sans); text-align: left; cursor: pointer; }
.vmenu__item:hover { background: var(--bg-3); }
.vmenu__item.is-on { color: var(--accent); }
.vmenu__dir { font-family: var(--mono); font-size: 11px; }
.vmenu__item--reset { margin-top: 4px; border-top: 1px solid var(--border);
  border-radius: 0 0 6px 6px; color: var(--fg-2); }

/* A group header is a row INSIDE the table, so columns stay aligned across
   groups. Sticky under the thead so the bucket you are reading stays named. */
.tbl tbody tr.grouphdr td { position: sticky; top: 34px; z-index: 5;
  background: var(--bg-2); font: 10px var(--sans); font-weight: 700;
  text-transform: uppercase; letter-spacing: .6px; color: var(--fg-1);
  height: 26px; border-bottom: 1px solid var(--border-hi); }
.tbl tbody tr.grouphdr:hover td { background: var(--bg-2); }
.grouphdr__n { color: var(--fg-2); font-weight: 600; margin-left: 8px; }
.grouphdr__n::before { content: '· '; }

/* ------------------------------------------------- watchlist filter chips */
.chiprow { display: flex; flex-wrap: wrap; gap: 6px; padding: 9px 13px;
  border-bottom: 1px solid var(--border); background: var(--bg-1); }
.fchip { display: inline-flex; align-items: center; gap: 6px; font: 11px var(--sans);
  padding: 3px 4px 3px 9px; border-radius: 20px; border: 1px solid var(--accent);
  color: var(--accent); background: rgba(91,156,248,.10); }
.fchip__x { border: none; background: none; color: inherit; cursor: pointer;
  font-size: 11px; line-height: 1; padding: 2px 5px; border-radius: 50%; opacity: .75; }
.fchip__x:hover { opacity: 1; background: rgba(91,156,248,.22); }
.fchip--clear { border-color: var(--border-hi); color: var(--fg-2);
  background: none; cursor: pointer; padding: 3px 10px; }
.fchip--clear:hover { color: var(--fg-0); border-color: var(--fg-2); }

/* A max-width is what actually makes the help text wrap. `white-space: normal`
   alone does nothing here: the popup had only a min-width, so it grew to fit
   the longest single line and one long sentence took it to 857px. */
.vmenu--wide { min-width: 268px; max-width: 300px; overflow-y: auto;
  /* The CAP has a floor. `70vh` alone collapses the menu to a 12px sliver —
     every entry present, none visible — anywhere the viewport reports zero
     height, and a percentage floor fails the same way because it resolves
     against the same zero. `max(320px, 70vh)` cannot degenerate: the worst case
     is a 320px menu that scrolls. */
  max-height: max(320px, 70vh); }
/* A STATIC cap. Deriving it from where the menu was placed —
   `calc(100vh - var(--menu-top))` — is circular: the position needs the height
   and the height needed the position, and the menu collapsed to 12px. The
   placement below clamps position; this clamps size; neither reads the other. */
/* min-width:0 lets a flex child shrink below its content width — without it
   the label row refuses to narrow and the wrap never happens. */
.vmenu__item--stack { flex-direction: column; align-items: flex-start; gap: 1px;
  min-width: 0; }
.vmenu__help { font-size: 10.5px; color: var(--fg-2); line-height: 1.35;
  white-space: normal; overflow-wrap: anywhere; max-width: 100%; }
.fopt { display: flex; align-items: center; gap: 8px; padding: 5px 10px;
  font-size: 12px; cursor: pointer; border-radius: 6px; }
.fopt:hover { background: var(--bg-3); }
.fopt input { accent-color: var(--accent); }

/* ---- the column picker -------------------------------------------------
   Each entry is a NAME on one line and its description on the next. Inline,
   the two ran together — "Company The registered name, not the ticker" reads
   as one broken phrase rather than a label and a note. */
.vmenu--cols .fopt { align-items: flex-start; }
.vmenu--cols .fopt > span { display: block; line-height: 1.4; }
.vmenu--cols .fopt input { margin-top: 2px; }
.vmenu--cols .vmenu__help { display: block; margin-top: 1px; }

/* A column that cannot be turned off: still ticked, visibly not yours to
   change, with the reason on hover. */
.fopt--locked { opacity: .72; cursor: default; }
.fopt--locked input { cursor: default; }
.fopt__lock { margin-left: 6px; font-size: 9px; text-transform: uppercase;
  letter-spacing: .5px; color: var(--fg-2); border: 1px solid var(--border);
  border-radius: 7px; padding: 0 5px; }
.fnone { padding: 8px 10px; font-size: 11.5px; color: var(--fg-2); }
.frow2 { display: flex; align-items: center; gap: 6px; padding: 6px 10px; }
.finput { font: 12px var(--mono); background: var(--bg-0); color: var(--fg-0);
  border: 1px solid var(--border-hi); border-radius: 6px; padding: 5px 7px;
  width: 100%; min-width: 0; }
.finput--op { font-family: var(--sans); width: 92px; flex: none; }
.funit { font-size: 11px; color: var(--fg-2); flex: none; }
.fbtns { display: flex; justify-content: flex-end; gap: 6px; padding: 8px 10px 4px;
  margin-top: 4px; border-top: 1px solid var(--border); }

/* --------------------------------------------- root menu + side flyouts */
.vmenu--root { min-width: 200px; }
.vmenu__item--parent { justify-content: space-between; }
.vmenu__item--parent.is-open { background: var(--bg-3); color: var(--fg-0); }
.vmenu__val { display: inline-flex; align-items: center; gap: 6px;
  color: var(--fg-2); font-size: 11px; }
.vmenu__caret { color: var(--fg-2); font-size: 13px; line-height: 1; }
/* A flyout is positioned in JS beside the root, so it only needs to sit on
   its own layer and never inherit the root's width. */
.vmenu--flyout { position: fixed; z-index: 81; }

/* Sortable headers inside the execution-layer panels. The arrow itself comes
   from the shared .tbl thead th.sorted rule the scanner already uses. */
.tpanel .tbl thead th[data-sortcol] { cursor: pointer; user-select: none; }
.tpanel .tbl thead th[data-sortcol]:hover { color: var(--fg-0); }
.tpanel .tbl thead th.sorted { color: var(--accent); }

/* ================================================================ builder
 *
 * /strategies. Editor left, palette right — a two-column split rather than a
 * modal, because writing a rule means reading the vocabulary while you type
 * and a dialog would put one behind the other.
 *
 * The palette is `position: sticky` so it stays put as the rule list grows; a
 * strategy with 11 rules is taller than the viewport and scrolling back up to
 * find `natr(50)` is the difference between usable and not.
 */
.sb { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 14px;
  align-items: start; padding: 14px; max-width: 1500px; margin: 0 auto;
  height: calc(100vh - var(--nav-h)); }
.sb__main { display: flex; flex-direction: column; gap: 12px;
  min-width: 0; min-height: 0; height: 100%; }
/* The rules panel is the one that grows; every other panel keeps its content
   height, so a long dry-run result cannot squeeze the rules to nothing. */
.sb__main > .tpanel { flex: none; }
.sb__main > .tpanel.sb__rules { flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; }
/* A dry run over a 500-name scope prints a lot of symbols. Cap it so it cannot
   push Save off the screen the whole layout exists to keep visible. */
#sb-dry { max-height: 30vh; overflow-y: auto; }
.sb__side { position: sticky; top: calc(var(--nav-h) + 12px);
  max-height: calc(100vh - var(--nav-h) - 24px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 12px; }

.sb__ident { display: grid; grid-template-columns: 220px minmax(0, 1fr);
  gap: 10px; padding: 12px 13px 4px; }
.sb .field { flex-direction: column; align-items: stretch; gap: 4px; }
.sb .field__label { font-size: 11px; color: var(--fg-2); }
.sb .finput { width: 100%; padding: 6px 8px; }
.sb .field--wide { padding: 0 13px 12px; }
.sb #sb-code { font-weight: 650; text-transform: uppercase; }
/* A placeholder that reads like a value is worse than no placeholder: `CODE
   [MY_SETUP]` looks filled in, and the save then refuses an empty field the
   user believes they supplied. */
.sb .finput::placeholder { color: var(--fg-2); font-weight: 400; opacity: 0.75; }
/* A locked field must LOOK locked. All three identity fields are read-only
   while editing, and one that still looked like an input would invite the edit
   the save is going to ignore. */
.sb .finput[readonly] { color: var(--fg-1); background: var(--bg-2);
  border-color: var(--bg-3); cursor: default; }
.sb .finput[readonly]:focus { outline: none; border-color: var(--bg-3); }
.sb__locked { padding: 0 13px 12px; margin: 0; line-height: 1.5; }
.sb .warn { margin: 0 13px 12px; padding: 8px 10px; border-radius: var(--r);
  background: var(--warn-bg); border: 1px solid #3d3110; line-height: 1.5; }

.sb__foot { display: flex; align-items: center; gap: 12px; justify-content: flex-end;
  padding: 4px 2px 20px; }
.sb__status { color: var(--fg-2); font-size: 11.5px; }

/* ---- one rule ---- */
.sbc { border-bottom: 1px solid var(--border); padding: 10px 13px; }
.sbc:last-child { border-bottom: 0; }
.sbc--bad { background: #1c1416; }
.sbc__row { display: flex; gap: 7px; align-items: center; margin-bottom: 6px; }
/* Both selectors are qualified by `.sb`, and both must be, because the
   identity fields above want `.sb .finput { width: 100% }` and that (0,2,0)
   beats a bare `.sbc__name` (0,1,0). Unqualified, the name field took the whole
   row, pushed the English field to zero width and shoved the move/remove
   buttons out of the column entirely — they rendered on top of the palette. */
.sb .sbc__name { width: 190px; flex: 0 0 190px; font-weight: 600; }
.sb .sbc__plain { flex: 1 1 auto; min-width: 0; width: auto; font-family: var(--sans); }
.sbc__row .abtn { flex: none; padding: 4px 9px; }
.sbc__expr { width: 100%; resize: vertical; padding: 7px 9px; line-height: 1.5;
  font: 12px var(--mono); color: var(--fg-0); background: var(--bg-0);
  border: 1px solid var(--border); border-radius: var(--r); }
.sbc__expr:focus { outline: none; border-color: var(--accent); }
.sbc--bad .sbc__expr { border-color: #4a2226; }

/* The verdict is `pre-wrap` because a syntax error carries a caret line under
   the source, and collapsing that whitespace points the caret at the wrong
   character — worse than not pointing at all. */
.sbc__status { display: block; margin-top: 5px; font: 11px var(--mono);
  white-space: pre-wrap; color: var(--fg-2); }
.sbc__status--ok { color: var(--pass); }
.sbc__status--bad { color: var(--fail); }

/* ---- palette ---- */
/* `flex: none` is load-bearing. The sidebar is a flex column with a max-height,
   so its children default to `flex-shrink: 1` and were each squashed to fit —
   with `overflow: hidden` on the box, that silently CUT the lists: "Prices and
   volume" showed 1 of 7 entries and looked like the API had returned one. The
   sidebar is what scrolls; a section keeps its own height. */
.pal { flex: none; border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--bg-1); overflow: hidden; }
.pal__h { font-size: 12px; font-weight: 650; padding: 9px 12px;
  border-bottom: 1px solid var(--border); }
.pal__note { font-size: 11px; color: var(--fg-2); padding: 7px 12px 0; margin: 0;
  line-height: 1.45; }
.pal__grp { font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--fg-2); padding: 9px 12px 3px; }
.pal__item { display: grid; grid-template-columns: 1fr auto; gap: 2px 6px;
  width: 100%; text-align: left; padding: 7px 12px; border: 0; background: none;
  color: var(--fg-1); cursor: pointer; border-top: 1px solid transparent; }
.pal__item:hover { background: var(--bg-2); color: var(--fg-0); }
.pal__item code { font: 11.5px var(--mono); color: var(--accent); }
.pal__desc { grid-column: 1 / -1; font-size: 11px; color: var(--fg-2); line-height: 1.4; }
.pal__eg { grid-column: 1 / -1; font: 10.5px var(--mono); color: var(--fg-2); opacity: .8; }
.pal__used, .pal__flag { font-size: 10px; color: var(--fg-2); background: var(--bg-3);
  border-radius: 3px; padding: 1px 5px; justify-self: end; }
.pal__flag { color: var(--warn); background: var(--warn-bg); }

@media (max-width: 1100px) {
  /* Single column: the PAGE scrolls again. A viewport-tall grid here would put
     the palette below a full-height editor with nothing able to reach it. */
  .sb { grid-template-columns: minmax(0, 1fr); height: auto; }
  .sb__main { height: auto; }
  .sb__side { position: static; max-height: none; }
  #sb-clauses { max-height: 60vh; }
  #sb-dry { max-height: none; }
}

/* ---- dry run: what the strategy does before it exists ---- */
.dry__note { margin: 0; padding: 11px 13px; font-size: 12px; line-height: 1.55;
  color: var(--fg-2); }
.dry__note code { font: 11.5px var(--mono); color: var(--fg-1); }
.dry__note--stale { color: var(--warn); }
.dry__stats { padding: 11px 13px 8px; font-size: 12.5px; color: var(--fg-1); }
.dry__stats b { color: var(--fg-0); font-weight: 650; }
.dry__stats b.pos { color: var(--pass); }
.dry__syms { padding: 0 13px 10px; font: 11.5px var(--mono); color: var(--fg-1);
  line-height: 1.7; }
.dry__more { color: var(--fg-2); }
.sb .chiprow { padding: 0 13px 12px; }
/* The scope picker is ScopePicker (js/scope_picker.js), the same component the
   scanner uses — `.sp` carries its own chrome, so all this has to do is stop it
   stretching in the panel header it shares with the Test button. */
.sb__scope { flex: none; width: 190px; }

/* ---- the rules list scrolls on its own ---- */
/* A strategy with 11 rules is three screens tall, and letting the page carry
   all of it pushed the palette, the dry run and Save out of reach — you had to
   scroll past every rule to get to the button that saves them. The list is its
   own scroll region so everything that acts on it stays visible. */
/* The editor column is exactly one viewport tall and the RULES take whatever
   is left. `max-height: 56vh` was a guess that still let Try it and Save fall
   below the fold at 100% zoom on a shorter screen; this makes them structurally
   reachable instead of nearly reachable. `min-height: 0` on every flex ancestor
   is what lets a flex child actually shrink — without it the default
   `min-height: auto` keeps the list at its content height and nothing scrolls. */
#sb-clauses { flex: 1 1 auto; min-height: 120px; overflow-y: auto;
  overscroll-behavior: contain; }
/* Without this the last rule sits flush against the panel edge and reads as
   cut off rather than ended. */
#sb-clauses .sbc:last-child { border-bottom: 1px solid transparent; }

/* ---- an indicator and its ready-made periods ---- */
.pal__row { padding: 8px 12px; border-top: 1px solid var(--bg-2); }
.pal__row:first-child { border-top: 0; }
.pal__fn { display: grid; grid-template-columns: 1fr auto; gap: 2px 6px; }
.pal__fn code { font: 11.5px var(--mono); color: var(--accent); }
.pal__fn .pal__desc { grid-column: 1 / -1; }
.pal__periods { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.pal__p { font: 11px var(--mono); padding: 2px 8px; border-radius: 5px; cursor: pointer;
  background: var(--bg-2); color: var(--fg-1); border: 1px solid var(--border); }
.pal__p:hover { border-color: var(--accent); color: var(--fg-0); }
/* The user's own period sits with the presets, not below them — it is the same
   kind of choice, and putting it elsewhere would imply the presets are the
   supported set and anything else is an escape hatch. */
.pal__own { display: inline-flex; gap: 3px; }
.pal__n { width: 46px; font: 11px var(--mono); text-align: center; padding: 2px 4px;
  background: var(--bg-0); color: var(--fg-0);
  border: 1px dashed var(--border-hi); border-radius: 5px; }
.pal__n:focus { outline: none; border-color: var(--accent); border-style: solid; }
.pal__n::-webkit-outer-spin-button, .pal__n::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0; }
.pal__p--own { padding: 2px 7px; font-weight: 700; }

/* ---- version picker + hidden strategies ---- */
.sb__vers { display: flex; align-items: center; gap: 9px; flex-wrap: wrap;
  padding: 0 13px 12px; }
/* No width. A fixed 210px left 60px of dead host to the right of a 150px
   button — `.sel__btn` sizes to its own content and does not fill its host —
   and that dead space read as a huge gap before the note beside it. Hugging
   the button means the only space there is the 9px flex gap. */
.sb__vsel { flex: none; }
.sb__vnote { font-size: 11.5px; color: var(--fg-2); }
.sb__vnote--warn { color: var(--warn); }
.sb__toggle { display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; color: var(--fg-2); cursor: pointer; }
.sb__toggle input { accent-color: var(--accent); }
/* A hidden strategy is listed but is not in play — it reads as an archive
   entry rather than a row you can act on beyond bringing it back. */
.tbl tbody tr.is-hidden { opacity: 0.55; }
.tbl tbody tr.is-hidden:hover { opacity: 1; }

/* Host for M.select where a native <select> used to sit — the buy/sell order
   type, the numeric filter's operator, the trade log's event filter. The
   component brings its own chrome (`.sel`); these only give it a width in the
   row it shares. */
.selhost { display: inline-block; min-width: 130px; }
.selhost--op { min-width: 118px; flex: none; }

.sb__vspacer { flex: 1 1 auto; }


/* ---- custom index upload panel -----------------------------------------
   A <details>, closed by default: it is a spec, and a spec permanently open
   above the table pushes the thing people came for off the screen. */
.customlist { border: 1px solid var(--border); border-radius: var(--r); background: var(--bg-1); margin: 10px 0; }
.customlist > summary { padding: 9px 13px; cursor: pointer; font-size: 12px; font-weight: 600; color: var(--fg-1); user-select: none; }
.customlist > summary:hover { color: var(--fg-0); }
.customlist__body { padding: 4px 13px 14px; border-top: 1px solid var(--border); }
.customlist__cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 22px; margin-top: 10px; }
.customlist h4 { margin: 0 0 8px; font: 600 11px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-2); }
.customlist .btn { text-decoration: none; }
.spec { margin: 0; font-size: 12px; }
.spec > div { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 10px; padding: 4px 0; border-bottom: 1px solid var(--border); }
.spec dt { font-family: var(--mono); color: var(--fg-0); margin: 0; }
.spec dd { margin: 0; color: var(--fg-2); line-height: 1.45; }
.spec code { font-family: var(--mono); color: var(--fg-1); }

.cl-head { margin: 14px 0 2px; font-size: 13px; color: var(--fg-0); }
#cl-preview .tablewrap { margin-top: 8px; max-height: 340px; }
#cl-preview .tbl td { font-size: 12px; }
/* Row tint by verdict — the eye should find the rejections without reading. */
tr.cl-ok td { }
tr.cl-warn td { background: rgba(219,163,42,.06); }
tr.cl-bad td { background: rgba(248,81,73,.07); }
tr.cl-bad .td-sym { color: #ff8b84; }

.cl-save { display: flex; align-items: center; gap: 10px; margin: 12px 0 4px; }

/* A price that just ticked, in the scanner table.
   A FLASH, not a colour that sticks: the direction of the last tick is a
   fleeting fact, and a cell left green would read as "up on the day" — a
   different claim, and one the column does not make. */
@keyframes tickup   { from { background: rgba(69,194,106,.28); } to { background: transparent; } }
@keyframes tickdn   { from { background: rgba(233,86,86,.28);  } to { background: transparent; } }
.td-close.tick-up { animation: tickup .9s ease-out; }
.td-close.tick-dn { animation: tickdn .9s ease-out; }

/* The live dot in the column header, matching the nav badge's. */
th .livedot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--pass); margin-right: 5px; vertical-align: middle;
  animation: feedpulse 2s ease-in-out infinite;
}

/* The live price cell, shared by the scanner, the watchlist and the portfolio.
   Arrow + colour are the DAY (against the previous close); the flash is the
   last tick. Two claims, deliberately shown differently — see M.livePrice. */
/* The tick arrow occupies the same space whether or not it is showing, so a
   price column cannot resize when a direction appears, flips or goes flat.
   `visibility`, not `display`, is the whole point. */
.tickarrow { display: inline-block; margin-right: 3px; }
.tickarrow.is-flat { visibility: hidden; }

.td-live { white-space: nowrap; }
.td-live.tick-up { animation: tickup .9s ease-out; }
.td-live.tick-dn { animation: tickdn .9s ease-out; }

/* The scanned price — what the row was JUDGED at. Muted against the live
   Price beside it, because the eye should land on the number that is moving
   while the verdict still belongs to this one. */
.td-scanned { color: var(--fg-1); }

/* THE PRICE DOT. Always present, in one of two states, on every price cell in
   the scanner, the watchlist and the portfolio.

     green + pulsing   streaming right now
     grey              settled — the last trade of a finished session

   Always rendered, never conditional: a dot that only appears when the market
   is shut makes its own absence ambiguous, because "live" and "this page forgot
   to say" look identical. Both colours are borrowed rather than invented — the
   green is the nav badge's live pulse and the grey is its closed state, so the
   page never explains one fact with two colour vocabularies. */
.pricedot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  margin-right: 5px; vertical-align: middle;
}
.pricedot--closed { background: var(--fg-2); }
.pricedot--live {
  background: var(--pass);
  animation: feedpulse 2s ease-in-out infinite;
}
.td-live.is-closed { color: var(--fg-1); }

/* ========================================================= position sizing
 *
 * Three columns per derived row — formula, the numbers put into it, the answer
 * — so the arithmetic is checkable at a glance rather than asserted. The whole
 * premise of the risk framework is that nobody types a risk percentage, which
 * only holds if you can see what the engine computed.
 */
/* Two columns: what you SET on the left, what it WORKS OUT TO on the right.
   Both owners are on screen at once — the ceilings and the capital multiply
   into one share count, and putting either behind a tab meant you could not
   see the pair that produced it. */
/* A 2x2 GRID. Column = owner, row = kind:

     Customer            Admin              <- what you SET
     what works out to   where you are now  <- what it WORKS OUT TO

   Four panels as direct grid children rather than two stacked columns, which
   is what keeps the rows aligned when one panel grows. `align-items: stretch`
   on purpose — the two cells in a row share a height, so "Customer" and
   "Admin" have a common baseline to read across.

   The left column is wider because it holds inputs and an account card;
   `minmax(0, …)` on both so a long client name or symbol cannot push a column
   past the viewport. */
.ps { max-width: 1480px; margin: 0 auto; padding: 16px;
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: 16px; align-items: stretch; }
.ps__cell { min-width: 0; }

/* The save bar commits edits from EITHER top panel, so it spans both columns
   rather than sitting under one of them and misstating its own scope.
   `position: sticky; bottom: 0` keeps it reachable without scrolling — but as
   the last row of a 2x2 grid it then HOVERS OVER the two derived panels and
   clipped their figures. The grid gets bottom clearance so the last row can
   scroll clear of it. */
.ps__save--wide { grid-column: 1 / -1; margin-top: -2px; }


.ps__notice { margin: 0; padding: 9px 12px; border-radius: var(--r);
  background: var(--warn-bg); border: 1px solid #3d3110; color: var(--warn);
  font-size: 12px; line-height: 1.5; }

/* Deliberately sized to sit at the same visual weight as the derived rows on
   the right. A rail set two points smaller read as secondary, when the two
   halves are one thought: these inputs produce those numbers. */
/* TWO COLUMNS, because the panel is ~690px wide in the 2x2 layout and one
   column spent 202px of vertical budget on two inputs. That mattered: the top
   row was 451px tall, which pushed the two derived panels below the fold on any
   laptop with browser chrome — the page wanted 959px of a 900px viewport, and
   more than that once a real browser's toolbars are counted.

   `auto-fit` with a 220px floor rather than a hard `1fr 1fr`, so the Admin
   panel's three fields flow 2+1 and a narrow window collapses to one column
   without a second breakpoint. */
.ps__grid { display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px 14px; padding: 14px 16px; }
.ps .field { flex-direction: column; align-items: stretch; gap: 5px; }
/* Label and hint STACKED, not inline. Inline, "Max risk per trade" plus
   "cap 1.00% at this stance" overflowed a 210px grid column and both wrapped
   mid-phrase, so the hint read as part of the label. */
.ps .field__label { display: flex; flex-direction: column; align-items: flex-start;
  gap: 1px; font-size: 12.5px; color: var(--fg-1); font-weight: 500;
  letter-spacing: 0; text-transform: none; }
.ps__cap { font-size: 11.5px; color: var(--fg-2); opacity: 0.9;
  font-weight: 400; }
/* A widened TOR has nothing refusing it, so the stance's own figure being
   visible IS the safeguard. It must not read as ordinary help text. */
.ps__cap--warn { color: var(--warn); opacity: 1; }
.ps .finput { width: 100%; padding: 9px 11px; font-size: 14px; }
.ps .inputwrap { display: flex; align-items: center; gap: 7px; }
.ps .unit { font-size: 12.5px; }

.ps__rows { padding: 4px 0 8px; }
/* Four columns: label, formula, the numbers substituted in, the answer.
   The label and formula are CONTENT-SIZED rather than fixed at 165px/200px —
   the 2x2 grid made this panel ~615px instead of ~950px, and fixed columns
   then squeezed the working into a strip narrow enough to break "1.00% ÷
   8.00%" across two lines mid-expression. An expression split by a line wrap
   is harder to read than a longer row.

   `max-content` on the formula, and `nowrap` on the working below, so an
   expression stays on one line and the panel scrolls if it truly cannot fit —
   the numbers are the point of the row. */
.ps__row { display: grid;
  grid-template-columns: minmax(110px, max-content) minmax(0, max-content) 1fr auto;
  gap: 12px; align-items: baseline; padding: 10px 16px; font-size: 13px; }
.ps__row + .ps__row { border-top: 1px solid var(--bg-2); }
.ps__label { color: var(--fg-1); }
.ps__formula { font: 12px var(--mono); color: var(--accent); }
/* Wraps rather than truncating. Ellipsing it turned "3.00% × ₹10,45,705 −
   ₹33,584" into "3.00% × ₹10,45,705 − …", which hides the operand the row
   exists to show — the one number a reader would want to check. */
.ps__working { font: 12px var(--mono); color: var(--fg-2);
  white-space: nowrap; overflow-x: auto; }
.ps__result { font: 14px var(--mono); font-weight: 650; color: var(--fg-0);
  text-align: right; white-space: nowrap; }
.ps__result.pos { color: var(--pass); }
.ps__result.neg { color: var(--fail); }


.ps__reject { margin: 0; padding: 12px 13px; color: var(--fail); font-size: 12px; }
.ps__over { margin: 8px 13px 4px; padding: 9px 11px; border-radius: var(--r);
  background: var(--fail-bg); border: 1px solid #4a2226; color: #ff8b84;
  font-size: 11.5px; line-height: 1.55; }

/* Below this the derived rows cannot hold four columns beside a 400px rail,
   so the page stacks and each row folds its own working underneath. */
/* ONE COLUMN below 1180px, and there is only one rule saying so.
   A second copy was briefly added at 1100px while making this a 2x2 grid —
   two breakpoints for one decision, which drift apart the first time either is
   touched. The 2x2 needs roughly 1200px to give both columns room for a
   formula row without wrapping, so 1180 is the honest floor.
   Stacked, the order still reads set-then-derived. */
@media (max-width: 1180px) {
  .ps { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 760px) {
  .ps__row { grid-template-columns: 1fr auto; }
  .ps__formula, .ps__working { grid-column: 1 / -1; }
}

/* ---- buy dialog: the working, and the ceiling warning ---- */
.tmodal .frow--head { border-bottom: 1px solid var(--border); padding-bottom: 6px;
  margin-bottom: 4px; color: var(--fg-2); font-size: 11px; }
.tmodal .mono { font: 11.5px var(--mono); color: var(--fg-1); }
.qtywrap { display: inline-flex; gap: 5px; align-items: center; }
/* Only offered once the quantity has been overridden — a reset that does
   nothing invites a click that appears to fail. */
#bd-reset-qty[hidden] { display: none; }
.bd__note { margin: 8px 0 0; padding: 8px 10px; border-radius: var(--r);
            background: rgba(255,255,255,.04); border: 1px solid var(--line);
            color: var(--fg-2); font-size: 12px; line-height: 1.5; }
.bd__note b { color: var(--fg-1); }
.bd__warn { margin: 8px 0 0; padding: 8px 10px; border-radius: var(--r);
  background: var(--fail-bg); border: 1px solid #4a2226; color: #ff8b84;
  font-size: 11.5px; line-height: 1.5; }
.bd__warn b { color: #ffb9b3; }
.bd__warn .chipbtn { margin-left: 4px; padding: 1px 8px; font-size: 11px; }

/* ---- the contract buy modal's terms strip ----
   The one row an equity dialog has no use for: WHICH contract this is. A ref
   is the identity and unreadable as a heading, so the terms are spelled out
   under the name — kind, strike and right where they exist, expiry always,
   and the lot size, which is the unit every figure below is quoted in. */
.cb__terms { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin: -4px 0 10px; font: 11.5px var(--mono); color: var(--fg-2); }
/* Why an OPTION's stop box is blank while a future's arrives filled. Sits
   directly under the pair it explains, quiet enough to be skipped by anyone
   who already knows and present enough that the emptiness reads as a
   decision rather than a bug. */
.cb__hint { margin: -4px 0 10px; font-size: 11px; line-height: 1.5;
  color: var(--fg-2); }
.cb__hint b { color: var(--fg-1); }
.cb__kind { padding: 1px 6px; border-radius: 3px; letter-spacing: .04em;
  background: rgba(255,255,255,.06); border: 1px solid var(--line);
  color: var(--fg-1); font-size: 10px; }
/* Pushed right so the lot — the unit, not a term of the contract — reads as
   a different kind of fact from the strike and the expiry beside it. */
.cb__lotsize { margin-left: auto; color: var(--fg-1); }

/* ---- position sizing: the deferred save ---- */
/* Sticky to the bottom of the viewport. The settings rail is taller than a
   short screen, and Save is the primary action on this page — it dropping
   below the fold is how you end up with edits you believed were applied. */
/* NOT sticky any more. Sticking it to the viewport bottom cost 72px of dead
   padding so the last row could scroll clear of it — worth it for a long
   scrolling form, and pure waste now the whole page is ~830px. The bar sits in
   flow at the end of the grid, where a page this short always shows it. */
.ps__save { display: flex; align-items: center; gap: 10px; padding: 11px 16px;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--bg-1); }
.ps__dirty { flex: 1; font-size: 12.5px; color: var(--fg-2); }
/* Unsaved work should not read like ordinary help text. */
.ps__dirty.is-dirty { color: var(--warn); }
.ps .tpanel__title { font-size: 15px; }
.ps .tpanel__head { padding: 12px 16px; }
.ps__note { margin: 0; padding: 0 16px 14px; font-size: 12px;
  color: var(--fg-2); line-height: 1.55; }
.ps__note b { color: var(--fg-1); }

/* The definitions under the trading record. It sits below a block of derived
   rows rather than under a heading, so it needs a rule above it or it reads as
   a fourth column of the last row. */
.ps__note--defs {
  border-top: 1px solid var(--border); margin: 0 16px; padding: 10px 0 14px;
  font-size: 11.5px;
}
/* The one formula stated as code rather than prose — R has an expression, and
   setting it in the mono face the formula column already uses says "this is
   the arithmetic" without a sentence saying so. */
.ps__note--defs code {
  font-family: var(--mono); font-size: 11px; color: var(--accent);
  background: var(--bg-0); border-radius: 4px; padding: 1px 5px;
  white-space: nowrap;
}


/* A capital field that is not a field. On a live broker the trading capital is
   the broker's cash — a fact about an account, not a setting — so it is shown
   rather than offered. Muted and unmistakably inert, because an input that
   looks editable and silently refuses is worse than one that never invited
   the click. */
.inputwrap--locked { opacity: .72; }
.inputwrap--locked .finput { cursor: not-allowed; color: var(--fg-1); }
.ps__note--locked { color: var(--fg-1); }


/* The account card on Position Sizing — who the money belongs to. Quiet by
   design: it is context for the numbers beside it, not a headline. */
/* The account card, in TWO COLUMNS. Five stacked key/value rows cost 135px of
   the vertical budget the derived panels needed; paired, the same five read in
   three rows. The label sits above its value rather than opposite it, so a long
   client name or broker label has the full half-width and cannot collide with
   its own key — which is what made these look "broken up" on a narrower
   window. */
.acct { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 2px 18px; padding: 10px 16px 12px;
  border-bottom: 1px solid var(--line); }
.acct__row { display: flex; flex-direction: column; gap: 0;
             padding: 4px 0; font-size: 12px; min-width: 0; }
.acct__k { color: var(--fg-2); font-size: 11px; letter-spacing: .02em; }
/* Truncate rather than wrap: a two-line value would make its row taller than
   its neighbour and break the pairing the two columns exist to create. The
   title attribute carries the full text. */
.acct__v { color: var(--fg); font-weight: 500; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct__v.mono { font-family: var(--mono); letter-spacing: .02em; }
/* Green, and NOT pulsing: the feed dot pulses because a price is arriving.
   A funded account is a standing fact, and borrowing the animation would say
   something is streaming when nothing is. */
.livedot--money { animation: none; }


/* Each cell is a column of its own content, so a short panel still fills the
   height its row was given and the two bottom panels end level. */
.ps__cell { display: flex; flex-direction: column; }
.ps__cell > .ps__rows, .ps__cell > .ps__grid { flex: 1 1 auto; }

/* ---- the account panel ---------------------------------------------------
   Full width, under the 2x2 grid, because it answers a question neither column
   asks: the four panels above describe ONE trade and the book it lands in,
   while this describes the account as a whole.

   The two `.ps__rows` blocks inside it are separated rather than merged — the
   first is what the account is WORTH and the second is what it has MADE, and
   running them together would read as one list of nine facts rather than two
   separate claims. */
.ps__cell--wide { grid-column: 1 / -1; }
.ps__cell--wide > .ps__rows + .ps__rows {
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
}
.ps__cell--wide .tablewrap { margin-top: 12px; }

}


/* ---- trading calendar ----------------------------------------------------
   Scoped to this page rather than reusing `.rowbadge`. That class carries a
   `margin-left` for use beside a symbol and is shared by the scanner, the
   portfolio and the watchlist — restyling it to suit one table would move four
   screens to fix one. */
.cal-banner {
  margin: 0 0 12px; padding: 10px 14px; border-radius: var(--r);
  font-size: 12.5px; line-height: 1.5; border: 1px solid var(--border);
}
.cal-banner--ok   { color: var(--pass); border-color: #1f6b3a; background: var(--pass-bg); }
.cal-banner--warn { color: var(--warn); border-color: #6b5310; background: var(--warn-bg); }
/* The one state worth alarming on: no calendar means the closures below are
   NOT in force, and fail-open makes that invisible everywhere else. */
.cal-banner--bad  { color: var(--fail); border-color: #6b2320; background: var(--fail-bg); }
.cal-banner em    { font-style: normal; text-decoration: underline; }

.caltag {
  font-size: 9.5px; padding: 1px 7px; border-radius: 8px;
  border: 1px solid var(--border); color: var(--fg-2);
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.caltag--trades  { color: var(--pass); border-color: #1f6b3a; }
/* Amber, not red: a weekday closure is correct behaviour worth noticing, not
   a fault. */
.caltag--closed  { color: var(--warn); border-color: #6b5310; }
/* A closure that lands on a weekend changes nothing — the weekday rule already
   covered it. Muted so the eye stays on the 16 that matter. */
.caltag--weekend { color: var(--fg-2); }

.kindtag { font-family: var(--mono); font-size: 11px; color: var(--fg-2); cursor: help; }
.calmono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.calmuted { color: var(--fg-2); }
.tbl tr.is-closed td { background: rgba(219, 163, 42, .04); }

/* ---- palette: a short default, everything behind one button ----
 *
 * 96 indicators, 25 ready-made rules, 8 columns and 18 operators is 147 rows.
 * Rendering all of them made finding `sma` a scroll, which is worse than
 * knowing its name — the cost ADR-0043 records as the price of the vocabulary.
 * Each section shows its first eight, ordered common-first by the server. */
.pal__count { font-size: 10px; color: var(--fg-2); background: var(--bg-3);
  border-radius: 9px; padding: 1px 7px; margin-left: 7px; font-weight: 500; }
.pal__more { display: block; width: 100%; text-align: left;
  padding: 8px 12px; border: 0; border-top: 1px solid var(--border);
  background: none; color: var(--accent); font-size: 11.5px; cursor: pointer; }
.pal__more:hover { background: var(--bg-2); }

/* ================================================================= import
 * The AI screener import. One column: what you give it, then what it made.
 */
/* The panel owns the viewport and the RESULT is what scrolls.
   Before this, everything lived in one page-height column: the dropped
   screenshot alone took ~400px, so the rules it produced fell off the bottom
   and the only way to read them was to scroll past the input every time. */
.imp { display: flex; flex-direction: column; min-height: 0;
  height: calc(100vh - var(--nav-h) - 28px); }
.imp__body { display: flex; flex-direction: column; gap: 12px; padding: 13px;
  flex: none; }

/* Once there is a result the input shrinks out of the way — the big preview
   has done its job (confirming the right image was attached) and the rules
   need the room more. `change what it reads` brings it back. */
.imp__body--slim { display: none; }
.imp__collapsed { display: flex; align-items: center; gap: 10px;
  padding: 9px 13px; border-top: 1px solid var(--border);
  font-size: 11.5px; color: var(--fg-2); }
.imp__collapsed span { font-family: var(--mono); color: var(--fg-1); }

/* The AI mark. `currentColor` so it takes the panel title's colour and cannot
   drift out of the theme the way a two-tone asset would. */
/* The sparkle. `currentColor` so it takes its parent's colour and cannot drift
   out of the theme the way a two-tone asset would. */
.ai-spark { flex: none; vertical-align: -2px; }
.tpanel__head .ai-spark { color: var(--accent); margin-right: -2px; }
/* On the button it sits inline with the label, so the button needs to be a
   flex row or the glyph and the text disagree about their baseline. */
.abtn--ai { display: inline-flex; align-items: center; gap: 6px; }
.abtn--ai .ai-spark { color: var(--accent); }

/* The drop zone. Dashed and large because it has to READ as a target — a
   flat rectangle gets treated as decoration and people never try dropping. */
.imp__drop { border: 2px dashed var(--border-hi); border-radius: var(--r-lg);
  background: var(--bg-0); padding: 22px; text-align: center; cursor: pointer;
  transition: border-color .12s, background .12s; }
.imp__drop:hover, .imp__drop:focus { border-color: var(--accent); outline: none; }
/* The drag state must be unmistakable: a drop that silently did not register
   looks exactly like a model that misread the image. */
.imp__drop.is-over { border-color: var(--accent); background: var(--bg-2);
  border-style: solid; }
.imp__drop b { display: block; font-size: 13px; margin-bottom: 4px; }
.imp__drop span { font-size: 11.5px; color: var(--fg-2); }
.imp__thumb { max-height: 132px; max-width: 100%; border-radius: var(--r);
  border: 1px solid var(--border); display: block; margin: 0 auto 8px; }

.linkbtn { background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--accent); font: inherit; text-decoration: underline; }

.imp__go { display: flex; align-items: center; gap: 10px; }
.imp .sb__scope { flex: none; width: 190px; }

/* ---- the result ---- */
/* THE SCROLL. `min-height: 0` is what makes it work — a flex child defaults to
   min-height:auto and refuses to shrink below its content, so without it the
   panel just grows and the page scrolls again. */
#imp-out { flex: 1 1 auto; min-height: 0; overflow-y: auto;
  overscroll-behavior: contain; }
.imp__result { border-top: 1px solid var(--border); padding: 13px; }
.imp__sum { margin: 0 0 10px; font-size: 12.5px; color: var(--pass); }
.imp__sum--bad { color: var(--warn); }
.imp__sum span { color: var(--fg-2); font-size: 11.5px; }

/* The unmapped block is the safety property of this whole feature, so it is
   styled as a WARNING rather than a footnote: a filter that was silently
   dropped produces a strategy that screens a wider universe while looking
   complete, and the only defence is that it is impossible to miss. */
.imp__unmapped { border: 1px solid #3d3110; background: var(--warn-bg);
  border-radius: var(--r); padding: 10px 12px; margin-bottom: 12px;
  font-size: 12px; }
.imp__unmapped ul { margin: 6px 0; padding-left: 18px; }
.imp__unmapped li { margin: 4px 0; line-height: 1.5; }
.imp__unmapped li span { color: var(--fg-1); }
.imp__warn { display: block; margin-top: 6px; color: var(--warn); font-size: 11.5px; }

.imp__ident { padding: 8px 0 12px; font-size: 13px; }
.imp__ident code { color: var(--accent); font-weight: 650; margin-right: 8px; }
.imp__ident div { color: var(--fg-2); font-size: 11.5px; margin-top: 3px; }

.imp__rule { border-top: 1px solid var(--border); padding: 9px 0; }
.imp__rule--bad { background: #1c1416; }
.imp__name { font: 12px var(--mono); font-weight: 650; }
.imp__expr { font: 12px var(--mono); color: var(--fg-0); margin: 3px 0;
  white-space: pre-wrap; word-break: break-word; }
.imp__plain { font-size: 11.5px; color: var(--fg-1); }
/* The original line, kept beside the translation. A misread 1.03 compiles and
   dry-runs and looks entirely plausible — this is the only place the
   difference is visible to a person. */
.imp__src { font-size: 11px; color: var(--fg-2); margin-top: 3px;
  font-style: italic; white-space: pre-wrap; word-break: break-word; }
.imp__err { font: 11px var(--mono); color: var(--fail); margin-top: 4px;
  white-space: pre-wrap; }
/* Sticky, so the action stays reachable however long the rule list is —
   scrolling to the bottom to find the only button is a small tax paid on
   every single import. */
.imp__actions { display: flex; justify-content: flex-end; gap: 10px;
  padding: 12px 0 2px; position: sticky; bottom: 0;
  background: linear-gradient(transparent, var(--bg-1) 40%); }

/* ⚠ The fallback is keyed on HEIGHT, not width.
   It was `max-width: 1100px` — copied from the builder's two-column breakpoint,
   where width genuinely is the constraint. Here it is not: this panel is one
   column at every width, and what decides whether an inner scroll helps is how
   much VERTICAL room there is. Keyed on width, a narrow-but-tall window fell
   back to page scrolling and the result was exactly the cut-off list the inner
   scroll exists to fix.

   Below ~560px of viewport the box would be too short to be worth scrolling
   inside, so the page takes over. */
@media (max-height: 560px) {
  .imp { height: auto; }
  #imp-out { overflow: visible; }
  .imp__actions { position: static; }
}

/* A scope note is not a warning — it says which control to use, and styling it
   like the missing-data block would keep implying something was lost. */
.imp__unmapped--scope { border-color: var(--border-hi); background: var(--bg-2); }
.imp__note { display: block; margin-top: 6px; font-size: 11.5px; color: var(--fg-2);
  line-height: 1.5; }

/* ---- account switcher (js/accounts.js) --------------------------------
   Anchored to the nav so it is answerable from every page: which account do
   these numbers describe. */
#acct-switch { position: relative; }
.acct-menu {
  position: absolute; top: 100%; right: 0; z-index: 60; min-width: 280px;
  margin-top: 6px; padding: 4px;
  background: var(--bg-1); border: 1px solid var(--border);
  border-radius: 8px; box-shadow: 0 8px 28px rgba(0, 0, 0, .35);
}
.acct-row {
  display: grid; grid-template-columns: 16px 1fr; gap: 2px 8px;
  width: 100%; padding: 8px 10px; text-align: left;
  background: none; border: 0; border-radius: 6px;
  color: inherit; font: inherit; cursor: pointer;
}
.acct-row:hover { background: var(--bg-2); }
.acct-row.is-on { background: var(--bg-2); }
.acct-row__mark { grid-row: 1 / span 2; color: var(--accent); }
.acct-row__name { font-weight: 600; }
.acct-row__sub { grid-column: 2; color: var(--fg-2); font-size: 11px; }
.acct-row--add { display: block; padding: 8px 10px; color: var(--accent);
                 text-decoration: none; border-top: 1px solid var(--border);
                 margin-top: 4px; }

/* ---- the login page (templates/login.html) ---------------------------
   Standalone: it does not extend base.html, because the signed-in chrome has
   no business loading on the page that exists because you are not signed in. */
.login-body { display: grid; place-items: center; min-height: 100vh; margin: 0; }
.login-card { width: 100%; max-width: 380px; padding: 26px 24px;
              background: var(--bg-1); border: 1px solid var(--border);
              border-radius: 10px; }
.login-card h2 { margin: 0 0 6px; }
.login-card .field { display: block; margin-top: 12px; }
.login-card .finput { width: 100%; box-sizing: border-box; }
.login-card .hint { margin: 0 0 16px; line-height: 1.45; }
.login-card .login-hint { margin-left: 6px; font-weight: 400;
                          text-transform: none; letter-spacing: 0;
                          color: var(--fg-2); }
.login-card .err { min-height: 18px; margin-top: 10px; color: var(--fail); }
.login-card .btn { width: 100%; margin-top: 14px; }

/* ---- the account setup wizard — /accounts/new ---------------------------
   Reuses the switcher's rows (.acct-row) and the shared controls. A wizard that
   invents its own vocabulary is a second design system reachable from one link.

   ⚠ `.tpanel` styles only its HEAD — it has no padding of its own, and every
   other panel in the app wraps its contents in something that does
   (`.ps__grid`, `.tablewrap`). Dropped straight in, the label, the note and the
   buttons all sat flush against the panel border. `.anew__body` is that
   wrapper. */
.anew__body { padding: 14px 16px; }
/* A trailing block under content that is already padded — the credential
   groups and the done list bring their own — so it only needs to close the gap
   to the actions bar. */
.anew__body--tail { padding-top: 0; }

/* The actions bar stays a DIRECT child of the panel so its top border spans the
   full width, matching every other panel in the app; the padding it lacks is
   added here rather than by moving it inside the body. */
.anew .actions { margin-top: 0; padding: 14px 16px; }

.anew__legend {
  color: var(--fg-2); font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.6px; font-weight: 600; margin: 16px 0 6px;
}

/* A broker choice. `.acct-row` already lays out name-over-subtitle and carries
   the selected state, so this adds only what a radio needs: room for the
   control, and a hit area covering the explanation rather than just the dot.
   The label element IS the target — clicking the reason picks the option.

   The negative margin pulls the row's background out to the body's padding
   edge while its text still aligns with everything above it. */
.anew__opt {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px 10px;
  align-items: baseline;
  cursor: pointer;
  padding: 10px 16px;
  margin: 0 -16px;
  border-radius: 0;
}
.anew__opt input { grid-row: 1 / span 2; align-self: center; margin: 0; }
.anew__opt .acct-row__sub { white-space: normal; line-height: 1.45; }

/* The credential groups. Four fields you may copy from a colleague's setup and
   three that are yours alone, so the split has to survive a glance. */
.anew__group { margin: 16px 0 4px; }
.anew__group + .anew__group { border-top: 1px solid var(--border); padding-top: 16px; }
.anew__group > .tpanel__title { padding: 0 16px; }
.anew__group > .hint { padding: 2px 16px 0; }

/* ⚠ LABEL, INPUT AND HINT STACKED — the same fix, for the same reason, as
   `.ps .field` two hundred lines above.
   `.field` is a horizontal flex by default, so a label beside its input put the
   two on one line and a hint beside the label wrapped mid-phrase: "PUBLIC
   IPIPV4 — MUST MATCH THE PRIMARY STATIC IP", read as one word. Position sizing
   fixed it once (`.ps .field`, with a comment saying exactly this) and
   `login.js` fixed it again (`.login-card .login-hint`) — both scoped to their
   own page, so neither was inherited. Three surfaces have now hit it; the
   horizontal default is the thing that is wrong. */
.anew .field { flex-direction: column; align-items: stretch; gap: 5px; }
.anew .field__label {
  font-size: 12.5px; color: var(--fg-1); font-weight: 500;
  letter-spacing: 0; text-transform: none;
}
.anew .finput { width: 100%; padding: 9px 11px; font-size: 14px; }
.anew .err:empty { display: none; }

/* Below the input, not glued to the label. A hint that must be read BEFORE
   typing (the TOTP seed is a seed, not a code) still reads first, because the
   field is empty and the eye lands on the box. */
.anew__hint {
  display: block; font-size: 11.5px; line-height: 1.45;
  color: var(--fg-2); font-weight: 400; letter-spacing: 0;
  text-transform: none;
}

/* An API key and a base32 seed are long enough that a half-width box shows the
   middle of them and nothing else. */
.anew__wide { grid-column: 1 / -1; }

/* The confirmation. A two-column definition list: what it is, and what it came
   out as. It briefly reused `.ps__row`, which is a FOUR-column grid for putting
   a formula beside its result — with no formula, three columns sat empty and
   each row grew to the height of the gap. */
.anew__done {
  display: grid; grid-template-columns: max-content 1fr;
  gap: 10px 18px; margin: 0; padding: 16px;
  align-items: baseline;
}
.anew__done-t { color: var(--fg-2); font-size: 12.5px; }
.anew__done-v { margin: 0; font-size: 13.5px; }
.anew__done-v .anew__hint { margin-top: 2px; }

/* The ⋯ that opens rename / archive / delete, inside a switcher row.
   Quiet by default: two of the three actions are destructive and this menu's
   ordinary job is a one-click switch, so it must be reachable and never the
   thing the eye lands on. Visible on hover AND on keyboard focus — focus as
   well, or it cannot be reached without a mouse. */
.acct-row { position: relative; }
.acct-row__more {
  position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
  opacity: 0; transition: opacity 90ms ease;
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0;
  background: transparent; border: 1px solid transparent; border-radius: var(--r);
  color: var(--fg-2); cursor: pointer;
}
.acct-row:hover .acct-row__more,
.acct-row__more:focus { opacity: 1; }
.acct-row__more:hover { background: var(--bg-3); border-color: var(--border); color: var(--fg-1); }

/* Reconnect. ⚠ ALWAYS VISIBLE, unlike the ⋯ beside it, and that is the whole
   difference between them: the ⋯ hides rename/archive/delete because they are
   things you occasionally want, while this is the account TELLING you it cannot
   trade. A fix that only appears on hover is a fix nobody finds on the row it
   was not already pointing at.

   ⚠ **A THIRD GRID LINE, NOT A FLOATING BUTTON, AND THE MEASUREMENT IS WHY.**
   It was `position: absolute` beside the text, which looked right at the 430px
   I mocked it at and is wrong at the 280px this menu actually is: the row went
   to 123px against 54px for a healthy one, with the name on two lines and the
   subtitle on four. Reserving horizontal room for a 76px button inside a 226px
   column cannot work — there is no padding value that fixes it, only a
   different place to put the button. */
.acct-row__fix {
  grid-column: 2; justify-self: start; margin-top: 4px;
  padding: 2px 8px; font-size: 11px; font-weight: 600; line-height: 1.6;
  border: 1px solid var(--warn); border-radius: var(--r);
  color: var(--warn); background: transparent;
  text-decoration: none; white-space: nowrap;
}
.acct-row__fix:hover { background: var(--warn); color: var(--bg-1); }

/* The Angel One setup guide in the Add-an-account wizard. Half of that setup
   happens on the broker's site, so this is the only place the two values a
   person must paste there are visible at all. */
.setup { padding: 14px 16px 4px; border-bottom: 1px solid var(--border); }
.setup__head { font-size: 12px; font-weight: 600; color: var(--fg-1);
               margin-bottom: 10px; }
.setup__list { list-style: none; margin: 0; padding: 0; }
.setup__step { display: grid; grid-template-columns: 20px 1fr; gap: 10px;
               padding: 7px 0; border-top: 1px dashed var(--border); }
.setup__step:first-child { border-top: 0; }
.setup__n { display: flex; align-items: center; justify-content: center;
            width: 20px; height: 20px; border-radius: 50%;
            border: 1px solid var(--border); color: var(--fg-2);
            font-size: 11px; line-height: 1; }
/* ⚠ Struck through AND ticked. Colour alone would say "done" to nobody using
   a screen reader, and this list is the one thing standing between a person
   and a vendor error that names the wrong field. */
.setup.is-ready .setup__step.is-done .setup__title,
.setup__step.is-done .setup__title { text-decoration: line-through;
                                     color: var(--fg-2); }
.setup__step.is-done .setup__n { border-color: var(--pass, #2ea043);
                                 color: var(--pass, #2ea043); }
.setup__title { font-size: 12.5px; font-weight: 600; color: var(--fg-1); }
.setup__note { font-size: 11.5px; color: var(--fg-2); line-height: 1.5;
               margin-top: 2px; }
.setup__extra { margin-top: 7px; }
.setup__row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
              margin-bottom: 6px; }
.setup__label { font-size: 11px; color: var(--fg-2); min-width: 108px; }
/* ⚠ `break-all` and `min-width: 0`: a redirect URL is long and lives in a
   ~400px panel. Without both it forces the whole wizard to scroll sideways. */
.setup__value { flex: 1 1 180px; min-width: 0; padding: 3px 7px;
                background: var(--bg-3); border: 1px solid var(--border);
                border-radius: var(--r); font-size: 11px;
                word-break: break-all; }
.setup__copy { flex: 0 0 auto; font-size: 11px; padding: 3px 9px; }
/* ⚠ `calc(100% - 108px)`, NOT `100%`. A flex basis of 100% PLUS a 108px margin
   is a box 108px wider than the row it sits in, which pushed the whole page
   into a horizontal scroll at 375px — measured, 67px of overflow. The basis and
   the indent have to add up to the row, not stack on it. */
.setup__hint { flex: 1 0 calc(100% - 108px); margin-left: 108px; }
/* Below ~430px the label column stops earning its width: the value wraps to
   almost nothing beside it. Stack instead, and the indent goes with it. */
@media (max-width: 430px) {
  .setup__label { min-width: 100%; }
  .setup__hint { flex-basis: 100%; margin-left: 0; }
}
/* The mark spans whatever the row grew to, so the ○ stays beside the name
   rather than centring itself against three lines. */
.acct-row.has-fix .acct-row__mark { grid-row: 1 / span 3; }

/* The action list. Ordered safe -> reversible -> permanent, and the last one
   is the only coloured thing in the switcher. */
.acct-actions {
  position: absolute; right: 6px; top: 100%; z-index: 70;
  min-width: 148px; padding: 4px;
  background: var(--bg-2); border: 1px solid var(--border-hi);
  border-radius: var(--r); box-shadow: 0 10px 28px rgba(0,0,0,.55);
}
.acct-actions button {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 7px 9px; border: 0; border-radius: 4px;
  background: transparent; color: var(--fg-1);
  font: inherit; font-size: 12.5px; text-align: left; cursor: pointer;
}
.acct-actions button:hover { background: var(--bg-3); }
.acct-actions button svg { flex: none; opacity: .85; }
/* Red on hover only. Permanently red, it reads as an error state on a menu the
   user opens to rename something. */
.acct-actions .is-danger:hover { background: var(--fail-bg); color: var(--fail); }

/* The confirm button on a destructive modal. Same shape as .btn--primary so the
   dialog does not change layout, and coloured so the last click before an
   irreversible one does not look like every other Save. */
.btn--danger { background: var(--fail); border-color: var(--fail); color: #fff; }
.btn--danger:hover:not(:disabled) { filter: brightness(1.08); }
.btn--danger:disabled { opacity: .45; cursor: not-allowed; }

/* An archived account: listed so a soft delete does not look like a hard one,
   dimmed and unselectable so the app does not offer what the server refuses.
   The ⋯ stays at full strength — rename and delete both still apply. */
.acct-row.is-archived { opacity: .5; cursor: default; }
.acct-row.is-archived:hover { background: transparent; }
.acct-row.is-archived .acct-row__more { opacity: 0; }
.acct-row.is-archived:hover .acct-row__more,
.acct-row.is-archived .acct-row__more:focus { opacity: 1; }

/* A confirmation that can be READ and ANSWERED on one screen.
   The delete dialog on a real account carries thirteen row counts, two warning
   paragraphs naming every position and every standing stop rule, and a
   type-to-confirm field — taller than a laptop viewport. As one block it pushed
   Cancel and Delete off the bottom, so the dialog could be read and not
   answered. Title and actions are pinned; only the middle scrolls.

   Scoped to `--scroll` so the buy and sell dialogs, which are short and rely on
   the plain .tmodal box, are untouched. */
.tmodal--scroll {
  display: flex; flex-direction: column;
  max-height: 86vh;
  width: 480px;                    /* 430 wrapped the symbol lists to ribbons */
}
.tmodal--scroll h3,
.tmodal--scroll .tmodal__foot,
.tmodal--scroll .actions { flex: none; }
.tmodal--scroll .tmodal__body {
  flex: 1 1 auto;
  min-height: 0;                   /* without it a flex child refuses to shrink */
  overflow-y: auto;
  overscroll-behavior: contain;    /* stop the page scrolling once this bottoms out */
  /* Bleed to the padding edge so the scrollbar tracks the dialog border rather
     than floating in the middle of the text. */
  margin: 0 -16px;
  padding: 0 16px;
}
/* The actions keep a visible edge once content scrolls under them. */
.tmodal--scroll .actions { margin-top: 12px; padding-top: 12px; }

/* The type-to-confirm field, pinned between the scroller and the buttons.
   A rule the reader must scroll to reach is a rule they answer without having
   read what is above it — so the one control that gates a permanent delete sits
   where the buttons are, always on screen. */
.tmodal--scroll .tmodal__foot {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.tmodal--scroll .tmodal__foot .field { margin: 0; }

/* ⚠ **THE GRADE PICKER IS `M.select`, LIKE EVERY OTHER DROPDOWN IN THE APP.**
   It was a native `<select>`, and the justification written here was that one
   per row would cost "a hundred mounts to solve a problem this placement does
   not have". Measured rather than assumed: 500 mount in ~10 ms, six DOM nodes
   each. The cost was imaginary and the inconsistency was not — `select.js` says
   it "replaces native <select> everywhere", an OS-drawn popup ignores every
   token in this file, and this cell sat inside a table that is nothing but
   those tokens.

   `.sel__btn` is 150px wide by default, which is right for a filter bar and
   absurd for a cell holding "B" — so the trigger is sized down here rather
   than the component being changed for everyone. */
.jgrade { display: inline-block; }
.jgrade .sel__btn { min-width: 56px; max-width: 76px; padding: 2px 7px;
                    font-size: 12px; gap: 5px; }
.jgrade .sel__pop { min-width: 76px; }
/* The note drawer's, and the notes tab's date order — same component, sized
   for where each one sits. */
.nd__gradesel .sel__btn { min-width: 64px; max-width: 84px; padding: 3px 8px;
                          font-size: 12px; gap: 5px; }
.notesort .sel__btn { min-width: 136px; padding: 5px 10px; font-size: 12px; }

/* A journal By-stock row opens that name's whole history. Signalled rather than
   left to be discovered: a clickable row that looks identical to a static one
   is a feature nobody finds. */
.rowlink { cursor: pointer; }
.rowlink:hover { background: var(--bg-1); }
/* The history table needs more width than a confirm dialog. */
.tmodal--wide { max-width: 900px; width: 92vw; }

/* ---------------------------------------------------------------- journal
   The judging half: the verdict row, the R distribution and grade x outcome.
   Everything here is a function of data the journal already returned — no new
   query, no new column — so the cost of showing it is this file. */

/* The coverage line under a stat. NOT decoration: expectancy over 1 of 16
   trades and over 16 of 16 are different claims, and this is where that gets
   said. Sized well below the value so it reads as a caveat, not a second
   number. */
.stat__sub { color: var(--fg-2); font-size: 10px; line-height: 1.35; margin-top: 2px; }

/* COVERAGE AS A SHAPE. `0 of 11` and `11 of 11` render identically as text —
   same size, same weight, same colour — and coverage is the most load-bearing
   fact on this page. The meter makes "this statistic rests on almost nothing"
   readable without the division. Full width is the whole book; the fill is what
   the figure above it actually measured. */
.stat__meter { height: 2px; margin-top: 4px; background: var(--bg-2);
               border-radius: 1px; overflow: hidden; }
.stat__meter i { display: block; height: 100%; background: var(--fg-2);
                 border-radius: 1px; }
/* Nothing measured at all: a hairline of the border colour rather than an empty
   track, for the reason `.rbar__fill--empty` carries one — absent and zero are
   different, and both need to be visible. */
.stat__meter--none i { background: var(--border); }
/* Fully covered: the figure above needs no caveat, so the meter stops being a
   warning and becomes a confirmation. */
.stat__meter--full i { background: var(--pass); }

/* The habit callout under the grade x R grid. One sentence, and it fires only
   above a threshold — a warning that is always on is one the reader learns to
   skip. Bordered rather than tinted so it reads as a note the table produced,
   not as an alert from somewhere else. */
.gradenote { display: flex; gap: 9px; align-items: flex-start;
             padding: 10px 16px 12px; border-top: 1px solid var(--border);
             color: var(--fg-1); font-size: 11.5px; line-height: 1.5; }
.gradenote__icon { flex: none; line-height: 1.5; }
.gradenote b { color: var(--fg-0); font-weight: 650; }
.gradenote--warn .gradenote__icon { color: var(--warn); }

/* ------------------------------------------------------- the P&L calendar
   Day by day — the one view of a book that reads the way trading feels.

   Mon-Fri only: NSE cash equity does not settle at the weekend, so a
   seven-column grid spends 2/7 of its width on cells that can never carry a
   number. Colour intensity is relative to the book's OWN biggest day (see
   `pnlCalendar`), which is why the bands are opacity steps over one hue rather
   than five hand-picked colours — the scale has to work on a Rs 1,400 account
   and a Rs 10,00,000 one without either being told what "big" means. */
.calwrap { display: flex; gap: 18px; flex-wrap: wrap; padding: 14px 16px; }
/* `max-width` matters more than the basis here: leading empty months are
   trimmed, so a book with one trading month renders ONE grid — and without a
   cap that single month stretches to the full panel and the day cells balloon
   into squares the size of buttons. */
.cal__month { flex: 1 1 220px; min-width: 0; max-width: 320px; }
.cal__mhead { display: flex; align-items: baseline; gap: 8px;
              padding-bottom: 6px; margin-bottom: 6px;
              border-bottom: 1px solid var(--border); }
.cal__mname { font-size: 11px; font-weight: 650; color: var(--fg-1);
              text-transform: uppercase; letter-spacing: 0.5px; }
.cal__mtot { margin-left: auto; font-family: var(--mono); font-size: 12px;
             font-weight: 650; color: var(--fg-2); }
/* Five columns until a weekend carries money, then seven — `pnlCalendar` sets
   --cal-cols from the data, because a grid that SKIPS Saturday drops any result
   that landed on one. */
.cal__month { --cal-cols: 5; }
.cal__dow, .cal__grid { display: grid; gap: 3px;
                        grid-template-columns: repeat(var(--cal-cols), 1fr); }
.cal__dow { margin-bottom: 3px; }
.cal__dow span { font-size: 9px; color: var(--fg-2); text-align: center;
                 letter-spacing: 0.4px; }
.cal__pad { visibility: hidden; }

.cal__day { position: relative; aspect-ratio: 1 / 1; border-radius: 4px;
            background: var(--bg-2); border: 1px solid transparent;
            display: flex; flex-direction: column; justify-content: space-between;
            padding: 3px 4px; overflow: hidden; }
.cal__n { font-size: 9px; color: var(--fg-2); line-height: 1; }
.cal__v { font-family: var(--mono); font-size: 9.5px; font-weight: 650;
          line-height: 1; white-space: nowrap; overflow: hidden;
          text-overflow: clip; }

/* A day nothing closed on. Deliberately the FLATTEST thing in the grid: it is
   not a zero, it is an absence, and the two must not read alike. */
.cal__day--none { background: transparent; border-color: var(--border); }
.cal__day--none .cal__n { opacity: 0.45; }

/* Breakeven: a real measured zero, so it gets a fill the untraded day does not. */
.cal__day--flat { background: var(--bg-3); }
.cal__day--flat .cal__v { color: var(--fg-2); }

.cal__day--pos .cal__n, .cal__day--neg .cal__n { color: var(--fg-1); }
.cal__day--pos .cal__v { color: var(--pass); }
.cal__day--neg .cal__v { color: var(--fail); }
.cal__day--pos.cal__b1 { background: rgba(69, 194, 106, 0.10); }
.cal__day--pos.cal__b2 { background: rgba(69, 194, 106, 0.22); }
.cal__day--pos.cal__b3 { background: rgba(69, 194, 106, 0.38); }
.cal__day--pos.cal__b4 { background: rgba(69, 194, 106, 0.58); }
.cal__day--neg.cal__b1 { background: rgba(242, 86, 76, 0.10); }
.cal__day--neg.cal__b2 { background: rgba(242, 86, 76, 0.22); }
.cal__day--neg.cal__b3 { background: rgba(242, 86, 76, 0.38); }
.cal__day--neg.cal__b4 { background: rgba(242, 86, 76, 0.58); }
/* At the top two bands the fill carries the meaning, so the number goes white
   rather than fighting its own background for contrast. */
.cal__day--pos.cal__b4 .cal__v, .cal__day--neg.cal__b4 .cal__v,
.cal__day--pos.cal__b3 .cal__v, .cal__day--neg.cal__b3 .cal__v { color: var(--fg-0); }

@media (max-width: 900px) {
  /* The rupee figure is the first thing to go: the colour still carries the
     day's shape, and a truncated number is worse than none. */
  .cal__v { display: none; }
  .cal__month { flex-basis: 160px; }
}

/* ------------------------------------------------------------ leak analysis
   Each row is priced, because a leak named without a number is an opinion. */
.leaks { display: flex; flex-direction: column; }
.leak { display: flex; align-items: baseline; gap: 12px; padding: 9px 16px;
        border-bottom: 1px solid var(--border); }
.leak:last-child { border-bottom: none; }
.leak__name { font-size: 12px; color: var(--fg-0); font-weight: 600;
              flex: 0 0 auto; }
.leak__why { font-size: 11px; color: var(--fg-2); flex: 1 1 auto;
             line-height: 1.45; }
.leak__n { font-family: var(--mono); font-size: 10.5px; color: var(--fg-2);
           flex: 0 0 auto; }
.leak__cost { font-family: var(--mono); font-size: 13px; font-weight: 650;
              flex: 0 0 auto; text-align: right; min-width: 84px; }

/* The trade log's pager. Rendered even at "Page 1 of 1", with both controls
   disabled: a pager that only APPEARS when there is more is one nobody knows to
   look for, which is exactly the state this page was in when 557 events sat
   unreachable behind row 50. */
.tlpager { display: flex; align-items: center; gap: 14px; justify-content: center;
           padding: 12px 16px; border-top: 1px solid var(--border); }
.tlpager__at { font-family: var(--mono); font-size: 11.5px; color: var(--fg-2);
               min-width: 110px; text-align: center; }
.tlpager .chipbtn[disabled] { opacity: 0.4; cursor: default; }

/* The housekeeping toggle sits in the panel head with the other filters. */
.tl-hk { display: inline-flex; align-items: center; gap: 5px;
         font-size: 11.5px; color: var(--fg-2); cursor: pointer;
         white-space: nowrap; }
.tl-hk input { accent-color: var(--accent); cursor: pointer; }

/* ======================================================== THE NOTE CANVAS
   ONE TRADE, ONE NOTE. It briefly held a list (0020) and the drawer grew folded
   cards and a "+ New note" control to match; 0021 put a unique index on it and
   those went with it. The drawer IS the note.

   ⚠ WRITE AND READ WERE TABS AND ARE NOT ANY MORE. You wrote in one and looked
   at the result in the other, so a pasted screenshot was a line of markdown
   while writing and a picture only after switching. One surface now: the text,
   the charts as charts, and a Save button that lights up when there is
   something to save.

   Reuses `.drawer` and `.scrim` from the stock drawer — one scrim in the DOM,
   one Escape shape — and widens, because this one is written in rather than
   read. */
/* ⚠ ABOVE THE MENUS, unlike the stock drawer. Column and view menus sit at
   z-index 60-81 and the stock drawer at 50, which is fine for a panel you open
   FROM a table — but this one is opened from a row while a picker may still be
   up, and a menu floating over a modal you are typing into is wrong. */
.drawer--note { width: min(900px, 96vw); display: flex; flex-direction: column;
                overflow: hidden; z-index: 90; }
.scrim--note { z-index: 89; }
/* ⚠ THE HEAD NAMES THE TRADE, NOT THE NAME. "BAJAJFINSV" alone does not say
   which of two trades in it you have open; the span it was held between does,
   and the size confirms it. */
.nd__id { min-width: 0; }
.nd__sym { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap;
           font-family: var(--mono); font-size: 16px; font-weight: 650;
           color: var(--fg-0); }
.nd__span { font-family: var(--sans); font-size: 12px; font-weight: 500;
            color: var(--fg-1); border: 1px solid var(--border);
            border-radius: 999px; padding: 1px 9px; white-space: nowrap; }
.nd__livetag { font-family: var(--sans); font-size: 10px; font-weight: 600;
               text-transform: uppercase; letter-spacing: 0.5px;
               color: var(--warn); border: 1px solid var(--warn);
               border-radius: 4px; padding: 0 5px; }
.nd__size { color: var(--fg-1); }
.nd__facts { font-size: 11.5px; color: var(--fg-2); margin-top: 4px; }
.nd__facts .dot2 { opacity: 0.5; margin: 0 3px; }
/* ⚠ **THERE IS NO "Done" BUTTON.** The head carried one beside the footer's
   Save, and the two read as competing actions for one intent — Done saved and
   closed, Save saved, and nothing said which you wanted. Save is the action; ✕
   is a dismissal, which is a different kind of thing and is drawn like one. */
.nd__x { flex: none; background: none; border: none; color: var(--fg-2);
         font-size: 22px; line-height: 1; padding: 0 4px; cursor: pointer; }
.nd__x:hover { color: var(--fg-0); }
.nd__grade { display: flex; align-items: center; gap: 8px; flex: none;
             font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;
             color: var(--fg-2); }
/* The app's own select needs a block host to mount into; 68px is enough for
   "A+" and the chevron without the trigger swallowing the head. */
.nd__gradesel { display: inline-block; }

/* ⚠ **THE NOTE FILLS THE DRAWER, TOP TO BOTTOM.** It used to size to its
   content and leave a field of empty drawer under it, which reads as a form
   that has finished rather than a page you are still writing on.

   `min-height:0` on both is what makes it work: without it a flex child's
   intrinsic height wins and neither the list nor the textarea inside it ever
   gives up space, so nothing bounds and nothing scrolls. */
.nd__list { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 20px 20px;
            display: flex; flex-direction: column; gap: 10px; }

/* ------------------------------------------------------------- the note
   Title, body, charts and a footer, in one bordered block that reads as a
   sheet of paper rather than as three controls that happen to be adjacent. */
.ncanvas--wait { min-height: 160px; display: flex; align-items: center;
                 justify-content: center; }
.ncanvas { border: 1px solid var(--border-hi, var(--border));
           border-radius: var(--r-lg); background: var(--bg-1);
           display: flex; flex-direction: column;
           flex: 1 1 auto; min-height: 320px; }
.nd__title { background: none; border: none; border-bottom: 1px solid var(--border);
             color: var(--fg-0); font: inherit; font-size: 15px; font-weight: 650;
             padding: 13px 16px; border-radius: var(--r-lg) var(--r-lg) 0 0; }
.nd__title:focus { outline: none; background: var(--bg-0); }
.nd__title::placeholder { color: var(--fg-2); opacity: 0.6; font-weight: 400; }
/* `flex:1` + `min-height:0` so the writing area takes whatever the title, the
   charts and the footer leave — and `resize:none` because a drag handle on a
   box that already fills its container only makes it disagree with the drawer. */
.nd__text { background: none; color: var(--fg-1); border: none; resize: none;
            flex: 1 1 auto; min-height: 120px; font: inherit; font-size: 13.5px;
            line-height: 1.7; padding: 12px 16px; }
.nd__text:focus { outline: none; color: var(--fg-0); }
.nd__text::placeholder { color: var(--fg-2); opacity: 0.6; }
/* Bounded, and scrolls on its own: six pasted charts must not push the Save
   button off the bottom of a drawer. */
.nd__shots { flex: none; max-height: 168px; overflow-y: auto; }
.nd__shots:not(:empty) { padding: 0 16px 4px; }
.ncanvas__foot { flex: none; display: flex; align-items: center; gap: 10px;
                 padding: 10px 12px 10px 16px; border-top: 1px solid var(--border); }
.nd__when { font-size: 10.5px; color: var(--fg-2); }
.nd__state { font-size: 10.5px; color: var(--fg-2); }
.nd__state--saving { color: var(--fg-1); }
.nd__state--saved { color: var(--pass); }
/* THE POINT THAT SAYS SOMETHING CHANGED. A word alone is read only when looked
   for; the dot is what you catch out of the corner of your eye. */
.nd__state--unsaved { color: var(--warn); }
.nd__state--error { color: var(--fail); }
.nd__save--dirty { box-shadow: 0 0 0 3px rgba(219,163,42,0.22); }
.chipbtn--danger { border-color: #7a2b28; color: #ff8b84; }
.chipbtn--danger:hover { background: var(--fail-bg); border-color: var(--fail); }

.nd__loose { border: 1px dashed var(--border); border-radius: var(--r-lg);
             padding: 10px 14px; }
.nd__loosehd { font-size: 11px; color: var(--fg-2); margin-bottom: 8px; }

/* ------------------------------------------------------------ the gallery
   ⚠ A PASTED SCREENSHOT IS NOT MARKDOWN IN THE PROSE ANY MORE. It used to
   insert `![chart](/api/journal/image/12)` at the caret — a line of syntax
   where the writer expected a chart. Charts are attachments now: thumbnails the
   moment they land, FULL SIZE on click, which is what a chart is pasted for. */
.ngal { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 0; }
.ngal__shot { padding: 0; border: 1px solid var(--border); background: var(--bg-0);
              border-radius: var(--r-sm, 6px); cursor: zoom-in; overflow: hidden;
              line-height: 0; }
.ngal__shot:hover { border-color: var(--accent); }
.ngal__shot img { display: block; object-fit: cover; }
.ngal--md .ngal__shot img { width: 190px; height: 118px; }
.ngal--sm .ngal__shot img { width: 118px; height: 74px; }

/* Full size, on a black ground, one click away from gone. A thumbnail answers
   "is there a chart"; only this answers "what was the setup". */
.lbox { position: fixed; inset: 0; z-index: 120; background: rgba(0,0,0,0.88);
        display: flex; align-items: center; justify-content: center;
        padding: 28px; cursor: zoom-out; }
.lbox__img { max-width: 100%; max-height: 100%; object-fit: contain;
             border-radius: 4px; }
.lbox__x { position: absolute; top: 14px; right: 18px; background: none;
           border: none; color: #fff; font-size: 30px; line-height: 1;
           cursor: pointer; opacity: 0.75; }
.lbox__x:hover { opacity: 1; }

/* An image inside rendered prose — an external `![](https://…)` somebody wrote
   as markdown. Our own attachments are pulled out into the gallery instead. */
.noteimg { display: block; max-width: 100%; max-height: 420px; width: auto;
           margin: 10px 0; border: 1px solid var(--border);
           border-radius: var(--r-sm, 6px); background: var(--bg-0); }

/* ------------------------------------------------------ the affordance
   A BUTTON, NOT AN INPUT — a note is a document you open, not a cell you tab
   through. It carries STATE, so a table of trades says at a glance which ones
   have been thought about, and it is the same control on every screen. */
.notebtn { display: inline-flex; align-items: center; gap: 7px; max-width: 260px;
           background: none; border: 1px dashed var(--border); cursor: pointer;
           border-radius: 999px; padding: 3px 11px 3px 8px; color: var(--fg-2);
           font: inherit; font-size: 11.5px; }
.notebtn:hover { border-color: var(--accent); color: var(--fg-0); }
.notebtn__icon { font-size: 9px; line-height: 1; flex: none; opacity: 0.8; }
.notebtn__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Written rows read as SOLID, blank ones as dashed and quiet. The difference
   has to survive a glance down a column of forty. */
.notebtn--has { border-style: solid; border-color: var(--border-hi, var(--border));
                color: var(--fg-1); }
.notebtn--has .notebtn__icon { color: var(--accent); opacity: 1; }
.notebtn__shots { flex: none; font-size: 10px; color: var(--fg-2);
                  font-family: var(--mono); }
.td-note { max-width: 280px; }
/* The By-stock cell: same control, sized for a table cell. */
.notebtn--mini { padding: 1px 8px; font-size: 10.5px; gap: 5px; }

/* ------------------------------------------------------- the notes tab
   A LIST OF DOCUMENTS, READ THE WAY A LIST OF DOCUMENTS IS READ: a heading, an
   opening, a date, and enough of a label to know what it is about. Pouring
   every paragraph onto this surface is what made the last version unreadable
   the moment one note ran past a few lines.

   ⚠ **THE LIST SCROLLS AND THE SEARCH BAR DOES NOT.** A search box that
   scrolls away the moment you use it is one you have to scroll back up to
   correct. The panel is bounded to the viewport and the list scrolls inside
   it, so the bar stays put without floating over the stat cards above.

   ⚠ **THERE IS NO "Notes" TITLE ROW.** The tab is already called Notes and is
   already the selected one; a panel heading repeating it is chrome that says
   nothing, and that row is worth more as the search bar. */
.tpanel--notes { display: flex; flex-direction: column;
                 max-height: calc(100vh - 250px); min-height: 320px; }
.notesbar { flex: none; display: flex; align-items: center; gap: 10px;
            padding: 10px 14px; border-bottom: 1px solid var(--border);
            background: var(--bg-1); border-radius: var(--r-lg) var(--r-lg) 0 0; }
.notesearch { flex: 1; min-width: 0; background: var(--bg-0); color: var(--fg-0);
              border: 1px solid var(--border); border-radius: 999px;
              padding: 6px 14px; font: inherit; font-size: 12.5px; }
.notesearch:focus { outline: none; border-color: var(--accent); }
.notesearch::placeholder { color: var(--fg-2); }
.notesort { flex: none; }
.notesbar__count { flex: none; font-size: 11px; color: var(--fg-2);
                   min-width: 62px; text-align: right; }
.notes { flex: 1; min-height: 0; overflow-y: auto;
         display: flex; flex-direction: column; gap: 10px; padding: 14px 16px; }
.notes .notecard[hidden] { display: none; }

/* `position: relative` + a floated tag rather than a flex header row: the tag
   sits in the corner and the heading flows the full width under it. */
.notecard { position: relative; border: 1px solid var(--border);
            border-radius: var(--r-lg); background: var(--bg-1);
            padding: 13px 15px; }
.notecard::after { content: ""; display: block; clear: both; }
.notecard__tag { float: right; margin: 0 0 6px 14px; background: none;
                 border: 1px solid var(--border); border-radius: 999px;
                 padding: 2px 10px; cursor: pointer; color: var(--fg-2);
                 font: inherit; font-size: 10.5px; white-space: nowrap; }
.notecard__tag:hover { border-color: var(--accent); color: var(--fg-1); }
.notecard__tag .dot { opacity: 0.45; margin: 0 1px; }
.notecard__open { cursor: pointer; }
.notecard__open:focus-visible { outline: 2px solid var(--accent);
                                outline-offset: 4px; border-radius: 4px; }
.notecard__title { font-size: 13.5px; font-weight: 650; color: var(--fg-0); }
.notecard__open:hover .notecard__title { color: var(--accent); }
.notecard__excerpt { font-size: 12.5px; line-height: 1.6; color: var(--fg-2);
                     margin-top: 4px; overflow: hidden; display: -webkit-box;
                     -webkit-line-clamp: 2; -webkit-box-orient: vertical;
                     line-clamp: 2; }
.notecard__meta { font-size: 10.5px; color: var(--fg-2); margin-top: 6px;
                  opacity: 0.8; }
.notecard__meta .dot { opacity: 0.5; }

.gradechip { flex: none; font-family: var(--mono); font-size: 11px;
             font-weight: 650; color: var(--fg-1); border: 1px solid var(--border);
             border-radius: 4px; padding: 1px 7px; }
.gradechip--sm { font-size: 10px; padding: 0 4px; border-radius: 3px; }

/* ⚠ THE BY-STOCK DRAWER HAS NO NOTES PANEL. It briefly had one — `.shnotes` —
   repeating every note for the name below the trades, which said exactly what
   that table's own Note column already said, one row at a time. The column is
   the answer; a second surface reading the same rows is duplication. */

/* The percentage beside a rupee figure in a stat card — a broker's header
   idiom. Deliberately quieter and smaller than the money: the rupees are the
   fact, the percentage is the scale of it, and two figures at equal weight on
   one line makes the reader choose which to read first. */
.stat__pct { margin-left: 7px; font-size: 12px; font-weight: 600;
             font-family: var(--mono); }
.stat__pct.pos { color: var(--pass); }
.stat__pct.neg { color: var(--fail); }

/* The table's twin of `.stat__pct`. Same relationship — the money is the fact,
   the percentage is the scale of it — at the smaller weight a dense table needs.
   Uncoloured by default so a RISK percentage inherits the cell rather than
   claiming a direction it does not have. */
.cellpct { margin-left: 6px; font-size: 10.5px; font-weight: 600;
           color: var(--fg-2); }
.cellpct.pos { color: var(--pass); }
.cellpct.neg { color: var(--fail); }

/* ---- the risk badge ------------------------------------------------------
   The third state the header never had. `--pass` and `--fail` are a binary and
   the ceiling is not: a book at 85% of its Max TOR has breached nothing and is
   one ordinary trade from doing so, which is neither green nor red. Amber is
   that state, and `--warn` is the token the calendar banner and the surveillance
   tags already use for it — so a caution here reads the same as a caution
   anywhere else in the app. */
.stat__value--warn { color: var(--warn); }

/* THE STANCE SITS IN THE TOP-RIGHT CORNER, because it is the SETTING the rest
   of the bar is measured against rather than another measurement — the ceiling
   in "Open risk … of 3.00%", and the thing that moves every band when it
   changes.

   ⚠ **`margin-left: auto` IS NOT ENOUGH, and it fails in the one case it was
   added for.** An auto margin only absorbs free space on the line the item
   already sits on. The bar carries ten cards and wraps, so the stance did not
   reach line 1 at all — it wrapped to line 2 and was then pushed to the right
   edge of an otherwise empty row, which reads as an orphan rather than as a
   corner. That is worse than leaving it in the flow.

   Taken OUT of the flow instead, so "top right" is true at every width rather
   than only when the cards happen to fit. The bar reserves the corner with
   `padding-right`, so a wrapping card can never run underneath it.

   ⚠ Scoped away from `--merged` (the journal's bar), which has no stance card
   and would otherwise carry 200px of padding for nothing. */
.dash__statbar:not(.dash__statbar--merged) { position: relative; padding-right: 200px; }
.dash__statbar .stat--right {
  position: absolute; top: 12px; right: 16px;
  margin-left: 0; text-align: right; max-width: 184px;
}
@media (max-width: 1100px) {
  /* Narrow, the bar is a stack of equals and a floating corner would sit on
     top of it. Back into the flow, left-aligned like everything else. */
  .dash__statbar:not(.dash__statbar--merged) { padding-right: 16px; }
  .dash__statbar .stat--right {
    position: static; text-align: left; max-width: none;
  }
}

/* The word beside the numbers. A colour alone is a claim only a sighted reader
   who knows the convention can decode, and this one governs real money. */
.riskchip { display: inline-block; padding: 0 5px; border-radius: 3px;
            font-size: 9.5px; font-weight: 700; letter-spacing: 0.04em;
            border: 1px solid transparent; margin-right: 3px; }
.riskchip--safe    { color: var(--pass); border-color: #1f6b3a; background: var(--pass-bg); }
.riskchip--caution { color: var(--warn); border-color: #6b5310; background: var(--warn-bg); }
.riskchip--danger  { color: var(--fail); border-color: #6b2020; background: var(--fail-bg); }


/* ONE BAR, SPREAD EDGE TO EDGE.
   The counting row and the verdict row were two stacked bars, and each laid its
   cards out with `gap: 30px` and no `justify-content` — so both bunched against
   the left and left a growing well of dead space on the right as the window
   widened. Merged and given `flex: 1 1 <basis>`, the eight cards divide the full
   width instead, which is the same information using the space it already had.

   ⚠ THE TWO KINDS OF STATISTIC ARE STILL DIFFERENT and the merge must not lose
   that: "11 closed trades" is inventory and expectancy is a verdict on it. The
   stacked layout said so with a second bar; this says it with a hairline, which
   is the cheapest divider that survives wrapping. */
.dash__statbar--merged { gap: 0; padding: 0; align-items: stretch; }
/* `:not()` rather than a bare `> div`: the separator is a div too, and
   `.dash__statbar--merged > div` (0,1,1) outranks `.statbar__sep` (0,1,0) — so
   the divider took the CARD rule, inherited `flex: 1 1 132px` and the padding,
   and rendered as a full-width block painted in the border colour instead of a
   hairline. Excluding it here is what makes the two rules stop fighting. */
.dash__statbar--merged > div:not(.statbar__sep) {
  flex: 1 1 132px; min-width: 0; padding: 11px 16px;
}
.dash__statbar--merged > .statbar__sep {
  flex: 0 0 1px; padding: 0; margin: 10px 0; background: var(--border);
}

@media (max-width: 900px) {
  /* Wrapped into two or three rows the hairline lands mid-row and reads as a
     stray mark rather than a boundary, so it goes. */
  .statbar__sep { display: none; }
}

/* ---- the journal's panel tabs ------------------------------------------
   Three panels used to stack: R distribution, Grade x R, By stock and Trades,
   one under the other, which is a lot of page to scroll past to reach the
   trades — and the trades are what the page is for. Tabs make the choice
   explicit and keep the stat bar, which is the summary of all of them, always
   visible above. */
.jtabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); }

/* ---- the tab that holds a table fills the window -----------------------
   ⚠ **`.tablewrap--auto`'s `max-height: 46vh` IS THE WRONG CAP FOR A TAB.**
   That rule was written for the dashboard, where two panels share a column and
   neither may eat the other. The journal shows ONE panel at a time, so 46vh
   left the trades table ending just past the halfway mark with the bottom half
   of the window empty beneath it — measured at 1440x900: a table 414px tall
   under 300px of nothing, showing 10 of 13 closed trades on a page with room
   for 20.

   The chain has to be unbroken from the scroller down, or `flex: 1` resolves
   against a parent that is still sizing to its content and nothing grows:
   `#page-root` → `.dash` → `.tpanel--fill` → `.tablewrap--auto`. `min-height:0`
   at every step is what lets each shrink below its content so the innermost
   scroller is the one that engages — the same pair `.page` and `#page-root`
   already carry, for the reason their comment gives.

   Scoped by `[data-page]` to the pages that show ONE panel — the journal and
   the watchlist. Measured on /watchlist at 1440x860: a 486px panel in a 786px
   page, its table capped at 395.6px with 272px of nothing under it, scrolling
   17 rows through a window with room for far more. Every other `#page-root`
   page still scrolls as a whole, which is right for pages that stack several
   panels. */
/* A figure the server RECONSTRUCTED rather than read — the opening quantity of
   a position adopted from a broker, recovered from its fill in the trade log.
   Dotted underline, the same signal `abbr` uses for "there is more to this",
   and it carries the explanation on hover. Deliberately quiet: the number is
   almost certainly right (it reproduces all six rows that stored one), so this
   is a provenance mark, not a warning. */
.td-derived {
  border-bottom: 1px dotted var(--fg-2); cursor: help;
}

#page-root[data-page="journal"],
#page-root[data-page="watchlist"],
#page-root[data-page="portfolio"] { display: flex; flex-direction: column; }
#page-root[data-page="journal"] > .dash,
#page-root[data-page="watchlist"] > .dash,
#page-root[data-page="portfolio"] > .dash {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
}

/* The portfolio page: the table it is named after, and the alert list beside
   it rather than under it. 30% for the rail, on the owner's number — alerts
   here are the FULL list, not the dashboard's clipped preview, so they earn a
   share of the width instead of the dashboard's fixed 260px.
   `min-width: 0` on the main column or a wide table refuses to shrink and
   pushes the rail off the grid. */
.pfgrid {
  display: grid; grid-template-columns: minmax(0, 1fr) 30%; gap: 14px;
  flex: 1 1 auto; min-height: 0; align-items: stretch;
}
.pfgrid__main, .pfgrid__rail {
  display: flex; flex-direction: column; gap: 14px; min-width: 0; min-height: 0;
}
/* Below this the two columns stack, and a 30% rail would be unreadable. */
@media (max-width: 1100px) { .pfgrid { grid-template-columns: 1fr; } }
.tpanel--fill {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 220px;
}
/* `.scrolly` beside the table: the portfolio's alert rail is a filling panel
   holding a list rather than a table, and without this it grows past the
   panel instead of scrolling inside it. */
.tpanel--fill > .tablewrap--auto,
.tpanel--fill > .scrolly {
  flex: 1 1 auto; max-height: none; min-height: 0; overflow-y: auto;
}
.jtab { appearance: none; background: none; border: none; cursor: pointer;
        font-family: inherit; font-size: 12px; font-weight: 600; color: var(--fg-2);
        padding: 8px 14px; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.jtab:hover { color: var(--fg-0); }
.jtab.is-active { color: var(--fg-0); border-bottom-color: var(--accent); }
/* The count rides along so a tab says how much is behind it before it is
   opened — otherwise the choice is blind. */
.jtab__n { color: var(--fg-2); font-weight: 500; margin-left: 5px; }
.jtab.is-active .jtab__n { color: var(--fg-1); }

.rstrip { display: flex; align-items: flex-end; gap: 10px; padding: 14px 16px 10px; }
.rbar { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 0; }

/* Fixed-height track so an empty bucket still occupies its column — the SHAPE
   is the information, and a strip that collapses where there is no data reads
   as a narrower distribution rather than an absent one. */
.rbar__track { width: 100%; height: 64px; display: flex; align-items: flex-end;
               background: var(--bg-2); border-radius: 3px; overflow: hidden; }
.rbar__fill { width: 100%; border-radius: 3px 3px 0 0; min-height: 2px;
              background: var(--pass); }
.rbar__fill.neg { background: var(--fail); }
.rbar__fill.pos { background: var(--pass); }
/* A hairline, not nothing: the grid stays readable across empty buckets. */
.rbar__fill--empty { min-height: 1px; background: var(--border); }

.rbar__n { font-family: var(--mono); font-size: 12px; font-weight: 650; }
.rbar__label { color: var(--fg-2); font-size: 9px; letter-spacing: 0.3px;
               white-space: nowrap; }

@media (max-width: 720px) {
  /* The two tail buckets carry the most information on a momentum book, so the
     labels shrink rather than the strip scrolling. */
  .rbar__label { font-size: 8px; }
  .rstrip { gap: 5px; padding: 12px 8px 8px; }
}

/* ------------------------------------------------------------ global search
   The ⌘K box in the nav. Panel is absolutely positioned under the input so
   no page layout moves; rows are one flex line each — symbol, name, chips,
   price — matching the tables' type scale. */
/* ⚠ **`min-width` IS A FLOOR, AND WITHOUT IT THIS WAS 22px WIDE.** It was the
   only nav child that could shrink at all — `.nav__links` and `#acct-switch`
   were pinned by `min-width: auto` — so it absorbed the whole 315px of
   overflow and became a search box you cannot read what you typed into. A
   control that is present and useless is the failure this codebase deletes on
   sight, and the layout produced one on its own. 150px is roughly twenty
   characters. It is still the FIRST thing to give (see `flex-shrink` in the
   nav block above), just not to nothing. */
/* ⚠ **THE SHRINK FACTOR LIVES IN THE SHORTHAND, OR IT DOES NOT LIVE.** It was
   first written as `.gsearch { flex-shrink: 8 }` up in the nav block, which
   did nothing: this `flex` shorthand is declared later at the same
   specificity and RESETS `flex-shrink` to 1. Measured — the search gave 30px
   where it should have given 110, and the links paid the difference. */
.gsearch { position: relative; margin: 0 12px; flex: 0 8 340px; min-width: 150px; }
.gsearch__input {
  width: 100%; height: 30px; padding: 0 10px;
  background: var(--bg-2);
  border: 1px solid var(--border); border-radius: 6px;
  color: var(--fg-0); font: 400 13px var(--sans);
}
.gsearch__input:focus { outline: none; border-color: var(--accent); }
.gsearch__panel {
  position: absolute; top: 36px; left: 0; right: 0; z-index: 60;
  max-height: 480px; overflow-y: auto;
  background: var(--bg-1); border: 1px solid var(--border-hi);
  border-radius: 8px; box-shadow: 0 12px 32px rgba(0,0,0,.35);
  min-width: 480px;
}
.gs-head {
  padding: 8px 12px 4px; font: 600 10px var(--sans);
  letter-spacing: .08em; text-transform: uppercase; color: var(--fg-2);
}
.gs-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 12px; cursor: pointer;
}
.gs-row.is-active, .gs-row:hover { background: var(--bg-3); }
.gs-sym { font: 600 13px var(--mono); white-space: nowrap; }
.gs-star { color: var(--accent); font-size: 11px; }
.gs-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font: 400 12px var(--sans); color: var(--fg-1);
}
.gs-chip {
  flex: none; padding: 1px 6px; border-radius: 4px;
  font: 600 10px var(--sans); color: var(--fg-1);
  border: 1px solid var(--border);
}
.gs-chip--fno { color: var(--accent); border-color: var(--accent); }
.gs-chip--isin { color: var(--pass); border-color: var(--pass); }

/* WHAT A ROW IS — one chip, five surfaces, `M.instrumentKindChip`.
   Same metrics as `.gs-chip` so a kind reads as the same kind of fact
   wherever it appears.

   ⚠ **THE COLOUR IS THE SEGMENT — the label's FIRST word — and it used to be
   the family.** Muted for cash equity, accent for a derivative, amber for a
   commodity: three colours across two axes, so `Equity stock` and
   `Equity option` rendered as different classes of thing while sharing a
   word, and the stock looked second-class beside the contracts on its own
   watchlist. Asked directly: *"why is the tag of the equity stock not blue
   like the others? why is it different?"*

   It is not different now. Every `Equity …` is accent and every
   `Commodity …` is amber, so the colour and the first word say the same
   thing and the label's second word carries the family on its own. Commodity
   keeps its own colour because that is the distinction worth seeing across a
   list — its margin, its lot and its settlement all differ, and ADR-0055
   keeps it read-only. */
.ik {
  display: inline-block; flex: none; padding: 1px 6px; border-radius: 4px;
  font: 600 10px var(--sans); color: var(--accent);
  border: 1px solid var(--accent); white-space: nowrap;
}
.ik--comm { color: var(--warn); border-color: var(--warn); }
.gs-px { flex: none; font: 500 12px var(--mono); }
.gs-px--none { color: var(--fg-2); }
.gs-chg.is-up { color: var(--pass); }
.gs-chg.is-dn { color: var(--fail); }
.gs-empty { padding: 14px 12px; font: 400 12px var(--sans); color: var(--fg-1); }

/* ------------------------------------------------------- lean drawer + F&O
   Phase 3: the drawer's non-equity pane and the EQ Derivatives strip. */
.chiprow { display: flex; gap: 6px; margin: 4px 0 8px; flex-wrap: wrap; }
.chip {
  padding: 2px 10px; border-radius: 999px; cursor: pointer;
  font: 500 11px var(--sans); color: var(--fg-1);
  background: var(--bg-2); border: 1px solid var(--border);
}
.chip:hover { border-color: var(--border-hi); color: var(--fg-0); }
.chip.is-on { color: var(--accent); border-color: var(--accent); cursor: default; }
.lean-facts {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px 18px; margin: 0 0 14px;
}
.lf__k { display: block; font: 600 10px var(--sans); letter-spacing: .07em;
         text-transform: uppercase; color: var(--fg-2); }
.lf__v { font: 500 13px var(--mono); color: var(--fg-0); }
.fno-ban {
  margin: 0 0 8px; padding: 6px 10px; border-radius: var(--r);
  background: var(--fail-bg); color: var(--fail);
  font: 500 12px var(--sans);
}
/* Futures and Options under one `Derivatives` heading. Smaller than
   `.sectitle` — these are parts of that section, not siblings of it. */
.fno-sub {
  display: flex; align-items: baseline; gap: 8px;
  font: 600 11px var(--mono); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--fg-1); margin: 14px 0 2px;
}
.fno-sub:first-child { margin-top: 0; }
.fno-sub__n {
  font: 400 10px var(--mono); letter-spacing: 0; text-transform: none;
  color: var(--fg-2);
}
.fno-say {
  margin: 0 0 8px; font-size: 11.5px; line-height: 1.5; color: var(--fg-2);
  max-width: 62ch;
}

/* ⚠ **`.fno-strip` / `.fno-row` ARE GONE — a `1fr` in the middle of five
   columns.** That grid read fine in a 760px drawer and fell apart at the
   1,190px the instrument page gives it: the free track threw OI and the lot
   value to opposite ends, so three expiries read as three unrelated bars
   rather than as a curve, which is the one thing a strip of them is for. It
   is the app's own `.tbl` now, so a column can be compared down. */
.fno-wrap { margin: 0 0 4px; }
/* Bounded, or a five-column table on a 1,190px page puts `Expiry` and
   `Lot value` a foot apart with white space between every figure. */
.fno-t { max-width: 780px; }
.fno-t tbody tr { cursor: pointer; }
.fno-t tbody tr:hover { background: var(--bg-2); }
.fno-lot { color: var(--fg-2); }

/* The explanation, folded. Worth reading once, noise on the ninth open — the
   same call `.studies` makes with its Hide toggle. */
.fno-why { margin: 0 0 8px; }
.fno-why > summary {
  cursor: pointer; list-style: none;
  font: 500 11px var(--sans, inherit); color: var(--fg-2);
}
.fno-why > summary::-webkit-details-marker { display: none; }
.fno-why > summary::before { content: '▸ '; }
.fno-why[open] > summary::before { content: '▾ '; }
.fno-why > summary:hover { color: var(--fg-1); }
.fno-why .fno-say { margin: 6px 0 0; }

/* ⚠ **THE THREE NUMBERS THAT FRAME THE LADDER, ABOVE IT.** They were a
   footnote under twenty-six rows — read only by someone who had already read
   everything they explain. `as of` most of all: an end-of-day chain that does
   not date itself reads as live. */
.chain__facts {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  margin: 6px 0 8px; padding: 7px 11px;
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--r);
}
.chain__fact { font: 500 12px var(--mono); color: var(--fg-0); }
.chain__fact b {
  font: 600 9px var(--sans, inherit); letter-spacing: .06em;
  text-transform: uppercase; color: var(--fg-2); margin-right: 6px;
}
.chain__fact--asof { margin-left: auto; color: var(--fg-1); }

/* -------------------------------------------------------- the option chain */
.chain__gap { flex: 1; }
/* ⚠ **BOUNDED, OR THE CHIPS SIT A METRE APART.** `.chain__gap` is a `flex: 1`
   between the expiry chips and the ±window chips — right in a 760px drawer,
   and at 1,190px it puts the two halves of one control at opposite ends of the
   page. The ladder itself is seven columns of numerals; past ~1,000px the
   crosshair between a premium and its strike stops being followable. */
.fno-panel .chiprow, .fno-panel .chain__t,
.fno-panel .chain__facts { max-width: 1000px; }
.chain__t { width: 100%; border-collapse: collapse; font: 500 11px var(--mono); }
.chain__t th {
  padding: 4px 6px; font: 600 9px var(--sans); letter-spacing: .06em;
  color: var(--fg-2); text-transform: uppercase; text-align: center;
}
.chain__t td { padding: 3px 6px; text-align: right; color: var(--fg-1); }

/* ⚠ **THE OI BAR IS THE ANSWER TO WHAT A CHAIN IS FOR.** *Where are the walls*
   took reading twenty-six rows twice. It is a BACKGROUND behind the figure and
   never a replacement for it — a bar is a shape and a decision is made on a
   number, so the number stays exactly where it was, at the same size, on top.
   ⚠ **AND IT GROWS AWAY FROM THE STRIKE.** The calls side is rendered
   reversed, so anchoring both to the same edge would mirror one of them into
   reading backwards against the other. */
.chain__oi { position: relative; }
.chain__oi > span { position: relative; z-index: 1; }
.chain__oi.has-oi::before {
  content: ''; position: absolute; top: 2px; bottom: 2px;
  width: var(--oi); border-radius: 2px; z-index: 0;
  background: rgba(41, 98, 255, .16);
}
.chain__oi.has-oi { --oi: 0%; }
/* Puts: the strike is to the LEFT, so the bar grows right. */
.chain__oi.has-oi::before { left: 0; }
/* Calls: reversed, the strike is to the RIGHT, so it grows left. */
.chain__oi--ce.has-oi::before { left: auto; right: 0; }
.chain__k {
  text-align: center !important; color: var(--fg-0) !important;
  background: var(--bg-2); font-weight: 600;
}
.chain__prem { color: var(--fg-0) !important; cursor: pointer; }
.chain__prem:hover { color: var(--accent) !important; text-decoration: underline; }
/* ⚠ **THE ATM ROW HAS TO BE FINDABLE IN TWENTY-SIX.** `--bg-3` alone is one
   step off the row above it, which is enough when six strikes are shown and
   not when twelve are on each side. The strike itself carries the weight —
   that is the cell a reader scans down — and the row is bracketed rather than
   merely tinted. */
.chain__atm td { background: var(--bg-3); }
.chain__atm .chain__k {
  color: var(--fg-0); font-weight: 700;
  box-shadow: inset 0 0 0 1px var(--border-hi);
}
/* The spot divider says its own number. A hairline between two rows is a
   rendering artefact until it is labelled. */
.chain__spot td {
  text-align: center; color: var(--accent);
  font: 600 10px var(--sans); letter-spacing: .07em; text-transform: uppercase;
  border-top: 1px dashed var(--accent); border-bottom: 1px dashed var(--accent);
  padding: 3px 2px; background: rgba(41, 98, 255, .07);
}

/* phase 5: live garnish on the chain */
.chain__iv { font: 400 9px var(--mono); color: var(--info); }

/* The ladder dims while the next expiry loads rather than collapsing to a
   placeholder. `pointer-events` off so a second click cannot queue a fetch. */
.chain__t, [data-chain] { transition: opacity 0.12s ease; }
[data-chain].is-busy { opacity: 0.45; pointer-events: none; }
.fno-basis { font: 500 11px var(--mono); }

/* phase 6: the watch star in search */
.gs-starbtn {
  flex: none; background: none; border: 0; cursor: pointer;
  font-size: 13px; color: var(--fg-2); padding: 0 2px;
  /* Holds an SVG now (`M.instrumentActions.bookmark`), not a glyph. */
  display: inline-flex; align-items: center;
}
.gs-starbtn:hover { color: var(--accent); }
.gs-starbtn.is-on { color: var(--accent); }
/* An F&O root with no stored contract: shown so the column does not jump, but
   a `<span>`, because there is nothing to follow. */
.gs-starbtn--none { color: var(--fg-2); opacity: 0.35; cursor: default; }

/* --------------------------------------------------------------- option pad */
.pad { margin: 6px 0 12px; padding: 10px 12px; border: 1px solid var(--border-hi);
       border-radius: var(--r-lg); background: var(--bg-1); }
.pad__grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
             gap: 8px 18px; margin-bottom: 8px; }
.pad__step { display: inline-flex; align-items: center; gap: 10px;
             font: 600 14px var(--mono); }
.pad__step button { width: 24px; height: 24px; border-radius: 6px;
                    border: 1px solid var(--border); background: var(--bg-2);
                    color: var(--fg-0); cursor: pointer; }
.pad__in { width: 100%; height: 26px; padding: 0 8px; border-radius: 6px;
           border: 1px solid var(--border); background: var(--bg-2);
           color: var(--fg-0); font: 500 12px var(--mono); }
.pad__warn { margin: 4px 0; font: 500 12px var(--sans); color: var(--warn); }
.chip--buy { color: var(--pass); border-color: var(--pass); }

/* The lean pane's "why there is no button here" line — sits inline with the
   contract/continuous chips so the explanation is where the control would be. */
.lean-why { font: 400 11px var(--sans); color: var(--fg-2); margin-left: 8px; }

/* An expiry chip for a venue that cannot be traded: dimmed, and carrying the
   venue's name so the reason is legible before the click, not after it. */
.chip--muted { opacity: .62; }
.chip__ex {
  margin-left: 4px; padding: 0 3px; border-radius: 3px;
  font: 600 9px var(--sans); color: var(--fg-2); border: 1px solid var(--border);
}

/* The chain's per-leg Buy (order-surfaces 4b). Deliberately quiet: it sits in
   a dense ladder where a full button per row would out-shout the numbers the
   ladder exists to show. Reveals on row hover and on keyboard focus. */
.chain__buy {
  margin-left: 6px; padding: 0 5px; line-height: 15px;
  background: transparent; color: var(--fg-2);
  border: 1px solid var(--border); border-radius: 4px;
  font: 600 11px var(--mono); cursor: pointer;
  opacity: 0; transition: opacity .12s ease;
}
.chain__t tr:hover .chain__buy, .chain__buy:focus { opacity: 1; }
.chain__buy:hover { color: var(--pass); border-color: var(--pass); }

/* The drawer header's jump to the option chain. The Derivatives section sits
   ~91% of the way down a 4,400px drawer, behind nine other sections — the buy
   path worked and was unfindable, which for a control is the same as absent. */
.dd-fno { color: var(--accent); border-color: var(--accent); }

/* The Universe's family tabs carry their own counts. */
.chip__n { opacity: .65; font-variant-numeric: tabular-nums; }
/* The F&O tag is a link now — it goes to that underlying's contracts. */
a.tag--fno { text-decoration: none; cursor: pointer; }
a.tag--fno:hover { filter: brightness(1.25); }

/* ---------------------------------------------- the three risk kinds
   plans/fno-in-the-book.md phase 2. `capped` must not read as a WARNING —
   a long option's loss is bounded, and colouring it like an unprotected
   position would tell the reader the opposite of the truth. It gets the
   accent (a fact about the instrument), unstopped keeps the warn colour
   (a fact about what is missing), and stopped is unmarked because it is
   the ordinary case. */
.rk--capped { color: var(--accent); }
.rk--unstopped { color: var(--warn); }
.rk--stopped { color: inherit; }

/* Days to expiry — the one clock an equity row does not have. Three states,
   because "0 days" and "expired" are different facts. */
.dte { font: 500 12px var(--mono); }
.dte.warn { color: var(--warn); }
.dte.neg { color: var(--fail); }

/* The drawer's watch control. Deliberately quiet when off and accented when
   on — following something is a state, not an action you are being urged to
   take.

   ⚠ **`margin-left: auto` IS GONE, and it had to be.** It pushed this button
   to the far right of the header while Buy and Sell sat in `.drawer__acts`
   beside the close — so the three controls that read as a set were split
   across the width of the pane by a rule whose only job was to fill space.
   The star lives INSIDE that flex container now (phase 1 of
   plans/watchlists-and-the-strategy-lens.md), spaced by its `gap` like its
   two neighbours. */
.drawer__watch.is-on { color: var(--accent); border-color: var(--accent); }
/* ⚠ **`inline-flex`, OR THE ICON AND THE WORD STACK.** `.btn` is a plain
   inline-block sized for text; an SVG plus a `<span>` inside one wrapped onto
   two lines and the header control grew to twice the height of Buy and Sell
   beside it. It was a single glyph in a text run before, which needed nothing.
   `white-space: nowrap` keeps `Watching` on one line at narrow drawer widths
   rather than breaking mid-word. */
.drawer__watch {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
}

/* ---- the bookmark -------------------------------------------------------
   One SVG path in `instrument_actions.js`, drawn in `currentColor`, so the
   palette stays here in the button's own `color`. */
.wicon {
  width: 14px; height: 14px; display: block; flex: none;
  /* Optical centring: the notch means its visual mass sits above its box. */
  margin-top: -1px;
}
.wlab { font: inherit; }

/* The bookmark, in a TABLE row. `.abtn` gives it the size and border every
   other row control has; this only makes it square and quiet, because
   following something is a state rather than the action the row is urging.
   ⚠ It is `min-width` rather than `width`: the drawer's variant carries a
   WORD beside the icon ("Watching") and a fixed width would clip it.
   ⚠ **`inline-flex`, because the contents are an SVG now** — `text-align`
   cannot centre a replaced element the way it centred a glyph, and a block
   `.wicon` in a text-aligned button hugged the left edge. */
.abtn--watch {
  min-width: 30px; padding: 4px 8px; color: var(--fg-2);
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
.abtn--watch:hover { border-color: var(--border-hi); color: var(--fg-0); }
.abtn--watch.is-on { color: var(--accent); border-color: var(--accent); }
/* The drawer's is bigger, because everything in that header is. */
.drawer__watch .wicon { width: 15px; height: 15px; }

/* The three verbs in the hover panel's head, between the venue and the ✕.
   Same `gap` as the drawer's slot, for the same reason: two buttons in a bare
   span touch, green edge to red edge. */
.hovercard__acts { display: inline-flex; align-items: center; gap: 6px; flex: none; }

/* The drawer's strategy picker — the lens the clause breakdown is read
   through. It sits directly ABOVE the breakdown rather than in the header,
   because it changes what the section below it says and nothing else on the
   pane; a control in the head would read as acting on the instrument, beside
   Buy and Sell, which it does not. */
/* ⚠ **THE STRIP CARRIES THE VERDICT NOW, and it is the only place it can.**
   The header badge said `Signal` without naming a strategy — the ambiguity
   the picker removes — so it went; and that left the state readable only
   inside the dropdown. Here it sits beside the lens it belongs to and above
   the breakdown it explains, where it cannot be read as a claim about a
   different rule.

   It wraps rather than truncating: at 760px the name, the state, the score,
   the date and Scan now do not fit on one line, and a strip that clipped its
   own date would be the "when was this scanned" question all over again. */
.strategypick {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 22px 0 10px; padding: 9px 11px;
  background: var(--bg-2); border: 1px solid var(--border);
  border-radius: var(--r);
}
.strategypick__verdict {
  display: inline-flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font: 11.5px var(--mono); color: var(--fg-1);
  font-variant-numeric: tabular-nums;
}
/* The separator is an element, not a character in the text, so it can be
   dimmed without dimming the facts it separates. */
.strategypick__verdict i { font-style: normal; color: var(--fg-3, var(--fg-2)); opacity: 0.5; }
.strategypick__verdict--pass { color: var(--pass); }
.strategypick__verdict--fail { color: var(--fail); }
.strategypick__verdict--warm { color: var(--fg-2); }
/* The date, the scope and the live flag are QUALIFIERS on the verdict, not
   part of it — muted so the state reads first and they are still there. */
.strategypick__when { color: var(--fg-2); }
.strategypick__live {
  font: 600 9.5px var(--mono); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--warn); border: 1px solid var(--warn); border-radius: 3px;
  padding: 0 4px;
}
.strategypick__label {
  font: 600 11px var(--sans); letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--fg-2); flex: none;
}
/* Bounded rather than full-width: the option rows carry a name, a score and a
   date, and a control stretched to the drawer's width puts the date a long
   way from the name it belongs to. */
.strategypick__sel { flex: 1 1 auto; max-width: 340px; }

/* ---- named watchlists ---------------------------------------------------
 *
 * ⚠ **A SELECTOR, NOT TABS AND NOT A GROUPING.** Tabs are legible at three
 * lists and broken at eight; grouping is a single slot lists would want
 * permanently, and it buries the first decision on the screen two clicks deep
 * beside formatting options — navigation and formatting are different jobs.
 * This costs ~150px, renders 3 lists and 30 identically, and is the same
 * control on the narrow dashboard panel.
 */
.listpick { flex: none; min-width: 130px; max-width: 210px; }
.listpick .sel__btn { font-size: 12px; padding: 3px 8px; }

/* Which lists a row is on — the ALL view's answer, and the reason that view
   is a COLUMN rather than a grouping: a name on two lists is ONE row with two
   chips, so the counts cannot sum past the row count. */
.td-lists { white-space: normal; }
.listchip {
  display: inline-block; font: 500 10px var(--mono); border-radius: 3px;
  padding: 1px 5px; margin: 1px 3px 1px 0;
  border: 1px solid var(--border-hi); color: var(--fg-2); background: var(--bg-2);
}

/* ⚠ **✕, NOT `Remove`.** It rendered at the same visual weight as Buy — a
   full-width control in the reject red, for the least consequential act in
   the row. Quiet until pointed at, and the destructive colour arrives only
   when the cursor is actually on it. */
.abtn--icon {
  min-width: 26px; padding: 4px 7px; text-align: center;
  background: transparent; border-color: transparent; color: var(--fg-2);
}
.abtn--icon:hover, .abtn--icon:focus-visible {
  background: var(--fail-bg); border-color: var(--fail); color: #ff8b84;
}

/* The star's list picker — one mechanism for add AND remove, multi-select
   because an instrument belongs to several lists at once. */
.listmenu {
  position: fixed; z-index: 90; min-width: 220px; max-width: 280px;
  background: var(--bg-1); border: 1px solid var(--border-hi);
  border-radius: var(--r); box-shadow: 0 10px 30px rgba(0,0,0,0.5);
  padding: 6px; font-size: 12px;
}
.listmenu__head {
  font: 600 10px var(--mono); letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--fg-2); padding: 4px 7px 7px;
  border-bottom: 1px solid var(--border); margin-bottom: 4px;
}
/* Type-to-filter, past four lists only (see `openPicker`). Styled after
   `.sel__input` — it is the same act as the app's other filters, and should
   not look like the create field below it. */
.listmenu__search { padding: 0 1px 6px; border-bottom: 1px solid var(--border);
                    margin-bottom: 4px; }
.listmenu__filter {
  width: 100%; background: var(--bg-0); border: 1px solid var(--border);
  border-radius: 5px; padding: 5px 8px; color: var(--fg-0);
  font: 12px var(--mono);
}
.listmenu__filter:focus { outline: none; border-color: var(--accent); }
.listmenu__filter::placeholder { color: var(--fg-2); }

/* Only the rows scroll — an unbounded menu runs off the bottom of the window
   and `openPicker`'s clamp then pushes its top off instead. */
.listmenu__list { max-height: 240px; overflow-y: auto; overscroll-behavior: contain; }

.listmenu__none {
  padding: 7px; color: var(--fg-2); font-size: 11.5px; text-align: center;
}

.listmenu__row {
  display: flex; align-items: center; gap: 8px; padding: 5px 7px;
  border-radius: 5px; cursor: pointer; color: var(--fg-1);
}
.listmenu__row:hover { background: var(--bg-2); }
.listmenu__row > span:first-of-type { flex: 1; min-width: 0; }
.listmenu__n { font: 11px var(--mono); color: var(--fg-2); }
.listmenu__foot { border-top: 1px solid var(--border); margin-top: 4px; padding-top: 6px; }
.listmenu__new {
  width: 100%; background: var(--bg-2); border: 1px solid var(--border);
  border-radius: 5px; padding: 5px 8px; color: var(--fg-0); font-size: 12px;
}
.listmenu__new:focus { outline: none; border-color: var(--accent); }

/* `Scan now` — it WRITES a run and, on an unfrozen version, locks it forever.
   Quiet rather than primary: it is a refresh in the common case, and the one
   case where it is not says so on the button itself. */
.strategypick__scan { flex: none; margin-left: auto; display: inline-flex;
  align-items: baseline; gap: 7px; }
/* ⚠ **THE CONSEQUENCE, STATED BEFORE THE CLICK.** Scanning latches
   `first_scanned_at` and a database trigger makes it permanent, so a version
   that has never run can never be edited again afterwards. Amber rather than
   red: it is a real one-way door, not a failure. */
.strategypick__locks {
  font: 600 9.5px var(--mono); letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--warn); border: 1px solid var(--warn); border-radius: 3px;
  padding: 1px 4px;
}
/* The state dot in a `M.select` option — the same three the tables use, so a
   passing strategy reads the same here as a passing row does there. */
.sel__opt .dot { flex: none; margin-right: 2px; }

/* ---- the watchlist's list strip ----------------------------------------
 *
 * It was an `M.select`, which shows ONE label closed — so five lists rendered
 * as the word `All lists` and the feature was invisible until opened. A strip
 * says what exists without being asked, which is the whole reason lists were
 * kept out of the ⚙'s grouping slot.
 *
 * ⚠ **IT SCROLLS RATHER THAN WRAPS.** The objection to tabs was overflow at
 * eight lists; a second row of tabs pushes the table down and moves under the
 * cursor as lists are added. One row that scrolls sideways keeps the table
 * where it is, and `flex: none` on the children is what stops them being
 * squeezed to fit instead.
 */
/* Underline tabs, like the journal's. `--bg-0` and a border on both edges
   give the strip a ground of its own — without one it dissolves into the
   table header directly beneath it. */
.wltabs {
  display: flex; gap: 2px; align-items: stretch;
  background: var(--bg-0);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 0 14px; overflow-x: auto; scrollbar-width: thin;
  /* `.tpanel` is a flex column and is itself `flex: 1 1 0` on the dashboard,
     so without this the strip is shrunk to ~11px and its tabs are clipped. */
  flex: none;
}
/* `--fg-1`, not `--fg-2`: at `--fg-2` an inactive tab is the same value as the
   column header one row below. */
.wltab {
  appearance: none; background: none; border: none; cursor: pointer;
  font-family: inherit; font-size: 12px; font-weight: 600; color: var(--fg-1);
  padding: 9px 12px; border-bottom: 2px solid transparent; margin-bottom: -1px;
  white-space: nowrap; flex: none;
}
.wltab:hover { color: var(--fg-0); background: var(--bg-1); }
/* The active tab takes the panel's ground: square and flush, continuous with
   the content under it. */
.wltab.is-active {
  color: var(--fg-0); background: var(--bg-1); border-bottom-color: var(--accent);
}
/* Smaller and dimmer than the name, as `.jtab__n` is — a list called `test 2`
   with one member would otherwise render `test 2 1`.
   ⚠ `All lists` counts instruments ONCE and a per-list tab counts
   memberships, so the tabs do not sum to the first number. */
.wltab__n {
  color: var(--fg-2); font-weight: 500; font-size: 10.5px; margin-left: 7px;
  font-variant-numeric: tabular-nums;
}
.wltab.is-active .wltab__n { color: var(--fg-1); }
/* Shares the active tab's underline: it is rendered beside that tab and acts
   on it, so the two have to read as one unit. */
.wltab--manage {
  padding: 9px 8px; margin-left: -10px; margin-right: 6px; letter-spacing: 1px;
  border-bottom-color: var(--accent); background: var(--bg-1);
  color: var(--fg-2);
}
.wltab--manage:hover { color: var(--fg-0); background: var(--bg-1); }
.wltab--new { color: var(--accent); }
.wltab--new:hover { color: var(--accent); filter: brightness(1.2); }
/* Replaces whichever control opened it, so the strip does not reflow while
   you type into it. */
.wltab__field {
  align-self: center; flex: none; width: 150px; margin: 0 6px;
  background: var(--bg-2); color: var(--fg-0);
  border: 1px solid var(--accent); border-radius: 5px;
  padding: 4px 8px; font: 600 12px var(--sans);
}

.wlmenu {
  position: fixed; z-index: 60; min-width: 140px;
  background: var(--bg-1); border: 1px solid var(--border-hi);
  border-radius: 6px; padding: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.45);
  display: flex; flex-direction: column; gap: 1px;
}
.wlmenu__item {
  appearance: none; background: none; border: none; cursor: pointer;
  font: 500 12px var(--sans); color: var(--fg-1); text-align: left;
  padding: 6px 10px; border-radius: 4px;
}
.wlmenu__item:hover { background: var(--bg-2); color: var(--fg-0); }
.wlmenu__item--danger:hover { background: var(--fail-bg); color: #ff8b84; }
/* A refusal you can read beats a missing option you have to guess at — the
   default list cannot be deleted, and the reason is on the title. */
.wlmenu__item.is-off { color: var(--fg-2); cursor: not-allowed; padding: 6px 10px; }

/* ---- the score cells that open the breakdown ---------------------------
 *
 * ⚠ **THE DRAWER'S TWO-COLUMN CLAUSE PANE IS GONE, and so is the widening it
 * needed.** Phase 3 made every clause row a control opening a side pane, and
 * took the drawer from 760px to 1180px to fit it. The owner asked for it out:
 * *"any clause can be clickable. I just don't want that feature."*
 *
 * The affordance moved to where the question is actually asked — the `9/11`
 * in the scanner's Clauses column and the `2/5` in the watchlist's Verdict
 * column. Those cells open the drawer scrolled to `Judged by` and the
 * breakdown under it; nothing inside the breakdown is clickable, so `.clause`
 * carries no cursor, no hover and no selected state.
 */
.td-clauses, .td-verdict { cursor: pointer; }
.td-clauses:hover, .td-verdict:hover { background: var(--bg-2); }
/* A cell that acts needs a keyboard ring like a button does — it carries
   `role="button"` and `tabindex`, so it has to be reachable and visible. */
.td-clauses:focus-visible, .td-verdict:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -2px;
}


/* The R-basis marker — a superscript `p` on an R measured against a premium
   rather than a stop distance. Deliberately quiet: it is a qualifier on a
   number, not a badge, and it must not compete with the figure it modifies.
   Its whole job is to make a reader hover (ADR-0054's unmarked-R gap). */
.r-basis {
  font: 600 9px var(--sans);
  color: var(--fg-2);
  margin-left: 1px;
  cursor: help;
}
