/* Portfolio Manager - dense terminal styling, themed by CSS custom properties.
   Dark is the default; light and custom are applied via [data-theme] on <html>.
   Custom theme values are injected as inline variables from Data & Settings. */

:root {
  /* Dark is what this palette IS, and the browser has to be told so or every
     surface it paints for itself stays light: the scrollbars on `main`, on
     `.grid-wrap` - a forty-column table always has a horizontal one - on
     `.modal-body`, `.dropdown-panel`, `.job-log` and `.price-rows`, plus the
     `<select>` popup, the number spinners, the date picker and the text caret.
     A bright #f0f0f0 scrollbar across the foot of a #0f172a grid is the
     loudest object on the screen and none of it is content.
     One declaration per theme, and only two values ever reach the root -
     `main.js` maps the custom theme onto `dark`, because custom overrides the
     six colours and keeps these surfaces. */
  color-scheme: dark;

  /* The surface family is slate - a navy-tinted neutral, not the flat charcoal
     it was until 2026-08-09. It is only the SURFACES, and it is deliberately a
     small change: a page whose greys lean the same way as its accent reads as
     one material, where the old dead-neutral #0d1117/#161b22 pair under a blue
     accent left every panel looking slightly dirty.

     --panel is the one value here that is arithmetic rather than taste. It is
     the surface the grid and every chart are drawn on, and the eight series
     slots below are only guaranteed to clear 3:1 against it. The options
     engine's own panel (#1e293b) is too light for that - it puts --series-6 at
     2.92:1 and --series-other at 2.99:1 - so this is the slate step that holds
     the floor: 3.15:1 and 3.23:1. Lightening it further means re-stepping the
     categorical palette, which is a separate exercise with its own validation
     (see the note above --series-1). */
  --bg: #0f172a;
  --panel: #1a2436;
  --panel-2: #232f45;
  --line: #334155;
  --line-soft: #2a3648;
  /* A card's own edge, which is not the same job in the two themes and so is
     not the same token. Dark leans on its borders, so softening one card's edge
     is what stops twelve of them reading as twelve boxes: `--line-soft`. Light
     leans on its shadows, and `--line-soft` there is #eef2f7 on a #f1f5f9 card
     - no edge at all - so it takes `--line`, the #e2e8f0 hairline. Written as a
     reference rather than a value so a custom theme's own lines still carry
     through. */
  --card-edge: var(--line-soft);
  --text: #e8ecf3;
  --muted: #94a3b8;
  --accent: #4c8dff;
  /* --positive is BLUE, not green (owner call, 2026-08-09). The token name is
     semantic - "the up side" - and its value is deliberately not green: this
     app's entire positive/negative distinction is read by a red/green
     colour-blind owner, and red-vs-green is precisely the pair that does not
     separate for them, while red-vs-blue separates under both protanopia and
     deuteranopia. Green survives nowhere in the SEMANTIC palette, because one
     leftover green swatch would read as a state the rest of the UI no longer
     has.
     The ratios are quoted against --panel-2, not --panel, because that is the
     WORST surface either of them lands on and it is not a rare one: it is the
     grid's header, every `.section` card in the popup, `.study-card`,
     `.metric-box` and `.rating-card`. --negative was #f0616d, which is 4.92:1
     on --panel and 4.24:1 there - a figure that passed where it was measured
     and failed where it is read. #f4737d is 5.63:1 and 4.86:1, so it clears on
     both. --positive is 7.26:1 and 6.27:1 and needed no step.
     The knock-on was checked rather than assumed: --pnl-neg-fg on --pnl-neg-bg
     is mixed from this token and moves with it, 7.19:1 -> 6.64:1, still well
     clear of the 4.5:1 that pair has to carry.
     Hue is never the only channel regardless - see --pnl-neg-bg below, and the
     ▲/▼ the strip and the price pill print ahead of the colour. */
  --positive: #38bdf8;
  --negative: #f4737d;
  --warn: #f59e0b;
  --tip-bg: #fef3c7;
  --tip-text: #1f2937;
  --tip-border: #d97706;

  /* Tinted surfaces, mixed from the semantic colour rather than written out.
     A custom theme overrides --positive and --negative and nothing else, and a
     hard-coded #ecfdf5 beside somebody's chosen green would be the one thing
     on screen that did not follow. Substitution is lazy, so these resolve
     against whatever the element's cascade says the base colour is. */
  --positive-soft: color-mix(in srgb, var(--positive) 16%, transparent);
  --negative-soft: color-mix(in srgb, var(--negative) 16%, transparent);
  --warn-soft: color-mix(in srgb, var(--warn) 16%, transparent);
  --accent-soft: color-mix(in srgb, var(--accent) 15%, transparent);
  /* The loss block. A gain and a loss must differ in LIGHTNESS and in FILL as
     well as in hue, because hue is the channel that may fail: read the pair in
     greyscale and the loss is the dark block while the gain is a pale tint of
     --positive-soft. Mixed from --negative on the same argument as the soft
     surfaces above - a custom theme overrides the colour, and a hard-coded
     #7c1d26 beside somebody's chosen red would be the one thing that did not
     follow. 7.2:1 of light ink on the block in both themes.
     Worn by the price pill, the SL badge and the stop-loss flash: the three
     places that say "this one has gone against you" rather than merely "this
     number is negative", which stays plain --negative text. */
  --pnl-neg-bg: color-mix(in srgb, var(--negative) 55%, #000);
  --pnl-neg-fg: color-mix(in srgb, var(--negative) 12%, #fff);
  /* Row hover and the zebra stripe. Kept as their own tokens because the two
     have to stay distinguishable from each other and from the surface. */
  --row-hover: color-mix(in srgb, var(--accent) 14%, transparent);
  --row-stripe: rgba(127, 127, 127, .05);
  /* A control's resting fill: the circular ground under a micro-button. */
  --ghost: rgba(127, 127, 127, .16);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .35);
  --shadow-md: 0 2px 10px rgba(0, 0, 0, .38);
  --shadow-lg: 0 12px 32px rgba(0, 0, 0, .45);
  /* Corner radius, as a ladder rather than as twenty-one loose numbers. Two of
     these already existed and were bypassed in about six rules out of ten -
     eight literal `6px` and six literal `8px` were the tokens written out by
     hand, beside 2, 3, 4, 5, 7, 9 and 10 that were nothing in particular. On a
     wall of cards, chips and pills the corner is the most repeated shape on the
     screen, so the count of distinct ones is the count of times it fails to
     rhyme.
     The two full-screen registers keep their own literals on purpose: the
     sign-in card's 16px and its mark's 11px are one panel on an empty page,
     not part of the dense chrome this ladder is for. `50%` is a circle and
     `--radius-pill` is a lozenge; neither is a step on a ladder. */
  --radius-xs: 3px;
  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 8px;
  --radius-pill: 999px;

  /* Type, on the same argument. There were twenty distinct sizes here, ten of
     them between 9px and 13px - 7.5, 8.5, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.5,
     13 - which is not a scale but eleven separate answers to one question, none
     of them named, every new rule having to guess which its neighbour picked.
     Six steps, and the half-pixels that survive do so as a token's value rather
     than as a magic number: --fs-xs is the grid header and every spaced-out
     capital label, --fs-sm is a grid cell, --fs-base is body text and menus.
     Nothing moved more than 1px except the two that had no business existing -
     `.fs-fold` at 7.5px and `.pp-edge b` at 8.5px, both of them read rather
     than glyphs, both now at the 9.5px floor.
     The hero figures - 16, 18, 20, 22, 28, 40 - stay literals. Each is a
     one-off on a single component where a shared step buys nothing. */
  --fs-2xs: 9.5px;
  --fs-xs: 10.5px;
  --fs-sm: 11px;
  --fs-base: 12px;
  --fs-md: 13px;
  --fs-lg: 14px;

  /* The size bands, as pills. Neutral for the two small ends - a micro cap is
     the absence of size, not a category worth a hue - then indigo and violet
     as the ladder climbs. Backgrounds are mixed against the panel rather than
     written flat, so one set of rules serves both themes. */
  --band-neutral: var(--muted);
  --band-mid: #6366f1;
  --band-large: #8b5cf6;
  --band-super: #a855f7;

  /* The conditional-format palette: the twelve backgrounds a column rule may
     paint a cell with, from `columns.COLUMN_COLORS`. A *token* per name rather
     than a literal, because that is the whole argument for a palette over the
     hex box beside it - one id is two colours, and a cell the user tinted Red
     has to stay readable when they switch themes. The light steps are in the
     [data-theme="light"] block below.

     These are the user's own vocabulary, so the no-green rule that governs the
     positive/negative pair does not reach them: green here means whatever the
     person who chose it decided it means, and refusing them a green would be
     this stylesheet overruling their ladder. The pair the app itself speaks in
     is still --positive/--negative, and none of these is either.

     Mixed rather than written flat wherever they are used - see `.rf` - so the
     figure over the tint stays legible in both themes at one set of rules. */
  --rf-red: #ef4444;
  --rf-orange: #f97316;
  --rf-amber: #f59e0b;
  --rf-yellow: #eab308;
  --rf-lime: #84cc16;
  --rf-green: #22c55e;
  --rf-teal: #14b8a6;
  --rf-blue: #3b82f6;
  --rf-indigo: #6366f1;
  --rf-violet: #8b5cf6;
  --rf-pink: #ec4899;
  --rf-grey: #94a3b8;
  /* How much of the hue reaches the cell. One number for all twelve so the
     column reads as one ladder rather than as twelve unrelated washes, and low
     enough that --text carries over any of them: a solid #ef4444 behind a
     figure is a cell nobody can read. */
  --rf-tint: 22%;

  /* Chart tokens. Eight categorical slots, in the fixed order they validate
     in - a series takes the next slot and hues are never cycled or generated.
     Worst adjacent CVD ΔE 8.4, worst adjacent normal-vision ΔE 19.3. Those two
     are separations BETWEEN series and are unaffected by a change of surface,
     so the 2026-08-09 re-tint left them alone.
     The third check is not: it is each slot against the surface it is drawn on,
     which moved from #161b22 to #1a2436. Recomputed rather than reasserted -
     the two weakest are --series-6 at 3.15:1 and --series-other at 3.23:1, and
     all eight still clear 3:1. That floor is what pins --panel where it is.

     --series-6 is green, and stays green: these are IDENTITIES, not states. The
     no-green rule above is about the semantic pair, where green would be a
     claim a red/green colour-blind reader cannot read. A green category next to
     a red one is a weaker version of the same problem - but slots 6 and 8 are
     not adjacent, so their separation was never what the ΔE figure above
     guaranteed, and re-stepping a validated palette needs the validation re-run
     rather than a hue somebody preferred.
     Status marks are reserved for good/bad and never used as a series. */
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #c98500;
  --series-5: #d55181;
  --series-6: #008300;
  --series-7: #9085e9;
  --series-8: #e66767;
  /* The folded tail of a pie. Grey on purpose: "Other" is a remainder, not an
     identity, and a hue here would compete with the categories that are one.
     3.63:1 on this surface. */
  --series-other: #69747f;
  /* The status pair, the same sky/rose steps as --positive and --negative so a
     rising line, a positive cell and a tinted pill are one colour rather than
     three near-misses - literally the same two steps on this theme, which is
     what `theme.test.mjs` holds them to. --mark-bad moved with --negative when
     that was lifted for --panel-2 (see the note there); left behind it would
     have been the chart drawing a loss in last week's red. 7.26:1 and 5.63:1 on
     --panel, against the 3:1 a graphic needs.
     Reserved for good/bad meaning and never taken as a series. */
  --mark-good: #38bdf8;
  --mark-bad: #f4737d;
  /* Slate, like the surfaces they are drawn over. These were warm greys
     (#2c2c2a) left from the charcoal palette, and a warm gridline under a cool
     panel is the one place the old mismatch was actually visible. */
  --grid-line: #2a3648;
  --axis-line: #3b4a61;
  --axis-text: #8b98ab;
  --row-h: 36px;
  /* Segoe UI, then whatever the OS calls its interface face. Nothing is
     downloaded: the app loads no CDN and ships no font binary, so this is a
     preference list over what the machine already has, and on the Windows box
     this runs on it resolves to Segoe UI every time.
     This used to lead with Inter and a queue of four other faces behind it,
     which meant the app rendered in a different face depending on what the
     machine happened to have installed - and none of them was the one the
     figures were being read against elsewhere. A short stack is the point, not
     an oversight.
     Every face here has the same metrics-agnostic requirement, which is why
     `table.grid th` states an explicit `line-height`: the sticky filter row is
     parked at the header's height in pixels, and a face whose default leading
     differs by 2px would silently open a gap or overlap it. */
  --font: "Segoe UI", system-ui, sans-serif;
  /* Code, paths and glossary keys only. It is deliberately NOT the face for
     figures any more - a column of numbers lines up on `tabular-nums`, which
     every face here supports, so the mono was buying alignment that was
     already paid for and costing legibility to do it. */
  --mono: "Cascadia Mono", "Consolas", ui-monospace, monospace;
}

/* The light theme is a soft slate canvas with white cards on it, not white on
   white. The page used to be #f4f6f8 under #ffffff panels - a 2% step, which is
   no step at all, so nothing had an edge and every panel border had to do the
   work a surface change should be doing. #f8fafc under #ffffff, with the
   secondary surface a full step darker at #f1f5f9, gives the cards something to
   sit on and lets the borders go quiet.

   The two semantic colours are the light steps of the same sky/rose pair the
   dark theme carries - blue for the up side, never green; see --positive there
   for why. Stepped for *text on white*: sky-800 #075985 is 7.6:1 and #b02330 is
   6.7:1, so both carry a figure in a cell. The mark steps below are a shade
   brighter, which is a mark colour and not a text one. */
html[data-theme="light"] {
  color-scheme: light;
  --bg: #f8fafc;
  --panel: #ffffff;
  --panel-2: #f1f5f9;
  --line: #e2e8f0;
  --line-soft: #eef2f7;
  --card-edge: var(--line);
  --text: #0f172a;
  /* #556579, not the #64748b it was. Same argument as --negative on the dark
     theme: the figure was measured on --panel and the label is read on
     --panel-2, where slate-500 is 4.34:1 - and --muted is not a rare colour
     there. It is `table.grid th`, eighty column names at 10.5px in spaced-out
     capitals, plus `.section h3`, `.kv dt`, `.metric-box h4` and every
     `.hint` inside a card. 5.44:1 on --panel-2 and 5.96:1 on --panel. */
  --muted: #556579;
  --accent: #2563eb;
  --positive: #075985;
  --negative: #b02330;
  --warn: #b45309;
  --tip-bg: #0f172a;
  --tip-text: #f8fafc;
  --tip-border: #38bdf8;

  /* Shadows carry the elevation here, where the dark theme leans on its
     borders: a soft drop on white is what makes a card read as a card. */
  --shadow-sm: 0 1px 3px rgba(15, 23, 42, .06);
  --shadow-md: 0 2px 8px rgba(15, 23, 42, .08);
  --shadow-lg: 0 12px 32px rgba(15, 23, 42, .16);
  --row-stripe: rgba(100, 116, 139, .05);
  --row-hover: #f1f5f9;
  --ghost: rgba(100, 116, 139, .12);

  /* Light steps of the same eight hues, validated against #ffffff: worst
     adjacent CVD ΔE 9.1, worst adjacent normal-vision ΔE 19.6. Aqua, yellow
     and magenta sit below 3:1 on white, so anything drawn in them ships its
     value as text beside it - the pie's legend table is that relief. */
  --series-1: #2a78d6;
  --series-2: #eb6834;
  --series-3: #1baf7a;
  --series-4: #eda100;
  --series-5: #e87ba4;
  --series-6: #008300;
  --series-7: #4a3aa7;
  --series-8: #e34948;
  --series-other: #8b95a1;   /* 3.04:1 on white */
  /* Sky-600 and a lifted rose on white: 4.1:1 and 4.5:1, both clear of the 3:1
     a mark needs. Deliberately a step brighter than --positive/--negative,
     which have to carry small text and cannot afford it - a 1.5px line at
     7.6:1 is darker than the chart around it needs to be. */
  --mark-good: #0284c7;
  --mark-bad: #d63d49;

  /* The conditional-format palette on white: one step deeper than the dark
     theme's, for the reason --positive is - these carry a 14px icon at
     full strength as well as tinting the cell behind a figure, and the brighter
     dark-theme steps wash out to nothing on white. */
  --rf-red: #dc2626;
  --rf-orange: #ea580c;
  --rf-amber: #d97706;
  --rf-yellow: #ca8a04;
  --rf-lime: #65a30d;
  --rf-green: #16a34a;
  --rf-teal: #0d9488;
  --rf-blue: #2563eb;
  --rf-indigo: #4f46e5;
  --rf-violet: #7c3aed;
  --rf-pink: #db2777;
  --rf-grey: #64748b;
  /* Lighter on white than on the dark panel: the same 22% of a deeper hue over
     a bright ground reads considerably heavier. */
  --rf-tint: 16%;

  --grid-line: #e4e9f0;
  --axis-line: #c8d2de;
  --axis-text: #6b7788;
}

* { box-sizing: border-box; }

/* `dvh` throughout, with the `vh` line kept above it as the fallback for a
   browser that does not know the unit and would otherwise drop the whole
   declaration. On a phone `100vh` is the viewport with the address bar and the
   toolbar pretending not to be there: the shell is laid out taller than the
   screen it is on, so the bottom of it - the last rows of the grid, a modal's
   buttons - sits behind browser chrome that only retracts once you scroll,
   which `body { overflow: hidden }` is there to stop you doing. `dvh` is the
   space actually available now. */
.auth-gate {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(circle at 25% 15%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 32%),
    var(--bg);
}

.auth-card {
  width: min(100%, 390px);
  padding: 34px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--panel);
  box-shadow: 0 24px 70px rgba(0, 0, 0, .28);
}

.auth-mark {
  display: grid;
  place-items: center;
  width: 42px;
  height: 42px;
  margin-bottom: 22px;
  border-radius: 11px;
  background: var(--accent);
  color: white;
  font-weight: 800;
  letter-spacing: -.04em;
}

.auth-card .eyebrow {
  margin: 0 0 6px;
  color: var(--accent);
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
}

.auth-card h1 { margin: 0 0 8px; font-size: 28px; }
.auth-card > .hint { margin: 0 0 26px; }

.auth-field {
  display: grid;
  gap: 7px;
  margin: 0 0 16px;
  color: var(--muted);
  font-size: var(--fs-base);
  font-weight: 650;
}

.auth-field input {
  width: 100%;
  min-height: 42px;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
}

.auth-field input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.password-control { position: relative; }
.password-control input { padding-right: 62px; }
.password-toggle {
  position: absolute;
  top: 50%;
  right: 7px;
  min-width: 46px;
  padding: 5px 7px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
  transform: translateY(-50%);
}
.password-toggle:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.password-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.auth-submit { width: 100%; min-height: 42px; margin-top: 4px; }
.auth-error { margin: 14px 0 0; color: var(--negative); font-size: var(--fs-base); }
.auth-private { margin: 22px 0 0; color: var(--muted); font-size: var(--fs-sm); text-align: center; }
.auth-user { max-width: 190px; overflow: hidden; color: var(--muted); font-size: var(--fs-sm); text-overflow: ellipsis; white-space: nowrap; }
/* The deployed build, beside the signed-in address. Monospace and tabular
   because it is an identifier that gets read character by character and quoted
   back - `v14` and `v141` have to be told apart at a glance - and muted because
   it is chrome that is consulted, not chrome that is watched. `white-space:
   nowrap` so a label never folds mid-date and reads as two stamps. */
.build-version {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  white-space: nowrap;
  letter-spacing: .02em;
}
/* An unstamped checkout says so rather than showing a number nobody can trust.
   Warn rather than negative: nothing is broken, the page simply cannot vouch
   for which build it is. */
.build-version.unknown { color: var(--warn); }

/* The `hidden` attribute is only a user-agent rule (`[hidden] {display:none}`),
   and ANY author rule that sets `display` outranks it - author origin beats UA
   origin regardless of specificity. `.modal-root` sets `display:flex`, so the
   modal backdrop stayed visible while hidden: a full-screen rgba(0,0,0,.6)
   layer at z-index 150 that dimmed the whole UI and swallowed every click on
   the tabs. This rule makes the attribute authoritative for all overlays. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  /* Split out of the `font` shorthand it used to be. A var() inside a shorthand
     is substituted before the shorthand is parsed, so an unresolvable token
     takes the family down with the size - and this is the one rule that sets
     the application's face at all. Three longhands cannot fail that way. */
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: 1.45;
  overflow: hidden;
}

#app { display: flex; flex-direction: column; height: 100vh; height: 100dvh; }
main {
  flex: 1;
  overflow: auto;
  padding: 10px 12px 24px;
  /* Clear of the home indicator on a notched phone, and unchanged everywhere
     else - `env()` resolves to 0 without `viewport-fit=cover` in the meta tag,
     and to 0 on every browser that has never heard of it. */
  padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px));
}

button, select, input, textarea { font-family: inherit; font-size: inherit; color: inherit; }
/* The tick and the track the browser draws for itself. Stated once, here,
   rather than on the one checkbox in the settings menu that used to carry it -
   the column picker has eighty of these, the grid's select column one per row,
   and the study picker its own list, and every one of them was rendering in
   the UA's default blue beside an app painted in --accent. */
input[type="checkbox"], input[type="radio"], progress { accent-color: var(--accent); }

.btn {
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 3px 9px;
  cursor: pointer;
  line-height: 1.6;
}
.btn:hover { border-color: var(--accent); }
/* The fill is a DEEPER step of the accent, not the accent. --accent has to be
   light enough to carry small text on a dark panel - that is its main job, in
   links, the lit tab, the sort arrow - and a colour light enough for that is
   too light to put white on: #4c8dff under #fff is 3.20:1, under the 4.5:1 a
   button's word needs. Mixed towards black rather than written out as a second
   hex, so a custom accent still reaches it - the same argument as the soft
   surfaces above. 78% lands at 4.95:1 on the dark theme and 7.48:1 on light,
   and stays recognisably the same blue in both. */
.btn.primary {
  background: color-mix(in srgb, var(--accent) 78%, #000);
  border-color: color-mix(in srgb, var(--accent) 78%, #000);
  color: #fff;
}
.btn.danger:hover { border-color: var(--negative); color: var(--negative); }
.btn.sm { padding: 1px 6px; font-size: var(--fs-sm); }

/* --- a button that carries its own mark ------------------------------ */
/* The grid toolbar's four actions. They were bare words in a row, which is the
   one place in this application where four controls sit side by side with
   nothing but their labels to tell them apart - and two of them are destructive
   in opposite directions.
   The hue rides on the GLYPH, not on the button. A filled or outlined button
   per action would put four competing blocks above a grid whose own job is to
   colour cells by what they mean, and the heat map would lose that fight. A
   14px mark in the action's colour is enough to find the right button without
   the toolbar shouting over the table. Same bargain `.btn.sell` already makes
   below - this is that pattern with a name, so a fifth action can join without
   a fifth private rule.
   `--act-mark` defaults to the accent, so `.act` on its own is a button with a
   mark rather than a button with a broken variable. */
.btn.act {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  --act-mark: var(--accent);
}
.btn.act > svg { color: var(--act-mark); flex: none; }
.btn.act:hover { border-color: var(--act-mark); }
/* Disabled is the RESTING state of Delete and Restore - nothing is ticked most
   of the time - and until now nothing said so: `.btn` sets its own colour and
   background, which overrides the browser's greying, so a dead button looked
   exactly like a live one. `.45` and `cursor: default` are what
   `.step-through .btn[disabled]` and the day-move cards already use.
   Scoped to `.act` rather than written as a global `.btn:disabled`, because
   every page in this application has buttons and repainting all of them is not
   what this change is. */
.btn.act:disabled { opacity: .45; cursor: default; }
.btn.act:disabled:hover { border-color: var(--line); }
/* The mark goes quiet with the word rather than staying lit over a button that
   does nothing. `currentColor` picks up whatever the row above dimmed the text
   to, which is the one value that cannot drift from it. */
.btn.act:disabled > svg { color: currentColor; }

/* Emerald for the one that adds, red for the one that removes, amber for the
   undo, and the plain accent for Clear filters - which is not an action on the
   data at all, only on what is on screen. Red is `--rf-red` rather than
   `--negative`: negative means "this number went down", and a delete is not a
   loss - the two would read as the same statement in a page full of both. */
.btn.act-add { --act-mark: var(--rf-green); }
.btn.act-delete { --act-mark: var(--rf-red); }
.btn.act-restore { --act-mark: var(--rf-amber); }
.btn.act-clear { --act-mark: var(--muted); }
/* Clear filters is the quietest of the four until it is pointed at: it undoes
   typing rather than doing anything, and a lit mark on it would compete with
   the two that change the shared list. */
.btn.act-clear:hover { --act-mark: var(--accent); }

select, input[type="text"], input[type="number"], textarea {
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
}
select:focus, input:focus, textarea:focus { outline: 1px solid var(--accent); }

/* --- top strip ------------------------------------------------------ */
/* A row of chips rather than cells divided by rules. Vertical rules between
   readouts made the strip read as one wide table whose columns happened to
   hold different things; a bordered chip per index says "these are separate
   facts" without a single divider, and it is the same shape the summary bar
   and the badges use, so the whole application states a figure one way. */
.strip {
  display: flex;
  gap: 8px;
  align-items: stretch;
  padding: 7px 10px;
  /* The canvas colour, not the panel colour: the chips are the surface here,
     and a white chip needs something other than white behind it before a
     border and a shadow can say where it ends. */
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  min-height: 42px;
}
/* The indices wrap onto a second row rather than scrolling sideways. A
   horizontal scrollbar in a 42px strip ate a third of its height, and the
   indices it hid were hidden for good on a mouse-less machine: nothing on the
   page said there were more. Wrapping costs a row of height only when the
   window is too narrow to hold them all, and every index is then readable
   without a gesture. The portfolio tile stays pinned to the left of them,
   spanning however many rows the indices take. */
.strip-indices {
  display: flex;
  align-items: stretch;
  align-content: flex-start;
  flex-wrap: wrap;
  gap: 8px;
  flex: 1;
  min-width: 0;
}
.strip-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
  white-space: nowrap;
}
/* A chip that opens the index's charts. It is a real `<button>` - main.js only
   draws one once Yahoo has resolved a ticker for the index - so the reset is
   what stops the browser's own button chrome from making it look unlike the
   three chips beside it. `text-align: left` because a button centres its
   content and the strip reads label-then-figure across a row.

   The affordance is the border and the lift, not a colour: the chips already
   spend colour on the direction of the move, and a blue "clickable" tint next
   to a red change would be two colour systems in a 42px strip. */
.strip-item.strip-open {
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.strip-item.strip-open:hover {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
  box-shadow: var(--shadow-md);
}
.strip-item.strip-open:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .strip-item.strip-open { transition: none; }
}

/* main.js wraps the name with its price, and the change with its sparkline.
   Both wrappers are plain boxes, so they stack - which made every chip two
   lines tall and the strip half again as deep. Laying them out as rows puts a
   chip on one line, label then figure, the way a ticker is read.

   Two tags, because the index chips are `span`s inside a `<button>` - a button
   may only hold phrasing content - while the pinned portfolio tile beside them
   is a `div` holding `div`s. One rule, so the two cannot drift apart. */
.strip-item > div,
.strip-item > span { display: flex; align-items: center; gap: 7px; }
.strip-item .name { color: var(--muted); font-size: var(--fs-xs); letter-spacing: .04em; text-transform: uppercase; }
.strip-item .px {
  font-family: var(--mono);
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* Tinted from the status colour itself, like `.pill-chg` in the detail popup:
   the direction of the move is carried by a shape as well as by a hue. */
.strip-item .chg {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
}
/* Background only - the colour of the figure is `.pos` / `.neg`'s to give, and
   naming it again here is how the strip would come to disagree with the grid
   about what a gain looks like the next time either token moves. */
.strip-item .chg.pos { background: var(--positive-soft); }
.strip-item .chg.neg { background: var(--negative-soft); }
/* Direction as a GLYPH, printed ahead of the colour it is painted in. The tint
   above and the sign on the figure are two channels that both live in the
   colour system; the arrow is the one that survives a greyscale screenshot and
   a reader for whom the hue does not resolve, which is the same argument that
   makes --positive blue. Sized well under the figure it marks: it qualifies the
   number, it is not the number. `font-style: normal` because main.js writes it
   as an `<i>` - the element is a glyph slot, not an emphasis. */
.arw { font-style: normal; font-size: var(--fs-2xs); line-height: 1; margin-right: 1px; }
/* After `.strip-item` on purpose: the two tie on specificity, so the pinned
   tile only keeps its accent tint if it is the later rule. */
.strip-today {
  flex: 0 0 auto;
  gap: 12px;
  background: color-mix(in srgb, var(--accent) 8%, var(--panel));
  border-color: color-mix(in srgb, var(--accent) 38%, var(--line));
}
.strip-today .px { font-size: var(--fs-lg); }
.strip-item svg { display: block; }
.strip-empty { padding: 12px; color: var(--muted); }

/* The strip's disclosure.
   `align-self: flex-start` pins it to the top-right corner rather than letting
   it stretch: the indices wrap onto a second row on a narrow window, and a
   control that grows a row taller with the thing it collapses is a control you
   have to look at before you can press it. */
.strip-toggle {
  align-self: flex-start;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
  color: var(--muted);
  cursor: pointer;
  transition: color .12s, border-color .12s;
}
.strip-toggle:hover { color: var(--text); border-color: var(--accent); }
/* One drawing for both states - turned over rather than swapped, so the two
   directions cannot drift into two pictures. */
.strip-toggle svg { transition: transform .12s ease; }
.strip.collapsed .strip-toggle svg { transform: rotate(180deg); }

/* Shut: the chips go and the handle that brings them back stays.
   `min-height` has to go with them, or the 42px row they were saving is still
   reserved and nothing below moves up - and `margin-left` keeps the handle in
   the corner it was in, now that the flexible `.strip-indices` that used to
   push it there is not laid out at all. main.js measures the element, so
   `--strip-h` follows on its own and the grid's reserve shrinks with it. */
.strip.collapsed { min-height: 0; padding: 3px 10px; }
.strip.collapsed > .strip-item,
.strip.collapsed > .strip-indices { display: none; }
.strip.collapsed .strip-toggle { margin-left: auto; }
@media (prefers-reduced-motion: reduce) {
  .strip-toggle, .strip-toggle svg { transition: none; }
}

/* --- top bar -------------------------------------------------------- */
/* `min-height` is the declared figure the grid's reserve falls back on before
   main.js has measured anything - see `--topbar-h` there and in `.grid-wrap`
   below. It is the height the tab rail gives the bar on a desktop, written
   down here so the fallback has something to be checked against rather than
   being a number in a comment. */
.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 12px;
  min-height: 38px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
  /* Anchors `.topbar-progress`, the hairline that runs while anything is
     fetching. Absolute against the bar rather than the page, so it stays on the
     bar's own bottom edge when the strip above it is folded away. */
  position: relative;
}
/* The refresh action leads the bar. The user picker follows it, ahead of the
   tabs, because it scopes what three of them show. `margin-right: auto` on the
   tabs keeps that group at the left while the status stays pinned right. */
.tabs { display: flex; gap: 2px; margin-right: auto; }
/* The rail's phone handle. A desktop has the rail itself, so the handle exists
   only inside the 820px block at the end of the file, where the rail folds
   into the menu it opens. */
.tabs-toggle { display: none; }
.user-picker {
  gap: 6px;
  padding: 4px 10px 4px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.user-picker > span { text-transform: uppercase; letter-spacing: .04em; font-size: var(--fs-xs); }
.user-picker select { font-weight: 600; color: var(--text); }
/* Text links with an active PILL, not five boxes under a 2px underline. An
   underline is the right anchor for a rail that sits ON a rule - `.subtab` and
   the popup's `.mtab` both keep theirs, because there is a rule under them to
   sit on. Up here there is none, so the mark was floating under a word.
   The selected tab now takes a tinted seat, the accent ink AND more weight:
   three channels, which matters because hue is not a channel this reader can
   rely on (see --positive). Inactive tabs keep full text colour at reduced
   opacity - painting them --muted made them read as disabled rather than merely
   unselected.
   The 5px padding and 5px margin are not free choices: they hold the pill at
   the 38px the 9px-padded tab measured, which is the height `.topbar` states as
   its minimum and the grid's reserve falls back on. The reserve itself is
   measured now rather than assumed, so a taller rail - the 40px target a finger
   gets below - costs the grid exactly its own height and nothing is silently
   pushed off the bottom of the screen. */
.tab {
  background: none;
  border: none;
  border-radius: var(--radius-pill);
  color: var(--text);
  opacity: .7;
  padding: 5px 12px;
  margin: 5px 0;
  cursor: pointer;
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
  transition: opacity .12s, background .12s, color .12s;
}
.tab:hover { opacity: 1; background: color-mix(in srgb, var(--text) 8%, transparent); }
.tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; opacity: 1; }
.tab.active {
  opacity: 1;
  font-weight: 600;
  color: var(--accent);
  background: var(--accent-soft);
}
/* The tab's shape sits a step back from its word, and lifts to full strength
   only on the selected pill - the icon is the fourth channel on the active
   state, after the seat, the ink and the weight, and it must not compete with
   the word on the other four. Dropped below 1080px, where the five pills are
   close to wrapping and a second row of chrome is exactly what this rail was
   folded into the header to avoid. */
.tab .tab-i {
  display: inline-flex;      /* icons.js paints a `display: block` svg into it */
  align-items: center;
  flex: none;
  opacity: .62;
  transition: opacity .12s;
}
.tab:hover .tab-i, .tab.active .tab-i { opacity: 1; }
@media (max-width: 1080px) { .tab .tab-i { display: none; } }

/* --- sub-tabs within a page ------------------------------------------ */
/* Data & Settings' three sections, and inside Imports, the users. Smaller and
   flatter than `.tab` on purpose: they sit a level below the header strip, and
   matching its weight would make the page look like it had two navigations.
   `flex-wrap` because the user strip grows with the folders on disk. */
.subtabs {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  margin-bottom: 12px;
}
.subtab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 6px 12px 5px;
  font: inherit;
  font-size: var(--fs-base);
  color: var(--text);
  opacity: .7;
  cursor: pointer;
}
.subtab:hover { opacity: 1; background: var(--panel-2); }
.subtab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; opacity: 1; }
.subtab.on { opacity: 1; font-weight: 600; color: var(--accent); border-bottom-color: var(--accent); }
/* The nested strip: no rule of its own under it, so the two levels do not read
   as a ladder of identical lines with a panel hanging off the bottom. */
.subtabs.users { border-bottom: 0; margin: -4px 0 8px; }
.subtabs.users .subtab { font-size: var(--fs-base); padding: 4px 10px; }

/* --- the strips on the stock popup's rail ------------------------------ */
/* Every tab of the popup that has sections draws them here, under the tabs:
   the Details tab's Position / My inputs / Business / Quality / Valuation /
   Data, the Charts tab's strip of studies, the Rating tab's five sections and
   the AI tab's two halves. One place and one look, so switching a section is
   the same gesture in the same row whichever tab is open. */
/* On the rail a strip brings no rule of its own: its row sits under the one
   the tabs are underlined on, and a second rule beneath it would close a box
   around the pills. The lit tab loses its underline for the same reason - two
   sets of tab-marks under one another read as a ladder rather than as a level
   and the level below it. A pill in the same accent instead. */
.modal-rail .subtabs { flex: 1 1 auto; min-width: 0; margin: 0; border-bottom: 0; }
.modal-rail .subtab {
  border-bottom: 0;
  margin-bottom: 0;
  border-radius: var(--radius-pill);
  padding: 3px 9px;
  font-size: var(--fs-sm);
}
.modal-rail .subtab.on { background: var(--accent-soft); }
/* The Details strip alone carries a count per tab - two or three cards each.
   The count is what says a tab is worth clicking: a watchlist-only name has
   nothing on Quality, and a tab that would open on an empty row should say so
   first. */
.detail-subtabs .subtab { display: inline-flex; align-items: center; gap: 6px; }
.detail-subtabs .count {
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  line-height: 1;
  padding: 2px 4px;
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--muted);
}
.detail-subtabs .subtab.on .count { background: var(--accent-soft); color: inherit; }
/* Mid-drag, the tab a card would land on. The same accent wash a column's tail
   zone gets, because it is the same gesture: drop here and the card moves. */
.detail-subtabs .subtab.drop-into {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border-bottom-color: var(--accent);
  opacity: 1;
}

/* ONE cluster on ONE seat. What is left on it is STATUS: when the page last
   refreshed and which build is serving it. The action that changes it now
   leads the bar at the top left. Everything somebody chooses - the
   theme, the interval, the palette, the row height, the account - moved into
   the gear beside this seat, which is why the hairline dividers that used to
   group the row into {when it refreshed} | {how often, and now} | {theme} are
   gone: three things that all answer "is this page current?" do not need
   dividing from each other.
   No vertical margin, so the seat is shorter than the 39px the loose row
   measured and the topbar's height is still set by the tab rail. */
.topbar-right {
  display: flex;
  align-items: center;
  gap: 4px 10px;
  padding: 3px 11px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--panel-2) 70%, transparent);
  flex-wrap: wrap;
}
.field { display: flex; align-items: center; gap: 5px; font-size: var(--fs-sm); color: var(--muted); }
.refresh-info { display: flex; align-items: center; gap: 6px; font-size: var(--fs-sm); color: var(--muted); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--positive); flex: 0 0 auto; }
.dot.stale { background: var(--warn); }
/* A refresh in flight is the one thing in the chrome that says the page is not
   a screenshot. The halo replaces a 1s opacity blink: a dot that vanishes for
   half of every second reads as a rendering fault, where an expanding ring
   reads as work. It is a third channel over a dot whose label already says
   "refreshing", which is what makes it safe to drop entirely below. */
.dot.busy { background: var(--accent); animation: dot-pulse 1.8s ease-out infinite; }
.dot.error { background: var(--negative); }
@keyframes dot-pulse {
  0% { box-shadow: 0 0 0 0 var(--accent-soft); }
  70%, 100% { box-shadow: 0 0 0 6px transparent; }
}

/* --- the settings menu ------------------------------------------------ */
/* The gear on the right end of the bar, and the panel it drops. It holds the
   five things that used to be strung across the header - theme, refresh
   interval, the custom palette, row height and the signed-in account - which
   is what let the seat beside it shrink to status alone.
   `position: relative` is what the panel hangs off, and it is on this wrapper
   rather than on `.topbar` so the panel lands under the gear on a desktop
   instead of at the right edge of the whole bar. The phone menu below hangs
   off `.topbar` on purpose, because there the bar IS the width. */
.settings-menu { position: relative; display: inline-flex; align-items: center; }
/* Not inside `.topbar-right`: that seat is `display: none` below 820px and
   nothing within a hidden box can be shown again, so the gear would have gone
   with it. Outside, the phone keeps the whole menu - which is the first time
   the theme, the interval and Sign out have been reachable there at all. */
.settings-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  /* The shelf `.dropdown-panel` and the phone tab menu both float on. Above
     the grid's sticky header (4) and the strip, below the modal layer. */
  z-index: 30;
  width: 264px;
  /* A phone is narrower than the panel plus the padding the bar keeps, and an
     absolutely positioned box does not shrink to fit on its own - without this
     the right-anchored panel pushes the document sideways. */
  max-width: calc(100vw - 20px);
  padding: 5px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
}
/* Label left, control right, on a row that lights up under the pointer - the
   `.dropdown-panel label` idiom, so the two menus in this header read as the
   same control. */
.settings-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 5px 7px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-base);
  color: var(--text);
  cursor: pointer;
}
.settings-item:hover { background: var(--panel-2); }
.settings-item select { flex: none; min-width: 104px; font-size: var(--fs-base); }
/* Right-aligned like the selects above it, rather than tucked in front of its
   own word the way the grid toolbar drew it. In a menu the label column and the
   control column are the whole layout, and a tick-box that led its label would
   be the one row reading the other way round. */
.settings-item input[type="checkbox"] {
  flex: none;
  width: 15px;
  height: 15px;
  margin: 0 4px 0 0;
  cursor: pointer;
}

/* The account, under a rule: everything above it changes how the page looks,
   and this one ends the session. A misfire here costs a sign-in, so it is
   separated rather than being a sixth row in the same list. */
.settings-account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 5px;
  padding: 8px 7px 3px;
  border-top: 1px solid var(--line);
}
/* The address wraps to a second line rather than truncating, which the header
   had no room for and a 264px panel does: it is the one thing in here that
   says *whose* portfolio is on screen. */
.settings-account .auth-user {
  max-width: none;
  overflow: visible;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* --- toolbar above a grid ------------------------------------------- */
.toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.toolbar .spacer { flex: 1; }
/* 228px, not the old 190px: the magnifier takes 28px off the left of the box
   and the placeholder is a sentence, so at 190px it clipped to "notes, dom". */
.toolbar input[type="text"] { min-width: 228px; }
.count { color: var(--muted); font-size: var(--fs-sm); }

/* The toolbar's own tick-boxes - Held and Deleted on the watchlist. Grouped
   under an id rather than left loose in the toolbar, because the phone rule far
   below sheds this toolbar's controls one selector at a time and every one of
   them has to be tight enough to miss the Dashboard's toolbar, which is a
   `.toolbar` too. `#g-toggles` is the grid's, `.toggle` alone would not be. */
#g-toggles { display: inline-flex; align-items: center; gap: 6px; }

/* A label
   rather than a box with text beside it, so the word is part of the hit area:
   these sit between two buttons, and a 13px square is a smaller target than
   anything else on the row.
   Not a `.btn`, deliberately. They are not actions - nothing happens elsewhere
   when one is clicked, the page simply shows more or less of itself - and
   dressing a filter as a button would put four things that look like buttons in
   a row of which two toggle and two act. */
.toolbar .toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px 3px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.toolbar .toggle:hover { border-color: var(--accent); color: var(--text); }
.toolbar .toggle input { margin: 0; cursor: pointer; }
/* Ticked reads as the stronger state, because it is: the page is showing more
   than the box beside it would. */
.toolbar .toggle:has(input:checked) { color: var(--text); }
.toolbar .toggle input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The count behind a toggle that is OFF - "Deleted (37)". Dimmer than the
   label, because it is a fact about the list rather than part of the control's
   name, and empty most of the time. */
.toolbar .toggle-note { color: var(--muted); font-variant-numeric: tabular-nums; }

/* The portfolio's own figures, above the dashboard's widgets, in the strip's
   chip. Figure first and label after, rather than a caption above a number:
   what is being read is the number, and
   putting it in front means the row scans as a sentence of values instead of a
   row of headings you have to look under. `order` rather than `row-reverse` so
   the DOM keeps saying label-then-value for a screen reader. */
.summary-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.summary-bar .cell {
  display: flex;
  align-items: baseline;
  gap: 7px;
  padding: 6px 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
}
.summary-bar .k { order: 2; color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; }
.summary-bar .v {
  order: 1;
  font-family: var(--mono);
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* A signed figure tints the whole chip, not just its digits. Four green words
   and one red one in a row of plain chips is a colour you have to hunt for; a
   tinted chip is found without reading it. Keyed off the value's own class, so
   nothing has to be told twice about what counts as good. */
.summary-bar .cell:has(.v.pos) {
  background: color-mix(in srgb, var(--mark-good) 10%, var(--panel));
  border-color: color-mix(in srgb, var(--mark-good) 35%, transparent);
}
.summary-bar .cell:has(.v.neg) {
  background: color-mix(in srgb, var(--mark-bad) 10%, var(--panel));
  border-color: color-mix(in srgb, var(--mark-bad) 35%, transparent);
}

/* The stops-hit chip carries tickers, not a figure, so it opts out of the
   mono/tabular treatment the other cells get and prints each symbol as its own
   bordered chip. A comma-separated run of five tickers inside one cell read as
   a sentence; five chips read as five positions, which is the count that
   matters. Amber rather than red for the frame - the cell is a standing
   warning, and a solid red block beside four neutral chips shouts louder than
   a breach of one stop out of fifty deserves. */
.summary-bar .cell.alert {
  background: color-mix(in srgb, var(--warn) 10%, var(--panel));
  border-color: color-mix(in srgb, var(--warn) 40%, transparent);
}
.summary-bar .cell.alert .v { display: flex; flex-wrap: wrap; gap: 4px; font-family: var(--font); }
.chip {
  display: inline-block;
  padding: 0 6px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--panel);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 17px;
  letter-spacing: .01em;
  color: var(--text);
}
.chip.alert {
  border-color: color-mix(in srgb, var(--negative) 45%, transparent);
  background: var(--negative-soft);
  color: var(--negative);
}

/* --- data grid ------------------------------------------------------ */
.grid-wrap {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: auto;
  /* The strip, the top bar and the toolbar all grew when they became cards; the
     reserve grew with them. Two of those three have no fixed height and are
     therefore measured rather than assumed: the strip's index chips wrap onto a
     second row on a narrow window, and the top bar folds its tab rail and its
     control seat onto rows of their own on a phone, where it is two and three
     times its desktop 38px. A reserve that wrote either down as a constant
     pushed the grid's last rows off screen and scrolled `main` instead of the
     grid, taking the sticky header up with it.
     main.js publishes `--strip-h` and `--topbar-h`; 102px is the rest of the
     chrome, and each fallback is the element's own declared height.
     It was 146px while the summary bar stood above the grid. That bar is on the
     Dashboard now, and a reserve still counting its 44px is 44px of dead band
     under every table. */
  max-height: calc(100vh - 102px - var(--strip-h, 42px) - var(--topbar-h, 38px));
  max-height: calc(100dvh - 102px - var(--strip-h, 42px) - var(--topbar-h, 38px));
  background: var(--panel);
  box-shadow: var(--shadow-sm);
}
table.grid { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; }

/* Micro-caps in --muted, not full-strength body text. Eighty column names set
   at the weight of the figures under them compete with those figures for the
   eye, and the names are the part you already know. `line-height` is stated
   rather than left to the font, because the filter row's sticky offset below
   is this rule's height and a computed line box is not a number you can put in
   a stylesheet.

   The band is TWO of those lines, always, and `height` says so: a squeezed
   column wraps its name onto a second row instead of losing the end of it to
   an ellipsis, and a column wide enough for one line centres that line in the
   same band. Stated rather than left to the content for two reasons. The
   filter row below is parked at this row's height in pixels, so a header that
   grew and shrank with whichever column happened to be narrow would drag the
   filter inputs off the labels every time one was dragged past its wrapping
   point. And the whole table body would step up and down mid-drag with it,
   which is the kind of movement that makes a resize feel broken even when the
   width lands where it was aimed.
   Height = 5 + 28 + 5 + the 1px bottom border = 39px. */
table.grid th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  border-right: 1px solid var(--line-soft);
  padding: 5px 8px;
  line-height: 14px;
  height: 28px;
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
  white-space: normal;
  cursor: pointer;
  user-select: none;
}
/* The label, its mark and its sort arrow, in a box of their own - `renderHead`
   wraps all three. The cap has to live here rather than on the `th`: `height`
   on a table cell is a minimum, not a maximum, so a three-line name in a very
   narrow column would push the row taller and take the filter row's offset out
   with it however the cell was clipped. `max-height` on an ordinary block does
   bound it, so the third line is simply not drawn - the full name is on the
   header's tooltip, which is where it was already.
   `break-word` and not `anywhere`: a name breaks mid-word only when the word
   cannot fit a line on its own, so "FREE CASH FLOW" wraps between its words
   while "CONSOLIDATED" still breaks when the column is 40px wide. */
table.grid th .th-l {
  display: block;
  max-height: 28px;
  overflow: hidden;
  overflow-wrap: break-word;
}
table.grid th:hover { color: var(--accent); }
table.grid th .sort { color: var(--accent); font-size: var(--fs-2xs); margin-left: 2px; }

/* No vertical rule between body cells. A rule on all four sides of every cell
   draws the grid twice - once in ink and once in the alignment of the figures
   themselves - and on a forty-column screen the ink wins. The header keeps its
   hairline, which is also where the resize handle is grabbed. */
table.grid td {
  border-bottom: 1px solid var(--line-soft);
  padding: 8px 10px;
  height: var(--row-h);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--fs-sm);
  line-height: 1.3;
}
/* The whole ladder moved up a rung. Compact used to be a zero-padding 22px row
   - a wall of figures with nothing between them, where the eye loses its place
   on a sideways scroll and reads the wrong row's P/L. It is still the densest
   of the three and still the one that fits the most on a screen; it is no
   longer the one that makes you count rows with a finger. Roomy is the brief's
   12px.

   25px, down a notch from 27 (owner call, 2026-08-23). The height is not a
   preference and cannot be typed at random: every compact row has to clear the
   tallest thing that can stand in one, and two things tie at 18px - the
   `.row-btn` in the symbol cell, which is deliberately the same size on every
   density, and a `.band` pill, whose 16px line box plus its own hairline comes
   to the same. So the row is 18 + padding + padding + the 1px rule, and the
   only figure on that line this notch could move was the padding: 4 to 3, and
   27 to 25 with it. `height` on a table cell is a minimum, so a smaller number
   here would not shrink the row - it would only stop describing it.

   The next notch down is not free. It has to come out of the 18px, which means
   either the hit area of the row buttons or the size of the band pills, and
   both are decisions about legibility rather than about density. Neither should
   be made by editing this line. */
table.grid.compact td { padding: 3px 9px; height: 25px; font-size: var(--fs-xs); }
table.grid.roomy td { padding: 12px 12px; height: 44px; font-size: var(--fs-base); }

/* Figures are set in the UI sans like everything else, and `tabular-nums` is
   what lines the column up on its decimal point. The face used to be the mono
   stack, which bought that same alignment a second time and paid for it in
   legibility - Consolas digits at 11px against Segoe UI labels in the next
   column read as two different documents. Tabular figures are a property the
   sans has too; asking for them is enough. */
table.grid td.num, table.grid th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* Per-column alignment, chosen under Columns ▾ and saved with the view. These
   come after `.num` on purpose: they carry the same specificity, so the later
   rule wins and a numeric column can be dragged left without losing the mono
   figures that make it readable. */
table.grid td.al-left, table.grid th.al-left { text-align: left; }
table.grid td.al-center, table.grid th.al-center { text-align: center; }
table.grid td.al-right, table.grid th.al-right { text-align: right; }

/* A column's mark, in the two places it is drawn: the grid header and the
   column picker. `inline-flex` because `.icon` is `display: block`, which
   inside a `th` or a `label` would put the text on a line of its own; the
   negative `vertical-align` sits the drawing on the cap line of the text rather
   than on its baseline, where it reads as having fallen off.

   Quieter than the label deliberately. The mark is a second channel for finding
   a column among forty, not a thing to read - at full strength a row of forty
   icons competes with the text it is there to help locate. It comes up with the
   row it belongs to, so whatever is under the pointer is unambiguous. */
.col-i {
  display: inline-flex;
  justify-content: center;
  vertical-align: -2px;
  margin-right: 4px;
  opacity: .55;
  /* Never a target. A header is `draggable`, so an svg the pointer can grab is
     a browser-native image drag instead of the column reorder; and in the
     picker the mark sits inside the `label`, where a hit that stops here is a
     click that does not tick the box. Transparent to the pointer, both work. */
  pointer-events: none;
}
table.grid th:hover .col-i { opacity: 1; }
/* In the picker the slot is kept even when the column has no mark - see
   `markMarkup` - so the labels still line up down the list, which means the
   width has to be stated rather than left to the drawing inside it. */
.picker-row .col-i { width: 15px; margin-right: 6px; }
.picker-row label:hover .col-i { opacity: 1; }
/* Banding carries the horizontal read now that the vertical rules are gone, so
   it stays - just quieter, because it is doing the job alongside a row rule
   rather than instead of one. The hover is mixed from --accent rather than
   hard-coded blue: a custom theme changes what the application's accent is,
   and the row under the pointer was the one place still answering in the
   default. */
/* The stripe is barely there and the hover is unmistakable, which is the right
   way round: the stripe only has to stop a forty-column row being read across
   two lines, while the hover has to say which row the pointer is on before the
   eye has finished travelling to it. */
table.grid tbody tr:nth-child(even) { background: var(--row-stripe); }
table.grid tbody tr:hover { background: var(--row-hover); }

/* Whichever column the user has dragged into first place is pinned, so the row
   stays identifiable on a wide sideways scroll. Applied by position, not by
   column, precisely because the order is the user's to change. */
table.grid th.pin, table.grid td.pin {
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--panel);
  border-right: 1px solid var(--line);
}
table.grid th.pin { z-index: 4; background: var(--panel-2); }
table.grid tbody tr:nth-child(even) td.pin { background: color-mix(in srgb, var(--panel) 96%, var(--text)); }
table.grid tbody tr:hover td.pin { background: color-mix(in srgb, var(--panel) 88%, var(--accent)); }

/* --- row selection ----------------------------------------------------- */
/* The tick-box column, watchlist only. It sits outside the column registry -
   there is no data behind it - so it is the one column whose width lives here
   and in grid.js rather than in the saved view. The two have to agree: the
   colgroup sizes the column from SELECT_W and this offsets the pinned data
   column that follows it, so a change in one place alone puts the pinned
   column on top of the boxes. */
table.grid { --sel-w: 34px; }
table.grid th.sel, table.grid td.sel {
  position: sticky;
  left: 0;
  z-index: 3;
  padding: 0;
  text-align: center;
  background: var(--panel);
  border-right: 1px solid var(--line);
}
table.grid th.sel { z-index: 5; background: var(--panel-2); }
table.grid tbody tr:nth-child(even) td.sel { background: color-mix(in srgb, var(--panel) 96%, var(--text)); }
table.grid tbody tr:hover td.sel { background: color-mix(in srgb, var(--panel) 88%, var(--accent)); }
/* The tick-box itself. 13px rather than the browser's default, which is 13 on
   one platform and 16 on another - and this column is sized by `--sel-w`, so a
   box that is three pixels wider than the file thinks re-centres itself
   off-centre. Stating it makes the column's width and its contents agree.

   Bigger than default on purpose, too: this is the smallest target on the page
   and the one a delete goes through. The 34px column gives it the room. */
table.grid th.sel input, table.grid td.sel input {
  margin: 0;
  vertical-align: middle;
  cursor: pointer;
  width: 13px;
  height: 13px;
}
/* An unticked box is an outline the eye can skip; a ticked one has to be found
   from across a screenful. `accent-color` fills it - that is set once for the
   whole application at the top of this file - and this adds the ring that makes
   the filled square read at a glance without animating anything on a table that
   can be eight hundred rows long.
   On the box rather than on the row: the row is already tinted by `tr.picked`
   below, and the two together are what make a selection legible when the tint
   is competing with a heat wash in every cell beside it. */
table.grid td.sel input:checked {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 2px;
}
/* Keyboard focus still has to beat the ring above, or tabbing through a column
   of ticked boxes shows nothing moving. */
table.grid th.sel input:focus-visible, table.grid td.sel input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* Whichever column the user dragged into first place is still pinned - it just
   starts where the boxes end rather than at the left edge. */
table.grid.has-sel th.pin, table.grid.has-sel td.pin { left: var(--sel-w); }

/* A ticked row. Tinted rather than outlined: an outline on a sticky-column row
   breaks at the pin boundary, and the tint has to survive the stripe and the
   hover, which is why it out-scores both above. */
table.grid tbody tr.picked > td,
table.grid tbody tr.picked:nth-child(even) > td {
  background: color-mix(in srgb, var(--panel) 82%, var(--accent));
}
table.grid tbody tr.picked:hover > td {
  background: color-mix(in srgb, var(--panel) 74%, var(--accent));
}

/* A row that has been deleted from the watch list, on screen only because the
   Deleted tick-box is on. Muted foreground and a struck ticker, NOT a tinted
   background: every cell in the row may already carry a heat tint, and a second
   background under them would read as one more ranking rather than as "this
   stock is not on the list". The D badge beside the ticker names it in a
   letter; this is what makes the whole row look different at a glance.
   `opacity` would have been the obvious way and is the wrong one - it opens a
   stacking context on the cell, and the first column of this table is sticky. */
table.grid tbody tr.deleted > td,
table.grid tbody tr.deleted > td .sym-link { color: var(--muted); }
table.grid tbody tr.deleted > td .sym-link {
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--muted) 60%, transparent);
}

/* --- column resizing ------------------------------------------------- */
/* Applied by resize.js to any table it manages. The grab zone is the last few
   pixels of a header cell, located from the pointer position rather than from a
   handle element, so there is nothing to re-insert when a table rewrites its
   own markup. Resizing only means anything under the fixed layout algorithm -
   with the automatic one the widest cell wins and dragging narrower does
   nothing. */
.resizable { table-layout: fixed; }
.resizable th, .resizable td { overflow: hidden; text-overflow: ellipsis; }
/* The note editor pops out of its cell and must not be clipped by it. */
.resizable td:has(.cell-area), .resizable td:has(.cell-input),
.resizable td:has(.cell-pick) { overflow: visible; }

.resizable th.rz-edge, .resizable td.rz-edge { cursor: col-resize; }
.resizable th.rz-edge { box-shadow: inset -2px 0 0 var(--accent); }
.resizable td.rz-edge { box-shadow: inset -2px 0 0 var(--line); }
body.rz-dragging { user-select: none; }
body.rz-dragging * { cursor: col-resize !important; }

/* Absorbs the space left over when the columns do not fill the panel. Without
   it the fixed algorithm shares that slack back out and a 10px drag lands 12px
   wide. Never a sort target and never a drag target. */
table.grid th.rz-filler, table.grid td.rz-filler {
  padding: 0;
  border-right: 0;
  cursor: default;
}
table.grid th.rz-filler:hover { color: inherit; }

/* Static tables are wrapped in this on the way to being resizable, so widening
   a column past the panel scrolls rather than bursting the layout. */
.table-scroll { overflow-x: auto; max-width: 100%; }

/* --- column reordering ------------------------------------------------ */
/* Native drag and drop on the header row. The dragged column fades, and the
   header under the pointer shows which of its edges the column will land on.
   The inset shadow is the same device the resize handle uses, in the same
   accent colour, one pixel wider - and it beats it on specificity, so a
   reorder in progress is never confused with a resize. */
/* Two tables wear these, not one: the grid's head and the Reports log's. Said
   as one rule rather than copied, because a reader who has learnt what a fading
   header and an accent edge mean on Core must not have to learn it again two
   tabs away - and the copy is the one that drifts. */
table.grid th.dragging, .dmlog-table th.dragging, .daymove-table th.dragging { opacity: .4; }
table.grid th.drop-before,
.dmlog-table th.drop-before,
.daymove-table th.drop-before { box-shadow: inset 3px 0 0 var(--accent); }
table.grid th.drop-after,
.dmlog-table th.drop-after,
.daymove-table th.drop-after { box-shadow: inset -3px 0 0 var(--accent); }

/* And every OTHER table, which is now all of them - see `tablekit.js`. They
   have no class in common to be named by, so the engine stamps `data-tk="on"`
   on each one it takes over and that attribute is the selector. Same three
   marks, same accent, deliberately not a fourth vocabulary to learn. */
table[data-tk="on"] th.dragging { opacity: .4; }
table[data-tk="on"] th.drop-before { box-shadow: inset 3px 0 0 var(--accent); }
table[data-tk="on"] th.drop-after { box-shadow: inset -3px 0 0 var(--accent); }

/* --- the shared table kit --------------------------------------------- */
/* Sorting, searching, resizing and reordering, on every table in the
   application that is not the data grid. The markup these rules paint is
   inserted by `tablekit.js` at run time, so nothing in any page's HTML asks
   for it - which is the whole point of the feature and the reason the styling
   has to be stated once, here, rather than per table.

   A header is a control now: it sorts on click and drags sideways to move its
   column, so it takes the pointer and stops selecting its own text mid-drag. */
table[data-tk="on"] thead th {
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
}
/* A column whose cells are controls rather than values - the shortlist's
   approve/reject pair - still drags and still resizes. What it must not do is
   offer a pointer to a click that has nothing to sort. */
table[data-tk="on"] thead th[data-tk-sort="off"] { cursor: default; }
/* The last few pixels are the resize handle's, and they have to say so even on
   a table that has not been pinned yet: a fluid table has no `.resizable`
   class, so the engine's own rule up beside the grid's cannot reach it. */
table[data-tk="on"] thead th.rz-edge {
  cursor: col-resize;
  box-shadow: inset -2px 0 0 var(--accent);
}
table[data-tk="on"] thead th.tk-sorted { color: var(--accent); }
/* Reserved even when empty, so the header does not jump a few pixels wider the
   first time a column is sorted and shove the row beside it. */
.tk-arrow {
  display: inline-block;
  min-width: 8px;
  margin-left: 3px;
  font-size: 8px;
  line-height: 1;
  vertical-align: middle;
  color: var(--accent);
}

/* The strip above the table: search, what it left showing, and a way back.
   Right-aligned and quiet at rest - there is one of these over every table in
   the application, and seventy loud ones would be a worse page than no search
   at all. It comes up to full strength on hover, or when it is doing something. */
.tk-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 4px;
  margin: 0 0 2px;
  opacity: .5;
  transition: opacity .12s;
}
.tk-bar:hover, .tk-bar:focus-within, .tk-bar.on { opacity: 1; }
.tk-bar .input-icon > .icon { left: 6px; }
.tk-bar .tk-q {
  width: 22px;
  padding: 1px 4px 1px 22px;
  font-size: var(--fs-sm);
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  transition: width .12s;
}
/* Wide from the start on a table long enough to be worth searching, and on any
   table whose box is focused or already carries a query. Everywhere else it is
   the icon alone: still there, still tabbable, still searches - it just does
   not spend 170px of a chart card offering to filter three rows. */
.tk-bar.wide .tk-q,
.tk-bar .tk-q:focus,
.tk-bar .tk-q:not(:placeholder-shown) { width: 170px; }
.tk-bar .tk-count { font-size: var(--fs-sm); color: var(--muted); }
.tk-bar .tk-reset {
  display: inline-flex;
  align-items: center;
  padding: 1px 4px;
  line-height: 1;
  color: var(--muted);
}
.tk-bar .tk-reset:hover { color: var(--accent); }

.sym-link { color: var(--accent); cursor: pointer; font-weight: 600; }
.sym-link:hover { text-decoration: underline; }

/* Inline row actions in the symbol cell: move to the other profile, remove from
   this one. Held back until the row is hovered so a long grid does not read as
   two columns of buttons.
   Circular micro-buttons carrying a line icon, not bracketed glyphs. `[⇄]` and
   `[✕]` were text in a box: they inherited the cell's font, so they changed
   size with the density control and sat on the text baseline rather than on the
   row's centre line. A round 18px target with a 13px icon in it is the same
   size on every density, is a real hit area rather than two characters wide,
   and reads as a control instead of as more content in the ticker cell. */
table.grid td .row-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  margin-left: 3px;
  vertical-align: middle;
  border: 0;
  border-radius: 50%;
  background: var(--ghost);
  color: var(--muted);
  opacity: .45;
  transition: opacity .12s, background .12s, color .12s;
}
table.grid tbody tr:hover .row-btn { opacity: 1; }
table.grid td .row-btn:hover { background: var(--accent-soft); color: var(--accent); }
table.grid td .row-btn.danger:hover { background: var(--negative-soft); color: var(--negative); }
table.grid td .row-btn:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }

.pos { color: var(--positive); }
.neg { color: var(--negative); }
.muted { color: var(--muted); }

td.editable { cursor: text; }
/* A Date cell is opened, not typed into - the calendar in `datepopup.js` is the
   only way to write one. A caret over it would promise a keyboard that never
   arrives, so it wears the pointer `.cell-pick` wears for the same reason. */
td.editable.picks { cursor: pointer; }
td.editable:hover { box-shadow: inset 0 0 0 1px var(--line); }
td .cell-input, td .cell-area, td .cell-pick {
  width: 100%;
  border: 1px solid var(--accent);
  border-radius: var(--radius-xs);
  background: var(--panel);
  padding: 1px 4px;
  font-size: var(--fs-sm);
}
/* A Dropdown column's cell. Same accent border as the box it replaces - it is
   the same state, "this is the cell being edited" - and a pointer rather than a
   caret, because there is nothing here to type into. */
td .cell-pick { cursor: pointer; }
td .cell-area { min-height: 76px; resize: vertical; position: absolute; z-index: 40; width: 320px; }
td.has-note { position: relative; }
td.has-note::after {
  content: "";
  position: absolute;
  top: 2px; right: 2px;
  border: 3px solid transparent;
  border-top-color: var(--accent);
  border-right-color: var(--accent);
}

/* stop-loss breach: the row's `sl-hit` class deliberately styles NOTHING in the grid
   (owner call, 2026-08-10). The history: a pulse, then a standing loss block on the
   symbol column - and on a screen of fifty rows the one ticker you most need to read
   was the one sitting on a tinted background, in both themes. The SL badge beside the
   price already names the breach in a word, so the symbol cell keeps the same
   background and foreground as every other row. The class stays on the <tr> because
   the detail popup's card (`.section.holding.sl-hit`, below) still keys off it.

   The popup card's `slflash` keyframe went the same way (2026-08-23). It pulsed
   the whole card between --panel and the solid loss block, 1.1s, forever, and
   it was the one animation in this file with no `prefers-reduced-motion` arm -
   the dot, the spinner and the top bar's progress line all have one. An
   infinite background flash under a card somebody is reading is the same call
   the grid row already made and lost. The 3px --negative rule down the card's
   left edge says it once, and keeps saying it. */
@media (prefers-reduced-motion: reduce) {
  .dot.busy { animation: none; box-shadow: 0 0 0 3px var(--accent-soft); }
}
/* Fully rounded and tinted from their own status colour, so a badge is legible
   as a badge at a glance rather than only once its four-letter word has been
   read. The line box is 14px, which with its own hairline makes 16 - inside the
   18px a compact row leaves for its contents, and so inside the shortest row
   the density control offers. */
.badge {
  display: inline-block;
  padding: 0 7px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  line-height: 14px;
  font-weight: 700;
  letter-spacing: .04em;
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--muted);
}
.badge.acct {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
/* The one badge that is an alarm rather than a label, so it is the one badge
   that is FILLED. It wears the same loss block as the cell under it and the
   price pill in the popup, which is what stops three different reds reading as
   three different severities of the same fact. */
.badge.sl {
  color: var(--pnl-neg-fg);
  border-color: transparent;
  background: var(--pnl-neg-bg);
}
.badge.moved {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 50%, transparent);
  background: color-mix(in srgb, var(--warn) 14%, transparent);
}
/* A warning that is not yet an alarm: an agency downgrade or a negative watch,
   which caps the score at 6 rather than at 4. Amber and outlined rather than
   filled, so it reads as one step below `.sl` at a glance instead of needing to
   be read - the two sit in the same column and are the same kind of fact. */
.badge.wn {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 55%, transparent);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
}
.badge.held {
  color: var(--positive);
  border-color: color-mix(in srgb, var(--positive) 50%, transparent);
  background: color-mix(in srgb, var(--positive) 14%, transparent);
}
/* Off the watch list. The one badge drawn in the muted grey rather than in a
   status colour, because it is not a verdict on the stock - nothing is wrong
   with it, it is simply not being followed - and the row it sits on is already
   the quietest thing on screen. */
.badge.gone {
  color: var(--muted);
  border-style: dashed;
}

/* --- size bands ------------------------------------------------------ */
/* A banded column shows a word where every other numeric column shows a
   figure, and as bare text that word read as data somebody had typed in. As a
   pill it reads as a classification, which is what it is.
   The ladder is deliberately not a green-to-red ramp: size is not goodness,
   and tinting Micro red would state the opposite of this repo's whole thesis.
   It is neutral for the two small ends and then indigo -> violet -> purple as
   the cap climbs, so the eye can sort the column without reading it while
   nothing in the colour says which end to prefer.
   Slugged from the band's own label by `bandSlug`, so an unknown band renders
   in the neutral base rather than unstyled - the thresholds live in the
   backend registry and this stylesheet must not need editing when they move. */
/* --- the stance, category and delta palette --------------------------------
   Absolute hexes rather than theme tokens, and deliberately so: these are a
   fixed vocabulary - a stance, an axis of the rubric, the sign of a number -
   and the whole point of one is that it means the same thing wherever it is
   drawn. A green that shifted between light and dark would be a second green
   to learn. They are declared once here so there is a single place to retune
   them, and every rule below reads them rather than repeating a hex.

   The delta pills are the one place to watch: their backgrounds are pastel by
   design, so on the dark theme they read as bright chips rather than as a
   tint. That is legible and intended - the pill is the verdict on the row -
   but it is the knob to turn first if the column ever reads too loud. */
:root {
  /* Stance. Hold is the slate-blue of a settled position, add the emerald of
     capital going in, queue the violet of capital committed but not yet sent,
     review the amber of a decision not yet taken, exit the crimson of one
     taken against. Quiet slate is the sixth: not a stance but the absence of
     one, worn by the free text this column also carries.

     Queue is violet rather than a second green on purpose. `Add Now` and
     `Add When` are one word apart and eleven rows apart in what they ask of
     you today; two shades of the same hue would have to be compared to be
     told apart, and a status column is scanned, not compared. */
  --stance-hold: #2563eb;
  --stance-hold-quiet: #475569;
  --stance-add: #059669;
  --stance-queue: #7c3aed;
  --stance-review: #d97706;
  --stance-exit: #dc2626;

  /* One hue per axis of the rubric. Not a verdict: valuation is red because it
     is valuation, never because valuation went badly. */
  --cat-growth: #10b981;
  --cat-quality: #6366f1;
  --cat-balance: #0ea5e9;
  --cat-promoter: #8b5cf6;
  --cat-valuation: #ef4444;
  --cat-hist-valuation: #f59e0b;
  --cat-discovery: #64748b;
  --cat-size: #14b8a6;
  --cat-liquidity: #06b6d4;
  --cat-cyclical: #f43f5e;
  --cat-tailwind: #22c55e;
  --cat-llm-events: #94a3b8;

  /* One hue per *scorer*, for the Analytics tab, where two answers about the
     same twelve axes sit in one table and the first question at every cell is
     "whose number is this". Same rule as the axis hues above - an identity,
     never a verdict - and it matters more here than anywhere else on the tab,
     because the cells these tint are full of pills that ARE verdicts. So both
     are used at a wash in the body and at strength only in the head, where
     nothing green or red sits beside them to be confused with.

     Cyan for the rubric because it is arithmetic - the cool end, and far enough
     from `--accent` that the two-band marker still reads as the odd one out.
     Violet for the model because that is the hue this application already
     spends on work a machine did on its own: `--stance-queue` and
     `--fetch-models` are both this violet.

     Stepped for both themes, like the Fetch-menu hues and for exactly the same
     reason: these are ink as well as wash - a group heading over two columns -
     and a single step carried across both themes is a heading that reads on one
     of them and disappears on the other. The light twins are below. */
  --an-rubric: #22d3ee;
  --an-model: #a78bfa;

  /* One hue per Fetch-menu group. Same idea as the axis hues above and the same
     rule about them: an identity, never a verdict - Scrapers is pink because it
     is Scrapers, not because scraping went badly. What they buy is a menu of
     seventeen near-identical buttons that can be scanned instead of read, since
     "which of the five kinds of work is this" is answered by the colour before
     the label is reached at all.

     Stepped per theme rather than declared once, which is where this parts
     company with the block above and follows --series-* instead. An axis hue is
     a 13px mark on a card and one value carries it; these are the ink of a
     group heading as well, and a #4ade80 that reads at 8.9:1 on slate is 1.6:1
     on white. Same five hues, one step each way, and every one of the ten
     clears 4.5:1 on both --panel and --panel-2 in the theme it belongs to -
     which is the text floor, not the 3:1 graphic one, because the heading is
     text.

     Amber and pink sit on a menu whose date stamps are tinted --warn and
     --negative, and that is a decision rather than an oversight. The stamp is
     the second line inside a button; the hue is a filled chip and a heading
     above it, and the group is named in words on that same line. The
     alternative was five hues that avoid two thirds of the wheel, which
     separates worse - this set's closest pair is ΔE 45.9 in normal vision, 18.3
     under protanopia and 23.5 under deuteranopia, against the 8.4 the eight
     chart slots ship with.

     Green is Pipeline, for the reason --series-6 is green: the no-green rule is
     about the SEMANTIC pair, where green would make a claim this application's
     reader cannot read. A group is an identity, and Pipeline is the group that
     fetches nothing. */
  --fetch-market: #22d3ee;
  --fetch-filings: #f59e0b;
  --fetch-models: #a78bfa;
  --fetch-pipeline: #4ade80;
  --fetch-scrapers: #f472b6;

  /* And one hue per KIND OF CALL, which is a different question from the group
     above and the reason `jobs.JobType.call` exists: the group says what a
     fetch is ABOUT, this says what pressing it reaches for. Three, printed as a
     bracketed word after the label - (LLM), (API), (Local).

     Violet for the model because that is what this file already spends on work
     a machine did on its own, and it is the same violet three tokens over:
     `--fetch-models`, `--an-model` and this. Teal for the call that leaves the
     machine and reaches a source for free - the wire, and far enough from both
     the cyan of the Market group and `--accent` to be its own thing. Muted for
     the one that reaches nothing, because a job that spends nothing has nothing
     to say for itself, and receding IS the answer there.

     An identity, never a verdict, like every hue above: LLM is violet because
     it is a model call, not because spending money is bad. Stepped per theme
     for the reason the five above are - these are ink, and a teal that reads on
     slate is invisible on white. */
  --call-llm: var(--fetch-models);
  --call-api: #2dd4bf;
  --call-local: var(--muted);

  /* The eight hues a Review Board category badge wears - a sector, a
     classification flag, or the reader's own label on a group of holdings.
     Built on the same terms as the five above and defended by the same
     argument, which the note over them makes in full: **a group is an
     identity**, and the no-green rule is about the SEMANTIC pair, where green
     would make a claim this application's reader cannot read. Emerald is a
     category here and never a verdict; the verdict end of this feature is the
     ticker, and that is --positive / --warn / --negative, which is
     blue-amber-red.

     Eight rather than five because the thing being coloured is open-ended -
     `research_signal` classifies four flags and the sector column is free text -
     so `review.js` names the ones worth recognising on sight and hashes the
     rest onto this ring. Eight is where the ring stops separating: the closest
     pair here is indigo/violet, and past eight the next addition would land
     between two neighbours rather than beside them.

     Every one of the sixteen carries a badge label at --fs-xs on --panel-2 and
     is stepped for the theme it belongs to, exactly as the Fetch hues are: the
     dark step is ink on slate and the light step is ink on white, and a hue
     carried across reads at a fraction of the contrast it needs. */
  --cat-indigo: #818cf8;
  --cat-emerald: #34d399;
  --cat-amber: #fbbf24;
  --cat-rose: #fb7185;
  --cat-sky: #38bdf8;
  --cat-violet: #c084fc;
  --cat-teal: #2dd4bf;
  --cat-slate: #94a3b8;

  /* The sign of a contribution. One hue per sign, and the pill's two colours
     mixed off it rather than written as a second and third hex.

     They used to be a flat pastel pair - `#dcfce7` on `#15803d` - tuned on the
     light theme and merely tolerated on the dark one, where a pastel fill is
     not a tint but a sticker: twelve of them down a wall of cards were the
     brightest things on the page, brighter than the score they qualify. The
     mix fixes that without splitting the vocabulary in two, which is the thing
     the note above this block warns against: the HUE is still one green, one
     slate and one red wherever the pill is drawn. What changes per theme is
     only how far the fill is carried towards the surface under it and the ink
     towards the text colour over it - so the pill is a soft green chip with
     deep green figures on white, and a deep green chip with soft green figures
     on slate. 5.0:1 and 6.4:1 respectively, both clear of the 4.5:1 a figure
     this size needs.

     14% is the tint every other pill in this file already wears - `.band`,
     `.status-pill`, `.badge.held` - so the delta pill stops being the one
     chip with a construction of its own.

     The three hues are the SEMANTIC pair and the muted step, read through
     `var()`, not three hexes of their own (2026-08-23). They were #10b981 and
     #ef4444 - a green and a red - and the note at the top of this block
     defended that as a fixed vocabulary that must not shift between themes.
     That argument holds for a stance and for a category, which are identities.
     It does not hold here: this pill encodes THE SIGN OF A NUMBER, which is the
     one statement --positive and --negative already make, and the whole reason
     --positive is blue is that the reader of this app does not separate red
     from green. A green-and-red pill was the app saying gain-and-loss in
     exactly the channel the rest of the palette spends 200 lines avoiding.
     Nothing else about the pill changes: the mix percentages are the same, and
     the chevron beside the figure is still the second channel. What it costs is
     the fixed-across-themes property - and these already mixed against
     `var(--panel)`, so the pill was never the same two colours on both themes
     anyway. What it buys is that a custom theme's own pair reaches it. */
  --delta-pos: var(--positive);
  --delta-zero: var(--muted);
  --delta-neg: var(--negative);
  --delta-pos-bg: color-mix(in srgb, var(--delta-pos) 14%, var(--panel));
  --delta-pos-fg: color-mix(in srgb, var(--delta-pos) 60%, var(--text));
  --delta-zero-bg: color-mix(in srgb, var(--delta-zero) 14%, var(--panel));
  --delta-zero-fg: color-mix(in srgb, var(--delta-zero) 60%, var(--text));
  --delta-neg-bg: color-mix(in srgb, var(--delta-neg) 14%, var(--panel));
  --delta-neg-fg: color-mix(in srgb, var(--delta-neg) 60%, var(--text));
}

/* The light steps of the same five hues. Beside their dark twins rather than up
   in the `[data-theme="light"]` block with the rest of the theme, because a hue
   and its other step are one decision and retuning one without the other is the
   failure this whole file is arranged to prevent. */
html[data-theme="light"] {
  --fetch-market: #0e7490;
  --fetch-filings: #b45309;
  --fetch-models: #6d28d9;
  --fetch-pipeline: #047857;
  --fetch-scrapers: #be185d;
  /* And the light step of the call hue. Only the teal is stepped: the other two
     are aliases - violet follows `--fetch-models`, which is stepped just above,
     and muted follows `--muted`, which the theme sets. */
  --call-api: #0f766e;
  /* And the eight category hues, on the same terms and for the same reason -
     a hue and its other step are one decision. */
  --cat-indigo: #4338ca;
  --cat-emerald: #047857;
  --cat-amber: #b45309;
  --cat-rose: #be123c;
  --cat-sky: #0369a1;
  --cat-violet: #7e22ce;
  --cat-teal: #0f766e;
  --cat-slate: #475569;
  /* And the two scorer hues, on the same terms. Deliberately the same steps as
     `--fetch-market` and `--fetch-models`: the rubric is the pipeline's own
     arithmetic and the model is the same model the Fetch menu buys a call from,
     so two names for one hue would be two things to retune. */
  --an-rubric: #0e7490;
  --an-model: #6d28d9;
}

.band {
  display: inline-block;
  padding: 0 7px;
  border-radius: var(--radius);
  border: 1px solid color-mix(in srgb, var(--band-neutral) 40%, transparent);
  background: color-mix(in srgb, var(--band-neutral) 12%, transparent);
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 16px;
  font-weight: 600;
  letter-spacing: .02em;
}
.band.band-mid {
  color: var(--band-mid);
  border-color: color-mix(in srgb, var(--band-mid) 45%, transparent);
  background: color-mix(in srgb, var(--band-mid) 13%, transparent);
}
.band.band-large {
  color: var(--band-large);
  border-color: color-mix(in srgb, var(--band-large) 45%, transparent);
  background: color-mix(in srgb, var(--band-large) 13%, transparent);
}
.band.band-super {
  color: var(--band-super);
  border-color: color-mix(in srgb, var(--band-super) 45%, transparent);
  background: color-mix(in srgb, var(--band-super) 15%, transparent);
}

/* --- score meter ----------------------------------------------------------
   A bounded score - AI Rating out of 10, the Trendlyne 0-100s - drawn as its
   figure with a track under it. Which columns get one is the backend registry's
   `scale`, the same way `.band` above is driven by its `bands`.

   The bar goes UNDER the number rather than beside it. These columns are 56-132px
   wide and right-aligned in a table that is read by running an eye down a
   column; a bar sharing the line would either push the figure off its right
   edge or shrink to a length that says nothing.

   And it is OVERLAID on the bottom of the line box rather than stacked below
   it, which is the part that took two attempts. `table.grid td` is a fixed
   height with `overflow: hidden`, and the three densities leave 19px, 20px and
   20px of content box against line boxes of roughly 14px, 16px and 17px - so a
   bar that adds even four pixels of its own fits in compact and is clipped in
   roomy, or grows the row and breaks the one thing a dense grid must not do.
   Absolutely positioned it costs nothing in any density, and digits have no
   descenders, so the leading under the baseline is empty space it can sit in.

   2px, and only as tall as that. It is a second reading of a number that is
   already printed - the moment it is thick enough to read on its own it starts
   competing with the figure, and the figure is what the column sorts on. */
/* `overflow`/`text-overflow` restated rather than left to `table.grid td`. That
   rule ellipsises the cell's own inline content, and wrapping the figure in a
   block child moves the overflow boundary in here - without these a score in a
   column dragged narrow would clip mid-digit instead of ellipsising, which is
   the one way this could have made an ordinary cell worse than it was.
   `white-space: nowrap` still comes down from the cell. */
.score {
  position: relative;
  display: block;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The figure keeps whatever alignment the column gave the cell, which for a
   score is right. */
.score-v { text-align: inherit; }
.score-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 1px;
  background: color-mix(in srgb, var(--text) 12%, transparent);
  overflow: hidden;
}
/* The fill. `--fill` is the only thing the grid writes - see `scalePct` - and
   everything about how it looks is decided here.

   Hue by position on the scale rather than one colour for every score: a 12/100
   and an 88/100 are opposite readings, and drawing both in the accent makes the
   column a row of blue stubs you still have to read the numbers off. The stops
   are the conditional-formatting palette, so a weak score is the same red the
   rules paint a weak cell with - one vocabulary, not two.

   `--fill` lands as a WIDTH on a solid block, not as a gradient stop: a
   gradient that ends at the fill would give every bar the same colour at its
   own tip, which is the one thing this must not do. The block is tinted by a
   variable the ladder below sets. */
.score-bar::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--fill, 0%);
  background: var(--score-hue, var(--accent));
  border-radius: inherit;
}
/* The ladder, as four named steps rather than as a reading of the percentage.
   `scaleTone` in `fmt.js` decides which one a fill earns, so the thresholds are
   a pure function that can be asserted on - and the stylesheet is left holding
   only the colours, which is the half it should own. */
.score-bar.tone-weak { --score-hue: var(--rf-red); }
.score-bar.tone-fair { --score-hue: var(--rf-amber); }
.score-bar.tone-good { --score-hue: var(--accent); }
.score-bar.tone-strong { --score-hue: var(--rf-green); }

/* --- stance ---------------------------------------------------------------
   Two shapes, one vocabulary. `.badge.status-*` is the rating hero's row of
   qualifiers; `.status-pill` is the grid's `status` cell. They share the four
   colours because they are the same four statements, and a reader who learns
   amber-means-undecided in one place must not have to learn it twice.

   Tinted rather than filled, the way `.band` is: these sit inside dense rows
   beside real figures, and a solid block of colour there out-shouts the number
   it is meant to qualify. */
.badge.status-hold, .status-pill.status-hold { --stance: var(--stance-hold); }
.badge.status-add, .status-pill.status-add { --stance: var(--stance-add); }
.badge.status-queue, .status-pill.status-queue { --stance: var(--stance-queue); }
.badge.status-review, .status-pill.status-review { --stance: var(--stance-review); }
.badge.status-exit, .status-pill.status-exit { --stance: var(--stance-exit); }
/* The free text - `Satori`, `PSL`, `50+50`, a bare trailing level. Framed like
   its neighbours so the column reads as one column, in the quiet slate so the
   frame claims nothing the word does not say. Spelled out rather than left to
   the `var()` fallback below, so a stance that is deliberately absent looks
   deliberate in the stylesheet too. */
.status-pill.status-neutral { --stance: var(--stance-hold-quiet); }

.badge.status-hold, .badge.status-add, .badge.status-review, .badge.status-exit,
.status-pill {
  color: var(--stance, var(--stance-hold-quiet));
  border: 1px solid color-mix(in srgb, var(--stance, var(--stance-hold-quiet)) 45%, transparent);
  background: color-mix(in srgb, var(--stance, var(--stance-hold-quiet)) 14%, transparent);
}
.status-pill {
  display: inline-block;
  padding: 0 7px;
  border-radius: var(--radius);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: .02em;
}

/* --- the scorecard's category head ----------------------------------------
   The mark takes the axis hue and the label stays in the body colour: a whole
   head painted in the category colour would compete with the delta pill beside
   it, and the pill is the half that carries a verdict. The `.sc-mark-*` tokens
   below ride on the card, so anything inside it can read `--cat-mark`. */
.sc-card-head b {
  display: flex;
  align-items: center;
  gap: 5px;
  /* A `<b>` is 700 by default and that is heavier than this heading needs to
     be: it is the largest text on the card and it names a category the mark
     beside it has already drawn. 600 at 12px reads as the head without
     shouting over the figures the card is opened for. */
  font-weight: 600;
  font-size: var(--fs-base);
}
/* The mark on a tinted ground of its own hue, rather than a bare stroke on the
   card. Twelve cards read as twelve of the same thing when the only difference
   between their heads is the colour of a 13px line drawing; a filled chip is a
   shape the eye lands on first and reads the colour off second, which is the
   order this head is actually scanned in.

   `content-box` because the size rides on the svg's own width/height attributes
   - the padding has to go outside that box or a 13px drawing becomes a 7px one
   inside a 13px chip. The tint is mixed against transparency rather than
   written flat so it sits correctly on both themes' card surfaces, and stops at
   14% because this is a ground, not a badge: the drawing on top of it is the
   thing being read.

   Card heads only. `.rb-name` and `.rating-weights .sc-cat` wear the same hue
   on the same drawing, but those are rows in a list rather than headings, and a
   chip on each would add 6px to every row of both. */
.sc-card-head .icon {
  flex: none;
  color: var(--cat-mark, var(--muted));
  box-sizing: content-box;
  padding: 3px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--cat-mark, var(--muted)) 14%, transparent);
}
.sc-mark-growth { --cat-mark: var(--cat-growth); }
.sc-mark-quality { --cat-mark: var(--cat-quality); }
.sc-mark-balance { --cat-mark: var(--cat-balance); }
.sc-mark-promoter { --cat-mark: var(--cat-promoter); }
.sc-mark-valuation { --cat-mark: var(--cat-valuation); }
.sc-mark-hist_valuation { --cat-mark: var(--cat-hist-valuation); }
.sc-mark-discovery { --cat-mark: var(--cat-discovery); }
.sc-mark-size { --cat-mark: var(--cat-size); }
.sc-mark-liquidity { --cat-mark: var(--cat-liquidity); }
.sc-mark-cyclical { --cat-mark: var(--cat-cyclical); }
.sc-mark-tailwind { --cat-mark: var(--cat-tailwind); }
.sc-mark-llm_events { --cat-mark: var(--cat-llm-events); }

/* --- the score delta pill -------------------------------------------------
   Tinted rather than filled, and a 4px badge rather than a 999px lozenge. It
   is still the one thing on the card that carries a verdict and still the
   loudest thing on it, but it says so by being the only coloured object in a
   card of grey figures - it does not also need a saturated fill and a 700
   face to do it, which is what made the wall read as a row of stickers.

   The mark stays: the sign is said twice - chevron and colour - because a pill
   read at a glance across twelve cards is read by shape before it is read by
   digit, and because the reader of this app does not separate red from green.
   That is the channel that must not be spent, so the geometry and the weight
   are the ones that were. */
.delta-pill {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  margin-top: 3px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 1.4;
  background: var(--delta-zero-bg);
  color: var(--delta-zero-fg);
}
.delta-pill b { font-weight: 500; }
.delta-pill .icon { flex: none; }
.delta-pill.delta-pos { background: var(--delta-pos-bg); color: var(--delta-pos-fg); }
.delta-pill.delta-neg { background: var(--delta-neg-bg); color: var(--delta-neg-fg); }

/* --- conditional formatting ------------------------------------------- */
/* A cell whose value matched one of its column's rules: `rf` on every match,
   plus `rf-<colour>` for the twelve palette names. A rule painted with a
   literal hex carries its background inline instead - only the stylesheet knows
   which theme's step a palette *name* means, and only the rule knows its own
   hex, so the two arrive by the two different routes on purpose.

   The tint is `color-mix`ed against whatever is behind it rather than written
   flat, so one rule serves both themes and the zebra stripe and the row hover
   still show through underneath - a formatted cell has to stay part of its row.

   `.rf` alone is the layout half and is emitted even for an uncoloured rule:
   it is what puts air between the mark and the figure and keeps a column of
   icons on one baseline whichever way the column is aligned. */
.rf {
  background: var(--rf-bg, transparent);
}
.rf-mark {
  display: inline-flex;
  align-items: center;
  vertical-align: -2px;
  /* Only when something follows it. An icon-only cell would otherwise sit a
     margin's width off the centre it was aligned to. */
  margin-right: 5px;
  /* Deepened away from the surface rather than drawn in the palette hue itself.
     The tint under the mark is mixed from the SAME hue, so a full-strength
     #eab308 icon on a 16% #eab308 cell was 2.51:1 - the mark was the one thing
     on a formatted cell that could not be seen. Mixing 30% of --text in always
     moves the ink away from the ground, whichever theme it is: towards white on
     slate, towards near-black on paper. Worst of the twelve goes 2.51 -> 4.29
     on light and 2.72 -> 4.19 on dark, and all twenty-four now clear the 3:1 a
     graphic needs with room to spare.
     The figure over the tint is untouched and never needed help: --text is
     8.34:1 at worst on dark and 13.90:1 on light. */
  color: color-mix(in srgb, var(--rf-fg, currentColor) 70%, var(--text));
}

/* An icon standing alone follows the column's own alignment rather than being
   centred: `inline-flex` still answers to the cell's `text-align`, so a column
   somebody set to centre gets centred marks and one they left on the right edge
   keeps them there. Where a column sits is their decision, made once under
   Columns, and a rule must not quietly overrule it for the rows that match. */
.rf-mark:only-child { margin-right: 0; }
/* ...which is what it means to say, and not what it says. A text node is not a
   child, so a mark with its figure written after it is an `:only-child` too and
   loses the margin along with the icon-only cells the rule was written for.

   The grid keeps that behaviour: a 90px cell has no width to spend on air, and
   the figure is what the column is read down. The label/value lists have the
   room, and are read one row at a time, so there the mark and the figure it
   qualifies get told apart. Two classes and a type beats two classes. */
.kv dd .rf-mark { margin-right: 5px; }

.rf-red { --rf-bg: color-mix(in srgb, var(--rf-red) var(--rf-tint), transparent); --rf-fg: var(--rf-red); }
.rf-orange { --rf-bg: color-mix(in srgb, var(--rf-orange) var(--rf-tint), transparent); --rf-fg: var(--rf-orange); }
.rf-amber { --rf-bg: color-mix(in srgb, var(--rf-amber) var(--rf-tint), transparent); --rf-fg: var(--rf-amber); }
.rf-yellow { --rf-bg: color-mix(in srgb, var(--rf-yellow) var(--rf-tint), transparent); --rf-fg: var(--rf-yellow); }
.rf-lime { --rf-bg: color-mix(in srgb, var(--rf-lime) var(--rf-tint), transparent); --rf-fg: var(--rf-lime); }
.rf-green { --rf-bg: color-mix(in srgb, var(--rf-green) var(--rf-tint), transparent); --rf-fg: var(--rf-green); }
.rf-teal { --rf-bg: color-mix(in srgb, var(--rf-teal) var(--rf-tint), transparent); --rf-fg: var(--rf-teal); }
.rf-blue { --rf-bg: color-mix(in srgb, var(--rf-blue) var(--rf-tint), transparent); --rf-fg: var(--rf-blue); }
.rf-indigo { --rf-bg: color-mix(in srgb, var(--rf-indigo) var(--rf-tint), transparent); --rf-fg: var(--rf-indigo); }
.rf-violet { --rf-bg: color-mix(in srgb, var(--rf-violet) var(--rf-tint), transparent); --rf-fg: var(--rf-violet); }
.rf-pink { --rf-bg: color-mix(in srgb, var(--rf-pink) var(--rf-tint), transparent); --rf-fg: var(--rf-pink); }
.rf-grey { --rf-bg: color-mix(in srgb, var(--rf-grey) var(--rf-tint), transparent); --rf-fg: var(--rf-grey); }

/* The popup's version. A `dd` is not a table cell, so it needs the padding and
   the corner the cell gets from the grid - without them a tinted value bleeds
   into the label beside it. */
dd.rf {
  padding: 0 6px;
  margin-left: -6px;
  border-radius: var(--radius);
}

/* --- icons ----------------------------------------------------------- */
/* Every icon is `currentColor` and `aria-hidden`, so it takes the colour of
   whatever control it sits in and is never announced twice - the button beside
   it already carries an `aria-label` and a `data-tip`. `display: block` kills
   the inline baseline gap that otherwise makes a 16px icon occupy 20px. */
.icon { display: block; flex: none; }
/* A button that is only an icon: square, centred, no text padding to balance. */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
}
.icon-btn:hover { color: var(--accent); }

/* A text input with an icon inside its left edge. The icon is positioned
   rather than made a sibling so the field keeps behaving as one control: the
   whole box is still the click target, and the placeholder starts clear of the
   glyph instead of underneath it. `pointer-events: none` so clicking the
   magnifier focuses the field rather than swallowing the click. */
.input-icon { position: relative; display: inline-flex; align-items: center; }
.input-icon > .icon {
  position: absolute;
  left: 8px;
  color: var(--muted);
  pointer-events: none;
}
.input-icon > input[type="text"] { padding-left: 28px; }

/* --- column picker / dropdown --------------------------------------- */
.dropdown { position: relative; display: inline-block; }
.dropdown-panel {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 30;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 8px;
  width: 330px;
  max-height: 460px;
  overflow: auto;
}
.dropdown-panel h4 {
  margin: 8px 0 4px;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.dropdown-panel label {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 4px;
  border-radius: var(--radius-xs);
  cursor: pointer;
  font-size: var(--fs-base);
}
.dropdown-panel label:hover { background: var(--panel-2); }
/* Every dropdown in the app but one hangs off a button parked at the right end
   of its toolbar, which is why the panel is right-aligned by default. The Jobs
   tab's Run menu is the exception - it is the FIRST control on its bar, so
   right-alignment measured a 330px panel leftwards from a button 60px in and
   left most of it off the left of the screen. This flips the anchor for that
   case; the phone rule further down still caps the width to the viewport. */
.dropdown-left > .dropdown-panel { left: 0; right: auto; }
.picker-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }

/* --- the date picker ------------------------------------------------------
   The calendar a Date cell opens - see `datepopup.js` for why the browser's own
   picker is not what appears. Positioned from JS against whatever opened it, so
   only the offsets are set there; everything the panel LOOKS like is here.

   `fixed`, not absolute: the grid scrolls inside its own box in both directions,
   and a panel measured against the document would sit where the cell used to be
   the moment anything moved. Above the modal backdrop (150), because a date
   field on the stock popup opens one; below the tooltip layer (200), because the
   tip on a day is still worth reading over it. */
.datepop {
  position: fixed;
  z-index: 160;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 7px;
  width: 232px;
}
.dp-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  margin-bottom: 5px;
}
/* The month is the one thing on the panel that has to be legible at a glance -
   it is what a reader checks before pointing at a number - so it takes the base
   size and the full-strength ink while the grid under it sits at 11px. */
.dp-month {
  flex: 1;
  text-align: center;
  font-size: var(--fs-base);
  font-weight: 600;
  color: var(--text);
}
.dp-step { padding: 2px 6px; line-height: 1; }
.dp-week, .dp-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.dp-week span {
  text-align: center;
  font-size: var(--fs-xs);
  letter-spacing: .04em;
  color: var(--muted);
  padding-bottom: 3px;
}
.dp-day {
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
  /* Square-ish and a comfortable target. A calendar is pointed at rather than
     read, and 26px is the smallest cell that is still reliably hit on a laptop
     trackpad without the grid growing taller than the cell it hangs off. */
  min-height: 26px;
  padding: 0;
  cursor: pointer;
}
.dp-day:hover { background: var(--panel-2); }
/* The days either side of this month, offered rather than hidden: picking the
   1st of next month off the foot of this one is the commonest thing a date
   picker is asked for, and a blank corner reads as a broken calendar. */
.dp-out { color: var(--muted); opacity: .65; }
/* Today: ringed, not filled. It is a landmark for finding your place in the
   grid, and filling it would make it compete with the day actually selected. */
.dp-today { border-color: var(--accent); }
/* The day the cell holds. The only filled thing on the panel, so "what is
   stored" is answered before anything is read. */
.dp-on, .dp-on:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}
.dp-foot {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--line-soft);
}
.dp-foot .btn { display: inline-flex; align-items: center; gap: 4px; }

/* The same calendar, opened from a field on the stock popup's My Inputs card
   rather than from a grid cell. The button IS the field: it shows what is
   stored, in the house format, and pressing it is the only way to change it. */
.detail-date { display: block; }
.date-pick {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  width: 100%;
  justify-content: flex-start;
}

/* Find a column by name. First in the panel because on a page carrying eighty
   columns it is what the panel is opened for, and it takes focus on open. */
.picker-find { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.picker-find input[type="text"] { flex: 1; min-width: 0; }
.picker-find .count { flex: none; }
/* `display: flex` on the row below outranks the browser's own
   `[hidden] { display: none }`, so filtering has to say so itself. The heading
   is spelled out beside it rather than left to the default, so the two cannot
   drift apart. */
.picker-row[hidden], .dropdown-panel h4[hidden] { display: none; }

/* One row per column: the visibility checkbox and nothing else. A long name is
   clipped rather than wrapped, so the list stays one line per column and can be
   scanned down its checkboxes. */
.picker-row { display: flex; align-items: center; gap: 6px; }
.picker-row label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- saved column sets ------------------------------------------------
   Between the reset actions and the sixty-row column list, closed off with a
   rule: the block is a different kind of control from the checkboxes under it
   - it changes the whole layout at once rather than one column - and without
   the divider the eye reads straight past the heading into the first group. */
.colsets { border-bottom: 1px solid var(--line); padding-bottom: 8px; margin-bottom: 2px; }
.colsets[hidden] { display: none; }
.colsets .mini-empty { padding: 2px 4px 6px; line-height: 1.45; }
.colset-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 1px 4px;
  border-radius: var(--radius-xs);
}
.colset-row:hover { background: var(--panel-2); }
/* The name IS the load button - a separate "Load" beside it would be a fourth
   control on a row that already carries three, for the one action the row is
   most obviously for. Left-aligned and full-width so a long name truncates
   rather than pushing Update off the edge. */
.colset-load {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  background: none;
  border: 0;
  padding: 2px 0;
  color: var(--text);
  font-size: var(--fs-base);
  cursor: pointer;
}
.colset-load:hover { color: var(--accent); }
.colset-row .count { flex: none; font-size: var(--fs-xs); }
/* `.icon-btn` is a modifier on `.btn` everywhere else in this stylesheet - it
   contributes the square and the centring, nothing else - so used on its own it
   inherits the browser's own button chrome, which is a white box the dark theme
   makes the glyph invisible against. This delete is deliberately bare: three
   bordered controls on one row would read as clutter beside a name. So it says
   so, rather than borrowing `.btn` and then undoing it. */
.colset-drop {
  width: 20px;
  height: 20px;
  flex: none;
  background: none;
  border: 0;
  border-radius: var(--radius-xs);
  color: var(--muted);
  cursor: pointer;
}
.colset-drop:hover { color: var(--negative); background: var(--panel); }
/* The set the page is currently showing. Marked with weight and a dot rather
   than colour alone - the same reason the tab rail carries three channels. */
.colset-row.on .colset-load { font-weight: 600; color: var(--accent); }
.colset-row.on .colset-load::after { content: ' •'; }

/* Somebody else's sets, under the address that saved them. The heading carries
   the whole width so a long address truncates instead of wrapping the group
   open, and it is muted and small because it is a label on the rows below it,
   not a section of the panel competing with "Saved column sets". */
.colset-owner {
  margin: 8px 0 2px;
  padding: 0 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--muted);
  text-transform: none;
}
/* Indented rather than tinted: the rows are the same kind of thing as mine and
   only their buttons differ, so the shared half is set apart by where it sits
   under its owner's name, not by being coloured like a warning. */
.colset-row.shared { margin-left: 6px; }

.colset-save { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.colset-save input[type="text"] { flex: 1; min-width: 0; font-size: var(--fs-sm); }

/* --- filter row ----------------------------------------------------- */
/* Both header rows are sticky, so the filter row has to be parked at exactly
   the height of the row above it - 39px, being `table.grid th`'s 5px padding
   top and bottom, its stated 28px two-line band and the 1px bottom border. That
   rule states `line-height` and `height` for this reason alone: while the line
   box was left to the font, this offset was a measurement of a rendering rather
   than a sum of declared values, and re-styling the header moved it silently.
   `height: auto` here because that band is the label row's and this rule's
   selector is the only thing between it and a filter row 28px tall for one
   14px input. Left at `top: 0`
   the two rows stack on the same pixels the moment the grid is scrolled, and
   the filter inputs cover the headers: the labels and the sort arrows go, and
   the resize handle goes with them, because resize.js finds the drag edge by
   hit-testing the pointer and the cell it lands on is then the filter cell.
   Qualified with `table.grid` on purpose. `table.grid th` sets `top: 0` at
   (0,1,2) and a bare `.filter-row th` is (0,1,1), so it lost and the offset
   here never applied - which is how it came to be scrolled-only breakage that
   looked like resizing being broken on the longer pages and fine on the short
   ones. No z-index: it would tie with `table.grid th.pin` and, coming later,
   drop the pinned filter cell from 4 to 3 and let the other filter cells slide
   over the top of it on a sideways scroll. */
table.grid .filter-row th { padding: 2px 3px; top: 39px; height: auto; }
/* The label styling on `table.grid th` reaches these inputs too - they sit in
   `th`s of the same grid - and `text-transform`, `letter-spacing` and `color`
   all inherit. Left alone, a typed filter rendered as spaced-out muted capitals
   while `input.value` stayed exactly as typed, which is the worst kind of
   mismatch: the box disagrees with what it is about to filter on. */
.filter-row input {
  width: 100%;
  min-width: 0;   /* the column sets the width now; a floor here would overflow it */
  padding: 1px 3px;
  font-size: var(--fs-xs);
  line-height: 14px;
  /* Stated, not left to the line box, for the reason `table.grid th` states
     its 28px: the totals row below is parked at this row's height and that
     offset has to be a sum of declared values. 14px line + 1px padding + 1px
     border, twice over, under the border-box sizing everything here uses. */
  height: 18px;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
  background: var(--panel);
}
.filter-row input::placeholder { color: var(--muted); opacity: .6; }

/* --- totals row ------------------------------------------------------ */
/* What the money columns add up to over the rows currently passing the
   filters, parked under the filter inputs and above the first data row. Which
   columns those are is the registry's answer, not this file's - see `total` in
   `columns.py`.

   Third of the three sticky head rows, so its offset is the two above it added
   up: 39px for the label row, and the filter row below.

   In the `thead` rather than at the top of the `tbody`, for two reasons. A
   `tbody` row scrolls away, and a total you have to scroll back up for is a
   total nobody reads. And the stripe, the hover tint and the selection tint
   all key off `tbody tr`, so a summary row in there would strike itself out as
   an even row and light up under the pointer as though it were a position. */
table.grid {
  /* The filter row's height, and the only place it is written down: 2px
     padding top and bottom, the 18px input declared above, and the 1px bottom
     border `table.grid th` gives every cell in the head. */
  --filter-h: 23px;
}
table.grid .totals-row th {
  top: calc(39px + var(--filter-h));
  height: auto;
  padding: 4px 8px;
  /* `table.grid th` dresses a head cell as a label - muted, small, spaced-out
     capitals - and every one of those is wrong for a figure. The row is a row
     of numbers that happens to live in the head. */
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  border-top: 1px solid var(--line);
  cursor: default;
}
/* `table.grid th:hover` turns a header accent-coloured to say it is clickable.
   Nothing here is: the sort, the drag and the resize all find their cells by
   `data-key` and these carry `data-total` instead. */
table.grid .totals-row th:hover { color: var(--text); }
table.grid .totals-row th.pos:hover { color: var(--positive); }
table.grid .totals-row th.neg:hover { color: var(--negative); }
/* `.pos` / `.neg` are (0,1,0) and lose to `table.grid th`'s colour at (0,1,1),
   so the sign has to be restated at this weight rather than left to them. */
table.grid .totals-row th.pos { color: var(--positive); }
table.grid .totals-row th.neg { color: var(--negative); }
/* The caption, in whichever column the user has dragged into first place -
   which is the pinned one, so it stays beside the figures on a sideways
   scroll. Dressed as a label because that is what it is; the figures next to
   it are the thing to read. */
.totals-row .tot-tag {
  color: var(--muted);
  font-weight: 600;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  margin-right: 6px;
}

/* --- prominent tooltip ---------------------------------------------- */
.tooltip {
  position: fixed;
  z-index: 200;
  max-width: 420px;
  background: var(--tip-bg);
  color: var(--tip-text);
  border: 1px solid var(--tip-border);
  border-left: 3px solid var(--tip-border);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  font-size: var(--fs-base);
  line-height: 1.45;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  white-space: pre-wrap;
}
.tooltip .tip-title { font-weight: 700; display: block; margin-bottom: 2px; }

/* --- modal / stock detail ------------------------------------------- */
.modal-root {
  position: fixed;
  inset: 0;
  /* Dimmed AND blurred, rather than dimmed harder. At .6 the grid behind this
     was a flat grey field; at .45 over a 6px blur it is still legibly the table
     the stock was clicked out of - which is the context the popup is read in -
     while the blur stops any of it competing for the eye. The popup ends up
     further forward than a heavier scrim ever put it. `-webkit-` first, for the
     browser that shipped the prefix and kept it. */
  background: rgba(0, 0, 0, .45);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 150;
  /* The popup sizes itself and scrolls inside itself, so nothing here scrolls
     and nothing here can spill past the backdrop. */
  overflow: hidden;
}
/* One size, whatever is inside it: 80% of the window either way.
   It used to be `width:100%` with the height left to the content, which meant
   the popup grew and shrank as the reader moved between sub-tabs - Position is
   three short cards, Valuation is a wall of tables, Data is two cards and a
   paragraph - and the Close button walked up and down the screen with it. A
   fixed box keeps the head, the tab rail and the buttons in the same place all
   the way through, and hands the difference to a scrollbar on `.modal-body`.

   Width is still generous rather than a reading column: the Details tab's cards
   are placed into columns by `detail-layout.js` and simply get wider ones, and
   the Technicals tab is charts, where every pixel of width is another bar.
   `dvh` after `vh` so a phone's collapsing address bar does not leave the
   bottom of the popup under it; browsers without `dvh` keep the `vh` line. */
.modal {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  width: 80vw;
  height: 80vh;
  height: 80dvh;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  box-shadow: var(--shadow-lg), 0 24px 64px rgba(0, 0, 0, .28);
}
.modal-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
  background: var(--panel);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.modal-head h2 { margin: 0; font-size: 16px; }
/* The one part of the popup that scrolls. `min-height:0` because a flex item
   defaults to `min-height:auto` and would refuse to shrink below its content,
   pushing the tall tabs straight back out through the bottom of the box. */
.modal-body {
  padding: 10px 12px 12px;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
}
.modal-close { margin-left: auto; }

/* The stock popup's header controls: step through the list, the two settings
   toggles, move and remove, the Fetch and Export menus, and Close.

   They arrived one at a time and each took whatever size its own class gave
   it - `.btn` for Close, `.btn.sm` for the rest, the step arrows smaller again
   at a font size of their own - so a row that is one control made of parts read
   as a ragged line of boxes on three different baselines. The height, the
   padding and the icon gap are stated here for all of them instead, and the
   inline `style` the cluster used to carry is gone with them: a toolbar sized
   from eight different places cannot be kept level.

   Direct children and one level down, because `.step-through` and `.dropdown`
   each wrap a button - and no deeper, or the Download/Copy pair inside the
   Export panel would be resized by a rule meant for the strip above it. */
.modal-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
}
.modal-actions > .btn, .modal-actions > * > .btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 24px;
  padding: 2px 8px;
  line-height: 1.4;
}
/* The menu wrappers are flex items rather than inline boxes, so neither sits a
   pixel low on the strip's baseline the way an `inline-block` does. */
.modal-actions > .dropdown { display: flex; }

/* --- the Notes popup -------------------------------------------------------
   The grid's Notes cell opens the same log the stock popup's My Inputs tab
   draws, in a box of its own. It borrows `.modal` for the frame and everything
   under `.note-field` for the widget, and overrides only the size: 80vw x 80vh
   is right for a popup of cards and charts and absurd for one holding a list
   of six sentences and a button. This one is a reading column that grows with
   the log and stops before it owns the screen - so a stock with two notes gets
   a small box and one with forty gets a scrollbar, rather than both getting
   the same acre of empty panel. */
.notes-modal {
  width: min(640px, 92vw);
  height: auto;
  max-height: min(80vh, 80dvh);
}
/* The log gets the room here, where in the form it is one field among a dozen
   and is capped at 200px so it cannot push the rest of the card off screen. */
.notes-modal .note-log { max-height: none; }
.notes-modal .modal-head h2 { font-size: var(--fs-lg); }

/* Save and Cancel, on a strip of their own below the scrolling body so they
   stay put while the log is scrolled - the same reason the head is pinned
   above it. Written generically because a footer of actions is not specific to
   notes; `.modal` has simply never had one until now. */
.modal-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-top: 1px solid var(--line);
  flex: 0 0 auto;
  background: var(--panel);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.modal-foot .hint { margin: 0; }
/* Says why a save did not land, in the negative rather than the muted grey a
   `.hint` is otherwise: it is the one line on the strip that is not furniture,
   and it appears only when there is something wrong to say. */
.modal-foot [data-note-error] { color: var(--negative); }

/* The header's Export menu. Narrower than the column and study pickers, which
   are lists to scan - this one is two buttons and a sentence, and at 330px the
   sentence sets in three short lines with a ragged right. Its two actions share
   the row equally rather than sizing to their labels: neither is the default,
   and a wider "Download .json" beside a narrow "Copy" would read as though one
   of them were. */
.export-panel { width: 268px; }
.export-panel .picker-actions { margin: 6px 0 0; }
.export-panel .picker-actions .btn { flex: 1; }
.export-panel h4 { margin-top: 0; }

/* Placed columns, not a column flow and not a grid.

   A CSS grid rows every card to the height of the tallest in its row, so a
   400px "My inputs" left ~250px of dead space beside every short card sharing
   its row. `columns: 265px 4` fixed that but handed the placement to the
   browser's column balancer - which card lands where was not something the
   reader could decide. Each column being its own flex stack keeps the packing
   (a card sits straight under the one above it, whatever their heights) and
   gives the placement back: `detail-layout.js` says which column each card is
   in and where in that column, and a drag rewrites it. */
.sections { display: flex; flex-direction: column; gap: 0; }
.section-tracks { display: flex; align-items: flex-start; gap: 10px; }
.section-track {
  flex: 1 1 0;
  min-width: 0;                    /* or a wide table stops the column shrinking */
  display: flex;
  flex-direction: column;
}
/* The two full-width bands, above and below the columns. A card that cannot
   survive a 265px column lives here - the price chart, whose `lineChart` draws
   with preserveAspectRatio="none" and squashes its own axis text to 40% in a
   quarter-width column - and so does anything the reader would rather have
   across the whole popup. */
.section-band { display: flex; flex-direction: column; }
/* One inset and one radius for every card on the tab, with a shadow doing the
   separating that a 1px line was doing on its own. Twelve cards abutting at
   8px apart read as one wall of text otherwise - the border says where a card
   ends and the shadow says it is a card. */
.section {
  /* Softened to `--card-edge`: twelve cards at full `--line` drew twelve boxes
     competing with the figures inside them. The separating is done by the
     surface step and the shadow - the border only has to say where one card
     stops. Which value that is differs by theme; see the token. */
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-lg);
  background: var(--panel-2);
  padding: 10px 12px;
  margin: 0 0 8px;
  box-shadow: var(--shadow-sm);
}
/* The grip, the fold toggle, the heading and the width button on one line. The
   heading takes the slack so the button sits hard right whatever the card is
   called. Centred rather than on a baseline, because two of the four are now
   buttons with a box of their own and a baseline hangs them off it. */
.section-head { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.section-head h3 { margin: 0; flex: 1; min-width: 0; }
/* Folded to its heading, and nothing under it to be spaced away from. */
.section.folded { padding-bottom: 7px; }
.section.folded .section-head { margin-bottom: 0; }
/* Always lit, unlike the grip and the width button beside it: those change how
   a card is arranged, this one decides whether the card has any content on
   screen at all, and a control that hides a card must not itself be hidden. */
.section-head .card-fold {
  background: none;
  border: 0;
  width: 14px;
  height: 14px;
  opacity: .45;
  font-size: var(--fs-md);
}
.section-head .card-fold:hover, .section-head .card-fold:focus-visible {
  opacity: 1;
  color: var(--accent);
}
.section h3 {
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
/* Dimmed until the card is hovered, like the study cards' ✕ and the grid's row
   actions: a dozen always-lit grips read as a control panel rather than as one
   handle per card. `cursor: grab` is the only thing that says it is draggable
   before you try. */
.section-head .grip {
  flex: none;
  cursor: grab;
  opacity: .25;
  color: var(--muted);
  font-size: var(--fs-base);
  line-height: 1;
  user-select: none;
}
.section-head .grip:active { cursor: grabbing; }
.section:hover .grip { opacity: .8; }
.card-wide {
  flex: none;
  opacity: 0;
  padding: 0 5px;
  line-height: 16px;
  font-size: var(--fs-sm);
}
.section:hover .card-wide { opacity: .75; }
.card-wide:hover, .card-wide:focus-visible { opacity: 1; }
/* Where a dragged card would land. A sliver of nothing until a drag starts, so
   an empty column is not a permanent dashed hole in the popup. */
.drop-zone { min-height: 2px; border-radius: var(--radius-sm); }
.sections.dragging .drop-zone {
  min-height: 30px;
  margin-bottom: 10px;
  border: 1px dashed var(--line);
}
.sections.dragging .drop-zone.drop-into {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.sections.dragging .section { transition: box-shadow .08s; }
.section.drop-before { box-shadow: 0 -3px 0 var(--accent); }
.section.drop-after { box-shadow: 0 3px 0 var(--accent); }
/* The layout controls. A hint rather than a heading: the row exists to say the
   cards can be moved at all, which is the only thing a grip cannot say on its
   own until you have already found it. */
/* Next and previous, and where in the list you are. A popup is almost always
   one of a run - forty names screened down to nine, read one after another -
   and closing it to click the next row loses the tab, the sub-tab and the
   scroll every time. Grouped tight so the pair reads as one control rather
   than as two more buttons in a row of five. */
.step-through { display: flex; align-items: center; gap: 2px; }
/* Tighter than the labelled buttons either side, because a chevron needs no
   room to be read - the font size and line height the guillemets needed went
   with them when the marks became icons. */
.step-through .btn { padding-left: 5px; padding-right: 5px; }
/* The end of the list. Spelled out because nothing else in this row is ever
   disabled, so without it a dead arrow is a live-looking button that does
   nothing - and lights its border on hover while it is at it. */
.step-through .btn[disabled] { opacity: .45; pointer-events: none; }
.step-through .hint {
  margin: 0;
  min-width: 34px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.layout-bar {
  /* `none` until Edit layout asks for it - see the rule below the hint. */
  display: none;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  flex-wrap: wrap;
}
.layout-bar .hint { margin: 0; }
/* The whole bar is behind the one button that asks for it, and that button is
   in the popup's own head now - beside the ones that act on the stock, and only
   while the Details tab is the one being read. So the bar is not a row with
   nothing on it until then: it is not there. An empty flex box above the
   sub-tabs would be eight pixels of nothing on every stock anybody opens. */
.editing-layout .layout-bar { display: flex; }
.layout-bar .grip-hint { opacity: .8; font-size: var(--fs-base); }
/* The period tabs. Nine durations switched between constantly, so they are all
   on screen at once rather than folded into a select - the strip is also the
   only thing that says what durations exist. Underlined rather than filled: a
   row of nine filled buttons would out-shout the chart under it. */
.ptabs { display: flex; align-items: stretch; gap: 2px; flex-wrap: wrap; }
.ptab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 4px 9px 3px;
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text);
  opacity: .7;
  cursor: pointer;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.ptab:hover { opacity: 1; background: var(--panel); }
.ptab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ptab.on {
  opacity: 1;
  font-weight: 600;
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.toolbar-gap { flex: 1 1 auto; }

/* The period's return, stated once and read first. A price chart is opened to
   ask "what did this do over that window", so the answer is not left to be
   inferred from the slope of a line or dug out of the caption under it. */
.price-headline {
  margin: 2px 0 4px;
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  font-size: var(--fs-base);
}
/* Tinted from the status colour itself, so the pill cannot drift away from the
   text it wraps when a theme changes what "good" looks like. */
.price-headline .pill-chg, .price-selection .pill-chg {
  font-size: var(--fs-lg);
  font-weight: 600;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  background: var(--panel);
  color: var(--text);
}
/* The gain and the loss are deliberately NOT mirror images. A gain is a pale
   tint carrying the blue; a loss is a solid dark block carrying light ink. The
   pair therefore differs in FILL and in LIGHTNESS as well as in hue, and it
   survives both a greyscale screenshot and a reader for whom the hue channel
   does not resolve - which is the whole argument behind --positive being blue.
   The ↑/↓ charts.js writes inside the pill is the third channel. */
/* Scoped to BOTH rows. `.price-selection` is a sibling of `.price-headline`,
   not a child of it, so the drag-to-compare readout was rendering its pill
   uncoloured - the arrow carried the direction and nothing else did. The base
   shape below is shared for the same reason. */
.price-headline .pill-chg.pos, .price-selection .pill-chg.pos {
  color: var(--positive);
  background: var(--positive-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--positive) 45%, transparent);
}
.price-headline .pill-chg.neg, .price-selection .pill-chg.neg {
  color: var(--pnl-neg-fg);
  background: var(--pnl-neg-bg);
}
.price-headline .chg {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-md);
}
.price-chart { display: block; touch-action: none; }
/* The drag-to-compare readout. Always rendered - the invitation when nothing is
   selected - because a drag is invisible until somebody tries it, and because
   reserving the height stops the chart jumping down as a drag begins. */
.price-selection {
  margin: 0 0 3px;
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  min-height: 22px;
  font-size: var(--fs-sm);
}
.price-selection .sel-label {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.price-headline .pill-chg.sm, .price-selection .pill-chg.sm {
  font-size: var(--fs-base);
  padding: 1px 8px;
}
/* For a word a screen reader needs and the eye already has - the direction the
   headline's arrow points. Clipped rather than `display:none`, which would take
   it out of the accessibility tree along with the layout. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* The table twin is a bar per row - 250 of them for a year of daily closes -
   so it scrolls inside the card rather than making the popup that tall. */
.price-rows { max-height: 240px; overflow: auto; }
.price-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.price-table th {
  padding: 2px 4px;
  text-align: right;
  font-weight: 500;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
}
.price-table td { padding: 1px 4px; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
.price-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.price-table th:first-child, .price-table td:first-child { text-align: left; }
.price-caption { margin: 6px 0 0; }

/* --- the popup's two tabs -------------------------------------------- */
/* Their own row under the head rather than beside the title: the head already
   carries the name, the ticker, up to four badges and up to four buttons, and
   a tab strip pushed into that row would be the first thing to wrap.
   Head and tabs are one block above the scroller rather than a sticky band
   inside it: `.modal` is a fixed-height flex column now, so the block simply
   never scrolls and there is no offset to pin it at. `position:relative` is
   only there to give the `z-index` something to apply to - a card's shadow and
   the drag ghost both pass under it. */
.modal-top {
  position: relative;
  z-index: 3;
  flex: 0 0 auto;
  background: var(--panel);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
/* One rail, two rows - the tabs on the rule, the sub-tabs under it.

   The popup's four tabs and the Details tab's six sub-tabs were two full rows
   of navigation stacked on each other, and under them a third row carrying the
   layout controls; 0114 folded the second onto the first to be rid of a row of
   chrome. Side by side they were level with each other, which is the one thing
   they are not: `Position / My inputs / Business / ...` do not switch the page,
   they filter the page `Details` is already on. So the strip sits BELOW the
   tabs now, where what it belongs to is the thing above it.

   The rule moves with it. There is exactly one line under the tabs either way:
   the sub-strip's own top border when it is on screen, the rail's bottom border
   when it is not - never both, so `.mtab`'s -1px lands on a rule in both cases
   and the popup never grows a doubled hairline. Both borders are full-bleed
   because the horizontal padding is on the two rows rather than on the rail.

   The two levels stay told apart by SHAPE as well as position: an underline
   says which page you are on, a pill says how that page is filtered. A second
   set of tab-marks under the first would be a ladder of identical lines.

   Every tab's sub-tabs ride this same row now - the Charts tab's studies, the
   Rating tab's sections and the AI tab's halves, where the Details tab's six
   were - so one popup has one way of switching a section, and the row is
   there on every tab that has something to switch. */
.modal-rail {
  display: block;
  border-bottom: 1px solid var(--line);
}
.modal-tabs { display: flex; gap: 4px; flex-wrap: wrap; padding: 0 12px; }
/* One tab's half of the rail, taken off screen with the panel it switches -
   and kept off while it has nothing on it, see `detail.paintRail`. `[hidden]`
   has to be said again here: a `display` on the element itself outranks the
   attribute's own rule, and a hidden strip that still lays out is a rule with
   nothing after it. */
.rail-sub {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 4px 12px;
  border-top: 1px solid var(--line);
}
.rail-sub[hidden] { display: none; }
.mtab {
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  padding: 6px 12px 5px;
  margin-bottom: -1px;
  font: inherit;
  font-size: var(--fs-base);
  color: var(--text);
  opacity: .7;
  cursor: pointer;
}
.mtab:hover { opacity: 1; background: var(--panel-2); }
.mtab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.mtab.on { opacity: 1; font-weight: 600; color: var(--accent); border-bottom-color: var(--accent); }

/* --- the Technicals tab ---------------------------------------------- */
/* One study on screen, under a sub-tab strip. Not a grid any more: ten cards
   down a page put the one being read three scrolls below the controls that
   change it, and each chart got a third of the width its two panes wanted.
   Whichever study is selected now gets the whole popup, which since the popup
   is the whole display is a pair of charts you can actually read a crossing
   off.

   Left as a block rather than deleted, because `detail.paintStudies` measures
   this element to size the two panes inside it - see `studyBox`. */
.study-grid { display: block; }
/* The strip that chooses the chart is not in the grid: it rides the popup's
   rail with every other tab's sub-tabs - see `.modal-rail .subtabs`. */
/* The windows the studies are drawn over and the menu of which ones to draw
   are settings, and they sat as a block of controls above every chart on every
   popup opened to look at one. Behind the head's Chart settings now, on exactly
   the terms `.layout-bar` is behind Edit layout - `none` until the class that
   button puts on the popup asks for it, so the tab opens on the chart rather
   than on a row of controls above it. */
.study-toolbar { display: none; margin-bottom: 10px; }
.chart-settings .study-toolbar { display: block; }
.study-toolbar .hint { margin: 4px 0 0; }
/* One "last N days/weeks/months" control per pane, side by side, because the
   panes are two horizons and a single range would collapse that — six months
   is ~125 daily bars and ~26 weekly ones, and most studies cannot be drawn on
   26. Wrapping rather than shrinking: on a narrow popup the second control
   drops to its own line instead of squeezing the number box to two digits. */
.range-picks { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.range-pick { display: flex; align-items: center; gap: 5px; font-size: var(--fs-sm); }
.range-pick > span:first-child { font-weight: 600; }
.range-pick .hint { margin: 0; }
/* Four digits' worth. The number is 1-999 and a wider box would read as a
   field expecting something longer than it accepts. */
.range-pick input[type="number"] { width: 54px; }
.range-pick select { font-size: var(--fs-sm); padding: 1px 2px; }
.study-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  padding: 8px 10px;
  min-width: 0;                    /* or a wide legend stops the panes shrinking */
}
.study-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; }
.study-card-head h4 {
  margin: 0;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
  flex: 1;
  min-width: 0;
}
/* Dimmed until the card is hovered, like the grid's row actions: eight always-lit
   ✕ buttons read as a row of controls rather than as one per chart. Same round
   ghost target as those, so "remove this" is one shape everywhere in the app. */
.study-drop {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  opacity: .4;
}
.study-card:hover .study-drop { opacity: 1; }
.study-drop:hover { background: var(--negative-soft); color: var(--negative); }
.study-drop:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
/* The two panes: the same study over daily closes and over weekly ones, side
   by side. Both at once rather than a window to pick between, because a shape
   on the daily is a trade and the same shape on the weekly is a trend, and the
   useful question is almost never one without the other.

   Equal columns, and `minmax(0, 1fr)` rather than a bare `1fr`: a grid track
   floors at its content's min-content width by default, and a wide facts table
   under one pane would then push the pair past the card and out of it.

   They stack under 620px, where two panes would be ~290px each - narrower than
   the axis labels and the right-edge line labels need, so the charts would be
   unreadable in the name of keeping them adjacent. */
.study-panes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
@media (max-width: 620px) {
  .study-panes { grid-template-columns: 1fr; }
}
.study-pane { min-width: 0; }
/* Which window this pane is, and how many bars it got. Named rather than left
   to be inferred: they are side by side, and a reader glancing at one has to
   know which of the two they are looking at. */
.study-pane-head {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 2px;
}
.study-pane-head strong {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text);
}
.study-pane-head .hint { margin: 0; font-size: var(--fs-xs); }

/* The card's own line-style picker. On the card because a line is judged
   against the lines it has to be told apart from, which are in the boxes right
   below it — and because how the 200-day average is drawn is a decision about
   *this* chart, not about the fourth slot of a palette shared with every donut
   in the application. It governs both panes.

   Dimmed until hover like the ✕ beside it, and for the same reason: ten
   always-lit palette buttons read as a row of controls rather than as one per
   chart. Kept lit while its own panel is open, or the thing being used
   disappears the moment the pointer moves into it. */
.study-colours { flex: 0 0 auto; }
/* The cell this chart can set, beside the palette button. Always lit, unlike
   the two round buttons either side of it: those are actions, and an action is
   offered when it is wanted, but this one is *showing a value* - the current
   answer for this stock - and a control that has to be hovered before it will
   say what it holds is not showing anything. Its label rides along for the same
   reason: a bare menu reading "Recently" in a chart's heading answers a
   question nobody asked it. */
.study-flag {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
}
.study-flag > span {
  color: var(--muted);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.study-flag select { font-size: var(--fs-sm); padding: 1px 2px; max-width: 120px; }
/* A figure this chart reads and can store, on the same terms as the menu above
   and drawn to match it: always lit, its label beside it, and no hover needed
   before it will say what it holds. One per pane rather than one per card — the
   weekly %B and the daily one are two numbers and two columns — so they sit
   together immediately left of the palette button.

   The box is sized to the number rather than to the input's default, which is
   wide enough for a sentence: four significant figures and a sign is the whole
   range of anything that lands here. */
.study-reading {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
}
.study-reading > span {
  color: var(--muted);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
.study-reading input {
  width: 56px;
  font-size: var(--fs-sm);
  padding: 1px 4px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* A figure somebody typed, told apart from the one this chart just computed.
   The chart's reading is a suggestion the box is pre-filled with and changes
   whenever the closes do; a stored one is a decision, and it is what the grid
   is carrying. Weight rather than colour, because the difference is authorship
   and not goodness — and colour in here already means direction. */
.study-reading[data-reading-own="1"] input { font-weight: 600; color: var(--text); }
.study-colours > .btn {
  padding: 2px 5px;
  color: var(--muted);
  opacity: .4;
  border: 0;
  background: none;
}
.study-card:hover .study-colours > .btn,
.study-colours > .btn[aria-expanded="true"],
.study-colours > .btn:focus-visible { opacity: 1; }
.study-colours .dropdown-panel { width: 320px; }
.study-colours .dropdown-panel .hint { margin: 0 0 6px; }
/* Four controls per line, in columns rather than in a flow: the swatches, the
   two menus and the Default buttons line up down the panel, so the panel reads
   as a table of lines rather than as a pile of controls. The name takes the
   slack, and `minmax(0, 1fr)` keeps a long one ("Upper · Lower · Channel")
   from widening the whole grid instead of ellipsing. */
.colour-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
}
.colour-name {
  font-size: var(--fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.colour-row select { font-size: var(--fs-sm); padding: 1px 2px; max-width: 76px; }
.colour-row input[type="color"] {
  width: 28px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: none;
  cursor: pointer;
}
.study-colours .picker-actions { margin-top: 8px; }
.study-chart { display: block; }
.study-facts { margin-top: 4px; }
.study-facts td.lbl { color: var(--muted); }
.study-facts td.num { text-align: right; font-variant-numeric: tabular-nums; }
.study-note { margin: 4px 0 0; font-size: var(--fs-sm); }
/* --- the Master chart ---------------------------------------------------- */
/* One pane of the Master is a stack, not a chart: the price with every chosen
   overlay on it, then a short strip for each oscillator that cannot share that
   axis. They are drawn at the same viewBox width over the same bars, so the
   x-axis lines up straight down the column and a divergence between price and
   momentum is read by looking down rather than across.

   The gap is deliberately tight. These are panels of one chart rather than
   three charts that happen to be near each other, and a roomy gap would invite
   the eye to read each on its own — which is the one thing the stack exists to
   stop. */
.master-plot + .master-plot { margin-top: 2px; }
/* What this pane was asked for and could not draw. Its own line under the facts
   rather than a row inside them, because it is not something the chart is
   saying about the stock — it is something the chart is not saying at all, and
   silence is the failure mode worth spending markup on: a band missing because
   the weekly pane holds 14 bars reads exactly like a band nobody switched on. */
.master-skipped {
  margin: 4px 0 0;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--muted);
}
/* The Master's facts table is one sentence per overlay, where a study card's is
   four figures. So the value column stops being a number column: left-aligned
   and wrapping, with the study's name holding a sensible share of the width
   rather than taking whatever the longest sentence leaves. */
.master-card .study-facts td.lbl { width: 34%; white-space: normal; }
.master-card .study-facts td.num {
  text-align: left;
  white-space: normal;
  font-variant-numeric: normal;
}
/* Three controls per row here, not four: the Master's picker offers colour and
   weight but no dash, because on this chart the dash is what tells one study's
   own lines apart and is already spoken for. See `colourPicker`. */
.master-card .colour-row { grid-template-columns: minmax(0, 1fr) auto auto; }
/* Which studies the Master draws, in its two groups. Wider than a study card's
   palette panel because the rows are study names in full — "Trend line &
   parallel channel" ellipsed to nothing would make the menu unusable. */
.master-picker .dropdown-panel { width: 300px; }
.master-picker .dropdown-panel h4:first-of-type { margin-top: 0; }
.master-picker .dropdown-panel .hint { margin: 0 0 4px; font-size: var(--fs-xs); line-height: 1.4; }
/* A study name wraps here rather than ellipsing, unlike the column picker's
   rows: there are nine of them at most, they are read once when the chart is
   being set up, and a clipped name is a study nobody can tell from its
   neighbour. */
.master-picker .picker-row label { white-space: normal; overflow: visible; }


.section.holding.pl-pos { border-left: 3px solid var(--positive); }
.section.holding.pl-neg { border-left: 3px solid var(--negative); }
.section.holding.sl-hit { border-left: 3px solid var(--negative); }

/* The label/value list every card is mostly made of. Ruled rather than spaced:
   a fifteen-row list of two columns 10px apart is read across, and at 2px of
   row gap the eye loses which value belongs to which label about six rows down.
   A hairline per row costs nothing and makes the pairing unambiguous - the same
   bargain `.mini-table` and `.kv-table` already make. The last pair is left
   unruled so the list does not close with a line the card's own border repeats
   eight pixels later. */
.kv { display: grid; grid-template-columns: auto 1fr; gap: 0 12px; font-size: var(--fs-sm); }
.kv dt, .kv dd { padding: 3px 0; border-bottom: 1px solid var(--line-soft); }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; text-align: right; font-family: var(--mono); }
.kv > dt:last-of-type, .kv > dd:last-of-type { border-bottom: 0; }
/* A dd holding a link rather than a figure: the mono face is for numbers that
   have to line up down a column, and a URL is neither. */
.kv dd a { font-family: inherit; color: var(--accent); }
/* The column's own mark on the label - the picture its grid header wears, so a
   figure in the popup can be traced back to the column it came out of. Full
   strength here, unlike in the header: `.col-i` is dimmed to .55 against a
   header's full-strength text, and the same dimming over a `--muted` label
   compounds into a mark too faint to identify. The label is already the quiet
   half of this row, so the mark only has to match it, not duck under it. */
.kv dt .col-i { opacity: 1; margin-right: 5px; }

/* --- the Quality tab's period boxes ------------------------------------- */
/* One box per metric, its windows stacked inside it - see `METRIC_BOXES` in
   detail.js for why that tab is drawn this way rather than as a flat list.

   `auto-fit` rather than a fixed count, because these boxes live inside a card
   the user places: the same Growth card is four boxes across when it has been
   widened to the whole popup and one box wide in a 3-column layout, without
   either being a breakpoint anybody had to write. 150px is the narrowest a box
   holding "3 Years ago" beside a figure reads at.

   The rows reuse `.kv` for its label-left / mono-figure-right split and drop
   its hairline: that rule is there to keep a fifteen-row list pairable, and
   three rows under their own heading are already unambiguous. */
.metric-boxes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px;
}
/* `--panel` on a card painted `--panel-2`, so a box sits a shade *above* the
   card it is in rather than being cut out of it - white boxes on the card's
   light ground in the light theme, and the same one-step lift in the dark. */
.metric-box {
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}
.metric-box h4 {
  margin: 0 0 4px;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.metric-rows.kv dt, .metric-rows.kv dd { border-bottom: 0; padding: 2px 0; }
/* ...and a conditionally-formatted value keeps the side padding that override
   would otherwise take off it. `dd.rf` gives a tinted figure its chip - see
   `.rf` - and loses the fight to `.metric-rows.kv dd` on specificity, which
   left a background bleeding into the label beside it in the boxes and nowhere
   else. Vertical padding stays the box's own: there is no hairline in here for
   a taller tint to sit on, so the row keeps its rhythm. */
.metric-rows.kv dd.rf { padding: 2px 6px; }
/* A card that has both - a group with a column no box claims - keeps the boxes
   and the leftover list apart. Neither has a margin of its own, so the gap has
   to be declared where the two meet. */
.metric-boxes + .kv { margin-top: 8px; }
/* A box with nothing in it is drawn and kept off screen, exactly as an empty
   *card* is: a stock with no five-year history shows a two-row growth box
   rather than a box with a hole in it, and the box still has to exist while the
   fields are being arranged or emptying one would be a one-way door with
   nothing to put a field back into. */
.metric-box.empty { display: none; }
.editing-fields .metric-box.empty { display: block; }
/* The box's own Add menu sits tighter than a card's: it is the foot of a
   three-row box, not the foot of a column. */
.editing-fields .metric-box .field-add { margin-top: 6px; }
/* ...and opens wider than the box it hangs off. A box is one 150px track of the
   grid, and the menu lines are a column name followed by what the column is -
   clipped to the box, the half that tells them apart is the half that goes. */
.metric-box .field-add select:not([size="1"]) {
  right: auto;
  min-width: min(260px, 78vw);
}

/* --- arranging the fields --------------------------------------------------

   Which fields a card draws, in what order, is the reader's - see the field
   section of detail.js. The controls that say so are drawn on every row from
   the start and kept off screen by this one rule until "Edit fields" is
   pressed: showing them is a question about how the popup is being read, and
   answering it by rebuilding twelve card bodies would cost a half-typed note
   and a bound chart every time it was asked.

   `display: none` rather than `visibility: hidden`, because a reserved column
   of three buttons on every row of a 265px card is a third of the width of the
   card spent saying nothing. The rows do move when the mode goes on; that is
   the mode, and it is the same trade the grid's own edit affordances make. */
.field-tools, .field-add, .field-only, .field-count { display: none; }
.editing-fields .field-tools { display: inline-flex; gap: 1px; margin-right: 4px; }
.editing-fields .field-add { display: block; margin-top: 8px; }
.editing-fields .field-only { display: inline-flex; }
/* The count strip, under the columns it splits: the same control the sub-tab's
   own card columns get in the layout bar, one level down and in the same
   spelling, so "how many columns" is one question asked the same way twice. */
.editing-fields .field-count {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--line-soft);
}
.field-count .hint { margin: 0; }
/* A card with nothing on it is still drawn, so a field can be put back onto it,
   and is only on screen while the fields are being arranged. Without this an
   unscreened stock would open on a column of empty headings. A card taken off
   the popup is the same bargain for the same reason: it is drawn so the panel
   has something to put back, and it is not on screen. */
.section.empty, .section.gone { display: none; }
/* `.arranging` rather than `.editing-fields`: both panels are arranging, and
   the Tabs & cards list is the one that offers Restore beside a card taken off
   the popup - which has to be on screen for the button to look like it did
   anything. */
.arranging .section.empty, .arranging .section.gone { display: block; }
.arranging .section.gone { opacity: .5; }
.arranging .section.gone .section-head h3::after {
  content: ' — off the popup';
  color: var(--muted);
  font-weight: 400;
  text-transform: none;
}

/* --- the sub-tabs and the cards on them, as a list ------------------------

   Dragging is the right gesture for "put that there" and a poor one for "there
   should be a tab called Risk". This is the list those answers are given in:
   every sub-tab, every card on it - including the cards taken off, which is the
   only place they come back from - and, per card, whether it draws itself or
   draws rows. Off by default, because it is a list of everything and the popup
   is open to read one stock. */
.layout-panel {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel-2);
  padding: 10px 12px;
  margin-bottom: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  font-size: var(--fs-sm);
}
.lp-tab {
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 7px 9px;
}
.lp-tab-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.lp-tab-head .hint { margin: 0; }
.lp-cards {
  list-style: none;
  margin: 6px 0 0;
  padding: 2px 0 2px 4px;
  display: grid;
  gap: 4px;
  border-radius: var(--radius-sm);
}
.lp-card { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.lp-card .grip { cursor: grab; opacity: .5; font-size: var(--fs-base); line-height: 1; }
.lp-card .grip:hover { opacity: 1; }
/* The same two marks a card in the columns wears, on the row that stands for
   it: this list is the one place every sub-tab is on screen at once, and so the
   one place a card can be dropped onto the tab it belongs on and land where it
   was aimed rather than at the end. */
.lp-card.drop-before { box-shadow: 0 -3px 0 var(--accent); }
.lp-card.drop-after { box-shadow: 0 3px 0 var(--accent); }
/* A whole list is the target for "onto this sub-tab, wherever" - and the only
   way onto one with nothing on it yet. */
.sections.dragging .lp-cards.drop-into {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  outline: 1px dashed var(--accent);
}
.lp-empty { color: var(--muted); padding: 2px 0; }
/* Greyed rather than dropped: a card off the popup is exactly what this list is
   open for, and a row that vanished would leave nothing to press. */
.lp-card.gone { opacity: .6; }
.lp-name { flex: 1 1 150px; min-width: 0; font-size: var(--fs-sm); }
.lp-tab-head .lp-name { font-weight: 600; flex: 0 1 190px; }
.lp-place, .lp-display { flex: 0 0 auto; font-size: var(--fs-sm); }
.lp-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.lp-foot .hint { margin: 0; }
.arranging .section.empty .section-body::before {
  content: 'No fields — add one below.';
  display: block;
  color: var(--muted);
  font-size: var(--fs-sm);
  margin-bottom: 6px;
}
/* Small enough to sit inside an 11.5px label without setting the row's height,
   and quiet until the row is hovered - three lit buttons on every row of a
   forty-row tab would be the loudest thing on the popup. */
.field-tool {
  background: none;
  border: 0;
  padding: 0 2px;
  font: inherit;
  font-size: var(--fs-2xs);
  line-height: 1;
  color: var(--muted);
  opacity: .5;
  cursor: pointer;
  border-radius: var(--radius-xs);
}
.field-tool:hover:not(:disabled) { opacity: 1; background: var(--panel-2); color: var(--text); }
.field-tool:disabled { opacity: .18; cursor: default; }
.field-tool.field-drop:hover { color: var(--negative); }
/* The menu, and the box that narrows it. The menu is the control - eighty
   columns in thirteen groups, and the groups are half of what says which
   "Latest" you are looking at - and the box is the other half of the question,
   because "where is the pledge" is answered by typing three letters.

   `position: relative` on the container so the menu can stand open *over* the
   card while something is typed rather than pushing the rest of it down. */
.field-add { position: relative; }
.field-add select { width: 100%; font-size: var(--fs-sm); }
.field-add .field-find { width: 100%; font-size: var(--fs-sm); margin-bottom: 3px; }
/* Open, the menu is a list of what the box found. Absolute so the card keeps
   its height, and lifted above the cards beside it so a long list is readable
   over them rather than clipped by whichever one is drawn next. */
.field-add select:not([size="1"]) {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 6;
  box-shadow: var(--shadow-sm);
}
/* Belt and braces on `hidden`: `[hidden]` is a user-agent rule and an option is
   one of the few places a browser has been known not to honour it. */
.field-add option[hidden], .field-add optgroup[hidden] { display: none; }

/* The same list as a real table, for the card that is only label/value pairs
   off one quote - a market index's Level. `.mini-table` brings the rule and the
   row hover; what this adds is the `dl`'s own split, muted label left and mono
   figure right, and its zero side padding, so a card built this way lines up
   with the `.kv` cards beside it rather than sitting 4px in from them. The lead
   cell is the level itself: the one figure the card is opened for, kept a size
   above the rows that qualify it now that it is a row itself. */
.mini-table.kv-rows td { padding: 3px 0; }
.mini-table.kv-rows td.lbl { color: var(--muted); white-space: normal; }
.mini-table.kv-rows td.val { text-align: right; font-family: var(--mono); }
.mini-table.kv-rows td.val.lead { font-size: var(--fs-lg); font-weight: 600; }
.mini-table.kv-rows tr:last-child td { border-bottom: 0; }

/* --- the live price, plotted ------------------------------------------- */
/* Two pictures rather than eleven rows of label and figure. The prices are a
   line against a real time axis, inside the 52-week range that scales it; the
   moves are a row each off a shared zero. `pricePlot` and `moveRail` in
   `detail.js` carry the whole argument. */
.price-plot { margin: 6px 0 0; }
.move-rail { margin: 10px 0 0; }
/* What the plot underneath is measured over, left; the one figure that sizes
   it, right. Baseline-aligned because the two are set at different sizes. */
.pr-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 5px;
}
.pr-cap {
  font-size: var(--fs-xs);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.pr-now-val, .pr-scale {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.pr-now-val { font-size: var(--fs-sm); font-weight: 600; }
.pr-scale { font-size: var(--fs-xs); color: var(--muted); }

/* The price axis down the left, the plot beside it, the time labels under the
   plot only - which is why this is a grid and not three stacked rows: the
   labels have to start where the plot starts, or every one of them is 52px off
   the tick it names. */
.pp-grid {
  display: grid;
  grid-template-columns: 52px 1fr;
  column-gap: 7px;
}
/* The two ends of the price axis, hard against the top and bottom of the plot,
   with the word outside the figure so the numbers stay in one column. */
.pp-axis {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  line-height: 1.15;
  color: var(--muted);
}
.pp-edge b {
  display: block;
  font-family: system-ui, sans-serif;
  font-size: var(--fs-2xs);
  font-weight: 400;
  letter-spacing: .04em;
  text-transform: uppercase;
  opacity: .8;
}

/* The plot. Bounded top and bottom by the 52-week high and low, so those two
   edges are the chart's frame rather than lines drawn inside it, and every
   point's height is where that price sits in the year. The side borders are
   left off: the left edge is the oldest close and the right edge is now, and
   both already carry a dot. */
.pp-plot {
  position: relative;
  /* 62px, down from 88. The plot is read for WHERE in the 52-week range the
     line sits and how steeply it got there, and both survive the shorter box -
     what 88px bought was air. What it cost was the rows under this card going
     off the bottom of a column, which is the one thing the card is scrolled
     for. */
  height: 62px;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  background: var(--row-stripe);
}
/* Stretched to the box rather than scaled uniformly - the viewBox is 0-100 in
   both directions and the box is neither square nor a fixed width, so the
   coordinates are read as percentages. Nothing in this svg is a glyph or a
   circle, which is exactly why it may be stretched: `.pp-dot` is an HTML
   element for the same reason, because a circle in here would come out an
   ellipse and change shape with the card. */
.pp-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.pp-stroke {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;   /* the viewBox is stretched; the line must not be */
}
.pp-area { fill: var(--accent-soft); stroke: none; }
.pp-dot {
  position: absolute;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  transform: translate(-50%, 50%);
  background: var(--accent);
}
/* Today, and the only point on the line that is still moving. Bigger, in the
   ink colour, and ringed off the plot background so it reads as the end of the
   line rather than as one more close along it. */
.pp-dot.pp-last {
  width: 8px;
  height: 8px;
  background: var(--text);
  box-shadow: 0 0 0 2px var(--panel-2);
}
/* Today's low to high, standing at today: a range the stock has traded through,
   where every dot on the line is a single close. */
.pp-day {
  position: absolute;
  right: 0;
  width: 3px;
  min-height: 2px;
  transform: translateX(50%);
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}
/* Your own cost base: a price and not a moment, so it crosses the whole chart.
   Dashed, and in the one hue this palette keeps for "yours rather than the
   market's" - it is the only line here that is a fact about you. Amber also
   survives the red/green confusion the semantic pair is built around, see
   --positive. */
.pp-avg {
  position: absolute;
  left: 0;
  right: 0;
  height: 0;
  border-top: 1px dashed var(--warn);
  opacity: .85;
}
.pp-avg-tag {
  position: absolute;
  left: 2px;
  margin-bottom: 1px;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-2xs);
  line-height: 1;
  white-space: nowrap;
  color: var(--warn);
}

/* The time labels under the plot, stacked into as many lanes as it takes to
   keep two of them off each other, each roped back to its own point. The lane
   count and every `top` are computed in `laneOut`; the box is sized inline.
   They crowd because the axis is real time - 1W sits at 92% of a 90-day span -
   and that crowding is the honest shape, so it is absorbed here rather than
   removed by spacing the points out evenly. */
.pr-lanes { position: relative; margin-top: 4px; }
/* Roped back to its own point across the gap between the plot and this box,
   which is why it starts ABOVE the box it is drawn in. */
.pr-lead { position: absolute; top: -4px; width: 1px; background: var(--line); }
.pr-note {
  position: absolute;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  line-height: 13px;
  white-space: nowrap;
  color: var(--muted);
}
/* Today's label, matching today's dot: the one figure on the axis that is a
   live price rather than a settled close. */
.pr-note.pr-now-note { color: var(--text); font-weight: 600; }

/* One row per window: what it is, the bar, the figure. The bar column takes the
   slack, so the label and the figure sit in the same place all the way down
   whatever the card is widened to. */
.mv-row {
  display: grid;
  grid-template-columns: 44px 1fr 58px;
  align-items: center;
  gap: 7px;
  margin: 3px 0;
}
.mv-lbl { font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; }
.mv-val {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
  text-align: right;
}
/* Two moves on one shared scale can differ by two orders of magnitude, so the
   small one has to stay visible as a DIRECTION even when its length rounds to
   nothing. The figure beside it is what carries the size. */
.move-rail .rc-fill { min-width: 2px; }
/* Flat: no side to be on, so no colour to be it in. */
.move-rail .rc-fill.flat { background: var(--line); }
.pr-stamp { margin: 8px 0 0; font-size: var(--fs-xs); }

/* --- the Rating tab ---------------------------------------------------- */
/* One column of cards read top to bottom, not the Details tab's draggable grid.
   The order here is an argument - score, then what makes it up, then what held
   it down, then the evidence - and an argument that can be shuffled is not one.
   Full width of the popup rather than capped at a reading measure: the tab is
   mostly a six-column table and a two-column grid of figures, and those read
   worse squeezed into 880px than the prose beside them reads set wide. */
.rating-panel { width: 100%; }
.rating-card {
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel-2);
  padding: 10px 12px;
  margin: 0 0 10px;
  box-shadow: var(--shadow-sm);
}
.rating-card h3 {
  margin: 0 0 6px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.rating-card .hint { margin: 0 0 8px; }

/* The score, set at the size of the one thing the tab exists to explain, with
   what qualifies it stacked beside rather than under: a badge that says
   "provisional" changes what the number means and must not be a scroll away
   from it. Wraps to two rows on a narrow popup rather than shrinking the
   figure. */
.rating-hero {
  display: flex;
  align-items: flex-start;
  gap: 18px;
  flex-wrap: wrap;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel-2);
  padding: 12px 14px;
  margin-bottom: 10px;
  box-shadow: var(--shadow-sm);
}
.rating-score { display: flex; flex-direction: column; gap: 1px; }
.rating-value {
  font-family: var(--mono);
  font-size: 40px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
.rating-scale {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.rating-hero-body { flex: 1; min-width: 220px; }
.rating-hero-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.rating-stars { font-size: var(--fs-lg); font-weight: 700; color: var(--accent); }
/* A pinned or floored rating is not the rounded score, and the two sitting side
   by side disagreeing needs to read as deliberate rather than as a rounding
   bug. */
.rating-stars.pinned { border-bottom: 2px dotted var(--accent); }
.rating-why { margin: 6px 0 2px; font-size: var(--fs-base); line-height: 1.45; }
.rating-meta { margin: 2px 0 0; }

/* The headline row: the score on one half, every category's verdict on the
   other. The hero was the full width and the question it left open - "off
   what?" - was twelve cards and a scroll below it; the read-out now sits in the
   space the hero was not using. Two equal tracks rather than `auto 1fr`, so the
   score's half does not shrink around a short thesis and leave the two halves
   disagreeing about where the middle is. */
.rating-headline {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 10px;
  align-items: stretch;
  margin-bottom: 10px;
}
/* Both halves hand their own bottom margin to the row that now owns it, and a
   headline with one child - a payload carrying no components - spends the whole
   width on it rather than leaving a hole beside it. */
.rating-headline > .rating-hero,
.rating-headline > .rating-card { margin-bottom: 0; }
.rating-headline > :only-child { grid-column: 1 / -1; }
@media (max-width: 900px) { .rating-headline { grid-template-columns: minmax(0, 1fr); } }

/* Twelve rows in the height of the score beside them: six in each of two
   columns, because one column of twelve would be twice the hero's height and
   would turn the headline into the scroll it exists to save. */
.vd-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 14px;
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (max-width: 560px) { .vd-list { grid-template-columns: minmax(0, 1fr); } }
.vd-row { display: flex; align-items: center; gap: 6px; min-width: 0; padding: 1.5px 0; }
.vd-name {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: 1;
  min-width: 0;
  font-size: var(--fs-sm);
}
/* The label ellipsises rather than wrapping: "Valuation vs its own history" on
   two lines would stagger the twelve rows out of their grid. The full name, what
   the axis measures and the arithmetic behind the verdict are on the row's tip. */
.vd-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vd-name .icon { flex: none; opacity: .6; }
/* A neutral axis is dimmed rather than dropped - "nothing happened here" is an
   answer, and on most stocks it is half of this panel. */
.vd-row.vd-is-none .vd-name { opacity: .5; }

/* The verdict itself. The word carries it, not the colour: the four read apart
   in monochrome, which matters here for the same reason it does on the delta
   pills. The tint is those same two hues, one step stronger on the outer two,
   so Great and Good are told apart by weight as well as by their letters. */
.verdict {
  flex: none;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  background: var(--delta-zero-bg);
  color: var(--delta-zero-fg);
}
.verdict.vd-great {
  background: color-mix(in srgb, var(--delta-pos) 24%, var(--panel));
  color: color-mix(in srgb, var(--delta-pos) 78%, var(--text));
}
.verdict.vd-good { background: var(--delta-pos-bg); color: var(--delta-pos-fg); }
.verdict.vd-bad { background: var(--delta-neg-bg); color: var(--delta-neg-fg); }
.verdict.vd-vbad {
  background: color-mix(in srgb, var(--delta-neg) 24%, var(--panel));
  color: color-mix(in srgb, var(--delta-neg) 78%, var(--text));
}

/* The staleness banner. Not an error - the pipeline re-scores on its own
   cadence and the scrapers do not wait for it - so it is toned as a note with
   an accent edge rather than as a warning. */
.rating-note {
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 7px 10px;
  margin: 0 0 10px;
  font-size: var(--fs-sm);
  line-height: 1.5;
}

/* The scorecard: one card per category, three across, each a small table of
   that category's metrics - this stock's figure beside the scraped columns it
   was read off. It was one forty-row table with the category spanning its rows;
   the reason it is cards is in `scorecard()` in rating.js. What each metric
   measures, its weight and its reference band are a tooltip on the row, not
   columns.

   Three across while three fit, then two, then one - the same step-down
   `.source-cards` takes, and for the same reason: below about 1100px a third
   of the popup is narrower than a source cell that has to hold
   `debtor_days_3y_ago` and a figure on one line, so the wall steps down rather
   than the cards shrinking. `minmax(0, 1fr)` rather than `1fr`, or the widest
   card's content would set the track and the other two would squeeze. */
/* `auto-fit` and not a pair of media queries (2026-08-23). This wall is drawn
   by `ratingHtml` and `ratingHtml` is reached only from the popup, so the box
   these cards are laid out in is `.modal` - which is `width: 80vw`. A viewport
   query was therefore measuring a box a quarter wider than the one the cards
   are in: at a 1150px window the popup is 920px and the old rule still gave it
   three columns, ~287px each, well under the 300px the note above says a card
   needs. It stepped down 250px of viewport too late, every time.
   `auto-fit` measures the container instead, which is the thing that was
   always meant. Same idiom as `.metric-boxes` and `.queue-cards` above, and it
   also covers the case no breakpoint could: a card that has been dragged to
   full width and one sharing a three-column track are the same rule. */
/* `stretch` and not `start` (2026-08-27): every card in a row takes the
   tallest card in that row, so the row's bottom edges line up. Under `start`
   each card was sized to its own table, and the categories hold between one
   and five metrics each - a row of four came out as four different heights,
   which reads as a masonry wall rather than as one grid, and the eye spends
   the difference working out whether a short card is short because it is
   missing something. The extra height is blank under the last metric and costs
   nothing: the card is a boundary drawn around a category, not a box sized to
   it. Rows still size independently, so a row of one-metric cards stays
   short. */
/* Three cards to a row at the top end, never four (2026-08-27). Twelve
   categories over four columns is three rows of four narrow cards; over three
   it is four rows of wider ones, and the width is what the metric table inside
   actually wants - a source column like `debtor_days_3y_ago` next to its value
   and the stock's figure. The cap is written into the track floor rather than
   as a fourth column being forbidden: `max(300px, (100% - 20px) / 3)` is at
   least a third of the container minus the two gaps, so a fourth track can
   never fit, while `auto-fit` still steps down to two and then one exactly
   where it did before - below a 920px container the 300px floor wins and the
   arithmetic is unchanged. `100%` resolves against the container, so this
   stays the container-measured rule the note above argues for; a viewport
   breakpoint would be measuring the 80vw popup's parent again. */
.rating-scorecards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(max(300px, (100% - 20px) / 3), 1fr));
  gap: 10px;
  align-items: stretch;
}
/* A card takes the surface the inputs groups and source cards take, and
   `min-width: 0` so a long mono column name can break inside it rather than
   pushing the card wider than its track.

   `--card-edge` rather than `--line-soft` directly: that token exists because a
   card's edge is not the same job in the two themes - dark leans on the border
   and takes the soft one, light leans on the shadow and takes the firmer
   hairline, or a #eef2f7 edge on a white card over #f1f5f9 is no edge at all.
   The shadow is what separates the card from the `--panel-2` surface of the
   section holding it without a second, heavier line to do it.

   The top edge is 2px and takes the category's own hue - the third and last
   place the axis colour appears, after the mark and the mark's ground. A whole card tinted in its category would turn a wall of twelve into a
   paint chart and would collide with the delta pill, which is the one thing
   here allowed to carry a verdict; an edge states the category on the card's
   own boundary, where it labels rather than shouts. Mixed to 70% for the same
   reason the chip stops at 14%: full-strength across twelve cards is a lot of
   saturated line for a heading. A component with no hue token falls back to
   `--line` and reads as an ordinary card. */
.sc-card {
  /* The card's own three steps of ink, mixed off the theme's pair rather than
     written as greys: `--text` is slate-900 in light and near-white in dark,
     and using it flat for every figure on the card is what makes a wall of
     them glare. `--sc-ink` softens the answer column by a seventh towards
     `--muted` - about slate-800 on light - and `--sc-label` sits most of the
     way across at about slate-600, which leaves `--muted` itself free for the
     column identifiers below it. Three named steps and no fourth: they are
     read as a hierarchy, and a hierarchy with four rungs is a gradient. */
  --sc-ink: color-mix(in srgb, var(--text) 86%, var(--muted));
  --sc-label: color-mix(in srgb, var(--text) 30%, var(--muted));
  min-width: 0;
  background: var(--panel);
  border: 1px solid var(--card-edge);
  border-top: 2px solid color-mix(in srgb, var(--cat-mark, var(--line)) 70%, transparent);
  border-radius: var(--radius);
  padding: 7px 9px 8px;
  box-shadow: var(--shadow-sm);
}
/* The head: mark and name on the left, the component's points on the right.
   The points are the one score on the card, so they sit apart from the name
   rather than run on after it, where they read as part of the label. */
.sc-card-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  margin: 0 0 5px;
  font-size: var(--fs-sm);
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
}
/* The pill carries a 3px top margin from the days it sat under a label in a
   table cell; in a centred flex head that is 3px of drop on the one element the
   head is aligned around. */
.sc-card-head .delta-pill { margin-top: 0; }

/* Within a card the metrics are still a real `<table>`, because the columns
   lining up down the card is the whole value of it. `table-layout: fixed` is
   what buys that, and its column widths ride on a `<colgroup>` rather than on
   these rules: under a fixed layout only the first row's cells are measured, so
   a width declared on a body cell's class is read by nobody.

   Doubled up as `.mini-table.rating-scorecard` for the same reason the rule
   below it is: `.mini-table td` sets `white-space: nowrap` and sits later in
   this file, so a single-class selector here loses the cascade to it. */
.rating-scorecard { table-layout: fixed; }
.mini-table.rating-scorecard td {
  white-space: normal;
  vertical-align: top;
  line-height: 1.4;
  padding: 4px 5px;
}
/* Separated by the hairline `.mini-table td` already draws and by the hover
   `.mini-table tbody tr:hover` already carries, rather than by a shaded every
   other row. The stripe was here because a source cell is often three lines
   deep and a rule is thin cover to carry the eye across - true of the
   forty-row table this was, and no longer true of a card a third of the popup
   wide, where the row is short enough for its own divider to hold it and the
   hover paints the whole row the moment a reader needs one. Twelve striped
   cards side by side also read as twelve part-filled blocks, which is a
   texture the wall does not need.

   The last row gives its divider up: with nothing under it, the rule was
   drawing a line 8px above the card's own edge and parallel to it. */
.mini-table.rating-scorecard tr:last-child td { border-bottom: 0; }
/* The caption row set as micro-caps, the same treatment `.rating-card h3`
   gives a section title. It is a label for the column under it, and at the
   same size and case as the metric names below it read as a first data row
   whose figures had gone missing. Doubled up on `.mini-table` because
   `.mini-table tr.hd td` sits later in this file. */
.mini-table.rating-scorecard tr.hd td {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
}
/* The row's reading order, said in weight and tone rather than in colour: the
   figure this stock actually shows is the heaviest thing on the row and takes
   the full text colour, the metric's name steps back towards `--muted` because
   a reader scanning a card already knows they are reading metric names, and the
   source columns below are quieter again in mono. Three steps down one row, so
   the answer is the thing found first.

   The name is mixed rather than set to `--muted` outright: it is still the
   label the figure is read against, and a full drop to the muted tone puts it
   at the same weight as the column identifiers under it. */
.rating-scorecard .sc-metric {
  font-weight: 400;
  color: var(--sc-label);
}
/* The source columns: one line per scraped column, its name in mono so it reads
   as the identifier it is - the same string the inputs tab, the CSV and the
   database carry - and the value it held set off to its right. A name and a
   figure on one line, not a name over a figure, so a metric reading four
   columns is four lines and not eight. */
.rating-scorecard .sc-col {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fs-sm);
}
.rating-scorecard .sc-col code {
  font-family: var(--mono);
  /* The same 11px as the figure it shares the line with, rather than a half
     pixel under it: the identifier is dimmed to `--muted` now, and a face that
     is both the quietest and the smallest on the card is one step too far to
     read. `word-break` is what keeps `debtor_days_3y_ago` inside its track at
     this size. */
  font-size: var(--fs-sm);
  color: var(--muted);
  word-break: break-all;
}
.rating-scorecard .sc-col b {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  /* Dimmed to the same step as the metric's name, and no heavier. These are
     the working, not the answer: the identifier is already quiet at `--muted`,
     and leaving the figure beside it at full ink put the loudest number on the
     row in the column a reader is not there for. */
  font-weight: 500;
  color: var(--sc-label);
  white-space: nowrap;
}
/* The answer column: mono and tabular so the figures line up under each other,
   and the heaviest thing on the row because it is what the row is read for. */
.rating-scorecard .sc-value {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  /* 500, not 700. The answer column leads the row on tone and on being the
     only tabular figure at the right edge; it does not also need to be the
     heaviest thing on a card holding six of them, which is what turned a wall
     of twelve into a page of bold. */
  font-weight: 500;
  color: var(--sc-ink);
}
/* A blank is a column the scrapers have not filled - drawn faint rather than
   dropped, the same way `.rating-inputs` draws one. On the figure cell and on a
   single source column alike, so a metric reading two columns of which one is
   empty shows one faint line and one full one. */
.rating-scorecard td.rc-quiet,
.rating-scorecard .sc-col.rc-quiet { opacity: .38; font-weight: 400; }

/* The component table: one row each for name, points, diverging bar and what
   the component would score now - and, folded under the row, the rules that
   fired inside it. Rows are `<details>`, so this cannot be a `<table>`: a
   disclosure element is not a table row in any browser. It is a grid instead,
   and `grid-template-columns` on `.rc-line` is what `table-layout: fixed` used
   to buy - one template shared by every row, so the bars share one axis down
   the column. A bar chart whose zero line moves per row is not a bar chart. */
.rating-build { border-top: 1px solid var(--line-soft); margin-bottom: 8px; }
.rc-block { border-bottom: 1px solid var(--line-soft); }
.rc-line {
  display: grid;
  grid-template-columns: 1fr 58px 34% 64px;
  align-items: center;
  gap: 8px;
  position: relative;
  cursor: pointer;
  padding: 5px 4px 5px 17px;
  font-size: var(--fs-base);
  list-style: none;
}
/* Both halves of hiding the native marker: `list-style` is Firefox's lever and
   the pseudo-element is WebKit's. The caret below replaces it because it has to
   sit in the row's padding rather than in the first grid cell - a marker that
   is a grid item shunts the name column out of line with the rest. */
.rc-line::-webkit-details-marker { display: none; }
.rc-line::before {
  content: '›';
  position: absolute;
  left: 5px;
  color: var(--muted);
  font-size: var(--fs-md);
  line-height: 1;
  transition: transform .12s ease;
}
.rc-block[open] > .rc-line::before { transform: rotate(90deg); }
.rc-line:hover { background: var(--row-hover); }
.rc-line .rc-name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rc-line .rc-num { width: auto; }
.rc-line .rc-now { text-align: right; font-size: var(--fs-xs); }
/* Dimmed while shut, full strength once open: a component that scored nothing
   should recede from the scan without becoming unreadable when asked for. */
.rc-block.rc-quiet > .rc-line { opacity: .45; }
.rc-block.rc-quiet[open] > .rc-line { opacity: 1; }
.rc-body { padding: 2px 4px 8px 17px; }
.rc-body .hint { margin: 0 0 4px; }
.rc-num {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  width: 14%;
}
/* Diverging from a centre line, because these are contributions and half of
   them are negative: a left-anchored bar would draw a −1.3 valuation penalty as
   a shorter version of a bonus. */
.rc-bar { position: relative; display: block; height: 9px; }
.rc-axis {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--line);
}
.rc-fill { position: absolute; top: 1px; bottom: 1px; border-radius: var(--radius-xs); }
.rc-fill.pos { background: var(--positive); }
.rc-fill.neg { background: var(--negative); }

/* A rule and its reason: points hard right in mono, the claim in bold, the
   figures that tripped it wrapped underneath. `white-space: normal` overrides
   `.mini-table`'s nowrap - every cell here is a sentence - and it has to be
   doubled up as `.mini-table.rating-rules td` to do it: `.mini-table td` is
   declared further down this file at the same specificity, so a single-class
   selector here loses on source order and the sentences render on one line. */
.mini-table.rating-rules td, .mini-table.rating-caps td,
.mini-table.rating-filings td,
.mini-table.rating-bands td, .mini-table.rating-limits td {
  white-space: normal;
  vertical-align: top;
  line-height: 1.45;
}
.rating-rules .rc-num, .rating-caps .rc-num { width: 58px; white-space: nowrap; }
.rc-detail { display: block; color: var(--muted); font-size: var(--fs-sm); margin-top: 1px; }
.rc-evidence {
  margin: 8px 0 0;
  font-size: var(--fs-sm);
  color: var(--muted);
  line-height: 1.5;
  border-left: 2px solid var(--line);
  padding-left: 8px;
}
.rating-caps .rc-cap-code { font-family: var(--mono); font-size: var(--fs-sm); width: 130px; }
.rating-filings .rc-date {
  width: 76px;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* --- the rubric explainer ---------------------------------------------- */
/* The one sub-tab that is not about the stock in front of you: what the rubric
   is, what every metric can be worth, and the order the stages run in. Four
   drawings and two tables, and the drawings are the point - a component that
   paid +0.45 is unreadable until you can see that its axis tops out at 0.5 and
   growth's tops out at 3.1. */

/* The formula line, on the Summary tab's score build as well as on the stage
   rail here. Mono and spaced out because it is arithmetic being read term by
   term rather than a sentence: `5.00 + raw × 0.62` set in the body face runs
   together into something nobody checks. */
.rating-formula {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin: 6px 0 2px;
  font-family: var(--mono);
  font-size: var(--fs-base);
  font-variant-numeric: tabular-nums;
}
.rating-formula b { font-size: var(--fs-md); }
.rf-op { color: var(--muted); }
/* The mark on a raw sum that was weighted on the Rating page's Settings tab -
   the rows above it still show their own points, and this is what says the
   figure beside them is not their plain total. */
.rf-weighted { font-size: var(--fs-2xs); color: var(--accent); margin-left: 1px; }

/* The stage rail. Nine steps down a single line rather than nine boxes across,
   because a left-to-right flow of nine wraps onto three rows and a wrapped
   flowchart reads as three diagrams. The line itself is drawn per step rather
   than once behind the list, so the first step's half above its mark and the
   last step's half below it can be trimmed - a rail that overshoots the final
   mark reads as a tenth stage that failed to render. */
.rating-flow { list-style: none; margin: 0; padding: 0; }
.flow-step {
  display: grid;
  grid-template-columns: 22px 1fr;
  gap: 10px;
  align-items: start;
  padding: 7px 0;
  position: relative;
}
.flow-step::before {
  content: '';
  position: absolute;
  left: 10px;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--line);
}
.flow-step:first-child::before { top: 18px; }
.flow-step:last-child::before { bottom: calc(100% - 18px); }
/* Filled with the card's own background so the rail passes behind the marks
   rather than through them. */
.flow-mark {
  position: relative;
  width: 21px;
  height: 21px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--panel-2);
  color: var(--muted);
}
.flow-title { display: flex; align-items: center; gap: 6px; font-size: var(--fs-base); }
.flow-no {
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 0 3px;
  line-height: 1.6;
}
.flow-body .rc-detail { margin-top: 2px; }

/* The component budget: what each of the twelve can pay, either way. Rows are a
   grid rather than a table for the reason `.rc-line` is - one shared template,
   so the bars share one centre line down the column. A bar chart whose zero
   moves per row is not a bar chart. */
.rating-budget { border-top: 1px solid var(--line-soft); }
.rb-row {
  display: grid;
  grid-template-columns: 1fr 96px 32% 96px;
  align-items: center;
  gap: 8px;
  padding: 5px 4px;
  border-bottom: 1px solid var(--line-soft);
  font-size: var(--fs-base);
}
.rb-name {
  display: flex;
  align-items: center;
  gap: 5px;
  font-weight: 600;
  min-width: 0;
}
.rb-name .icon { flex: none; color: var(--cat-mark, var(--muted)); }
.rb-name > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rb-range {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  text-align: right;
  white-space: nowrap;
}
.rb-n { font-size: var(--fs-xs); color: var(--muted); text-align: right; line-height: 1.4; }
.rb-fin { display: block; color: var(--accent); }
.rb-foot { margin: 8px 0 0; }

/* The whole rubric as a table, both branches, no stock in it. Same instrument
   as `.rating-scorecard` at a different job, so it takes the same fixed layout
   and the same colgroup bargain - see the comment on that block. */
.rating-weights { table-layout: fixed; }
.mini-table.rating-weights td {
  white-space: normal;
  vertical-align: top;
  line-height: 1.4;
  padding: 5px 6px;
}
.rating-weights .sc-cat { border-right: 1px solid var(--line-soft); }
.rating-weights .sc-cat b {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-sm);
}
.rating-weights .sc-cat .icon { flex: none; color: var(--cat-mark, var(--muted)); }
.rating-weights .sc-metric { font-weight: 600; }
.rating-weights .sc-what { color: var(--muted); font-size: var(--fs-sm); }
.rating-weights .sc-ref { font-size: var(--fs-sm); }
/* The range and its bar stacked in one cell: the numbers are what you read and
   the bar is what you compare, and splitting them into two columns would put a
   ruled line between a figure and its own picture. Never wrapped, for the
   reason the scorecard's weight column is not. */
.rating-weights .wt-weight {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  text-align: right;
  white-space: nowrap;
}
.rating-weights .wt-weight .rc-bar { margin-top: 4px; }
.rating-weights .wt-only {
  margin-left: 5px;
  font-size: var(--fs-2xs);
  font-weight: 400;
  white-space: nowrap;
}

/* The Rating page's Settings section: the popup's rubric table (`.rating-weights`
   above) with a weight box per component and the weighted range beside it. The
   same classes on purpose - a reader who has opened the popup's explainer should
   recognise this table as the same one with a control added. `.rw-*` is what the
   control added. */
.rw-panel .rw-status { margin: 0 0 8px; }
.rw-panel .rw-status .pill { margin-right: 6px; }
.rw-bar { flex-wrap: wrap; row-gap: 6px; margin-bottom: 8px; }
.rw-bar > .hint { flex: 1 1 360px; margin: 0; }
.rw-bar .rw-note { flex: 1 1 100%; min-height: 1em; }
.rw-table .sc-cat .rw-key { display: block; margin-top: 3px; }
.rw-table .sc-cat .rw-key code,
.rw-table .rw-cols code {
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  background: var(--panel-2);
  padding: 0 4px;
  border-radius: var(--radius-xs);
}
/* The metrics under a component: one line each - name, range, then the scraped
   columns it reads - laid out as a list so four of them read as four rather
   than as a paragraph. */
.rw-table .rw-metrics ul { list-style: none; margin: 0; padding: 0; }
.rw-table .rw-metrics li { margin: 0 0 3px; line-height: 1.45; }
.rw-table .rw-metrics li:last-child { margin-bottom: 0; }
.rw-table .rw-metric { font-weight: 600; font-size: var(--fs-sm); }
.rw-table .rw-range {
  margin-left: 5px;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
.rw-table .rw-cols { display: block; margin-top: 1px; }
.rw-table .rw-cols code { margin-right: 3px; }
/* The weight box and its reset, side by side and never wrapped. 62px is what
   the Columns tab gives its number boxes - three characters and the spinner. */
.rw-table .rw-weight { white-space: nowrap; vertical-align: top; }
.rw-table .rw-weight input[type="number"] { width: 62px; text-align: right; }
.rw-table .rw-reset { margin-left: 4px; padding: 1px 5px; }
.rw-table .rw-reset:disabled { opacity: .35; cursor: default; }
.rw-table .rw-lender { display: block; font-size: var(--fs-2xs); margin-top: 2px; }
/* A component off the default wears a tint on its weight cell, and one edited
   but not yet saved a stronger one - so a glance down the column says what is
   re-weighted and what is merely typed. */
.rw-table tr.off-default .rw-weight { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.rw-table tr.dirty .rw-weight { background: color-mix(in srgb, var(--accent) 16%, transparent); }
.rw-table tr.off-default .rw-effective b { color: var(--accent); }

/* The Rating page's run registry. The list stays a real table because every
   count is compared down a column; the opened diff is a full-width row under
   its run, with its own small tables for before/after figures. */
.runs-panel .runs-bar { margin: 7px 0; }
.runs-table, .runs-table .changes { width: 100%; border-collapse: collapse; }
.runs-table th, .runs-table td { padding: 6px 8px; border-bottom: 1px solid var(--line-soft); }
.runs-table th {
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
.runs-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.run-row { cursor: pointer; }
.run-row:hover, .run-row.open { background: var(--row-hover); }
.run-when { font-family: var(--mono); white-space: nowrap; }
.run-chev { display: inline-block; width: 14px; color: var(--muted); }
.run-source { color: var(--muted); font-size: var(--fs-sm); }
.run-dist { display: flex; flex-wrap: wrap; gap: 3px 9px; white-space: nowrap; }
.run-dist .star { color: var(--accent); }
.run-detail > td { padding: 10px 14px 14px 22px; background: var(--panel-2); }
.run-against { margin: 0 0 10px; }
.run-group { margin-top: 12px; }
.run-group h4 { margin: 0 0 4px; }
.run-group .changes th, .run-group .changes td { padding: 4px 6px; }
.run-group .changes .why { min-width: 220px; }
.run-symbol {
  border: 0;
  padding: 0;
  background: transparent;
  color: var(--accent);
  font: inherit;
  font-family: var(--mono);
  font-weight: 600;
  cursor: pointer;
}
.run-symbol:hover { text-decoration: underline; }

/* The Rating page's "How the rating works" section. No `.panel` around it - the
   cards below bring their own boxes, and `.panel h3` would out-order
   `.rating-card h3` at equal specificity and undo the micro-caps every card head
   is set in. So the lead line carries the top margin a panel would have given
   it, and the cards carry the rest. */
.rh-lead { margin: 0 0 12px; max-width: 88ch; }

/* The 1-10 scale itself. Uniformly scaled rather than stretched to the panel,
   unlike `.rating-spark`: that one is a shape with no words on it, and this one
   is mostly words - `preserveAspectRatio` at its default is what stops the
   labels being drawn wider than they are tall. Every size below is in viewBox
   units, so they hold their proportions at any panel width. */
.rating-ruler { display: block; width: 100%; height: auto; margin: 6px 0 12px; }
.ruler-track { fill: var(--line-soft); }
.ruler-tick { stroke: var(--line); stroke-width: 1; }
.ruler-num {
  fill: var(--muted);
  font-family: var(--mono);
  font-size: var(--fs-md);
}
.ruler-mark line { stroke: var(--muted); stroke-width: 1; stroke-dasharray: 2 3; }
.ruler-mark text { fill: var(--muted); font-size: var(--fs-base); }
.ruler-gate line { stroke: var(--accent); }
.ruler-gate text { fill: var(--accent); }

/* Left rather than right, though it sits in an `.rc-num` cell: these are labels
   of unequal length rather than figures to be compared digit by digit, and
   right-aligning them staggers the left edge of a column that is read down. */
.rating-bands .rb-band {
  width: 62px;
  color: var(--accent);
  font-weight: 700;
  white-space: nowrap;
  text-align: left;
}

/* --- the run log ------------------------------------------------------ */
/* Every rate.py run that scored this stock. The rows that MOVED the rating are
   the ones worth finding in a column of thirty, so the quiet ones are dimmed
   rather than the moved ones being highlighted: highlighting would put a colour
   on a third of the table, while dimming leaves the eye one thing to land on. */
/* The run chart. Three layers in one cell: the SVG is the picture, the HTML
   overlay is every hit target and every hover card, and the axis labels are
   HTML too. The overlay is what makes a cell usable at all - a 2px dot is not a
   pointer target and is nothing at all to a keyboard - and it can only sit
   exactly over the points because the box and the viewBox are the same shape.
   See `CHART` in rating.js: the cell is sized by the grid and never by a
   height, precisely so they stay the same shape in every column count. */
/* Capped in WIDTH and never in height - a height cap would stop a cell being
   the viewBox's shape and letterbox the drawing inside it - but capped wide
   enough to hand the grid the whole popup, which is what eight columns need. */
.rating-chart { margin: 4px 0 12px; max-width: 1600px; }
/* Every series at once rather than one at a time behind a strip of chips - see
   `historyChart`. Eight to a row puts the whole rubric on two rows: the star,
   the score and six components on the first, the rest and the figures on the
   second, with no scrolling between the axis you are reading and the one you
   are comparing it against. The popup is 80vw, so a column is 130-190px on a
   normal screen - narrow, and the reason `CHART` spends a fifth of its width
   on the axis gutter and draws small dots.
   Below that it steps DOWN in column count rather than letting cells shrink
   past legibility. The breakpoints are viewport widths and the grid lives in a
   popup 80% of it, so they are set where the POPUP gets too narrow, not where
   the window does; the last fallback is one full-width chart per row. */
.rh-grid {
  display: grid;
  grid-template-columns: repeat(8, minmax(0, 1fr));
  gap: 8px;
}
@media (max-width: 1300px) { .rh-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (max-width: 1000px) { .rh-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 800px) { .rh-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 600px) { .rh-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 420px) { .rh-grid { grid-template-columns: minmax(0, 1fr); } }
/* A hovered cell's card overhangs its neighbours, and grid items paint in DOM
   order - so without the lift the card would slide UNDER the three cells to its
   right and be read through them. `position` is what makes `z-index` bite.
   `min-width: 0` because a grid track's default `min-content` floor would let a
   long label push its column wider than its share. */
.rh-cell { position: relative; min-width: 0; }
.rh-cell:hover, .rh-cell:focus-within { z-index: 4; }
/* The cell's head: what this chart is, and what it reads now. The figure sits
   here rather than only on a hover card because the grid is read DOWN as a
   scorecard with the shapes beside it - a dozen figures in one pass, none of
   them needing a pointer. */
.rh-cap {
  display: flex;
  align-items: baseline;
  gap: 4px;
  padding: 0 2px 2px;
  color: var(--muted);
  font-size: var(--fs-xs);
}
.rh-cap-k { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rh-cap-v {
  margin-left: auto;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.rh-cap .icon { flex: none; opacity: .8; }

/* The stack. `aspect-ratio` matches the viewBox exactly, so the default
   `preserveAspectRatio` fills the box with no letterboxing and the percentage
   offsets the overlay is positioned with are the offsets they claim to be. */
.rh-plots {
  position: relative;
  aspect-ratio: 2 / 1;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--panel);
}
.rating-spark { display: block; width: 100%; height: 100%; }
.rating-spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: miter;
}
.rh-dot { fill: var(--accent); }
/* A run the log below folds away: drawn, and drawn as the quiet run it is. The
   chart's whole argument for plotting every run is that the quiet ones are
   visible as quiet, so they recede rather than disappear - the same trade the
   log strikes when it dims a star that did not move. */
.rh-dot.rh-faint { opacity: .4; }
.rh-grid { stroke: var(--line-soft); stroke-width: 1; vector-effect: non-scaling-stroke; }
.rh-zero {
  stroke: var(--line);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  vector-effect: non-scaling-stroke;
}
/* The two ends of the axis, in HTML rather than as `<text>` in the viewBox: the
   box is four times wider than it is tall and scales with the popup, so a label
   drawn in viewBox units would be five pixels on a narrow one and thirteen on a
   wide one. These are the same size wherever the chart is drawn. */
.rh-axis {
  position: absolute;
  left: 2px;
  transform: translateY(-50%);
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  pointer-events: none;
}

/* The interaction layer: one button per run, each spanning its own slice of the
   width, so the target is a column of the chart rather than a dot. */
.rh-hits { position: absolute; inset: 0; }
.rh-hit {
  position: absolute;
  top: 0;
  bottom: 0;
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.rh-hit:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The guide line, drawn in the overlay rather than in the SVG - it belongs to
   the run, which every series shares, not to the series that happens to be on
   top. */
.rh-rule {
  position: absolute;
  top: 6%;
  bottom: 6%;
  width: 1px;
  margin-left: -0.5px;
  background: var(--accent);
  opacity: 0;
}
.rh-hit:hover .rh-rule,
.rh-hit:focus-visible .rh-rule { opacity: .55; }
.rh-hit.pinned .rh-rule { opacity: 1; }
.rh-hit.pinned::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--accent-soft);
  opacity: .5;
  pointer-events: none;
}
.rh-hit.rh-quiet .rh-rule { background: var(--muted); }

/* The card. Positioned off the point's own offset within its slice and shown by
   CSS on hover and on focus, so a tooltip cannot desynchronise from the point
   it belongs to - there is no lookup to get wrong and nothing to reposition.
   `pointer-events: none` because it overhangs its neighbours: a card that ate
   the pointer would flicker between two runs along its own edge. */
.rh-card-at {
  position: absolute;
  top: 6px;
  transform: translateX(-50%);
  display: none;
  z-index: 3;
  pointer-events: none;
}
.rh-card-at.rh-card-start { transform: translateX(-12px); }
.rh-card-at.rh-card-end { transform: translateX(calc(-100% + 12px)); }
/* Flipped to sit ABOVE the dot when the dot is low in the plot, so a card never
   spends its life hanging over the figures row under the chart. Carried on the
   inner card rather than on the positioned wrapper, because the wrapper's own
   transform is already spoken for by the three horizontal anchors above and
   `transform` does not stack - one rule here, rather than three more there. */
.rh-card-at.rh-above .rh-card { transform: translateY(-100%); }
/* Hover and focus only. A pin is NOT one of them: pinning is a lasting choice
   about which run the log below is showing, and the card is a reading of the
   point under the pointer. A card that stayed up after the pointer left would
   sit over the next three runs the reader was trying to look at, and it would
   still be there when they had moved on to another series entirely. The pin
   keeps its guide line and its row - what it is for - and the card goes with
   the pointer, which is what a tooltip does. */
.rh-hit:hover .rh-card-at,
.rh-hit:focus-visible .rh-card-at { display: block; }
/* Sized to its content rather than to a fixed 210px: a card wider than the cell
   it points at hangs over two neighbours whichever way it is anchored, and this
   one is two short lines. Capped all the same, so the folded-run foot wraps
   instead of drawing a card the width of the grid. */
.rh-card {
  display: block;
  width: max-content;
  max-width: 170px;
  padding: 5px 7px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  box-shadow: var(--shadow-md);
  text-align: left;
  font-size: var(--fs-sm);
  color: var(--text);
}
.rh-when {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  color: var(--muted);
}
.rh-card-badges { display: flex; flex-wrap: wrap; gap: 3px; margin: 3px 0 1px; }
.rh-card-line {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  padding: 1px 0;
}
.rh-card-k { color: var(--muted); }
.rh-card-v { font-variant-numeric: tabular-nums; }
.rh-card-rule {
  display: block;
  margin: 4px 0 2px;
  border-top: 1px solid var(--line-soft);
}
.rh-card-more { display: block; color: var(--muted); font-size: var(--fs-2xs); }
.rh-card-foot {
  display: block;
  margin-top: 5px;
  color: var(--muted);
  font-size: var(--fs-2xs);
  line-height: 1.35;
}

.rating-history-figures { padding: 0 2px; margin-bottom: 4px; }
/* The run log is a list of disclosure rows rather than a table. Every run opens
   onto the fields its ledger row moved, and a `<details>` cannot be a `<tr>` -
   so the one thing the table was buying, columns that line up down the log, is
   bought here instead by a track list the header, every open row and every flat
   one all share. */
.rating-line {
  display: grid;
  grid-template-columns: 12px minmax(116px, max-content) 74px 88px minmax(110px, 1fr);
  gap: 8px;
  align-items: baseline;
  padding: 4px 2px;
}
.rating-line.rc-head {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
}
.rating-run { border-bottom: 1px solid var(--line-soft); }
.rating-runs > .rating-run:last-child { border-bottom: 0; }
.rating-run > summary { cursor: pointer; list-style: none; }
.rating-run > summary::-webkit-details-marker { display: none; }
.rating-run > summary:hover { background: var(--row-hover); }
/* The caret is drawn on the summary rather than written into the markup, so the
   runs that do not open - the oldest in the ledger, which has nothing under it
   to have changed against - still hold the column and the labels beside them
   stay in one line down the log. */
.rating-run > summary .rc-caret::before {
  content: '▸'; color: var(--muted); font-size: var(--fs-xs);
}
.rating-run[open] > summary .rc-caret::before { content: '▾'; }
.rating-run-diff { padding: 0 2px 6px 20px; }
/* Which way a ledger figure went, on the fields where up is unambiguously the
   good news - the score, the star and the twelve components; see
   `ledgerDirection`. The mark is the point and the colour is the confirmation,
   not the other way round: `--positive` is BLUE in this app and the arrow is
   what survives being read in greyscale or by an eye that cannot separate the
   hues. Only the run log asks for this, because only the run log knows the
   polarity of what it is diffing. */
.job-result-now.up, .job-result-now.down { font-weight: 600; }
.job-result-now.up { color: var(--positive); }
.job-result-now.down { color: var(--negative); }
.job-result-now.up::before { content: "▲ "; font-size: var(--fs-2xs); }
.job-result-now.down::before { content: "▼ "; font-size: var(--fs-2xs); }
/* The row a data point on the chart above was clicked on. A tint rather than a
   scroll-and-forget: the chart and the log are two views of one sequence, and
   the whole point of the click is that the reader can see, without hunting,
   which row the point they pressed became. */
.rating-run.rh-lit {
  background: var(--accent-soft);
  box-shadow: inset 2px 0 0 var(--accent);
}
.rating-history .rc-run {
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
}
.rating-line .rc-num { text-align: right; }
.rating-run:not(.rc-moved) .rating-run-star { opacity: .5; }
.rating-run-star { font-weight: 600; }
.rc-move {
  display: inline-block;
  min-width: 34px;
  margin-left: 5px;
  font-family: var(--mono);
  font-size: var(--fs-xs);
  text-align: right;
}
.rc-move.pos { color: var(--positive); }
.rc-move.neg { color: var(--negative); }
.rating-why-run { border-top: 1px solid var(--line-soft); padding: 5px 0 3px; }
.rating-why-run > summary {
  cursor: pointer;
  display: flex;
  gap: 10px;
  align-items: baseline;
  font-size: var(--fs-sm);
}
.rating-why-run > summary::-webkit-details-marker { display: none; }
.rating-why-run[open] > summary { color: var(--text); }

/* Every scraped figure, in two columns of small tables. Blanks are drawn faint
   rather than dropped - which columns were empty is half the answer to "why did
   that rule not fire". */
.rating-inputs {
  display: grid;
  /* 260px rather than the 230px this held before the rows carried marks: each
     row gained a 12px picture on the label and can gain a second on the figure,
     so a track that was exactly wide enough for its longest label no longer is.
     Raising the floor keeps the same relationship between content and track and
     costs at most one column on a wide popup. */
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 4px 18px;
}
.rating-input-group .card-sub { display: flex; justify-content: space-between; gap: 8px; }
.rating-inputs .mini-table td { padding: 2px 4px; }
.rating-inputs tr.rc-quiet { opacity: .38; }
/* Each row is a registry column, so it wears that column's picture and that
   column's conditional formatting - the same two things its grid header and its
   grid cell wear. That is the whole point of the table: a figure here can be
   traced back to the column it came out of without reading the label, and a P/E
   the reader has painted red in the grid is red here too.

   The label may wrap, against `.mini-table td`'s nowrap. "Capital work in
   progress 3 years ago" with a mark in front of it is wider than a 260px track,
   and a nowrap cell does not shrink - it spills into the group beside it. Two
   lines is the better failure. The figure keeps the nowrap: it is short, and a
   number broken across lines is unreadable. */
.rating-inputs .mini-table td:first-child { white-space: normal; }
/* Only as wide as the figure needs, not `.rc-num`'s 14% - these tables are two
   columns in a narrow track rather than the six-column scorecard that width was
   written for, and 14% of 260px leaves nothing for the mark. */
.rating-inputs .rc-num { width: auto; }

/* --- the figures a card is opened for -------------------------------- */
/* Two or three numbers set above the list that details them. Every card used to
   be a flat `dl` where "market value" and "quoted at" were the same 11.5px, and
   the popup is opened to read one of those and almost never the other. The
   lead figure is the answer; the two beside it are what it has done.

   Sized against a *column*, not against the popup: at four columns on a 1440px
   display a card is ~330px wide, and a 28px lead with two 20px figures beside
   it wraps onto three lines and stops being a headline row. 18px over 14px is
   the pair that holds all three on ONE line in that column - which is the whole
   job of the row - and the lead still reads as the loudest thing on the card
   because nothing else on it is set above 12. It was 22 over 15, which fitted a
   wide popup and wrapped in a narrow column, and a headline row that wraps has
   stopped being one. */
.card-figures {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  margin: 0 0 7px;
}
.figure { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.figure-label {
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
  white-space: nowrap;
}
.figure-value {
  font-family: var(--mono);
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1.15;
  white-space: nowrap;
}
.figure.lead .figure-value { font-size: 18px; font-weight: 700; }

/* A heading inside a card - "Per account", "Your labels". Small enough not to
   compete with the card's own h3, which is what it sits under. */
.card-sub {
  margin: 8px 0 3px; font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase;
}
/* Prose in a card: the company's business summary is the only paragraph on the
   popup meant to be *read* rather than scanned, so it gets body colour and a
   line height instead of the muted, tight `.hint` treatment used for asides.

   Broken into paragraphs of two sentences and - past a screenful - clipped
   behind a Show more. Yahoo serves the summary as one ten-sentence block,
   which in a 330px column is fifteen unbroken lines and the tallest thing in
   it: every card under it goes off the bottom of the popup. Faded out rather
   than cut off, so a clipped block is visibly clipped and not mistaken for a
   summary that simply ends mid-sentence. */
.card-prose { margin: 6px 0 0; font-size: var(--fs-sm); line-height: 1.6; color: var(--text); }
.card-prose p { margin: 0 0 7px; }
.card-prose p:last-child { margin-bottom: 0; }
.card-prose.clamped {
  max-height: 7.4em;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent);
  mask-image: linear-gradient(to bottom, #000 62%, transparent);
}
.prose-more { margin-top: 5px; }

/* The editable fields. `.settings-row` is a 190px-label flex row built for the
   full-width Data & Settings page - inside a 300px modal column every input
   wrapped onto its own line, doubling the height of the tallest card here. */
.detail-edit { display: grid; grid-template-columns: auto 1fr; gap: 4px 8px; align-items: center; }
.detail-edit > label { color: var(--muted); font-size: var(--fs-sm); }
.detail-edit > input, .detail-edit > textarea,
.detail-edit > select { width: 100%; min-width: 0; font-size: var(--fs-base); }
.detail-edit > textarea { resize: vertical; line-height: 1.35; }

/* Notes, which is a log rather than a value: every entry written so far as its
   own bullet, and one short box under them for the next one. The three share a
   single grid cell, because the label names the pair - a second label over the
   box would read as a second field on the form.

   The log scrolls at 200px rather than growing without limit: three years of
   quarterly notes is a card taller than the popup, and the field beneath it is
   still a field somebody has to reach. */
.note-field { display: grid; gap: 5px; min-width: 0; }
.note-log { max-height: 200px; overflow: auto; }
/* The bullet is drawn rather than marked, because an entry is a row of three
   things now - the mark, the text, and the two buttons that act on it - and a
   list item laid out as a row is a list item with no marker of its own. Drawing
   it also puts it at the top of a wrapped entry rather than beside its middle
   line, which is where a three-line note wants it. */
.note-log ul { margin: 0; padding: 0; list-style: none; }
.note-log li {
  display: flex;
  align-items: flex-start;
  gap: 5px;
  font-size: var(--fs-base);
  line-height: 1.4;
  margin-bottom: 3px;
}
.note-log li::before { content: '•'; flex: none; color: var(--muted); }
.note-log li:last-child { margin-bottom: 0; }
.note-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.note-log .note-none { margin: 0; font-size: var(--fs-sm); }
/* The two buttons an entry wears, quiet until it is hovered - twenty entries
   with forty lit icons down the side of them would be louder than the notes.
   The same bargain `.field-tool` strikes one card over, and stated the same
   way; they hold their width whether they are lit or not, so nothing under the
   pointer reflows as it goes down the log. */
.note-row-tools { display: flex; flex: none; gap: 1px; }
.note-tool {
  background: none;
  border: 0;
  padding: 0 2px;
  font: inherit;
  font-size: var(--fs-xs);
  line-height: 1.4;
  color: var(--muted);
  opacity: 0;
  cursor: pointer;
  border-radius: var(--radius-xs);
}
.note-log li:hover .note-tool, .note-log li:focus-within .note-tool { opacity: .55; }
/* Between the two, deliberately: every one of these ties at (0,3,1), so the
   order in the file is what decides. An entry being rewritten is lit whether
   the pointer is on it or not, and the button under the pointer is lit hardest
   whichever state its row is in. */
.note-log li.note-editing .note-tool { opacity: .8; }
.note-log li .note-tool:hover { opacity: 1; background: var(--panel-2); color: var(--text); }
.note-log li .note-tool[data-note-del]:hover { color: var(--negative); }
/* The two dead ends - ▲ on the first entry, ▼ on the last - drawn and dimmed
   rather than left out, so the four buttons are the same width down the whole
   list and nothing shifts under the pointer as it goes down. Last of the set
   deliberately: these all tie at (0,3,1), so the order in the file is what
   decides, and a dead end must outrank both the row's hover and its own. */
.note-log li .note-tool:disabled {
  opacity: .18;
  cursor: default;
  background: none;
  color: var(--muted);
}
/* An entry being rewritten stands in the line it was read in, with its box as
   wide as the text was. Both buttons stay lit: the row is being worked on, and
   a control that appears only under the pointer is one somebody typing has no
   reason to expect. */
.note-editing > textarea {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-base);
  line-height: 1.35;
  resize: vertical;
}
.note-field > textarea {
  width: 100%;
  min-width: 0;
  font-size: var(--fs-base);
  resize: vertical;
  line-height: 1.35;
}
/* Beside its own box rather than in the card's action row, and for the same
   reason the ISIN search is: it adds to the form, it does not save it, and
   sitting next to Save is exactly where it would be mistaken for it. */
.note-field > .note-add { justify-self: start; }
/* A field and the button that acts on it, sharing one grid cell - the ISIN box
   in the Exchange codes card. The button belongs beside its box rather than in
   the action row below: it searches, it does not save, and sitting next to
   Save and fetch is exactly where it would be mistaken for it. */
.detail-edit > .code-find { display: flex; gap: 6px; min-width: 0; }
.code-find > input { flex: 1; min-width: 0; font-size: var(--fs-base); }
.code-find > .btn { flex: none; }
.detail-actions { display: flex; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
.detail-actions .hint { margin: 0; }

/* A card's fields, in the columns the reader has split them over.

   `auto-fit` rather than a flat `1fr 1fr`, because how wide this card is is the
   reader's decision and not this rule's: the same card is nearly the whole
   popup on a one-column sub-tab and 265px on a four-column one, and two 120px
   columns of boxes would be worse than the scrolling the split is there to
   stop. Under about 500px the second track collapses and the two halves stack -
   which puts the fields back in exactly the order `mineSplit` cut them in, so
   nothing is read out of sequence on the way down.

   Two grids inside rather than four tracks in this one, so each half sizes its
   label column to its own longest label. `min-width: 0` on them because a grid
   item's floor is its content, and "Golden Crossover" in a 240px track would
   otherwise push the card wider than the column it was placed in. */
.field-cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 4px 20px;
  align-items: start;
}
.field-col { min-width: 0; }
.field-col > .detail-edit { min-width: 0; align-content: start; }
/* Scoped to this form rather than to `.detail-edit`: the Exchange codes card
   uses the same grid and has no marks, and a flex label there would be a
   layout change for nothing. */
.field-col > .detail-edit > label { display: flex; align-items: center; }
/* The mark, at full strength and in its family's colour - unlike the grid
   header's, which is quiet until hovered. A header sits in a row of forty and
   only has to be findable; this is one of seventeen labels on a form somebody
   is typing into, and the colour is the channel that gets the eye to the right
   line. The width is stated so a column with no mark still lines up with the
   ones that have one, the same bargain `.picker-row .col-i` strikes.

   `--rf-fg` is set by the `.rf-<colour>` class on the label itself, which is
   the one place in this stylesheet where a palette *name* becomes a value both
   themes can answer for. Nothing paints a background: that is `.rf`, and no
   label here carries it. */
.field-col > .detail-edit > label > .col-i {
  width: 15px;
  margin-right: 5px;
  opacity: 1;
  color: var(--rf-fg, var(--muted));
}

/* --- dashboard ------------------------------------------------------ */
.dash-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-auto-rows: 92px;
  gap: 10px;
}
.widget {
  grid-column: span var(--w, 4);
  grid-row: span var(--h, 3);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  display: flex;
  flex-direction: column;
  /* Not `hidden`: the resize strips below hang into the grid gap and clipping
     here would swallow them, silently, leaving nothing to grab. The body has
     its own `overflow: auto` and clips the content that used to need it; the
     header rounds its own top corners, which is the rest of what was lost. */
  overflow: visible;
  position: relative;
}
.widget.drag-over { outline: 2px dashed var(--accent); outline-offset: -3px; }
.widget.dragging { opacity: .4; }
.widget-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 9px;
  border-bottom: 1px solid var(--line-soft);
  border-radius: var(--radius) var(--radius) 0 0;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
  cursor: grab;
  background: var(--panel-2);
}
.widget-head .grip { display: flex; cursor: grab; opacity: .5; }
.widget-head .grip:hover { opacity: 1; }
.widget-head .w-actions { margin-left: auto; display: flex; gap: 2px; }
/* The three header controls, like the grid's row actions: a round ghost target
   with a line icon in it, quiet until the widget is hovered. Sized off the
   header rather than the 26px `.icon-btn`, which would make the bar taller
   than the two lines of text it is there to caption. */
.widget-head .w-actions .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 19px;
  height: 19px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  opacity: .55;
  transition: opacity .12s, background .12s, color .12s;
}
.widget:hover .w-actions .btn { opacity: 1; }
.widget-head .w-actions .btn:hover { background: var(--accent-soft); color: var(--accent); }
.widget-head .w-actions .btn.danger:hover { background: var(--negative-soft); color: var(--negative); }
.widget-head .w-actions .btn:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
.widget-body { flex: 1; padding: 8px 10px; overflow: auto; }
.widget.stat .widget-body { display: flex; flex-direction: column; justify-content: center; }

/* --- widget resize separators ---------------------------------------- */
/* The 10px grid gap doubles as the separator between two widgets, and the
   handle lives in it: a strip down the right edge for the column span, one
   along the bottom for the rows, a corner for both. Mostly *outside* the panel
   on purpose - a strip inside it would sit on top of `.widget-body`'s own
   scrollbar, the one thing in a widget people already drag - which is why
   `.widget` cannot clip its overflow. Each strip still overlaps the panel by a
   few pixels so there is something to aim at on a widget whose neighbour is
   flush against it. */
.w-edge { position: absolute; z-index: 2; touch-action: none; }
.w-edge.x { top: 10px; bottom: 10px; right: -7px; width: 10px; cursor: col-resize; }
.w-edge.y { left: 10px; right: 10px; bottom: -7px; height: 10px; cursor: row-resize; }
.w-edge.xy {
  right: -7px;
  bottom: -7px;
  width: 16px;
  height: 16px;
  cursor: nwse-resize;
  z-index: 3;
}
/* Invisible until the pointer is on it, so a dashboard at rest is not a cage
   of blue lines. */
.w-edge::after {
  content: '';
  position: absolute;
  border-radius: var(--radius-xs);
  background: var(--accent);
  opacity: 0;
  transition: opacity .12s;
}
.w-edge.x::after { top: 0; bottom: 0; left: 3px; width: 3px; }
.w-edge.y::after { left: 0; right: 0; top: 3px; height: 3px; }
.w-edge.xy::after { right: 4px; bottom: 4px; width: 8px; height: 8px; border-radius: 0 0 var(--radius-sm) 0; }
.w-edge:hover::after, .w-edge.on::after { opacity: .8; }

/* The pointer is captured by the strip, so the cursor otherwise reverts to
   whatever the element under it wants half way through a drag. */
body.dash-resizing { user-select: none; }
body.dash-resizing * { cursor: inherit !important; }
body.dash-resizing.dash-x { cursor: col-resize; }
body.dash-resizing.dash-y { cursor: row-resize; }
body.dash-resizing.dash-xy { cursor: nwse-resize; }
/* Stat values keep the UI sans with proportional figures - tabular-nums makes a
   large standalone number read loose, and nothing aligns vertically here. */
.stat-value { font-size: 22px; font-weight: 600; line-height: 1.1; }
.stat-sub { color: var(--muted); font-size: var(--fs-sm); margin-top: 3px; }

.mini-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
/* 3px top and bottom, 4px either side. A card here is eight to twelve of these
   rows, so a pixel off each one is most of a row back per card - and the rule
   between them is what separates them, not the air. */
.mini-table td { padding: 3px 4px; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
.mini-table tbody tr:hover { background: var(--row-hover); }
.mini-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.mini-empty { color: var(--muted); font-size: var(--fs-base); padding: 8px 2px; }

/* 4px rounded data-end anchored to the baseline, thin mark, no border ring. */
.bar-track { background: var(--panel-2); border-radius: var(--radius-xs); height: 8px; overflow: hidden; }
.bar-fill { height: 100%; background: var(--series-1); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
/* Column captions for the few mini-tables that carry more than one number per
   row. Muted, and that is now all they need: the .num column below no longer
   sets a face of its own, so a caption sitting above one has nothing to opt out
   of. It kept `font-family: inherit` for exactly as long as those cells were
   mono. */
.mini-table tr.hd td { color: var(--muted); }
/* The row that adds the ones above it up. A firmer rule over it and a heavier
   face are what separate a total from one more data row; the bottom border goes
   because there is nothing below it to divide it from. */
.mini-table tr.ft td {
  border-top: 1px solid var(--line);
  border-bottom: none;
  font-weight: 600;
}
.mini-table.bars td.lbl { max-width: 92px; overflow: hidden; text-overflow: ellipsis; }

/* The Holding card's table is the label/value list it replaced, turned on its
   side: a row per figure, a column per owner. So the first column is a heading
   rather than a datum and reads muted like the `dt` it used to be, and it is
   the only cell allowed to wrap - "Distance to stop" is two words wide and
   every column beside it is a number that must not be.

   `td.mine` is the owner the rest of the popup is about. A tint rather than a
   rule or a weight: the column is already full of red and blue figures whose
   colour is the message, and a second emphasis competing with them would make
   the one column that matters the hardest one to read. */
.holding-table td.lbl { color: var(--muted); white-space: normal; }
.holding-table td.mine { background: var(--panel-2); }
.holding-table tr.hd td.mine { color: var(--text); font-weight: 600; }
/* The profit pair. Every row above them is the working - shares, cost, value -
   and P/L and P/L % are the answer it adds up to, so they are what the eye
   should land on first. Full bold rather than the 600 a total carries: at 11px
   in Segoe UI the semibold step is barely a step, and these two have to read as
   emphasised across a glance at the card rather than under comparison with the
   row above. Weight and not colour, because those cells are already red or
   green and that is their own message. */
.holding-table tr.strong td { font-weight: 700; }
/* The column that adds the ones beside it up, in the same weight `tr.ft` gives
   the row that does - one rule for both, so a total reads as a total whichever
   way the table happens to be turned. The rule on its left is what separates
   the sum from the last thing being summed. */
.mini-table td.ft { border-left: 1px solid var(--line); font-weight: 600; }
/* Free-text labels - a domain, not a ticker - need room to read as words. */
.mini-table.bars.wide td.lbl { max-width: 170px; }

.chart-legend {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  font-size: var(--fs-sm);
  color: var(--muted);
  margin-bottom: 4px;
}
.chart-legend i { display: inline-block; width: 9px; height: 9px; border-radius: var(--radius-xs); margin-right: 5px; }
/* A legend row that is also a switch. Only some are: a plan gives an entry an
   `id` when the reader can switch that row off, and the Master gives one to
   every study it draws. The rest stay captions.

   Styled to sit in the row it shares with those captions rather than to look
   like a button - a legend of five items where two are visibly controls and
   three are not would read as a broken row. What marks it as pressable is the
   cursor, the hover, and the focus ring; what marks its *state* is below. */
.chart-legend .legend-toggle {
  display: inline-flex;
  align-items: center;
  padding: 0 3px;
  margin: 0 -3px;
  border: 0;
  border-radius: var(--radius-xs);
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.chart-legend .legend-toggle:hover { background: var(--panel-2); color: var(--text); }
.chart-legend .legend-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* Off. Struck through as well as dimmed, and the swatch hollowed rather than
   greyed - the same reading in greyscale and to somebody who cannot separate
   the hues, which matters more here than almost anywhere else in the app
   because the swatch beside it is doing nothing but carry a colour. The hollow
   itself is drawn inline by `legendItem`, in that study's own colour, so an off
   row still says which study it is. */
.chart-legend .legend-toggle[data-off] { text-decoration: line-through; opacity: .55; }
.chart-legend .legend-toggle[data-off]:hover { opacity: 1; }

.line-chart { display: block; overflow: visible; }

/* The equity curve as figures. Dense like the other widget tables, but headed -
   seven columns of rupees need naming. Auto layout on purpose: the columns size
   to their content and follow the widget as it is resized, rather than being
   pinned in pixels the way `resizableTables` pins a static table. Too narrow a
   widget overflows into .widget-body's own scroll. */
.curve-table {
  width: 100%;
  min-width: max-content;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.curve-table th {
  padding: 2px 4px;
  text-align: right;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}
.curve-table td { padding: 1px 4px; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
.curve-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.curve-table th:first-child, .curve-table td:first-child { text-align: left; }
/* The window as one row. It sticks to the foot of the widget's own scroll so a
   long curve does not put its own total below the fold - the figure most worth
   reading must not be the one you have to scroll to. The rule above it is the
   heavier `--line`, matching the header, because this row is a boundary rather
   than another entry in the list. */
.curve-table tfoot td {
  position: sticky;
  bottom: 0;
  background: var(--panel);
  /* An inset shadow rather than `border-top`. Under `border-collapse: collapse`
     a border belongs to the collapsed grid rather than to the cell, and does
     not travel with a sticky cell - the rule would sit where the row would have
     been and scroll away from it. A shadow is painted by the cell itself. */
  box-shadow: inset 0 1px 0 var(--line);
  border-bottom: none;
  font-weight: 600;
}
/* The levels in that row are not totals, and are toned back so the eye lands on
   the ones that are. Their tooltip says so; this says it without a hover. */
.curve-table tfoot td.muted { font-weight: 400; }
.chart-toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; flex-wrap: wrap; }
/* The selected view in a two-button chart-type toggle. Filled, not merely
   outlined: an unfilled "pressed" button reads as hover on a dense panel. */
.btn.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.on:hover { border-color: var(--accent); }

/* --- the index chart widget ------------------------------------------ */
/* It draws the popup's price card inside a dashboard panel, and that card is
   sized for the ~1,100px the popup gives it: a 15px pill over a 12.5px row
   reads as a headline there and as a shout in a widget a third the width. The
   figures are toned down here only, so the popup keeps its own scale.

   Nothing about the chart itself is restyled - the plot, the crosshair and the
   drag band are the same ones, because the point of the widget is that it is
   the same chart. */
.widget-body .price-headline { margin: 0 0 3px; gap: 7px; font-size: var(--fs-sm); }
.widget-body .price-headline .pill-chg { font-size: var(--fs-md); padding: 2px 8px; }
.widget-body .price-headline .chg { font-size: var(--fs-sm); }
.widget-body .price-selection { min-height: 18px; font-size: var(--fs-xs); }
.widget-body .price-selection .pill-chg.sm { font-size: var(--fs-sm); }
.widget-body .price-caption { margin-top: 4px; font-size: var(--fs-xs); }
/* The three pickers sit in the shared `.chart-toolbar`, where the curve's
   period select already lives. Capped so a narrow widget wraps them onto two
   rows rather than letting "NIFTY SMALLCAP 100" or "Trend line & parallel
   channel" push the ones beside it off the panel - `.chart-toolbar` wraps, and
   a select that cannot shrink never gets the chance to. */
.widget-body .chart-toolbar .field select { max-width: 150px; }
/* A study's readings, under its chart. Tighter than the popup's: the popup
   gives one study the full width and can afford the rows, where a widget is
   showing the chart first and the numbers as support. */
.widget-body .study-facts { font-size: var(--fs-xs); }
.widget-body .study-note { font-size: var(--fs-xs); }
.widget-body .chart-legend { font-size: var(--fs-xs); }

/* Donut beside its legend, wrapping to stacked when the widget is narrow. The
   ring is capped so a 12-column widget does not draw a dinner plate. */
.pie-wrap { display: flex; gap: 10px; align-items: flex-start; flex-wrap: wrap; }
/* Basis and cap are the drawn width, set inline by the chart; the ring shrinks
   with the widget and the legend drops underneath it once both stop fitting. */
.pie { flex: 0 1 auto; min-width: 120px; height: auto; display: block; overflow: visible; }
.pie-legend { flex: 1 1 190px; width: auto; }
.pie-legend td.key { width: 14px; padding-right: 0; }
.pie-legend td.key i { display: block; width: 9px; height: 9px; border-radius: var(--radius-xs); }
.pie-legend td.lbl { max-width: 170px; overflow: hidden; text-overflow: ellipsis; }

/* --- the analytical dashboard charts -------------------------------- */
/* Risk/return bubbles, the domain sunburst, the capital-flow Sankey, the daily
   P/L calendar and the parallel plot. The sunburst reuses `.pie-wrap` and
   `.pie-legend` above wholesale - it is a ring beside a legend table, which is
   what those rules already describe - so only what those cannot say is here. */

/* The bubble chart's size key: three hollow reference circles with their value
   beside each. Baseline-aligned rather than centred, so the three sit on the
   same line as the two colour swatches they share the legend row with, however
   different their heights are. */
.size-key { display: inline-flex; gap: 12px; align-items: flex-end; }
.size-key-item { display: inline-flex; gap: 4px; align-items: flex-end; }
.size-key-item svg { display: block; overflow: visible; }

/* The calendar is 52 columns wide at 1Y and does not shrink - a session is a
   12px square or it is not a square anybody can hover. So it scrolls sideways
   inside the widget rather than squeezing, and `.widget-body`'s own `overflow:
   auto` is what would otherwise have to do it, scrolling the caption and the
   legend along with the grid. */
.cal-scroll { overflow-x: auto; overflow-y: hidden; padding-bottom: 2px; }
.cal-scroll svg { display: block; }
/* The diverging key reads as one ladder, so its swatches touch: a gap between
   them would break the ramp into nine separate marks. The words either end are
   what say which direction is which. */
.chart-legend.cal-key { gap: 10px; align-items: center; }
.chart-legend.cal-key i { margin: 0 0 0 1px; width: 11px; height: 11px; }
.chart-legend.cal-key i:first-of-type { margin-left: 5px; }

/* The parallel plot draws to its viewBox with `preserveAspectRatio: none`, so
   it stretches to whatever width the widget is - the axes are positional and
   the aspect ratio carries no meaning. */
.pc-wrap { width: 100%; }
.pc-wrap svg { display: block; }
/* Round joins on a polyline that changes direction at every axis: mitred
   corners on a 1.6px stroke crossing at a sharp angle throw a spike several
   pixels past the axis, and sixty of those read as noise around every axis
   line. */
.pc-line { stroke-linejoin: round; stroke-linecap: round; }
.pc-line.sym-link:hover { stroke-opacity: 1; stroke-width: 2.6; }
/* Brushed out. Not `display: none`: what the filter excluded is half of what
   the filter tells you, so those lines stay on the plot as a ghost of the
   whole book behind the selection. Pointer events go with the visibility -
   a line nobody can see must not be a line that answers a hover. */
.pc-line.off { pointer-events: none; }
.pc-caption { margin: 4px 0 0; }

/* The sunburst's inner ring is a drill-down. Lifted on hover rather than
   outlined, because an outline round an arc is ink that is not data and the
   ring is already eight arcs deep in colour. */
.sb-open { transition: opacity .12s; }
.sb-open:hover { opacity: .82; }
.mini-table .sb-row { cursor: pointer; }

/* --- data & settings ------------------------------------------------ */
.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 12px 14px;
  margin-bottom: 12px;
}
.panel h3 { margin: 0 0 10px; font-size: var(--fs-md); }
.panel h4 { margin: 12px 0 6px; font-size: var(--fs-sm); text-transform: uppercase; color: var(--muted); letter-spacing: .05em; }
/* A panel inside a panel: the Add-a-column form and the deleted-column list
   under the Columns table. Recessed rather than raised, so it reads as part of
   the panel it sits in and not as a second card floating on top of it. */
.panel .panel.sub {
  background: var(--bg);
  box-shadow: none;
  margin: 12px 0 0;
}
.panel .panel.sub:last-child { margin-bottom: 0; }
.panel .panel.sub h4 { margin-top: 0; }
/* A retired column: its name and the one press that brings it back. Flex
   because the chip has a button in it, which the inline-block form cannot
   line up against the text beside it. */
#retired-columns { flex-wrap: wrap; }
#retired-columns .chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px 3px 8px;
  line-height: 20px;
}
/* Collapsible section heading in the Import panel. The whole h4 is the control -
   flex so the glyph, the words and the count sit on one baseline, and
   `user-select: none` so a double-click that lands on it selects nothing. */
.panel h4.section-head {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
  width: max-content;
}
.section-toggle {
  flex: none;
  width: 16px;
  height: 16px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-2);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  font-family: var(--mono);
  font-size: var(--fs-base);
  line-height: 1;
  cursor: pointer;
}
.panel h4.section-head:hover { color: var(--text); }
.panel h4.section-head:hover .section-toggle { color: var(--text); border-color: var(--accent); }
/* The provenance list, as a wall of cards rather than a two-column table.
   Nine sources at a fixed 200px of label each left the part actually worth
   reading in a narrow column and the last source a screen and a half down;
   four abreast fits the lot on one screen. Cards in the same row share a
   height, so the wall reads as rows rather than as a ragged edge. */
.source-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 10px;
}
/* `minmax(0, 1fr)` above and `min-width: 0` here for the same reason: a path
   is one unbroken token, and a track sized to its content would push the
   fourth card past the edge of the panel. */
.source-card {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 5px;
  background: var(--panel-2);
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  padding: 9px 11px 11px;
}
/* The card's own name, not a section heading - so it drops the uppercase
   muted treatment `.panel h4` gives the folding headings in the Import panel. */
.source-card h4 {
  margin: 0;
  font-size: var(--fs-base);
  text-transform: none;
  letter-spacing: 0;
  color: var(--text);
}
.source-card .hint { margin: 0; }
/* Most of these lists run to two or three lines at a quarter of the panel's
   width, and loose inline-block pills separated by source whitespace leave a
   ragged gap wherever the line happens to break. */
.source-card .pills { display: flex; flex-wrap: wrap; gap: 4px; }

/* Four columns only while four still fit. Below that a card is narrower than
   the paths it carries, so the wall steps down rather than shrinking - and it
   steps down on the width of THIS box rather than of the window, which is the
   same correction `.rating-scorecards` took. Three media queries at 1500 /
   1120 / 720 are what one `auto-fit` says, and the three of them were also
   half the reason this stylesheet had eight breakpoints in near-duplicate
   pairs: 1120 beside 1100, 720 beside 700. */

.kv-table { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
.kv-table th, .kv-table td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--line-soft); }
.kv-table th { color: var(--muted); font-weight: 500; width: 200px; }
/* A stored export a newer one from the same broker replaced. Dimmed rather than
   hidden, and never painted --negative: it is not an error and nothing went
   wrong, it is simply not part of the portfolio. The filename stays at full
   contrast because it is the one thing the reader needs in order to act on it. */
.kv-table tr.superseded td { opacity: .55; }
.kv-table tr.superseded td.path { opacity: 1; color: var(--muted); }
.path { font-family: var(--mono); font-size: var(--fs-sm); word-break: break-all; }

/* --- Costing --------------------------------------------------------- */
/* The requested summary is still the object of the tab. Unlike the ordinary
   two-column key/value tables, its columns are peers and the figures line up
   on the right for a quick cost comparison.

   It is now one line per call type rather than three. Eleven types each
   spending a label, a sentence of counts and a caveat on their own lines was a
   tab that had to be scrolled to reach its own total, so the sentence became
   micro-chips and the five coverage cards became a pill strip ABOVE the table:
   they are the total's limits, and a caveat printed under the figure it
   qualifies is a caveat nobody reads.

   Every row wears the identity hue of the family the jobs catalogue already
   files it under - Models violet, Pipeline teal, Scrapers blue - carried by the
   `.rf-*` classes rather than by a second palette. Those classes set `--rf-fg`
   and `--rf-bg` and paint nothing on their own (only `.rf` does), so a row can
   borrow the pair for its mark and its meter without becoming a formatted cell,
   and `.rf-mark` arrives with the contrast mix that keeps a full-strength
   palette hue legible. Identity, not state: the outcome chips stay on the
   semantic pair, each behind its own glyph, because a failed call and a
   finished one have to separate for a red/green colour-blind reader.

   Sorting, searching and column order are tablekit's - it adopts this table on
   sight, one header row of four plain cells. What the markup has to declare is
   `data-sort` on the three figure columns: a count carries its chips, a share
   carries a bar and a cost carries dollars, rupees and a caveat, so the text of
   all three sorted on the decoration. */
.costing-summary-table th { width: auto; }
.costing-summary-table th.num,
.costing-summary-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* The one gradient in the tab, and it is a hairline: the head reads as the
   edge of an instrument rather than as another table rule. On the row group so
   it runs the width of the table once, instead of restarting in every cell. */
.costing-summary-table thead {
  background-image: linear-gradient(90deg,
    color-mix(in srgb, var(--accent) 75%, transparent),
    color-mix(in srgb, var(--rf-violet) 45%, transparent) 60%,
    transparent);
  background-repeat: no-repeat;
  background-position: bottom left;
  background-size: 100% 1px;
}
.costing-summary-table thead th { border-bottom: 0; padding-bottom: 6px; }
.costing-summary-table td { padding-top: 3px; padding-bottom: 3px; }
.costing-summary-table tbody tr:hover { background: var(--row-hover); }
.costing-summary-table small,
.costing-type code {
  color: var(--muted);
  font-size: var(--fs-2xs);
  font-weight: 400;
}
.costing-type { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.costing-mark { margin-right: 0; }
.costing-type strong { display: block; font-weight: 600; }
.costing-type small { display: flex; align-items: baseline; gap: 6px; }
.costing-type code { font-family: var(--mono); }
/* The family name, in the family's own colour - the only place the hue is
   spelt out in words, so the legend is the row itself. */
.costing-group {
  font-style: normal;
  color: color-mix(in srgb, var(--rf-fg, var(--muted)) 70%, var(--text));
}
/* The outcome breakdown, as chips rather than as prose. `done` is deliberately
   the neutral one: "164 finished" is the expected case and does not need a
   colour, which leaves the two that do. */
.costing-chips { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; gap: 3px; }
.costing-calls strong { margin-right: 6px; }
.costing-chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0 5px;
  border-radius: var(--radius-xs);
  background: var(--ghost);
  color: var(--muted);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
}
.costing-chip svg { flex: none; }
.costing-chip.bad { background: var(--negative-soft); color: var(--negative); }
.costing-chip.warn { background: var(--warn-soft); color: var(--warn); }
.costing-chip.live { background: var(--accent-soft); color: var(--accent); }
/* Which type is eating the bill, answered without reading four figures down a
   column. A meter, not a chart - the percentage beside it is the value. */
.costing-share { display: inline-flex; align-items: center; justify-content: flex-end; gap: 6px; }
.costing-share-bar {
  display: block;
  width: 54px;
  height: 6px;
  flex: none;
  border-radius: var(--radius-pill);
  background: var(--ghost);
  overflow: hidden;
}
.costing-share-bar i {
  display: block;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--rf-fg, var(--accent));
  opacity: .85;
}
.costing-share b { font-weight: 600; }
/* A type with no priced call has no share, which is not the same thing as a
   fault: the cost cell beside it already says "Unpriced" in the colour that
   means something is wrong, and saying it twice in red would make an ordinary
   scrape look like a failure. */
.costing-noshare { color: var(--muted); }
.costing-cost .advice-cost-usd { font-weight: 600; }
.costing-cost .advice-cost-inr {
  display: block;
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-2xs);
}
.costing-missing { color: var(--negative); }
/* The caveat under a figure - "at least", "5 unpriced" - qualified as a class
   and then lost the cascade: `.costing-summary-table small` is a class and a
   type, so it outranked a bare `.costing-caveat` and every one of these has
   been drawing muted since it was written. Given its colour back through the
   same table, and in `--warn` rather than `--negative`: an incomplete bill is
   a warning about the figure, not a loss, and this application paints red on
   money that has gone the wrong way. */
.costing-summary-table .costing-caveat { display: block; color: var(--warn); }
.costing-summary-table tfoot td {
  border-top: 1px solid var(--line);
  border-bottom: 0;
  padding-top: 7px;
}
/* The total's own labels sit beside its figures rather than under them - two
   words on a line the table has already earned, and one row shorter. */
.costing-summary-table tfoot small { margin-left: 5px; }
/* The pill strip. One line of chips, wrapping, above the table it qualifies. */
.costing-coverage { display: flex; flex-wrap: wrap; gap: 5px; margin: 0 0 10px; }
.costing-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 0;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: var(--rf-bg, var(--panel-2));
  border: 1px solid color-mix(in srgb, var(--rf-fg, var(--line)) 35%, transparent);
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
}
.costing-pill svg { flex: none; color: color-mix(in srgb, var(--rf-fg, currentColor) 70%, var(--text)); }
.costing-pill b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.costing-pill i { font-style: normal; opacity: .75; }
.costing-footnote { margin-bottom: 0; }

/* --- Data Fetch Stats ------------------------------------------------- */
/* Data & Settings' per-stock fetch table: a row per stock held or watched, a
   column per utility, a stamp per cell. Its own table rather than `table.grid`
   for the reason the news feed has one: the grid is built for eighty numeric
   columns behind a filter row, and this is three words and fourteen dates that
   only ever want sorting. The head is two rows - a group band over the
   utilities, then the sortable labels - and sticks, because the list is a few
   hundred rows and the label is what tells one date column from the next. */
.fs-panel .toolbar { flex-wrap: wrap; row-gap: 6px; }
.fs-panel .toolbar .input-icon > input[type="text"] { min-width: 260px; }

/* The shared stock control standing in for a grid's free-text box. It is the
   same control the Jobs filter is, so what is set here is only what a grid's
   toolbar wants of it: a box wide enough for a company name rather than for a
   symbol, since what it searches is the rows - notes, domain and theme - and
   not only the list under it. */
.toolbar .stock-pick .input-icon { display: flex; }
.toolbar .stock-pick input[type="text"] { min-width: 240px; }
/* The Where menu, beside the search box. Capped the way the news feed's menus
   are: its longest option is two words, and a select left to size itself off
   the widest option would sit wider than the button next to it. */
.fs-panel .toolbar select { max-width: 160px; }
/* The Columns menu. Narrower than the grid's column picker: that one lists
   eighty columns and needs a find box, this one lists fourteen under four
   headings and is read at a glance. The fold switch sits under its heading and
   above the ticks it governs, quieter than they are, because it is a statement
   about the group rather than one more member of it. */
/* The Fetch menu: one of the fetches, run over every ticked row. Wider than
   the Columns panel next to it because its buttons carry full labels on one
   line - "Bollinger / RSI", "LLM sector signal" - where that one carries ticks
   against them. */
.fs-bulk-panel { width: 290px; }
.fs-bulk-panel h4:first-of-type { margin-top: 4px; }
.fs-bulk-panel .picker-actions { margin-bottom: 0; }
.fs-bulk-panel .picker-actions + .picker-actions {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid var(--line-soft);
}
/* Past `MAX_BULK`. A refusal rather than a warning, so it is painted as one -
   and it is the only text in the panel that has to be read before the reader
   goes looking for the buttons that are no longer there. */
.fs-bulk-over { color: var(--negative); }
.fs-columns-panel { width: 250px; }
.fs-columns-panel h4:first-of-type { margin-top: 4px; }
.fs-columns-panel .picker-actions { margin-bottom: 0; }
.fs-fold-switch { font-size: var(--fs-xs); color: var(--muted); }
.fs-count-line { margin: 0 0 6px; }
.fs-scroll { max-height: calc(100vh - 300px); overflow: auto; }
.fs-table { border-collapse: separate; border-spacing: 0; font-size: var(--fs-sm); min-width: 100%; }
.fs-table th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 5px 8px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
}
.fs-table th:hover { color: var(--accent); }
.fs-table th .sort { color: var(--accent); font-size: var(--fs-2xs); margin-left: 3px; }
/* The group band. It sits above the label row when both stick - 22px is the
   label row's height - and it is now the fold control, so unlike before it
   points and highlights like the sortable labels under it. */
.fs-table .fs-groups th {
  top: 0;
  text-align: center;
  font-size: var(--fs-2xs);
  letter-spacing: .05em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-soft);
  border-left: 1px solid var(--line-soft);
}
/* The tick column. Narrow, centred and - unlike every other header cell here -
   not a control for sorting: it holds one instead. `cursor: default` and the
   hover colour put back, because the shared `th` rule above says pointer and
   accent for a click that this cell does not answer. */
.fs-table th.fs-c-pick,
.fs-table td.fs-c-pick { width: 26px; padding-left: 6px; padding-right: 4px; text-align: center; }
.fs-table th.fs-c-pick { cursor: default; }
.fs-table th.fs-c-pick:hover { color: var(--muted); }
.fs-c-pick input[type="checkbox"] { margin: 0; vertical-align: middle; cursor: pointer; }
/* The one band cell that is not a control: the blank over the fixed four. */
.fs-table .fs-groups th:first-child { border-left: 0; cursor: default; }
.fs-table .fs-groups th:first-child:hover { color: var(--muted); }
/* A folded group keeps the accent so the band says which of the four are
   standing open without the reader counting columns under each. */
.fs-table .fs-groups th.folded { color: var(--accent); }
.fs-fold { margin-left: 4px; font-size: var(--fs-2xs); opacity: .65; vertical-align: 1px; }
.fs-table thead tr:last-child th { top: 22px; }
.fs-table td {
  padding: 3px 8px;
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
  vertical-align: middle;
  line-height: 1.4;
}
/* Scanning a row across fifteen columns is what this table is for, and a rule
   under every row is a worse guide for the eye than a tint under every second
   one. Before the hover rule on purpose: the two have the same specificity, so
   source order is the whole of what decides which paints a hovered stripe. */
.fs-table tbody tr:nth-child(even) td { background: var(--row-stripe); }
/* A ticked row. The same two tints, in the same accent, that `table.grid` uses
   for its own selection - this is the second table in the application with
   tick-boxes down the left and they should not disagree about what ticked looks
   like. Both out-score the stripe and the hover above rather than relying on
   coming after them, and a ticked row stays tinted while it is hovered: the
   pointer moving over a list must not be able to hide what is in it. */
.fs-table tbody tr.picked td,
.fs-table tbody tr.picked:nth-child(even) td {
  background: color-mix(in srgb, var(--panel) 82%, var(--accent));
}
.fs-table tbody tr.picked:hover td { background: color-mix(in srgb, var(--panel) 74%, var(--accent)); }
.fs-table tbody tr:hover td { background: var(--row-hover); }
/* A held stock is the row you were looking for - the same rule down the left
   edge the news feed uses, so it survives the hover and the stripe. */
.fs-table tbody tr.held td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
.fs-c-symbol { font-weight: 600; }
.fs-c-name { color: var(--muted); }
/* The clip is on the span, not the cell: `text-overflow` on a `td` in an
   auto-layout table is a hint the browser is free to ignore, because the cell
   is sized by its content in the first place. A block inside it is not, so
   "Adani Ports and Special Economic Zone Ltd" ends in an ellipsis at a fixed
   width instead of setting the width of the column. The name in full is on the
   cell's tooltip. */
.fs-name {
  display: block;
  max-width: 190px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Tighter than the badge elsewhere in the app: this one is a two-letter label
   repeated down every row of a fifteen-column table, not a callout, and at the
   default padding three sleeves are wider than the dates beside them. */
.fs-c-where .badge { padding: 0 5px; font-size: var(--fs-2xs); letter-spacing: .02em; }
.fs-c-where .badge + .badge { margin-left: 3px; }
/* Centred, both the heading and the cell: the stamps are short tokens of
   uneven width - `Today`, `3d`, `24/07` - so a ragged left edge under a
   left-set heading reads as a column that has come loose from it. */
th.fs-c-stamp, td.fs-c-stamp { text-align: center; }
.fs-c-stamp { font-variant-numeric: tabular-nums; }
/* Fainter than muted. Whole columns of this table are mostly dashes, and at
   full muted they read as content rather than as the absence of it. Mixed
   towards transparent rather than dimmed with `opacity`, which composites the
   whole cell: the dash inside a fetchable one is a button, and a button its
   own rule cannot lift back out of the background is a poor thing to aim at. */
td.fs-c-stamp.blank { color: color-mix(in srgb, var(--muted) 40%, transparent); }
/* A folded group's cell stands for four or five columns, so it carries a
   little more weight than the single stamps either side of it. */
td.fs-c-group { font-weight: 600; }
/* Age is the thing to read down a column, so it is carried in the colour: a
   month-old stamp in the caution amber, a quarter-old one in the loss red.
   Colour alone, no fill - fourteen tinted columns would be a heat map nobody
   asked for, and the dates still have to be read. */
td.fs-c-stamp.old { color: var(--warn); }
td.fs-c-stamp.stale { color: var(--negative); }
.fs-count {
  display: inline-block;
  margin-left: 4px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--panel-2);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-2xs);
  line-height: 13px;
}
/* On a folded group's cell: how many of its utilities have never been round to
   this stock at all. Bordered rather than filled, like `.pill.bad` and unlike
   the stop-loss badge - it is a gap worth noticing, not an alarm. */
.fs-count.miss {
  color: var(--negative);
  border-color: color-mix(in srgb, var(--negative) 45%, transparent);
}
/* A stamp in a column that has a fetch behind it, which is seven of the
   fourteen. Quiet by default and quiet on purpose: an underline drawn in full
   under several hundred cells would turn the table into a mesh, and the point
   of the table is still the dates. A dotted rule at a third of the text's own
   colour is enough to answer "is this one pressable" for an eye looking for it
   and invisible to one reading down a column; it commits on hover and on
   focus. `color: inherit` on purpose too - the age tints are the thing to read
   here, and a column of accent-blue links would paint over them. */
.fs-fetch {
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline dotted;
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 3px;
}
.fs-fetch:hover,
.fs-fetch:focus-visible {
  color: var(--accent);
  text-decoration-color: var(--accent);
  text-decoration-style: solid;
}
/* A cell nothing has ever been fetched for is drawn faint - and is the likeliest
   cell in the table to want pressing, so the dash inside keeps enough of the
   muted colour to be aimed at. */
td.fs-c-stamp.blank .fs-fetch { color: color-mix(in srgb, var(--muted) 75%, transparent); }
/* While the job it queued is going. No underline and `cursor: progress`: there
   is nothing to press until it lands, and the button is disabled anyway. */
.fs-fetch.busy {
  cursor: progress;
  color: var(--muted);
  text-decoration: none;
}
.fs-fetch.busy .spinner { margin-right: 4px; }
.fs-fetch-state { font-size: var(--fs-2xs); }
/* The last press failed or was cancelled. The date stays, because it is still
   the honest answer to "when was this last fetched"; the mark beside it says
   the attempt to replace it did not land, and the reason is on the tooltip. */
.fs-fetch.bad { text-decoration-color: var(--negative); }
.fs-fetch-bad { margin-left: 3px; color: var(--negative); font-weight: 700; }
/* The two words in the hint that name the tints, in the tints. */
.fs-key.old { color: var(--warn); font-weight: 600; }
.fs-key.stale { color: var(--negative); font-weight: 600; }
.pill { display: inline-block; padding: 1px 7px; border-radius: var(--radius-lg); font-size: var(--fs-xs); border: 1px solid var(--line); }
.pill.ok { color: var(--positive); border-color: color-mix(in srgb, var(--positive) 50%, transparent); }
.pill.bad { color: var(--negative); border-color: color-mix(in srgb, var(--negative) 50%, transparent); }

/* --- DB Stats & Clean Up --------------------------------------------------
   Each growth card wears its table's identity hue - set as `--dbs-hue` on the
   card - on the glyph and a thin top edge, the same "identity, not state"
   bargain the chart series strike. Everything else reuses the source-card
   vocabulary the tab already speaks. */
.dbs-card { border-top: 2px solid color-mix(in srgb, var(--dbs-hue, var(--accent)) 55%, transparent); }
.dbs-card .dbs-mark { color: var(--dbs-hue, var(--accent)); margin-right: 5px; }
.dbs-card h4 { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.dbs-table {
  font-size: var(--fs-2xs);
  color: var(--muted);
  background: var(--panel-2);
  padding: 0 4px;
  border-radius: var(--radius-xs);
}
.dbs-pace { display: flex; align-items: center; gap: 5px; margin: 0; font-variant-numeric: tabular-nums; }
.dbs-pace svg { color: var(--muted); flex: none; }
/* One sweep per row: name, contract, controls, outcome - aligned as columns on
   a wide screen, stacked when the width goes. */
.dbs-actions { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
.dbs-action {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  padding: 6px 8px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius);
  background: var(--panel-2);
}
.dbs-action-name { display: inline-flex; align-items: center; gap: 5px; min-width: 200px; }
.dbs-action-name svg { color: var(--warn); }
.dbs-action-what { flex: 1 1 320px; margin: 0; }
.dbs-action-do { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.dbs-action-do label { display: inline-flex; align-items: center; gap: 5px; }
.dbs-action-do input[type="number"] {
  width: 58px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 2px 5px;
  font-size: var(--fs-sm);
  color: var(--text);
  text-align: right;
}
/* The outcome line: the whole point of the press, so it holds its colour. */
.dbs-note { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-xs); }
.dbs-note.ok { color: var(--positive); }
.dbs-note.bad { color: var(--negative); }
.dbs-h { display: flex; align-items: center; gap: 6px; margin: 14px 0 8px; }
.dbs-h svg { color: var(--muted); }
.dbs-table-list td.num, .dbs-table-list th.num { text-align: right; font-variant-numeric: tabular-nums; }
/* The share bar: a quiet meter, not a chart - enough to find the fat tables
   down the column without reading every percentage. */
.dbs-share {
  display: inline-block;
  width: 90px;
  height: 7px;
  vertical-align: middle;
  margin-right: 6px;
  border-radius: var(--radius-pill);
  background: var(--ghost);
  overflow: hidden;
}
.dbs-share i { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--accent); opacity: .75; }

/* --- Errors ---------------------------------------------------------------
   A triage list, not a table. Each row is one line until it is clicked, and it
   wears its status' identity hue - `--elog-hue`, set per row - on the left
   edge and the status chip, so a wall of amber 503s and a single red 500 are
   told apart before a word of any of them is read. */
.elog-head-row { flex-wrap: wrap; gap: 6px 8px; }
.elog-search { display: inline-flex; align-items: center; gap: 5px; }
.elog-search svg { color: var(--muted); flex: none; }
.elog-search input {
  width: 210px;
  max-width: 40vw;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 3px 7px;
  font-size: var(--fs-sm);
  color: var(--text);
}
.elog-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
.elog-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid color-mix(in srgb, var(--elog-hue, var(--line)) 45%, transparent);
  border-radius: var(--radius-pill);
  background: var(--panel-2);
  color: var(--text);
  font-size: var(--fs-xs);
  cursor: pointer;
}
.elog-chip svg { color: var(--elog-hue, var(--muted)); flex: none; }
.elog-chip .count { color: var(--muted); font-variant-numeric: tabular-nums; }
.elog-chip:hover { border-color: var(--elog-hue, var(--line)); }
/* Pressed, not merely hovered: this chip is a filter and the list below it is
   narrowed for as long as it stays on. */
.elog-chip.on {
  background: color-mix(in srgb, var(--elog-hue) var(--rf-tint, 22%), var(--panel));
  border-color: var(--elog-hue);
}
.elog-chip.on .count { color: var(--text); }
.elog-list { display: flex; flex-direction: column; gap: 4px; }
.elog-row {
  border: 1px solid var(--line-soft);
  border-left: 3px solid color-mix(in srgb, var(--elog-hue, var(--line)) 70%, transparent);
  border-radius: var(--radius);
  background: var(--panel-2);
  overflow: hidden;
}
.elog-row.open { border-color: var(--elog-hue, var(--line)); background: var(--panel); }
/* A button, so the row is reachable by keyboard and reads as openable - but it
   has to look like a row, which is the whole of what is reset here. */
.elog-head {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 9px;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.elog-head:hover { background: var(--ghost); }
.elog-status {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: none;
  min-width: 52px;
  color: var(--elog-hue, var(--text));
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.elog-route {
  flex: none;
  max-width: 42%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-sm);
}
.elog-route code {
  font-size: var(--fs-2xs);
  color: var(--muted);
  background: var(--ghost);
  padding: 0 4px;
  border-radius: var(--radius-xs);
}
.elog-kind { color: var(--muted); flex: none; }
.elog-repeat { flex: none; font-variant-numeric: tabular-nums; }
/* The sentence gives up its width first: on a narrow screen the status, the
   route and the age are what still has to be legible. */
.elog-msg {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.elog-when { flex: none; color: var(--muted); font-size: var(--fs-xs); }
.elog-body { padding: 2px 10px 9px 12px; border-top: 1px solid var(--line-soft); }
/* The cause: what the dependency actually answered. Full contrast, because it
   is the line the whole row was opened for. */
.elog-cause {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 8px 0 6px;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  color: var(--text);
}
.elog-cause svg { color: var(--elog-hue, var(--negative)); flex: none; align-self: center; }
.elog-facts { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin: 0 0 8px; }
.elog-facts svg { color: var(--muted); flex: none; }
.elog-trace {
  margin: 0;
  max-height: 340px;
  overflow: auto;
  padding: 8px 10px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  background: var(--ghost);
  font-family: var(--mono);
  font-size: var(--fs-2xs);
  line-height: 1.5;
  color: var(--text);
  white-space: pre;
}
.elog-empty { display: flex; align-items: center; gap: 6px; padding: 18px 4px; }
.elog-empty svg { color: var(--positive); flex: none; }
/* Inline editors in a provenance table - the unrecognised-ticker mapper. Quiet
   by default: every row carries one, so the grid's accent-bordered .cell-input
   (which marks the single cell being edited) would shout. */
.alias-input {
  /* 100% and no minimum: the column is fixed-width and clips, so a floor here
     would only hide the right-hand end of what was typed. */
  width: 100%;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 1px 5px;
  font-size: var(--fs-sm);
}
.alias-input.mono { font-family: var(--mono); }
.alias-input::placeholder { color: var(--muted); opacity: .6; }
.settings-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; }
.settings-row > label { min-width: 190px; color: var(--muted); font-size: var(--fs-base); }
.hint { color: var(--muted); font-size: var(--fs-sm); margin: 4px 0 0; line-height: 1.5; }
/* Yahoo's history for some indices has holes in it, and two closes a month
   apart draw as a straight climb. The caption states the bar count; this says
   what the count means, and it is toned as a warning rather than as another
   caption because the shape above it is actively misleading and the caption is
   the thing that gets skipped. */
.sparse-note {
  margin: 4px 0 0;
  padding: 3px 7px;
  border-radius: var(--radius-sm);
  background: var(--negative-soft);
  color: var(--pnl-neg-fg);
}

/* --- the column editor ----------------------------------------------- */
/* One row per column, five controls on each, a hundred-odd rows deep - so
   everything here is about keeping a row to a single line. The key column takes
   what it needs and the rest are sized to their control, because a `Decimals`
   box wide enough for four digits is wide enough. */
.col-table th, .col-table td { padding: 2px 6px; white-space: nowrap; }
.col-table thead th { width: auto; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; }
.col-table tbody th { width: auto; font-weight: 500; }
.col-table code { font-family: var(--mono); font-size: var(--fs-sm); }
.col-table input[type="text"] { width: 150px; }
.col-table input[type="number"] { width: 62px; }
.col-table select { max-width: 130px; }
/* The one control here that is a paragraph rather than a value. Two lines by
   default so a header's sentence is readable without opening anything, wide
   enough to be worth reading, and draggable taller for the long ones - the
   alternative was a single-line box showing six words of a tip that runs to
   three sentences. It sits at the far right of the row, where the extra width
   costs the columns before it nothing. */
.col-table textarea {
  width: 300px;
  min-height: 34px;
  line-height: 1.35;
  font-size: var(--fs-sm);
  resize: vertical;
  vertical-align: middle;
}
.col-table .field { gap: 4px; font-size: var(--fs-sm); }
/* --- the icon picker -------------------------------------------------- */
/* A `<select>` renders option *text* and nothing else, so a menu of fifty marks
   was fifty names to guess at. This is the same control with the drawings in
   it: a face that shows what is chosen, and one popup - see `bindIconPickers` -
   parked on `<body>` and moved to whichever face was pressed.

   The face keeps its 16px mark slot whether or not it holds anything, so
   picking None does not shuffle every picker in the column sideways. It stays
   `inline-flex` inside a cell that stays a table cell, because a `display: flex`
   td drops out of the table's column sizing and takes the alignment of the
   whole editor with it. */
.icon-pick { display: inline-block; vertical-align: middle; }
.icon-pick-face {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-width: 116px;
  padding: 2px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
  line-height: 18px;
  text-align: left;
  cursor: pointer;
}
.icon-pick-face:hover { border-color: var(--accent); }
.icon-pick-face:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The caret, drawn rather than typed: a `▾` in the markup would be read out by
   a screen reader as content of the button. */
.icon-pick-face::after {
  content: "";
  margin-left: auto;
  border: 3.5px solid transparent;
  border-top-color: var(--muted);
  transform: translateY(2px);
}
.icon-pick-mark {
  display: inline-flex;
  justify-content: center;
  flex: none;
  width: 16px;
  color: var(--muted);
}
.icon-pick-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.col-table td.col-icon .icon-pick { min-width: 130px; }

/* The popup. `fixed` because it has to escape `.table-scroll`, which clips, and
   placed from the pressed face's own rectangle by `bindIconPickers` - which is
   why it sits on `<body>` rather than inside the row it belongs to. */
.icon-pick-menu {
  position: fixed;
  z-index: 250;
  display: flex;
  flex-direction: column;
  width: 240px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--panel);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.icon-pick-menu[hidden] { display: none; }
.icon-pick-find {
  flex: none;
  width: 100%;
  margin-bottom: 6px;
  padding: 3px 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
}
.icon-pick-grid { overflow-y: auto; min-height: 0; }
.icon-pick-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 4px 7px;
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: var(--fs-base);
  text-align: left;
  cursor: pointer;
}
.icon-pick-opt[hidden] { display: none; }
.icon-pick-opt:hover, .icon-pick-opt:focus-visible { background: var(--row-hover); outline: none; }
.icon-pick-opt .icon-pick-mark { width: 18px; color: var(--text); }
/* What is chosen now, so opening the menu answers "which one is this" without
   the reader having to match the face against fifty drawings. */
.icon-pick-opt[aria-selected="true"] {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  font-weight: 600;
}
.icon-pick-opt[aria-selected="true"] .icon-pick-mark { color: var(--accent); }
/* A group heading inside the table rather than above it: the groups are the
   picker's own groups, and pulling them out into separate tables would lose the
   column alignment that makes the whole thing scannable.

   The whole row folds its group away, so it is a control and says so - the
   pointer and the hover lift are the only thing distinguishing it from the
   caption it used to be. `user-select` off because a fold is a click and a
   click that selects the heading text reads as a misfire. */
.col-table tr.col-group th {
  padding-top: 10px;
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  border-bottom-color: var(--line);
  cursor: pointer;
  user-select: none;
}
.col-table tr.col-group th .section-toggle {
  margin-right: 6px;
  vertical-align: middle;
  text-transform: none;
  letter-spacing: 0;
}
.col-table tr.col-group th .count { margin-left: 6px; }
.col-table tr.col-group:hover th { color: var(--text); }
.col-table tr.col-group:hover .section-toggle { color: var(--text); border-color: var(--accent); }
/* A column the user has changed. The accent edge is the only thing on the page
   that says which of a hundred rows are no longer the defaults, so it has to
   survive a filtered list scrolled to the middle. */
.col-table tr.changed > th:first-child { box-shadow: inset 3px 0 0 var(--accent); }
.col-table tr.changed [data-col-changed] {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
}
.col-table tr[hidden], .col-table tbody[hidden] { display: none; }

/* --- the rules sub-editor -------------------------------------------- */
/* One column's conditional formatting, in a row that opens under the column it
   belongs to. A second `<tr>` rather than a modal because the ladder is edited
   against the row that owns it - the format above decides whether a rule
   matches a value or a range, and the two have to be readable together.

   Inset and tinted so a panel sitting between two column rows reads as
   belonging to the one above it rather than as a row of its own. */
.col-table tr.rule-panel > td {
  padding: 10px 14px 12px 24px;
  white-space: normal;
  background: var(--panel-2);
  box-shadow: inset 3px 0 0 var(--accent);
}
.col-table tr.rule-panel .hint { margin: 0 0 8px; max-width: 90ch; }
/* The columns of the ladder. A grid rather than a nested table so the heading
   strip and every rule line stay locked to the same tracks - the swatch has to
   sit under the word Preview whatever is typed into the boxes on its left. */
.rule-head, .rule-row {
  display: grid;
  grid-template-columns: minmax(180px, 1.4fr) minmax(120px, 1fr) minmax(130px, 1fr)
    minmax(110px, .9fr) minmax(90px, .8fr) auto;
  gap: 6px;
  align-items: center;
}
.rule-head {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  padding-bottom: 3px;
}
.rule-row { padding: 3px 0; }
.rule-row + .rule-row { border-top: 1px solid var(--line-soft); }
.rule-when { display: flex; align-items: center; gap: 5px; }
.rule-to { color: var(--muted); font-size: var(--fs-sm); }
.col-table .rule-row .rule-num { width: 74px; }
.col-table .rule-row .rule-val { width: 100%; min-width: 0; }
.col-table .rule-row select { max-width: none; width: 100%; }
.col-table .rule-row input[type="color"] {
  width: 28px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: none;
  cursor: pointer;
}
.col-table .rule-row input[type="color"][hidden] { display: none; }
/* The swatch is the grid's own cell, at the grid's own size - see `paintRules`.
   Padded and cornered here because a `td` gets both from the table and this one
   is a `span` floating in a form. */
.rule-preview > span {
  display: inline-flex;
  align-items: center;
  min-width: 44px;
  padding: 1px 7px;
  border-radius: var(--radius);
  font-size: var(--fs-sm);
}
.rule-move { display: flex; gap: 3px; justify-content: flex-end; }
.rule-move .btn { padding: 1px 6px; line-height: 16px; }

/* --- upload ---------------------------------------------------------- */
/* The one control on this page that writes into a profile folder, and on a
   fresh install the only one that does anything at all - every table below it
   says "nothing here yet" until it has been used. Boxed and accent-edged so it
   reads as an action rather than as one more block of provenance prose. */
.upload {
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin: 10px 0 14px;
  background: var(--panel-2);
}
.upload h4 { margin: 0 0 2px; }
.upload .settings-row { margin: 10px 0 0; }

/* The broker-API block, which sits directly above Upload and is the same kind
   of thing: a way in for holdings. Borrowing the box wholesale rather than
   restyling it is the point - two ways to do one job should not look like two
   different features. */
.connect {
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin: 10px 0 14px;
  background: var(--panel-2);
}
.connect h4 { margin: 0 0 2px; }
.connect .settings-row { margin: 10px 0 0; align-items: baseline; }
/* The state sentence is the longest thing on the row and the only one that
   wraps; without this it stretches the row instead and pushes the buttons out
   of line with the broker above. */
.connect .settings-row .hint { flex: 1 1 320px; min-width: 220px; }
/* The credentials row sits under its broker rather than beside it - two text
   boxes and a button do not fit on the same line as the sleeve picker at any
   width worth designing for. Indented so it reads as belonging to the broker
   above it, and quieter, because it is set up once and then never again. */
.connect .connect-keys {
  margin: 4px 0 12px 18px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.connect .connect-keys:last-child { border-bottom: none; padding-bottom: 0; }
.connect .connect-keys input { width: 210px; font-size: var(--fs-base); }
/* The trail a running import leaves, under the broker it belongs to. Indented
   to the same 18px as the credentials row so it reads as that broker's, and
   boxed rather than left as loose lines because it is the one thing on this
   page that changes while somebody is looking at it - it has to be findable
   again after a glance away. */
.import-progress {
  margin: 6px 0 12px 18px;
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: var(--radius-sm);
  background: var(--panel);
}
.import-progress.running, .import-progress.started { border-left-color: var(--accent); }
.import-progress.done { border-left-color: var(--positive); }
.import-progress.failed { border-left-color: var(--negative); }
.import-progress-head { margin: 0; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
.import-progress .hint { margin: 6px 0 0; }
.import-progress .hint.bad { color: var(--negative); }
.import-steps { list-style: none; margin: 6px 0 0; padding: 0; font-size: var(--fs-base); }
.import-steps li { display: flex; gap: 7px; align-items: baseline; padding: 1px 0; }
/* Fixed width so the labels line up whatever mark is against them, and the
   marks themselves centred in it - a tick and a middot are nothing like the
   same width, and the column would otherwise shift on every step. */
.import-steps .mark { flex: 0 0 12px; text-align: center; color: var(--muted); }
/* Only the step being worked on is at full strength. The ones already done are
   history and the ones to come have not happened, so neither should compete
   with the line that is actually moving. */
.import-steps li.pending { color: var(--muted); }
.import-steps li.done { color: var(--muted); }
.import-steps li.done .mark { color: var(--positive); }
.import-steps li.running { color: var(--text); font-weight: 600; }
.import-steps li.running .mark { color: var(--accent); }
.import-steps li.failed { color: var(--negative); }
.import-steps li.failed .mark { color: var(--negative); }
/* The native file input is as wide as its longest filename otherwise, which
   pushes the Upload button off the row the moment anything real is picked. */
.upload input[type="file"] { font-size: var(--fs-sm); max-width: 340px; }

/* --- version history ------------------------------------------------- */
/* The diff heading sits inside a folded section, under an h4, so it is an h5 -
   which nothing else on the site uses and which therefore has no styling of
   its own to inherit. */
[data-diff-out] h5 {
  margin: 14px 0 6px;
  font-size: var(--fs-base);
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
/* A left edge per kind, so added / removed / changed are separable at a glance
   down a long list without reading the first column of every row. Colour is
   the reinforcement here, not the signal: the row also says which it is in
   words, because these three are exactly the distinction a red/green-blind
   reader would otherwise lose. */
[data-diff-kind] > td:first-child { border-left: 2px solid transparent; }
[data-diff-kind="added"] > td:first-child { border-left-color: var(--positive); }
[data-diff-kind="removed"] > td:first-child { border-left-color: var(--negative); }
[data-diff-kind="changed"] > td:first-child { border-left-color: var(--accent); }

/* --- toast ---------------------------------------------------------- */
.toast {
  position: fixed;
  bottom: 18px;
  right: 18px;
  z-index: 300;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 9px 14px;
  font-size: var(--fs-base);
  box-shadow: var(--shadow-lg);
}
.toast.err { border-left-color: var(--negative); }

/* --- database banner -------------------------------------------------
   Pinned to the top and above everything, including the sign-in gate: it can
   be raised before the shell has ever rendered. Not dismissable - the fault
   it reports is one where nothing can be saved, so it goes away when the
   database comes back and not before. */
.db-banner {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 400;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  padding: calc(9px + env(safe-area-inset-top, 0px)) 16px 9px;
  padding-left: calc(16px + env(safe-area-inset-left, 0px));
  padding-right: calc(16px + env(safe-area-inset-right, 0px));
  background: color-mix(in srgb, var(--negative) 16%, var(--panel));
  border-bottom: 1px solid var(--negative);
  color: var(--text);
  font-size: var(--fs-base);
  line-height: 1.5;
  box-shadow: var(--shadow-lg);
}
.db-banner strong { color: var(--negative); }
.db-banner .db-detail { color: var(--muted); }
/* What the database itself said, as opposed to what the reader has lost. Held
   at full contrast and monospaced: it is the one line on this banner somebody
   might have to read character by character or paste into a search. */
.db-banner .db-cause {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--fs-sm);
  color: var(--text);
}
.db-banner .db-cause svg { color: var(--negative); flex: none; align-self: center; }
.db-banner .btn { margin-left: auto; }

.empty {
  padding: 40px 20px;
  text-align: center;
  color: var(--muted);
  line-height: 1.7;
}
.empty code { background: var(--panel-2); padding: 1px 5px; border-radius: var(--radius-xs); font-family: var(--mono); }

/* --- the news feed ---------------------------------------------------- */
/* The AI page's News tab: a filing per row, with a toolbar, a strip of class
   chips and a sortable head over it.

   Its own table rather than `table.grid`, which every other list in this app
   uses. The grid is built for eighty numeric columns - `width: max-content`, a
   sticky first column, a filter row parked under the head - and this is six
   columns, one of which is a sentence that has to WRAP. Bending the grid into
   that would have cost more rules than declaring the six. */
.news-panel .toolbar { flex-wrap: wrap; row-gap: 6px; }
.news-panel .toolbar .input-icon > input[type="text"] { min-width: 240px; }
.news-panel .toolbar select { max-width: 160px; }

/* The two passes that fill the feed, above it. A panel of its own rather than
   two more buttons in the feed's toolbar: everything in that toolbar changes
   what is DRAWN and costs nothing, and a button that spends money sitting
   between "Clear filters" and "Refresh" would be a trap. */
.news-jobs { display: flex; flex-wrap: wrap; gap: 18px; margin: 10px 0 0; }
.news-job { display: flex; flex-direction: column; gap: 4px; min-width: 260px; }
.news-job-note { margin: 0; min-height: 1.2em; }
/* The button's own icon, from the same vocabulary the Fetch menu draws these
   types with - so a pass has one face wherever it is offered. `align-self:
   start` because the column stretches its children and a button stretched to
   the width of a two-line note reads as a banner. */
.news-job > .btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
}

/* When it last ran, and when it is next due. Its own line under the note rather
   than more words inside it: the note answers "what would this press do" and
   this answers "when was it last done", and a reader scanning a row of buttons
   for the overdue one should find it without reading either.

   Two spans on one line, wrapping to two on a narrow column. `--fs-xs` and the
   muted colour keep it beneath the note in the reading order it deserves - it
   is a stamp, not the offer. */
.job-when {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  margin: 0;
  font-size: var(--fs-xs);
  letter-spacing: .01em;
}
.job-when-last { color: var(--muted); }
/* Not due is deliberately as quiet as the last-run stamp beside it: a date in
   the future is reassurance, and reassurance does not need a colour. Due is
   the one state worth an eye, in the amber every other "this needs attention"
   in the app uses - not red, because an overdue pass is a thing to press, not
   a failure. */
.job-when-next { color: var(--muted); }
.job-when-next.due {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--warn);
  font-weight: 600;
}
/* The paid one, marked in the accent every other spending control in this app
   carries. Not red: it is a normal thing to press, it just is not free. */
.news-job-paid {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent);
}
.news-job-paid:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.news-cost { font-weight: 600; white-space: nowrap; border-bottom: 1px dotted var(--muted); }
.news-backlog { margin: 10px 0 0; }

.news-chips { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0 0; }
.news-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--line);
  border-radius: 11px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.news-chip:hover { border-color: var(--accent); color: var(--text); }
.news-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.news-chip .n { font-variant-numeric: tabular-nums; opacity: .7; font-size: var(--fs-xs); }
/* A class the feed currently holds none of. Dimmed rather than hidden: which
   classes exist is a fact about the classifier, and a chip that vanished when
   its count hit zero would make "no governance flags this month" look like a
   feature that had never been built. */
.news-chip.empty { opacity: .45; }
.news-chip.on {
  color: var(--text);
  border-color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}
/* The tone is carried on the DOT, not on the chip's own text: six chips in six
   colours is a rainbow, and the one that matters - a red flag - stops standing
   out the moment everything is coloured. */
.news-chip::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  align-self: center;
}
.news-chip.tone-bad::before { background: var(--negative); }
.news-chip.tone-good::before { background: var(--positive); }
.news-chip.tone-plain::before { background: var(--muted); }

.news-count { margin: 8px 0 6px; }

.news-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.news-table th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 5px 8px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
}
.news-table th:hover { color: var(--accent); }
.news-table th .sort { color: var(--accent); font-size: var(--fs-2xs); margin-left: 3px; }
.news-table td {
  padding: 5px 8px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.news-table tbody tr:hover { background: var(--row-hover); }
/* A stock actually held is the row you were looking for. A rule down the left
   edge rather than a background, so it survives the hover and the stripe. */
.news-table tbody tr.held td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

.news-c-date { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--muted); }
.news-c-stock { white-space: nowrap; }
.news-c-class, .news-c-sev { white-space: nowrap; }
.news-c-conf { white-space: nowrap; color: var(--muted); font-size: var(--fs-xs); }
/* A LOW-confidence row is one the rating model ignores outright. It is shown
   anyway - see the tooltip on the Confidence menu - so the cell says quietly
   that the classifier was guessing. */
.news-c-conf.weak { font-style: italic; opacity: .7; }
.news-c-text { width: 100%; line-height: 1.5; }

.news-symbol {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.news-symbol:hover { text-decoration: underline; }
.news-symbol:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.news-name {
  display: block;
  max-width: 190px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--muted);
  font-size: var(--fs-xs);
}
/* The exchange's own label for the filing, under the headline it prefixed.
   Below rather than beside, because it repeats on two thirds of the rows and a
   second column of "Award of Order / Receipt of Order" would out-shout the one
   line on each row that is actually different. */
.news-filing {
  display: block;
  margin-top: 1px;
  color: var(--muted);
  font-size: var(--fs-xs);
}

.news-panel .pill.tone-bad { color: var(--negative); border-color: color-mix(in srgb, var(--negative) 50%, transparent); }
.news-panel .pill.tone-good { color: var(--positive); border-color: color-mix(in srgb, var(--positive) 50%, transparent); }
.news-panel .pill.tone-plain { color: var(--muted); }

.news-more { display: flex; align-items: baseline; gap: 8px; margin: 8px 0 0; flex-wrap: wrap; }
.news-more .hint { margin: 0; }

/* --- the credit-rating feed ------------------------------------------ */
.credit-panel .credit-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 8px 0 10px;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.credit-panel .credit-stats b { color: var(--text); font-variant-numeric: tabular-nums; }
.credit-bar { flex-wrap: wrap; row-gap: 6px; }
.credit-bar .input-icon > input[type="text"] { min-width: 250px; }
.credit-count { margin: 8px 0 6px; }
.credit-table .credit-c-date,
.credit-table .credit-c-agency,
.credit-table .credit-c-action,
.credit-table .credit-c-rating,
.credit-table .credit-c-outlook,
.credit-table .credit-c-updated { white-space: nowrap; }
.credit-table .credit-c-date,
.credit-table .credit-c-updated { color: var(--muted); font-variant-numeric: tabular-nums; }
.credit-table .credit-c-updated span {
  display: block;
  max-width: 150px;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: var(--fs-xs);
}
.credit-table .credit-c-drivers { min-width: 260px; width: 100%; line-height: 1.45; }
.credit-panel .pill.tone-bad {
  color: var(--negative);
  border-color: color-mix(in srgb, var(--negative) 50%, transparent);
}
.credit-panel .pill.tone-good {
  color: var(--positive);
  border-color: color-mix(in srgb, var(--positive) 50%, transparent);
}
.credit-panel .pill.tone-plain { color: var(--muted); }

/* --- the Add-More verdict --------------------------------------------- */
/* Weighted towards the reasoning rather than the call. The verdict word is one
   line and the factor table is the body of the panel, because a recommendation
   nobody can audit is worth less than the evidence under it - see the note at
   the top of `advice.js`. */

/* The table of positions. One line per holding, verdict or not - a stock nobody
   has asked about is a row with an empty call, never an omission. */
.advice-table { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
.advice-table th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--line);
  padding: 4px 10px 4px 0;
}
.advice-table td { padding: 7px 10px 7px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
.advice-table tbody tr:hover { background: var(--row-hover); }

/* The controls above the table - a search box, the sleeve menu and four more,
   and a Clear that is dead until there is something to clear - and the heads,
   which sort. The same bones as the news feed's, so the two tables on this page
   work the same way. Scoped on the toolbar's own class rather than on
   `.advice-panel`, which the stock popup's AI tab also wears - and that class
   is `advice-toolbar`, not `advice-bar`: `.advice-bar` further down is the
   weight bar in the details popup, 54px by 6px with its overflow hidden, and a
   toolbar that wore it was painted as that sliver, every control inside it
   clipped away. */
.toolbar.advice-toolbar { flex-wrap: wrap; row-gap: 6px; margin: 6px 0 8px; }
.toolbar.advice-toolbar .input-icon > input[type="text"] { min-width: 236px; }
.toolbar.advice-toolbar select { max-width: 190px; }
.advice-count { margin: 0 0 6px; }
.advice-table th[data-advice-sort] { cursor: pointer; user-select: none; }
.advice-table th[data-advice-sort]:hover { color: var(--accent); }
.advice-table th .sort { color: var(--accent); font-size: var(--fs-2xs); margin-left: 3px; }
.advice-sym { font-weight: 600; white-space: nowrap; }
.advice-name { color: var(--muted); }
.advice-call-cell { white-space: nowrap; }

/* The coarse call, and the thing a reader scanning forty rows actually reads.
   Larger than the precise verdict beside it on purpose. */
.advice-action {
  font-weight: 700;
  font-size: var(--fs-md);
  letter-spacing: 0.04em;
}
.advice-action.good { color: var(--positive); }
.advice-action.bad { color: var(--negative); }
.advice-action.plain { color: var(--muted); }
.advice-verdict-sub { margin-left: 6px; color: var(--muted); font-size: var(--fs-xs); }

.advice-when { white-space: nowrap; font-variant-numeric: tabular-nums; }
.advice-age { display: block; color: var(--muted); font-size: var(--fs-xs); }
/* Past the standing period and worth a look. Not a warning - nothing is wrong,
   the answer is simply old enough that refreshing needs no argument. */
.advice-age.due { color: var(--accent); }
/* Whose position this answer was reasoned against, when it was not the reader's.
   The verdicts are shared, so this row may have been paid for by somebody else -
   quiet, because it is a caveat rather than a problem, but present, because the
   position half of the advice is about a portfolio that is not theirs. */
.advice-asked-by { display: block; color: var(--muted); font-size: var(--fs-xs); font-style: italic; }

/* What a row's answer cost. Tabular figures so a column of them lines up on the
   decimal point - the whole reason the column exists is comparing one row's
   cost against another's, and a proportional font makes that a reading exercise. */
.advice-cost { white-space: nowrap; font-variant-numeric: tabular-nums; text-align: right; }
.advice-cost-usd { font-weight: 600; }
.advice-cost-inr { display: block; color: var(--muted); font-size: var(--fs-xs); }
.advice-tokens { display: block; color: var(--muted); font-size: var(--fs-xs); }
/* A call nobody could price. Quiet rather than loud - it is a gap in the
   accounting, not a problem with the verdict - but never rendered as a zero:
   "$0.00" is a claim about money and this is the absence of one. */
.advice-unpriced { color: var(--muted); cursor: help; }
.advice-unpriced-note { color: var(--muted); font-style: italic; }

/* The spend summary above the table. Parts on their own lines at narrow widths
   and inline when there is room - it is three facts, not a paragraph. */
.advice-spend { display: flex; flex-wrap: wrap; gap: 4px 16px; align-items: baseline; }
.advice-spend-part { font-variant-numeric: tabular-nums; }
/* The rate card the money was computed at, and when it was written down. Printed
   rather than assumed: the tokens are the provider's count and the price is not,
   so this is the line that says how much to trust the figures beside it. */
.advice-rate-card { color: var(--muted); font-size: var(--fs-xs); flex-basis: 100%; }
.advice-rate-card code { font-size: var(--fs-xs); }

/* The per-answer breakdown in the popup. Narrow on purpose: it sits under the
   reasoning, where it is a footnote about cost rather than a second subject. */
.advice-usage-table { border-collapse: collapse; font-size: var(--fs-base); max-width: 420px; }
.advice-usage-table th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--line);
  padding: 3px 14px 3px 0;
}
.advice-usage-table td { padding: 4px 14px 4px 0; border-bottom: 1px solid var(--line-soft); }
.advice-usage-table tfoot td { border-bottom: none; border-top: 1px solid var(--line); font-weight: 600; }
.advice-usage-label { cursor: help; }
.advice-usage-tokens,
.advice-usage-cost { text-align: right; font-variant-numeric: tabular-nums; }

/* The ring, and the only one in the application - see js/loading.js. It was
   `.advice-spinner` while one table was the only thing that waited visibly; it
   is on every panel, every grid and the top bar now, and a shared mark carrying
   one page's name in its class was a lie about where it is used. */
.spinner {
  display: inline-block;
  width: 11px;
  height: 11px;
  border: 2px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  /* Nudged onto the text baseline: an inline-block ring sits a shade high
     beside 12px type and reads as misaligned rather than as spinning. */
  vertical-align: -1px;
  flex: none;
  /* The ring is always followed immediately by the words it belongs to, so the
     gap belongs to it rather than to each host: it turns up in a `.hint`, a
     `.mini-empty`, a table cell, a button label and an upload note, and a rule
     per host is a rule the next host will not have. */
  margin-right: 6px;
}
/* Zeroed where the host is a flex box already spacing its own children - the
   margin would be added to the gap rather than replacing it. */
.loading-line .spinner,
.loading-veil-pill .spinner { margin-right: 0; }
/* The block form, for a panel with nothing else on it. Bigger because it is the
   only thing on screen rather than a mark beside a word, and unmargined because
   it is centred in a column rather than sitting beside anything. */
.spinner.lg {
  width: 22px;
  height: 22px;
  border-width: 3px;
  vertical-align: 0;
  margin-right: 0;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* A reader who has asked not to see motion gets a steady ring rather than a
   spinning one - it still marks the row, it just does not move. Same trade the
   rest of the app makes; the information is in the mark, not in the animation. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; opacity: 0.7; }
}

/* A waiting line inside a panel that already has a frame. `inline-flex` rather
   than the inline ring on its own, so a message that wraps does not put its
   second line underneath the spinner. */
.loading-line {
  display: flex;
  align-items: center;
  gap: 6px;
}
.loading-line .spinner { margin-right: 0; }

/* A panel or a page with nothing on it yet. Centred, with real height: the
   point of the block form is that there is nothing else to look at, and a
   spinner pinned to the top-left of an empty panel reads as a rendering fault. */
.loading-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 44px 16px;
  min-height: 120px;
  color: var(--muted);
  font-size: var(--fs-base);
  text-align: center;
}
.loading-block-text { margin: 0; }
.loading-block .hint { margin: 0; max-width: 46ch; }

/* Content being replaced in place. The host keeps its old content mounted
   underneath - see `setBusy` in js/loading.js for why a refresh dims rather
   than replaces - so this has to be positioned against it. */
.is-loading { position: relative; }
.loading-veil {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 56px;
  background: color-mix(in srgb, var(--bg) 45%, transparent);
  /* Above everything the grid stacks inside itself - the sticky header rows go
     to 3, a pinned cell to 4 and the tick-box header to 5 - because `.is-loading`
     is `position: relative` with no z-index of its own and therefore opens no
     stacking context to hide them behind. At 4 the header poked through the
     veil. Still under the settings shelf (30) and far under the modals. */
  z-index: 10;
  /* The rows underneath stay readable and stay unclickable: a click landing on
     a row that is about to be replaced acts on the row that replaces it. */
  cursor: progress;
}
.loading-veil-pill {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
  color: var(--muted);
  font-size: var(--fs-base);
}

/* The application-wide one: a hairline under the top bar for as long as any
   request is in flight, anywhere. The header dot says the same thing in seven
   pixels and is easy to miss; this is the piece that answers "is it doing
   something" without the reader having to know where to look.

   Not a progress bar - nothing here knows how far along a fetch is, and a bar
   that pretends to would be inventing the one number it does not have. It is a
   band sliding across a fixed track, which claims only "still working". */
.topbar-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  overflow: hidden;
  pointer-events: none;
  z-index: 20;
}
.topbar-progress[hidden] { display: none; }
.topbar-progress::after {
  content: '';
  position: absolute;
  inset: 0 auto 0 0;
  width: 35%;
  background: linear-gradient(90deg,
    transparent,
    var(--accent) 35%,
    var(--accent) 65%,
    transparent);
  animation: topbar-slide 1.1s ease-in-out infinite;
}
@keyframes topbar-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(386%); }
}
/* Standing still, the sliding band would be a stripe parked on the left of the
   bar and read as a stalled progress bar rather than as work in progress. A
   steady full-width tint is the honest still frame of it. */
@media (prefers-reduced-motion: reduce) {
  .topbar-progress::after {
    animation: none;
    width: 100%;
    background: var(--accent);
    opacity: 0.5;
  }
}

/* The details popup. Fixed rather than absolute so it is not clipped by the
   panel's own scroll, and scrollable inside - a factor table plus the prose
   around it is taller than a laptop viewport. */
/* Sized exactly as the stock popup is - see `.modal-root` and `.modal` above,
   which this now mirrors line for line rather than resembling.

   It was a content-height box on a backdrop that scrolled, and that is the
   arrangement the stock popup was moved off for a reason worth repeating here:
   the box grew and shrank with whatever was in it, so the Close button walked up
   and down the screen as the reader moved between a two-line answer and a dozen
   sections of report. Worse for this one than for that one, because the whole
   backdrop scrolled - which put the head, the stock's name and the way out off
   the top of the window the moment anybody read past the first section.

   So: a fixed 80vw x 80vh box, `dvh` after `vh` so a phone's collapsing address
   bar does not leave the bottom of it underneath, one scrolling half inside, and
   nothing on the backdrop that can scroll at all. The head stays put, the
   report's sticky rail finally has a box to be sticky *in*, and the difference
   between a short answer and a long one is handed to a scrollbar. */
.advice-modal {
  position: fixed;
  inset: 0;
  /* Dimmed and blurred rather than dimmed harder, matching `.modal-root`: the
     table behind stays legibly the thing the report was opened out of, while
     the blur stops it competing for the eye. */
  background: rgba(0, 0, 0, .45);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 60;
  /* The box sizes itself and scrolls inside itself, so nothing here scrolls and
     nothing here can spill past the backdrop. */
  overflow: hidden;
}
.advice-modal-box {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), 0 24px 64px rgba(0, 0, 0, .28);
  width: 80vw;
  height: 80vh;
  height: 80dvh;
  display: flex;
  flex-direction: column;
  /* A flex item defaults to `min-height: auto` and would refuse to shrink below
     its content, pushing a long report straight back out through the bottom. */
  min-height: 0;
  overflow: hidden;
}
.advice-modal-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex: 0 0 auto;
  border-bottom: 1px solid var(--line);
  padding: 12px 18px 10px;
}
.advice-modal-head strong { font-size: var(--fs-lg); }
.advice-modal-head .btn { margin-left: auto; }
/* The one part that scrolls, and what `.report-index` is sticky against. */
.advice-modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 12px 18px 18px;
}
/* No reading measure inside. What the body holds is a report - a rail beside
   a dozen sections and a six-column factor table - not a page of prose, and a
   980px column centred in an 80vw popup wrapped that table while a quarter of
   the popup stood empty on either side of it. So the content takes the whole
   width of the popup - the same call `.stock-advice` makes inside the stock
   popup, on every half of its AI tab. */

.advice-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin: 14px 0 4px; }
.advice-call {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.04em;
}
.advice-call.good { color: var(--positive); }
.advice-call.bad { color: var(--negative); }
.advice-call.plain { color: var(--muted); }
.advice-meaning { color: var(--muted); font-size: var(--fs-base); }
.advice-headline { margin: 2px 0 10px; font-size: var(--fs-lg); line-height: 1.5; }

/* Loud on purpose, and used for exactly two things: a verdict reasoned on data
   that has since moved, and weights that do not sum to 100. Both are reasons to
   trust the panel less, which is the one thing it must not be quiet about. */
.advice-warn {
  margin: 8px 0;
  padding: 6px 9px;
  border-left: 2px solid var(--negative);
  background: color-mix(in srgb, var(--negative) 8%, transparent);
  font-size: var(--fs-base);
  line-height: 1.5;
}

.advice-block { margin: 14px 0; }
.advice-block h4 {
  margin: 0 0 6px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.advice-block p { margin: 0; font-size: var(--fs-md); line-height: 1.6; }

.advice-points, .advice-bullets { margin: 0; padding-left: 18px; }
.advice-points li, .advice-bullets li { margin-bottom: 7px; font-size: var(--fs-md); line-height: 1.55; }
.advice-points.good li::marker { color: var(--positive); }
.advice-points.bad li::marker { color: var(--negative); }
.advice-point { display: block; }
.advice-evidence { display: block; color: var(--muted); font-size: var(--fs-base); margin-top: 2px; }
.advice-weight-why { display: block; color: var(--muted); font-size: var(--fs-sm); margin-top: 3px; font-style: italic; }

.advice-factors { width: 100%; border-collapse: collapse; font-size: var(--fs-base); }
.advice-factors th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--line);
  padding: 4px 8px 4px 0;
}
.advice-factors td { padding: 7px 8px 7px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }
.advice-factor-name { font-weight: 600; white-space: nowrap; }
.advice-factor-why { line-height: 1.5; }
.advice-score { text-align: right; font-variant-numeric: tabular-nums; }

/* The weight as a bar as well as a number: the reader's question is "what
   decided this", which is a comparison rather than a value. */
.advice-weight { white-space: nowrap; }
.advice-bar {
  display: inline-block;
  width: 54px;
  height: 6px;
  border-radius: var(--radius-xs);
  background: var(--panel-2);
  overflow: hidden;
  vertical-align: middle;
}
.advice-bar i { display: block; height: 100%; background: var(--accent); }
.advice-weight-num { margin-left: 6px; font-variant-numeric: tabular-nums; }

/* --- the scorecard ----------------------------------------------------- */
/* Twelve aspects rated 1-10 with the reasons for each, and one overall rating
   over them - the same table bones as the factors above, because they are the
   same object to a reader: a name, a bar, a word and the evidence. What differs
   is that these bars are *judgements* rather than shares of a whole, so they are
   coloured by the rating instead of all wearing the accent, and the column is
   read down for the shape of it. See `aspectTable` in `advice.js`. */
.advice-aspects .advice-factor-name { white-space: normal; min-width: 150px; }
.advice-aspects .advice-weight { width: 108px; }
.advice-bar i.good { background: var(--positive); }
.advice-bar i.bad { background: var(--negative); }
.advice-bar i.plain { background: var(--muted); }
.advice-weight-num.good { color: var(--positive); }
.advice-weight-num.bad { color: var(--negative); }

/* The overall rating in the head, beside the verdict rather than under it. Sized
   between the call and the pills: it is an answer, not a label, and a reader
   scanning the top of the panel should land on the verdict and this number
   together. */
.advice-overall {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  padding: 1px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-variant-numeric: tabular-nums;
}
.advice-overall b { font-size: var(--fs-lg); font-weight: 700; }
.advice-overall-scale { color: var(--muted); font-size: var(--fs-sm); }
.advice-overall-grade {
  margin-left: 2px;
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.advice-overall.good { color: var(--positive); border-color: color-mix(in srgb, var(--positive) 50%, transparent); }
.advice-overall.bad { color: var(--negative); border-color: color-mix(in srgb, var(--negative) 50%, transparent); }
.advice-overall.plain { color: var(--muted); }

/* The overall rating's reasons, above the twelve rows they were formed from. */
.advice-overall-why { margin: 0 0 9px; font-size: var(--fs-md); line-height: 1.6; }
.advice-overall-why .good { color: var(--positive); }
.advice-overall-why .bad { color: var(--negative); }
.advice-overall-why .plain { color: var(--muted); }

.advice-panel .pill.good { color: var(--positive); border-color: color-mix(in srgb, var(--positive) 50%, transparent); }
.advice-panel .pill.bad { color: var(--negative); border-color: color-mix(in srgb, var(--negative) 50%, transparent); }
.advice-panel .pill.plain { color: var(--muted); }

.advice-provenance { margin: 14px 0 0; color: var(--muted); font-size: var(--fs-xs); }

/* The same verdict inside the stock popup's AI tab. It reuses every class above
   - the panel carries `.advice-panel` so the pills keep their colours - and adds
   only what a tab needs that a page does not: a button sitting with the heading
   rather than under it, and headings for the halves the tab stacks. Those
   headings are `.stock-advice-sub` rather than a bare `h4` because
   `renderVerdict` draws `.advice-block h4` inside this panel, and a rule on `h4`
   alone would restyle every block in the report at the same specificity -
   decided by source order, which is not a thing to decide it.

   The whole width of the popup, on every half, which is the same call
   `.rating-panel` makes and now for the same reason. It was capped at an 880px
   reading measure while Overview was a position line and a runs table - prose,
   read at the width prose is read at - with the cap lifted only while Detailed
   was open, because a six-column factor table wrapped inside it. Overview is not
   prose any more: it opens with a hero beside a twelve-row read-out and carries
   an eight-column table under it, and at 880px out of an 80vw popup that stood
   in a narrow column with a third of the popup empty either side. Nothing in
   this tab is a page of prose now, so nothing here wants a measure. */
.stock-advice { width: 100%; }
.stock-advice-top {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 8px;
}
.stock-advice-top h3 { margin: 0; font-size: var(--fs-lg); }
.stock-advice-top .btn { margin-left: auto; }
/* The button is drawn disabled while a call is in flight and when no API key is
   configured. Nothing else in the application disables a `.btn`, so without this
   the only difference between "asking" and "press me" would be the word on it -
   and the spinner is a paragraph away from where the click lands. */
.stock-advice-top .btn[disabled] { opacity: .45; pointer-events: none; }
.stock-advice-sub {
  margin: 18px 0 6px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.stock-advice-runs { margin-bottom: 6px; }

/* --- the AI tab's Overview: the catalogue of what can be asked ---------- */
/* Six rows, one per question this build can put to a model about one stock,
   and every one of them is a control: pressing anywhere on a row opens that
   question's own half. So the row has to look pressable without becoming a
   sixth kind of button - the popup already has tabs, sub-tab pills, icon
   buttons, the fetch menu and the field controls, and a table of link-blue
   rows would be a sixth vocabulary for "you can click this".

   The answer is the one the grid already uses: the row lifts on hover and a
   chevron leans into the gesture. Nothing about the row's *content* changes
   colour, because the content is the answer - a stance is coloured by what it
   said, and a hover that recoloured it would be the interface arguing with the
   data. */
.ai-catalogue-table tbody tr { cursor: pointer; }
.ai-catalogue-table tbody tr:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* The chevron leans in on hover rather than appearing on it. A control that is
   invisible until the pointer is over it cannot be found by somebody who has
   not already guessed it is there, which is exactly the reader this panel was
   built for. */
.ai-cat-go {
  width: 1%;
  color: var(--muted);
  text-align: right;
  transition: transform .12s ease, color .12s ease;
}
.ai-catalogue-table tbody tr:hover .ai-cat-go { color: var(--accent); transform: translateX(2px); }

/* The question, its mark and its cadence, stacked. The mark is the same icon
   `jobs.py` gives that job type, so a row here and a row in the Jobs tab wear
   the same picture for the same call. */
.ai-cat-name { min-width: 210px; }
.ai-cat-mark {
  display: inline-flex;
  vertical-align: -2px;
  margin-right: 6px;
  color: var(--accent);
}
.ai-cat-label { font-weight: 600; }
.ai-cat-name .advice-age { display: block; margin-left: 26px; }
.ai-cat-state { white-space: nowrap; }
.ai-cat-runs { text-align: right; font-variant-numeric: tabular-nums; width: 1%; }

/* A question nobody has ever asked. Dimmed rather than hidden and dimmed rather
   than greyed out: it is the most useful row on the table for a reader who came
   to find out what they have NOT done, and it has to stay legible while reading
   as the empty one. Only the answer columns fade - the question itself stays at
   full strength, because that is the half of the row that is still true. */
.ai-cat-none td:not(.ai-cat-name):not(.ai-cat-state) { opacity: .55; }

/* Each half's own body, so a switch between them is a class rather than a
   rebuild - see `subPanel` in `stockadvice.js`. Nothing else to say: the panes
   carry `.stock-advice`, `.whyfall-panel` or `.ai-catalogue`, all of which are
   already full-width blocks. */
.stock-ai { width: 100%; }

/* AI > Others: seven independent job outputs, dense enough to scan and with
   each historical row collapsed so one filing ledger cannot bury the rest. */
.others-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.others-card { min-width: 0; }
.others-card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.others-card-head h4 { margin: 0; }
.others-items { display: grid; gap: 6px; }
.others-item { border-top: 1px solid var(--line); padding-top: 7px; }
.others-item summary { cursor: pointer; font-weight: 600; overflow-wrap: anywhere; }
.others-fields { display: grid; gap: 6px; margin: 10px 0 0; }
.others-fields > div { display: grid; grid-template-columns: minmax(110px, .35fr) minmax(0, 1fr); gap: 10px; }
.others-fields dt { color: var(--muted); font-size: var(--fs-sm); }
.others-fields dd { margin: 0; min-width: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
.others-fields .others-fields { margin-top: 0; }
.others-list { margin: 0; padding-left: 18px; }

@media (max-width: 820px) {
  .others-grid { grid-template-columns: minmax(0, 1fr); }
  .others-fields > div { grid-template-columns: minmax(90px, .35fr) minmax(0, 1fr); }
}

/* The report itself: headings down the left, sections on the right.

   The rail is sticky against `.modal-body`, which is the popup's scroller - so
   it stays put while the report moves past it, and `top: 0` is measured against
   that box rather than the window. `align-items: start` is what gives it a box
   to be sticky *in*: a grid item stretches to the row height by default, and a
   full-height item has nowhere to stick to.

   `minmax(0, 1fr)` on the body, not `1fr`. The factor table and the usage table
   are wide, and a grid track's automatic minimum is its content - without this
   a long factor row pushes the column wider than the popup and the whole report
   scrolls sideways under a rail that is no longer beside it. */
.report-split {
  display: grid;
  grid-template-columns: 190px minmax(0, 1fr);
  gap: 4px 20px;
  align-items: start;
  margin-top: 10px;
}
.report-index {
  position: sticky;
  top: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding-right: 12px;
  border-right: 1px solid var(--line-soft);
}
.report-jump {
  background: none;
  border: 0;
  border-left: 2px solid transparent;
  padding: 5px 8px;
  text-align: left;
  font: inherit;
  font-size: var(--fs-sm);
  line-height: 1.35;
  color: var(--muted);
  cursor: pointer;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.report-jump:hover { color: var(--text); background: var(--row-hover); }
/* The mark follows the body's scroll, so it is a statement about where the
   reader is rather than about what they last pressed - it has to read as a
   position, which is what the rule down the left edge says and a background
   alone would not. */
.report-jump.on {
  color: var(--text);
  border-left-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}
/* The first section starts flush with the rail rather than under the margin
   every other block carries, so the two columns begin on the same line. */
.report-body > .advice-block:first-child { margin-top: 0; }
/* Pressed headings scroll the section to the top of the popup's body; this is
   the breathing room between it and the tab strip above. */
.report-body [data-report-section] { scroll-margin-top: 6px; }

/* --- the Analytics tab ------------------------------------------------- */
/* The rubric's twelve categories beside the model's, one row each. Built almost
   entirely out of borrowed classes - `.rating-card` for the sections, `.verdict`
   for both sides' band words, `.advice-bar` and `.advice-weight-num` for the
   model's rating, `.delta-pill` for the rubric's points - because this tab is a
   comparison of two panels that already exist and inventing a third vocabulary
   for it would make the very comparison it is for harder to read.

   What is new here is only what is new on screen: the two overall scores facing
   each other, and the gap column. */
.an-heads-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: 12px;
}
/* Facing each other rather than stacked, which is the whole point: the two
   answers are peers and the distance between them is the third thing on the
   row. So the left one is right-aligned and the right one left-aligned, and the
   spread sits in the gutter they both point at. */
.an-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; }
.an-head-rubric { justify-content: flex-end; text-align: right; }
.an-who {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .06em;
  width: 100%;
}
.an-head-rubric .an-who { justify-content: flex-end; }
.an-who .icon { opacity: .7; }
.an-score { font-size: 26px; font-weight: 600; line-height: 1.1; }
.an-score.good { color: var(--positive); }
.an-score.bad { color: var(--negative); }
.an-scale { color: var(--muted); font-size: var(--fs-sm); }
/* The gutter between the two. A number and a word, centred, in the mono face
   the rest of the application quotes deltas in - it is a distance, and it reads
   as one only if it cannot be mistaken for a third score. */
.an-spread {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 4px 10px;
  border-radius: var(--radius);
  background: var(--panel-2);
  min-width: 88px;
}
.an-spread-num { font-family: var(--mono); font-size: var(--fs-lg); font-weight: 600; }
.an-spread-note {
  color: var(--muted);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .04em;
}
/* A wide spread is toned, not alarmed: two scorecards a point and a half apart
   is the interesting case this tab exists to surface, not a fault. */
.an-spread-wide { background: var(--accent-soft); }
.an-spread-wide .an-spread-num { color: var(--accent); }
.an-spread-none { color: var(--muted); }

/* The table. Every cell in it is a `.mini-table td` first, and that rule sets
   `white-space: nowrap` - right for eleven columns of figures, and wrong for the
   two here that hold words. Undoing it takes `.an-table td.an-why` rather than a
   bare `.an-why`: one class loses the cascade to `.mini-table td`'s class-plus-
   element, so the plain selector reads as though it works, computes as though it
   does not, and lets a 400-character justification run off the side of the popup
   on one line. The `td` in these selectors is load-bearing, not decoration.

   Top-aligned rather than centred, because the reason column is the tall one: a
   pill floating halfway down a six-line cell reads as belonging to no line in
   particular, where at the top it sits against the sentence it is about. */
.an-table td { vertical-align: top; padding-top: 6px; padding-bottom: 6px; }
.an-table th {
  text-align: left;
  font-weight: 600;
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  border-bottom: 1px solid var(--line);
  padding: 4px 4px 5px;
  white-space: nowrap;
}

/* --- whose number is this ------------------------------------------------ */
/* Two answers to one question on every row, so each side carries an identity
   hue: cyan for the rubric, violet for the model. Set as one custom property
   that everything below reads, exactly as the axis marks do with `--cat-mark` -
   so adding a third scorer one day is two declarations, not fifteen.

   The hue is an identity and NEVER a verdict. That is the whole constraint on
   how strongly it can be used: these cells are full of pills that really are
   verdicts, and a green wash under a "Very bad" pill would be the panel
   arguing with itself. So the body gets a few per cent and the head gets the
   hue at strength, where no red or green sits beside it. */
.an-side-rubric { --an-side: var(--an-rubric); }
.an-side-model { --an-side: var(--an-model); }

/* The heading that names a side. This, not the colour, is what actually says
   which side is which - the tint only makes it scannable once you know. It
   carries the same mark as the headline block above it, so the two read as one
   thing rather than as two ways of saying it.

   It was a label spanning two columns while the rubric had a Verdict and a
   Points column. Dropping the points left one column a side, and a group label
   over a single column is a heading with nothing to group - so the span went and
   its mark, hue and rule came down onto the ordinary heading. */
.an-table th.an-grp {
  color: color-mix(in srgb, var(--an-side) 80%, var(--text));
  border-bottom: 2px solid color-mix(in srgb, var(--an-side) 55%, transparent);
}
/* `display: inline-block`, and it is not optional. The base `.icon` rule is
   `display: block`, so every icon in this application takes a line of its own
   unless something puts it back in the flow - which everywhere else means a flex
   parent (`.vd-name`, `.delta-pill`, `.an-who` below) setting `flex: none` on it.
   These two live in table cells, where a flex parent would stop the cell being a
   table-cell and take the column widths with it, so they go inline instead - the
   same way the day-move log's open mark does. Without this the mark sits on one
   line and its word on the next, which is what a `vertical-align` on a block
   element quietly fails to fix. */
.an-grp .icon {
  display: inline-block;
  opacity: .85;
  margin-right: 4px;
  vertical-align: -2px;
}
/* The reason column is the model's too, but it sits past the Agreement column
   and is the widest thing on the tab - a second named heading over it would read
   as a third side. It takes the band and the hue, and no mark. */
.an-table th.an-side-model {
  color: color-mix(in srgb, var(--an-side) 72%, var(--text));
}

/* The wash, on the head as well as the body so each band runs the full height of
   the table and the boundary between the two sides is simply the colour
   changing - no rules to draw and nothing to keep aligned.

   Deliberately weak, and re-stated for the hover state below: a cell background
   always paints over its row's, so a tinted column would otherwise swallow the
   row highlight on exactly the cells being compared. */
.an-table th.an-grp, .an-table th.an-side-model {
  background: color-mix(in srgb, var(--an-side) 10%, transparent);
}
.an-table td.an-side-rubric, .an-table td.an-side-model {
  background: color-mix(in srgb, var(--an-side) 7%, transparent);
}
.an-table tbody tr:hover td.an-side-rubric,
.an-table tbody tr:hover td.an-side-model {
  background: color-mix(in srgb, var(--an-side) 12%, var(--row-hover));
}

/* The headline blocks take the identity at strength: they are two words and a
   number with nothing coloured beside them, which is where a hue can be spent
   properly. The score itself keeps its verdict tone - it is the one figure here
   allowed to be good or bad news. */
.an-head .an-who { color: var(--an-side, var(--muted)); }
.an-head-rubric, .an-head-model { border-radius: var(--radius); padding: 6px 10px; }
.an-head-rubric, .an-head-model { background: color-mix(in srgb, var(--an-side) 8%, transparent); }
.an-table td.an-cat { width: 1%; min-width: 124px; white-space: normal; font-weight: 600; }
/* Inline for the reason `.an-grp .icon` is, and inline-block rather than a flex
   row for a second one: the label here wraps - "Valuation vs its own history" is
   two lines in this column - and as flex items the mark and the label would be
   two boxes side by side, pinning a 12px mark beside a two-line block. Inline,
   the mark simply starts the first line and the wrap continues under it, which
   is how a line of text with a mark in front of it is supposed to behave. */
.an-cat .icon {
  display: inline-block;
  opacity: .6;
  margin-right: 5px;
  vertical-align: -2px;
}
/* The four pill columns take their content and no more, so whatever is left of
   the popup goes to the reason. `width: 1%` is the auto-layout idiom for
   "shrink to fit": a real percentage would be a share of a width nobody knows. */
.an-table td.an-band, .an-table td.an-agree,
.an-table td.an-model { width: 1%; white-space: nowrap; }
/* The one column meant to wrap, and the one that absorbs the leftover width.
   `min-width` is a floor for the narrow case, not a cap - and the cap is the
   table's own 100%, which is what stops the sentence pushing the popup wider
   than itself.

   It is the model's column too, and takes a lighter wash than the model's
   Rating cell rather than the same one: a tint reads stronger over 900px than
   over 90, and the 7% that is a hint beside a pill is a slab behind six lines
   of prose. Halving it keeps the two columns reading as one side. This has to
   sit after the band rule above to win the tie between two equally specific
   selectors - which is why the two declarations are in one rule here rather
   than in a second `.an-table td.an-why` block somewhere else. */
.an-table td.an-why {
  white-space: normal;
  overflow-wrap: anywhere;
  background: color-mix(in srgb, var(--an-side) 3.5%, transparent);
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: 1.45;
  min-width: 220px;
}
/* A side that has nothing on file, rather than a side that scored nothing. The
   dash is dimmed so a column of them reads as absent data at a glance. */
.an-none { color: var(--muted); opacity: .55; }
/* Below the width the five pill columns need, something has to give, and it is
   this card rather than the popup: the table scrolls inside its own box and the
   scorecards above it stay put. A popup that grew wider than itself would take
   the header and every other tab with it. */
.an-table-card { overflow-x: auto; }

/* The gap itself. Three states and the middle one is deliberately quiet: one
   band apart is routine - the two rules cut the scale in different places - and
   painting it would leave nothing to say two bands apart with. */
.an-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: .03em;
  white-space: nowrap;
  background: var(--delta-zero-bg);
  color: var(--delta-zero-fg);
}
.an-pill .icon { opacity: .8; }
.an-is-agree { background: var(--delta-pos-bg); color: var(--delta-pos-fg); }
.an-is-near { background: var(--delta-zero-bg); color: var(--delta-zero-fg); }
/* The one row on the table worth stopping at, so it gets the accent rather than
   a sign colour: a two-band disagreement is not good news or bad news, it is
   news - one side is reading something the other cannot. */
.an-is-wide {
  background: color-mix(in srgb, var(--accent) 18%, var(--panel));
  color: color-mix(in srgb, var(--accent) 82%, var(--text));
}
/* Marked with a rule down the left edge rather than a tint across the row, and
   that is a consequence of the column bands above: a cell background always
   paints over its row's, so a row tint would have shown on Category and
   Agreement and vanished behind the very columns being compared. An inset
   shadow rather than a border, so lighting a row does not shift its text by
   3px. Same idiom as `.rating-note` and the report rail's open heading. */
.an-row.an-gap-wide td.an-cat { box-shadow: inset 3px 0 0 var(--accent); }
.an-row.an-gap-unknown .an-cat { opacity: .6; }
/* The note above the table when one half is missing. Toned as a note rather
   than a warning, exactly as `.rating-note` is - an unscored stock joins the
   next pass, and an unasked one needs a button pressed. Neither is a fault. */
.an-note { margin-bottom: 12px; }

@media (max-width: 760px) {
  .an-heads-row { grid-template-columns: minmax(0, 1fr); }
  .an-head-rubric { justify-content: flex-start; text-align: left; }
  .an-head-rubric .an-who { justify-content: flex-start; }
}

/* --- the Compare tab --------------------------------------------------- */
/* One stock beside up to five of its peers: metrics down the left, a column per
   company across. Dense and uniform on purpose - see the head of compare.js:
   every stock column the same fixed width, every metric row the same fixed
   height, the header and the metric column frozen, the groups foldable.

   The sizes live in custom properties on the panel because three things have
   to agree about them and a stylesheet cannot measure. The table's own width
   is the metric column plus `--cmp-cols` stock columns (compare.js writes the
   count on the table, because a stylesheet cannot count either); the group
   dividers stick under the header at exactly the header's height; and the
   phone block at the bottom of this file only has to restate the numbers, not
   the rules. The two tints are tokens for the same reason - the cell and the
   note that explains it have to be the same colour. */
.cmp-panel {
  --cmp-metric-w: 200px;
  --cmp-col-w: 150px;
  --cmp-row-h: 28px;
  --cmp-head-h: 76px;
  --cmp-best: color-mix(in srgb, var(--positive) 14%, transparent);
  --cmp-worst: color-mix(in srgb, var(--negative) 11%, transparent);
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  min-height: 0;
}
/* The panel takes the popup body's full height so that the table inside it can
   be the thing that scrolls. `.cmp-scroll` is that scroller, on both axes: a
   sticky header sticks to its nearest *scrolling* ancestor, and with the popup
   body doing the scrolling this box never scrolled vertically at all - so the
   header, sticky as it was declared, rode off the top with the rows. Now the
   toolbar stays put above, the table scrolls in its own box, and the header
   and the metric column have something to stick to. `[data-panel]` rather than
   a class so the other tabs, whose panels flow and let the body scroll, are
   untouched. `flex: 0 1 auto` so a short table is a short box, not a box
   stretched to the bottom of the popup with the hint stranded at its foot. */
.tab-panel[data-panel="compare"] { height: 100%; }
.cmp-scroll {
  flex: 0 1 auto;
  min-height: 0;
  overflow-x: auto;
  overflow-y: auto;
}

/* The toolbar: the three controls that put a stock on the table on the left,
   the two that say how it is read on the right, a hairline under the lot. One
   wrapping row, so on a desktop it is one line and on a phone the left half
   stacks (see the narrow block) with the right half under it. `align-items:
   center` rather than baseline: the toggle carries a checkbox and the count a
   button, and neither has a text baseline worth aligning the others to. */
.cmp-pick {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  flex: none;
  margin: 0 0 8px;
  padding: 0 0 8px;
  border-bottom: 1px solid var(--line);
}
.cmp-tools { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.cmp-count { font-size: var(--fs-sm); font-variant-numeric: tabular-nums; white-space: nowrap; }
.cmp-drop {
  flex: none;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1;
  cursor: pointer;
}
.cmp-drop:hover { background: var(--panel-2); color: var(--negative); }
.cmp-drop:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Mirrors `.field-add` on the Details tab - the same box-then-menu pair, so a
   search behaves the same way wherever this popup offers one. */
.cmp-add { position: relative; display: flex; align-items: center; gap: 6px; }
.cmp-add .field-find { width: 150px; font-size: var(--fs-sm); }
.cmp-add select { max-width: 300px; font-size: var(--fs-sm); }
.cmp-add select[disabled], .cmp-add .field-find[disabled] { opacity: .5; }
/* Open, the menu hangs over the table rather than pushing it down - the same
   trade `.field-add` makes on the Details tab, and for the same reason: a list
   of matches that shoves the thing you are comparing off the screen is a
   search that costs more than it found. Anchored to the right edge of the
   control group because that is where the closed control sits.

   `[size]` before the `:not`, and it has to be there. An attribute selector
   matches the **attribute**, not the IDL property, and a `<select>` drawn
   without one has no `size` attribute at all - so a bare `:not([size="1"])`
   matched the shut menu as well as the open one and hung the closed control
   over the table on every paint. `findStock` writes `size` on the first
   keystroke, which is why it looked right the moment anybody typed and wrong
   until they did. Now: no attribute, no float. */
.cmp-add select[size]:not([size="1"]) {
  position: absolute;
  top: 100%;
  right: 0;
  z-index: 6;
  min-width: 260px;
  box-shadow: var(--shadow-sm);
}
/* The scope picker sits in the row beside the search box and never floats: it
   is a four-line menu whose options are known before it is opened, so there is
   nothing for it to hang over anything for. `max-width` rather than a width -
   an industry name is "Electrical Equipment & Parts" and a sector is
   "Utilities", and a box sized for the second truncates the first. */
.cmp-scope { max-width: 260px; font-size: var(--fs-sm); }
.cmp-scope[disabled] { opacity: .5; }
/* Its own line under the controls, not another item squeezed into the row.
   `.cmp-pick` is a wrap-flex, so a full basis is what puts a sentence on a line
   of its own rather than letting it fight the controls for the last 80px. */
.cmp-scope-note { flex: 1 0 100%; margin: 0; }
/* Belt and braces on `hidden`, exactly as `.field-add` needs it: `[hidden]` is
   a user-agent rule and an option is one of the few places a browser has been
   known not to honour it. */
.cmp-add option[hidden] { display: none; }
.cmp-opt {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-sm);
  color: var(--muted);
  white-space: nowrap;
}
/* Everything in the panel that is not the table keeps its own height: the
   column is `min-height: 0` so the table can shrink, and without `flex: none`
   the error line and the spinner would be asked to shrink with it. */
.cmp-error { color: var(--negative); flex: none; }
.cmp-panel > .loading-block { flex: none; }
.cmp-note { max-width: 90ch; margin: 8px 0 0; }

/* The table. `table-layout: fixed` with a width that is the sum of its parts,
   so a column is exactly `--cmp-col-w` wide whether there is one peer or five:
   the old `min-width: 100%` stretched two peers to 400px each and put their
   figures nowhere near the labels they belonged to. A table narrower than the
   panel is simply narrower than the panel. */
.cmp-table {
  table-layout: fixed;
  width: calc(var(--cmp-metric-w) + var(--cmp-cols, 1) * var(--cmp-col-w));
  font-size: var(--fs-base);
}
.cmp-table th.cmp-col { width: var(--cmp-col-w); }
/* The metric column is fixed and stays put while the stocks scroll past it: it
   is the axis the eye returns to, and a label column that slides away makes a
   wide table unreadable. `left: 0` is measured against `.cmp-scroll`, and the
   background is opaque or the figures sliding underneath show through it. A
   label longer than the column is clipped with an ellipsis rather than
   wrapped, and carried whole on `title`: one label two lines tall is one row
   two heights tall, and a table whose rows are two heights is a table the eye
   reads as two tables. */
.cmp-table th.cmp-metric {
  position: sticky;
  left: 0;
  z-index: 1;
  width: var(--cmp-metric-w);
  background: var(--panel);
  text-align: left;
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* `tbody` in the selector rather than a bare `th.cmp-metric`, so this loses to
   `.cmp-table thead th` below by scope instead of by a specificity race: the
   corner cell is a header and keeps the header's padding and height. */
.cmp-table tbody th.cmp-metric {
  box-sizing: border-box;
  height: var(--cmp-row-h);
  padding: 6px 8px;
  border-bottom: 1px solid var(--line-soft);
  font-size: var(--fs-base);
  line-height: 16px;
}
/* The header row, frozen at the top of the scroller. Its rule is an inset
   shadow rather than a border: in a collapsed-border table the borders belong
   to the table, not the cell, and a sticky cell scrolls away from its own
   border - the line stayed where the row had been while the cells stuck. The
   shadow is the cell's and goes where the cell goes. */
.cmp-table thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  padding: 6px 8px;
  background: var(--panel);
  box-shadow: inset 0 -1px 0 var(--line);
  text-align: left;
  vertical-align: top;
}
/* The corner is where the two sticky axes cross, so it has to beat both. */
.cmp-table thead th.cmp-corner { z-index: 4; }
/* The four lines of a stock's header, each a fixed height and each clipped to
   one line, so that the header is `--cmp-head-h` tall whatever is in it:
   16 + 14 + (3 + 14 + 2) + 15 = 64, plus the cell's 6px above and below, is 76.
   The group dividers stick at that number, and the sum is the one thing here
   that must not drift - change a line height, change the token. */
.cmp-hd { display: flex; flex-direction: column; height: 64px; overflow: hidden; }
.cmp-head { display: flex; align-items: center; gap: 4px; height: 16px; line-height: 16px; }
.cmp-sym {
  min-width: 0;
  font-weight: 600;
  font-size: var(--fs-base);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cmp-head .cmp-drop { margin-left: auto; }
.cmp-name {
  display: block;
  color: var(--muted);
  font-weight: 400;
  font-size: var(--fs-xs);
  line-height: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cmp-tags { display: flex; flex-wrap: nowrap; gap: 3px; height: 14px; margin: 3px 0 2px; overflow: hidden; }
.cmp-ltp {
  display: block;
  font-family: var(--mono);
  font-size: var(--fs-sm);
  line-height: 15px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A column whose figures are still in flight is a column, not a promise: the
   symbol is drawn quiet until its row lands. */
.cmp-table th.cmp-pending .cmp-sym { color: var(--muted); }
/* The stock the popup is open on, tinted down its whole column so the eye can
   find the thing everything else is being read against without hunting for the
   badge in the header. Weak on purpose: it is a landmark, not a highlight, and
   the conditional formatting painted on individual cells has to stay legible
   over it. `background-image` rather than `background`, so a cell that carries
   a formatting rule paints its own colour on top rather than fighting this one
   for the same property. */
.cmp-table .cmp-self {
  background-image: linear-gradient(color-mix(in srgb, var(--accent) 7%, transparent),
                                    color-mix(in srgb, var(--accent) 7%, transparent));
}
/* A group's divider: a row that sticks just under the header while its rows
   scroll past, so the reader always knows which group the figure under the
   cursor belongs to. `- 1px` so it tucks under the header rather than leaving
   a slit of moving figures between the two - the header is above it in the
   stack, so the overlap is invisible and the slit would not be. The cell is
   the full width of the table and scrolls sideways with it; the button inside
   is sticky on the other axis, so the group's name stays at the left edge
   while the columns go by underneath. */
.cmp-table tr.cmp-group th {
  position: sticky;
  top: calc(var(--cmp-head-h) - 1px);
  z-index: 2;
  padding: 6px 0 0;
  background: var(--panel);
  box-shadow: inset 0 -1px 0 var(--line);
  text-align: left;
}
.cmp-fold {
  position: sticky;
  left: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 24px;
  padding: 0 8px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  cursor: pointer;
}
.cmp-fold:hover { color: var(--text); }
.cmp-fold:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.cmp-fold .muted { font-weight: 400; letter-spacing: 0; }
/* The same caret the Rating tab's build rows use, turned the same way: down
   for open, along for shut. */
.cmp-caret { display: inline-block; width: 8px; font-size: var(--fs-md); line-height: 1; }
.cmp-caret::before {
  content: '›';
  display: inline-block;
  transform: rotate(90deg);
  transition: transform .12s ease;
}
.cmp-table tr.cmp-closed .cmp-caret::before { transform: none; }

/* The cells. One height for every row, so the eye can run down a column
   without the rows it crosses being different sizes - `box-sizing` so the 28px
   is the whole row, padding included. The one row allowed to be taller is a
   text row somebody has opened. */
.cmp-table td {
  box-sizing: border-box;
  height: var(--cmp-row-h);
  padding: 6px 8px;
  vertical-align: top;
  line-height: 16px;
}
.cmp-table td.rc-num {
  width: auto;
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
/* Text metrics - a tier, a thesis, a list of caps - wrap rather than being
   clipped to one line by `.mini-table td`'s nowrap, and are then clipped to
   *two* by the box round them: a thesis is a paragraph, and a paragraph in a
   150px column is a row ten lines tall that puts the next forty figures below
   the fold. Two lines say what the cell is about; the whole of it is on the
   tooltip, and a click on the cell opens the row. `display: block` is what
   undoes the clamp - `-webkit-line-clamp` only acts on a `-webkit-box`. */
.cmp-table td.cmp-text { white-space: normal; font-size: var(--fs-base); }
.cmp-table .cmp-clip {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}
.cmp-table tr.cmp-open .cmp-clip { display: block; overflow: visible; }
.cmp-table td.cmp-long { cursor: pointer; }
.cmp-blank { color: var(--muted); opacity: .6; }
.cmp-table tr.cmp-quiet { opacity: .4; }
/* The best figure in its row, and the worst. A tint on the cell and weight on
   the best, rather than the ring this replaces - a box round a figure was one
   more edge on a table that is all edges, and said nothing at a glance that a
   soft field behind the number does not say better.

   The tint is an inset `box-shadow`, not a `background`: the cells here are
   already carrying up to twelve palette colours from the registry's
   conditional formatting, which owns `background`, and the base column owns
   `background-image`. A shadow paints above both and touches neither, so a
   cell that is formatted, the base *and* the best of its row shows all three.
   Weight only on the best, so that the two ends of a row do not read as two
   winners. */
.cmp-table td.cmp-best { font-weight: 600; box-shadow: inset 0 0 0 999px var(--cmp-best); }
.cmp-table td.cmp-worst { box-shadow: inset 0 0 0 999px var(--cmp-worst); }

/* --- narrow screens, and fingers ------------------------------------- */
/* Two questions, deliberately asked separately, because the answers do not
   travel together.
   WIDTH is about layout: a twelve-column dashboard, a 190px settings label and
   a 228px search box are all statements about a window a phone does not have.
   INPUT MODE is about targets and affordances: a finger has no hover to reveal
   a control with and no hotspot to find an 18px circle with - and that is as
   true of a touchscreen laptop at 1600px as it is of a phone.
   A single `max-width` query conflating the two would give a small window on a
   desk finger-sized buttons it does not need, and give a touchscreen laptop the
   hover-only controls it cannot reach.

   Both blocks are last in the file on purpose. Every rule in them overrides one
   declared above at the SAME specificity, and wins on source order; where a
   rule has to beat something more specific it says so, and why, in place. A
   media query contributes nothing to specificity - only position. */

/* ---- the shell folds ------------------------------------------------- */
@media (max-width: 820px) {
  main { padding: 8px 8px 16px; padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }

  /* The tab rail folds under a hamburger instead of scrolling sideways. The
     sideways scroll it replaces was already the reverse of the strip's call,
     and it worked - but it still spent a row of chrome on the screen with the
     least of it to spare, and it hid whichever pills were past the edge with
     nothing on screen saying so. The handle spends no extra row: it sits in
     the bar beside the user picker wearing the open page's name (show()
     writes it), and the rail drops over the page as a panel when pressed.
     Absolutely positioned on purpose: the open menu's height never enters the
     `--topbar-h` that main.js measures, so the grid's reserve does not jump
     on every open and shut. main.js owns `.open`, and shuts it on picking a
     page, on a tap anywhere else, and on Escape.
     The strip still wraps. Its chips shed their sparkline instead, below. */
  .tabs-toggle {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    margin-left: auto;
    padding: 5px 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--panel-2);
    color: var(--text);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }
  .tabs-toggle .tab-i { display: inline-flex; align-items: center; flex: none; }
  .tabs {
    display: none;
    position: absolute;
    top: calc(100% + 4px);
    right: 8px;
    z-index: 30;   /* the shelf .dropdown-panel floats on, for the same reason */
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    min-width: 190px;
    padding: 6px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
  }
  .tabs.open { display: flex; }
  /* Menu rows, not pills in a row: full width, left-aligned, and the page
     marks come back - the 1080px rule dropped them because the RAIL was
     cramped, and a vertical menu is not. Same specificity, later in the file,
     so both re-statements win on source order. */
  .tab { margin: 0; justify-content: flex-start; border-radius: var(--radius); }
  .tab .tab-i { display: inline-flex; }

  /* `position: relative` is what the menu above hangs off. */
  .topbar { gap: 6px; padding: 0 8px; position: relative; }
  /* The status seat goes - the last-refreshed stamp and the build stamp - on
     the screen with the least room to spare. The refresh button is a sibling
     at the top left, so the page can still be refreshed manually on a phone.
     The poll keeps running on the saved interval whether or not its stamp is
     painted.
     Hiding it takes that row out of `--topbar-h`, which `main.js` measures, so
     the grid below gets it back instead of reserving for chrome that is not
     painted.
     `display: none` rather than a removal from the markup, because every
     element is still in the DOM with its listener attached and `main.js` writes
     into them unguarded - `refreshLabel.textContent` on every tick.
     What this no longer costs is the settings: the theme, the interval, the
     palette and Sign out used to be on this seat and went with it, leaving a
     phone with no way to sign out at all. They are in `.settings-menu` now,
     which is a SIBLING of this box precisely so that hiding this one does not
     take them - nothing inside a `display: none` box can be shown again. */
  .topbar-right { display: none; }

  /* A chip that has shed its 74px sparkline is narrow enough for three to a
     row rather than one, so five indices cost the strip two rows instead of
     five. The spark is a shape read at a glance from a foot away; in a 100px
     chip on a phone it is a smudge, and it is the only part of the chip that
     is not also stated as a figure beside it. */
  .strip { padding: 6px 8px; gap: 6px; }
  .strip-item { padding: 3px 8px; gap: 7px; }
  .strip-item svg { display: none; }

  /* The grid's toolbar keeps the column picker and sheds everything else: the
     search box, Clear filters, the row count, Add stock and Delete. Same call
     as `.topbar-right` below - none of them is reached for on a phone, and
     together they wrapped the toolbar to three rows on the screen with the
     least room for chrome. `display: none`, not removal: grid.js binds each one
     by id at render, writes the count back on every refresh and reads the
     search box's value, so the nodes must stay in the DOM. Scoped to the ids
     and the toolbar's own classes, so the Dashboard's toolbar - a title, a
     stamp and two buttons - keeps its seat.
     The Colour tint and the row-height picker used to be hidden here too, by
     the `.toolbar .field` arm of this rule. They are not on the toolbar at all
     any more - they are app-wide settings under the gear, which is the one
     piece of chrome a phone now keeps - so the arm is gone rather than left
     matching nothing. `.field` still exists in the news feed's toolbar, which
     states itself back below, and an arm kept here would go on fighting that.
     What it costs: no free-text search from a phone, a watchlist stock cannot
     be added, a row cannot be deleted and a stuck search or column filter
     cannot be cleared - the grid still shows what the filter left, so a phone
     reads it and a desktop tends it. */
  .toolbar .input-icon,
  .toolbar #g-clear,
  .toolbar #g-count,
  .toolbar #g-add,
  .toolbar #g-delete,
  .toolbar #g-restore,
  /* The watchlist's two tick-boxes go with them, shed as the one id they are
     grouped under - `.toolbar .toggle` would be a class-only selector in a rule
     that must not be able to reach the Dashboard's toolbar. They are filters,
     and this block has already decided a phone reads the grid and a desktop
     tends it: the column filter row under the head is still there, and the rows
     a toggle would have hidden are marked on screen anyway - a held row carries
     H and a deleted one is struck through and carries D. */
  .toolbar #g-toggles,
  /* And the shared stock control where a page draws one in place of the search
     box - shed as a whole rather than leaving the frame behind, which is what
     hiding only the `.input-icon` inside it would do. */
  .toolbar .stock-pick { display: none; }

  /* ...but not the news feed's toolbar, which is stated back because the rule
     above would take away the ONLY way to filter it. The grid keeps a filter
     box per column under its head, so a phone that loses the toolbar can still
     narrow it; the news feed's search box and five menus are the whole of its
     filtering, and hiding them would leave a phone with two thousand filings
     and no way through them. They wrap onto as many rows as they need instead,
     which is the honest cost of keeping them.
     Only `.input-icon` is fighting anything now: the rule above no longer has
     a `.field` arm, because the two controls that arm existed for - the Colour
     tint and the row-height picker - are settings under the gear rather than
     toolbar controls. The news menus keep their `display` here anyway, so that
     what this block claims to state back is all of what it states back rather
     than half of it depending on a rule three screens up. */
  .news-panel .toolbar .input-icon,
  .news-panel .toolbar .field { display: inline-flex; }
  .news-panel .toolbar .input-icon { flex: 1 1 100%; }
  .news-panel .toolbar .input-icon > input[type="text"] { min-width: 0; width: 100%; }
  .news-panel .toolbar .field { flex: 1 1 130px; }
  .news-panel .toolbar select { max-width: none; width: 100%; }
  /* The company name under the ticker is the first thing to go when the row is
     six columns wide on a 380px screen - the ticker is what identifies it. */
  .news-name { display: none; }

  /* The AI report's index goes over the sections instead of beside them. A
     190px rail out of a 380px screen leaves the report itself unreadable, and
     dropping the rail altogether would take the jumps away on the device that
     needs them most - a phone scrolls a dozen sections a screen at a time.
     Not sticky here: pinned to the top it would eat a third of the screen for
     as long as the report is open. */
  .report-split { grid-template-columns: minmax(0, 1fr); }
  .report-index {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    gap: 4px;
    padding: 0 0 8px;
    border-right: 0;
    border-bottom: 1px solid var(--line-soft);
  }
  .report-jump {
    border-left: 0;
    border-bottom: 2px solid transparent;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    padding: 6px 8px;
  }
  .report-jump.on { border-left-color: transparent; border-bottom-color: var(--accent); }

  /* No phone-specific grid reserve any more. The base 102px is a ONE-ROW
     toolbar, and one row is all the toolbar can be here now that it is a lone
     Columns button - the override that reserved a second row (138px, and
     218px before the summary bar moved to the Dashboard) existed for the rows
     the search box and the option cluster wrapped onto, and went with them.
     Reserving the phantom row back would cost a 36px band of dead space under
     every grid on the screen with the least room to give it. */

  /* Everything that pins a width in pixels wider than the screen it is on. */
  .dropdown-panel { width: min(330px, calc(100vw - 24px)); max-height: min(460px, 70vh); }
  .tooltip { max-width: calc(100vw - 24px); }
  .toast { left: 10px; right: 10px; bottom: 10px; }
  .kv-table th { width: auto; }
  .settings-row > label { min-width: 0; flex: 1 1 100%; }
  /* 80% of a phone is a frame around nothing, and the popup inside it carries a
     price chart, four badges and a two-tab rail. It takes the screen, less a
     hairline of backdrop so it still reads as a popup over the grid. The inner
     scrolling is unchanged - only the box it happens in is bigger. */
  .modal {
    width: calc(100vw - 12px);
    height: calc(100vh - 16px);
    height: calc(100dvh - 16px);
  }

  /* ...and the way back out of it, which the wider box does not give back. The
     head is one nowrap flex line carrying the name, the ticker, up to four
     badges and up to four buttons, and `.modal` clips whatever runs past its
     edge - so on a phone the item furthest right is the one that falls off,
     and that item is the cluster ending in Close. Nothing else here is an exit
     either: Escape wants a keyboard, and the backdrop tap is aimed at the 6px
     hairline the rule above leaves either side of a popup taking the screen.
     So the row wraps. The buttons drop to a line of their own, still right-
     aligned by the `margin-left:auto` the cluster already carries, and the
     cluster wraps within itself as well - Move / Remove / Close on a 320px
     screen is two short rows rather than one that overflows.
     `min-width: 0` on the title because a flex item will not shrink below its
     longest word by default: a two-word company name would hold the first line
     open at that width and push the badges out through the side instead. */
  .modal-head { flex-wrap: wrap; }
  .modal-head h2 { min-width: 0; }
  .modal-actions { flex-wrap: wrap; justify-content: flex-end; }

  /* The Compare picker's Add pair is pushed right by `margin-left: auto` on a
     desktop, where it sits at the far end of one row. On a narrow window it is
     a row of its own and that margin would strand it against the right edge
     with the box and the menu squeezed into whatever is left, so it goes back
     to the flow and takes the full width. */
  .cmp-add { margin-left: 0; width: 100%; flex-wrap: wrap; }
  .cmp-add .field-find { flex: 1; width: auto; min-width: 0; }
  .cmp-add select { flex: 1; min-width: 0; max-width: none; }
  /* Three controls on a phone row is three controls too narrow to read the
     labels in - and the scope picker's labels are the whole point of it, since
     "Industry · Electrical Equipment & Parts (6)" truncated to "Industry · El…"
     answers nothing. It takes the first line to itself and the search-and-add
     pair keeps the second.

     Doubled up as `.cmp-add .cmp-scope` because it has to beat `.cmp-add
     select` three lines above, which is (0,1,1) to a bare `.cmp-scope`'s
     (0,1,0) - source order does not come into it, and the flat `flex: 1` there
     would otherwise hold this on the same row and truncate it to
     "Industry · Electri". */
  .cmp-add .cmp-scope { flex: 1 0 100%; max-width: none; }
  /* The sizes, restated: the rules are the same, the numbers are a phone's.
     190px of label against a 150px stock is most of the screen. */
  .cmp-panel { --cmp-metric-w: 120px; --cmp-col-w: 124px; }

  /* One widget per row. `grid-column` has to be restated rather than left to
     clamp itself: `span var(--w, 4)` against a single explicit track does not
     shrink to fit, it creates three implicit columns and puts the dashboard
     straight back off the side of the screen. The row span stays - how tall a
     widget wants to be is still the widget's business. */
  .dash-grid { grid-template-columns: minmax(0, 1fr); }
  .widget { grid-column: auto; }

  /* iOS zooms the page in when a control with text smaller than 16px takes
     focus, and it does not zoom back out when focus leaves: one tap in the
     search box leaves the entire app magnified and scrolled sideways, with no
     gesture short of a pinch that undoes it. 16px is a threshold, not a
     preference, and every control that takes a CARET has to clear it -
     checkboxes and buttons are left alone because neither does.
     Each selector below is the one that sets the smaller size earlier in this
     file, raised by `body` so it still wins: a bare `input` here loses to
     `.filter-row input` at (0,1,1), which is precisely the box where the zoom
     is worst - it is inside the one element on the page that scrolls in both
     directions. */
  body input, body select, body textarea,
  body .auth-field input,
  body .filter-row input,
  body td .cell-input, body td .cell-area, body td .cell-pick,
  body .alias-input,
  body .colset-save input[type="text"],
  body .detail-edit > input, body .detail-edit > textarea,
  body .detail-edit > select,
  body .code-find > input { font-size: 16px; }
  /* The 14px line box was measured for 10.5px text and clips 16px. */
  body .filter-row input { line-height: 20px; height: 24px; }
  /* A taller filter row moves the totals row down with it - 2px padding top
     and bottom, the 24px input above, and the 1px bottom border. Left at the
     desktop 23px the totals cells would park six pixels high on the filter
     inputs the moment the grid was scrolled. */
  body table.grid { --filter-h: 29px; }
}

/* ---- a finger, not a pointer ----------------------------------------- */
@media (pointer: coarse) {
  /* Everything the desktop UI holds back until the pointer arrives is simply
     on, because the pointer never arrives. `.row-btn` at .45 opacity was not
     "quiet until hovered" here - it was the permanent and only state of the
     one control that moves a position between Core and Satellite. */
  table.grid td .row-btn,
  .widget-head .w-actions .btn,
  .note-log li .note-tool:not(:disabled),
  .btn.sm.study-drop { opacity: 1; }

  /* Targets. Raised with padding and a floor rather than with a font size, so
     the type keeps its scale and nothing reflows around a bigger word. The tab
     rail gets the full 40px because it is the app's primary navigation and it
     is reached with a thumb; the dense in-panel controls get a little less,
     which is the honest trade for a screen that carries eighty columns. */
  .btn { min-height: 34px; padding: 6px 12px; }
  .btn.sm { min-height: 32px; padding: 5px 10px; }
  /* Both of these are stated back, larger, because the floor above would
     SHRINK them. `.auth-submit` sets 42px at (0,1,0) and `.btn` ties with it -
     so the later rule wins, and the later rule is now the one three lines up.
     The sign-in screen is the first thing a phone ever touches and its one
     button is the last place to lose 8px; the password reveal sat at 24px and
     was never a target at all. */
  .auth-submit { min-height: 46px; }
  .password-toggle { min-height: 34px; }
  .icon-btn, .colset-drop { width: 34px; height: 34px; padding: 0; }
  .tab { min-height: 40px; padding: 9px 14px; }
  /* The handle is how a thumb reaches the rail at all, so it gets the same
     40px the rail's own pills do. */
  .tabs-toggle { min-height: 40px; }
  .subtab, .mtab, .ptab { min-height: 36px; }
  select, input[type="text"], input[type="number"] { min-height: 32px; }
  table.grid td .row-btn { width: 26px; height: 26px; }
  /* Explicit `min-height`, or the `.btn` floor above wins and a 32px header
     control makes the widget's 10.5px caption bar taller than two lines of the
     table under it. */
  .widget-head .w-actions .btn { width: 30px; height: 30px; min-height: 0; }
  .btn.sm.study-drop { width: 30px; height: 30px; min-height: 0; padding: 0; }

  /* Three drag affordances that cannot work here, so none of them is shown.
     The dashboard's widget reorder and the grid's column reorder are both
     native HTML5 drag and drop, which fires no event whatsoever for a touch.
     The widget resize strips DO work through pointer events - but they are 10px
     invisible bands carrying `touch-action: none`, hanging into the gap between
     two widgets, so their entire contribution on a touchscreen is to swallow a
     scroll that happens to start on one.
     A grip that reorders nothing is worse than no grip: it advertises that the
     layout is rearrangeable by the one gesture that cannot rearrange it. */
  .widget-head .grip { display: none; }
  .widget-head { cursor: default; }
  .w-edge { display: none; }
}

/* --- Eval Profile: today's losses and today's wins --------------------- */
/* One panel per direction, and both reuse everything `.advice-panel` already
   defines - the blocks, the bullets, the evidence lines, the factor table the
   drivers borrow, the pills and their colours. What is added here is only what
   this panel has and that one does not: a picker, a strip of figures for the
   stock behind it, and the receipt saying which pages the answer was built from.

   The panel carries `.advice-panel` as well as `.daymove-panel` for exactly that
   reason. Copying forty lines of report styling under a second name would give
   two reports that drift, and they are the same report. */

/* The nested strip, one level below the AI page's own. No rule under it: two
   ruled rows stacked read as a ladder rather than as a level and the level
   inside it - the same trade `.subtabs.users` makes. */
.subtabs.daymove-tabs { border-bottom: 0; margin: -4px 0 10px; }
.subtabs.daymove-tabs .subtab { font-size: var(--fs-base); padding: 4px 10px; }

/* --- Eval Profile's picker ------------------------------------------------

   A search box over a dense table, where there used to be a `<select>` of forty
   positions. The select stopped being a control the moment the tick-boxes could
   put eight hundred names in it: a dropdown that long cannot be scanned, cannot
   be sorted, and cannot be searched except by whatever prefix match the browser
   happens to offer.

   Everything below is sized for density. The table is the tallest thing on the
   tab and the two cards under it are what the reader is heading for, so a row
   that cost 34px would push the buttons off a laptop screen at twelve rows. */
.daymove-picker { margin: 10px 0 8px; }

/* The row the shared stock picker sits on, and since the row under it merged
   in, the status menu and the Columns button too: all three answer the same
   question - which rows, in what order - so one row is one place to look, and
   the ~30px the second row cost goes back to the table. The search box is
   still given room to grow and a ceiling to stop at: the picker's box sizes
   itself to a toolbar by default, and a company name typed into a 60px field
   is the failure the pair below avoids. On a narrow screen the row wraps and
   the controls land underneath, which is where they used to live anyway. */
.daymove-find {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 18px;
  margin: 0 0 6px;
}
/* On the row's far edge, where the old controls row's space-between put it: a
   settings control rather than a filter, and the gap between them says so. */
.daymove-find [data-daymove-colhost] { margin-left: auto; }
.daymove-find .stock-pick { flex: 1 1 320px; min-width: 0; max-width: 620px; }
.daymove-find .stock-pick .input-icon { display: flex; }
.daymove-find .stock-pick input[type="text"] {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  font-size: var(--fs-base);
}
/* Wider than the 420px the picker asks for elsewhere, because it opens over a
   table rather than over a toolbar and has the room - four columns of symbols
   instead of three, on the tab where the list is longest. */
.daymove-find .stock-pick-panel { width: min(560px, calc(100vw - 24px)); }

.daymove-statusfilter { display: inline-flex; align-items: center; gap: 8px; }
.daymove-statusfilter select { font-size: var(--fs-sm); }

/* The Columns menu. `.dropdown` and `.dropdown-panel` are the grid's own, so the
   panel is the same 330px card in the same place with the same rows - what is
   here is only the line of explanation above the buttons, which the grid does
   not need because its own controls are labelled by the toolbar around them. */
.daymove-colhint {
  margin: 0 0 6px;
  font-size: var(--fs-xs);
  line-height: 1.45;
}
.daymove-colpanel .picker-row .icon { opacity: .75; flex: none; }

/* The table scrolls in its own box rather than growing the page: the two cards
   under it are what the reader is heading for, and eight hundred rows pushing
   them below the fold would make the tick-boxes a trap. The box is five rows
   tall - the header, five rows, and the top sliver of a sixth saying there is
   more to scroll to. */
.daymove-tablewrap {
  max-height: 158px;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
}
.daymove-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}
/* Once the width engine has taken the columns over it writes an explicit width
   equal to the sum of them, and `min-width: 100%` is what still lets the table
   fill its box - without it, narrowing one column would leave a gap down the
   right of the card. The same pair the grid and the Reports log state, for the
   same reason. */
.daymove-table.resizable { width: max-content; min-width: 100%; }
/* A head does three things - click sorts, drag moves, the right edge resizes -
   so it must not also be selectable text, or the drag reads as a botched
   double-click on half the attempts. The cursor stays a pointer rather than a
   grab: sorting is the gesture people come for and dragging is the one they
   discover, and `.rz-edge` overrides it on the few pixels that resize. */
.daymove-table th[data-daymove-col] { user-select: none; cursor: pointer; }
.daymove-table th[data-daymove-col]:hover { color: var(--text); }
.daymove-table th[data-daymove-col].rz-edge { cursor: col-resize; }
.daymove-th { display: inline-flex; align-items: center; gap: 5px; }
.daymove-th .icon { opacity: .65; flex: none; }
/* The arrow is the only thing on screen saying how the table is ordered, so it
   is the accent rather than the muted grey the rest of the head wears. The digit
   beside it on a second sort is the grid's own device and the same size. */
.daymove-sort { color: var(--accent); font-size: var(--fs-2xs); margin-left: 1px; }
/* Absorbs whatever the real columns do not fill. Never a sort target, never a
   drag target, and not a column the reader can see or hide. `.rz-filler`'s own
   rule is scoped to `table.grid`, so what it resets there is restated here. */
.daymove-table th.rz-filler, .daymove-table td.rz-filler {
  padding: 0;
  border-right: 0;
  cursor: default;
}
/* Sticky, because the columns are four numbers and a reader thirty rows down a
   watch list has no other way of knowing which is the month. */
.daymove-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 5px 10px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
}
.daymove-table td { padding: 3px 10px; border-bottom: 1px solid var(--line-soft); }
.daymove-table tr:last-child td { border-bottom: 0; }
.daymove-num { text-align: right; white-space: nowrap; }
.daymove-blank { color: var(--muted); }

/* The section headings - the book, the roster, and what was taken off it. They
   are three different universes rather than three sorts of the same one, which
   is why a sort chip cannot replace them the way it replaced Down and Up. */
.daymove-section th {
  position: sticky;
  top: 26px;
  z-index: 1;
  padding: 4px 10px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-weight: 600;
}
.daymove-section-count { font-variant-numeric: tabular-nums; opacity: 0.7; }

.daymove-row { cursor: pointer; }
.daymove-row:hover { background: var(--row-hover); }
/* Two highlights meaning two things: the keyboard cursor is where the arrows
   have walked to, the selection is what the cards below are about. The cursor is
   an outline and the selection a fill, so a reader arrowing past the selected
   row can see both at once. */
.daymove-row.is-active > td { box-shadow: inset 0 1px 0 var(--accent), inset 0 -1px 0 var(--accent); }
.daymove-row.is-selected { background: var(--accent-soft); }
.daymove-row.is-selected .daymove-c-name strong { color: var(--accent); }
/* Every column used to state a percentage width here, keyed on its position in
   the row - `nth-child(5)` was Verdict and `:last-child` was Analysed. None of
   that survives a table whose columns the reader can hide and reorder: the
   fifth cell is whatever they dragged into fifth place, and a width nailed to
   the position would be worn by the wrong column the moment they did.
   `columnWidths` in `daymove.js` states a default per column NAME instead, and
   the width engine writes them into the colgroup. What is left here is only how
   a cell BEHAVES once it has been given a width. */
.daymove-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Symbol first and loud, company name after it and quiet: the symbol is what
   was typed to get here and what every other page sorts on - so the symbol is
   the half that must never be the half that gets clipped. */
.daymove-c-name strong { font-weight: 600; }
.daymove-c-name span {
  margin-left: 7px;
  color: var(--muted);
  font-size: var(--fs-xs);
}

/* Soft tints rather than coloured text alone: the whole point of these two
   columns is that a reader sees which way the book went without reading any of
   it, and a 12px signed number does not carry that on its own.

   `--positive-soft` and `--negative-soft`, not a fresh `color-mix` here. The
   pair is mixed once in the token block so a custom theme that overrides
   `--positive` takes every tinted surface with it, and - the load-bearing half -
   **`--positive` in this application is blue, not green**, a deliberate owner
   decision because red-vs-green is the pair that does not separate for a
   red/green colour-blind reader. A hand-written pastel green here would be the
   one thing on screen that ignored that. */
.daymove-move-good { background: var(--positive-soft); color: var(--positive); }
.daymove-move-bad { background: var(--negative-soft); color: var(--negative); }
.daymove-move-plain { color: var(--muted); }

/* The five status columns - one per question the page can ask, plus the
   Analysed stamp that is about none of them. `.daymove-cell` already clips and
   nowraps every cell in the table, so what is left for these is only that their
   marks lay out as a row rather than wrapping the word under the icon. */
.daymove-c-verdict .daymove-mark,
.daymove-c-report .daymove-mark,
.daymove-c-stance .daymove-mark,
.daymove-c-premortem .daymove-mark,
.daymove-c-analysed .daymove-mark { max-width: 100%; }
.daymove-mark { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-xs); }
.daymove-mark .icon { flex: none; }
.daymove-mark.none { color: var(--muted); }
.daymove-mark.fresh { color: var(--positive); }
.daymove-mark.fresh.bad { color: var(--negative); }
.daymove-mark.fresh.plain { color: var(--muted); }
/* The bear case's middle answer - "monitorable", real damage that is bounded.
   Amber rather than the base `--positive`, which is what an unclaimed tone
   falls through to and would draw the one severity worth watching in the
   colour that means nothing to see. Same amber as `.stale` below, and for the
   same meaning: worth a look, not a fault. */
.daymove-mark.fresh.warn { color: var(--warn); }
/* Amber, not red. A verdict past its week is worth asking again, which is not
   the same thing as something being wrong. */
.daymove-mark.stale { color: var(--warn); }
.daymove-mark.asking { color: var(--accent); }
/* An age, not a state: the two status columns beside it use colour to say
   whether there is something worth pressing, and a date is never that. So the
   text is quiet and only the mark carries the page's model hue - which is what
   keeps the column readable as one thing rather than as a grey date per row. */
.daymove-mark.asked { color: var(--muted); }
.daymove-mark.asked .icon { color: var(--accent); }
/* The one moving thing on the panel, and it moves because "being produced right
   now" is the only row state a still picture cannot tell from "done". */
.daymove-mark.asking .icon { animation: daymove-spin 1.4s linear infinite; }
@keyframes daymove-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .daymove-mark.asking .icon { animation: none; }
}

.daymove-empty td { padding: 14px 10px; color: var(--muted); text-align: center; }
/* Empty for the whole time nothing is narrowing the list, which is most of the
   time. Collapsed rather than reserved, so the cards below do not sit under a
   blank line. */
.daymove-found { margin: 6px 0 0; font-size: var(--fs-xs); }
.daymove-found:empty { display: none; }

/* The chosen stock's figures. A row of small label-over-value pairs rather than
   a sentence: the reader is checking a number against what they already saw on
   the grid, and a paragraph makes them read it twice. */
.daymove-move { display: flex; flex-wrap: wrap; gap: 4px 18px; align-items: baseline; }
.daymove-figure { display: inline-flex; align-items: baseline; gap: 5px; }
.daymove-figure-label { color: var(--muted); font-size: var(--fs-xs); }
.daymove-figure strong { font-variant-numeric: tabular-nums; font-size: var(--fs-base); }
.daymove-figure strong.good { color: var(--positive); }
.daymove-figure strong.bad { color: var(--negative); }
.daymove-figure strong.plain { color: var(--text); }
/* A quote taken before the last close is right to show and wrong to reason
   from, so it is marked rather than hidden. */
.daymove-stale { color: var(--muted); font-style: italic; }

.daymove-actions { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; margin: 10px 0 4px; }
/* The way back to the stock itself, under the cards. Marked as a link rather
   than left to inherit body text: it sits below a pair of bordered boxes and a
   run of explanatory grey, and an unstyled anchor between the two reads as
   neither. */
.daymove-actions a { color: var(--accent); text-decoration: none; }
.daymove-actions a:hover { text-decoration: underline; }

/* --- the two cards ---------------------------------------------------------
   One card per question - why it moved, and what to do now - side by side under
   the chosen stock's figures. They are the only two things on this tab that
   spend anything, and the whole layout is built around one requirement: at the
   moment of clicking, a reader must be able to tell which call they are buying.

   So each card is a box with its own border rather than two buttons in a row.
   A row of buttons under one heading reads as one control with two settings,
   which is precisely the wrong idea - these are two different calls, of two
   different lengths, against two different guards, billed separately.

   `auto-fit` with a floor rather than a fixed two columns: at a narrow width
   they stack, and a card squeezed to 180px would wrap its verdict pill onto
   four lines and lose exactly the at-a-glance reading it exists for. */
.daymove-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  /* Two named rows - the question, then the runs already on file for it - and
     each stack spans both as a subgrid. That is what makes the four cards in a
     row end on one line and the receipts under them start and end on another,
     however differently long the four blurbs and the four headlines are. Left
     to itself each column would size its own two boxes and the row would read
     as four separate panels that happen to sit side by side. */
  grid-template-rows: auto auto;
  gap: 12px;
  margin: 12px 0 4px;
}
.daymove-card {
  /* `--card-edge` rather than `--line`, for the reason the widget cards use it:
     two boxes at full strength beside a picker and a strip of figures draw more
     furniture than content. It is themed - see the token. */
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.daymove-card > p { margin: 0; }
/* The question and the button that buys an answer to it, on one line. The
   trigger is at the top right of every card rather than at the foot of it: the
   foot of a card is no longer the foot of anything now that the runs on file
   hang underneath, and a control buried in the middle of a column is one a
   reader has to hunt for. Four triggers in four identical corners is the thing
   that can be found without reading. */
.daymove-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
/* Never squeezed by a long question - the title wraps instead, which is the
   right half to spend the width on. The four buttons line up with each other
   because they are all in this corner and every card in a row is one height -
   see `.daymove-stack`. It used to be `margin-top: auto` pushing each to the
   foot, which lined them up for the same reason and put them where nothing
   else was. */
.daymove-card-head .btn { flex: none; }
.daymove-card-title { margin: 0; font-size: var(--fs-base); }
/* The question itself, in the reader's own words - "Why did it fall?", "Is
   today a moment to add more?". Body weight rather than grey: it is the label
   for the button below it, not a footnote. */
.daymove-card-ask { color: var(--text); }
/* What is already on file. A baseline row, because it mixes a pill with a run
   of small grey text and the two would otherwise sit on different lines. */
.daymove-card-state {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  min-height: 22px;
}
.daymove-card-when { color: var(--muted); font-size: var(--fs-xs); }
.daymove-card-none { color: var(--muted); font-size: var(--fs-sm); }
/* A report on file that explains an earlier session, said on the card as well
   as over the report - the button beside this line is the one that buys
   today's, and that is the whole reason it is worth repeating. */
.daymove-card-stale { color: var(--negative); font-size: var(--fs-xs); }
/* The foot-of-the-card button. The four cards on Eval Profile do not use it any
   more - their triggers are at the top right, in `.daymove-card-head` - but the
   Scoreboard and Universe both borrow `.daymove-card` whole and still put their
   one button at the bottom, which is right for them: each is a single card with
   nothing under it, so the foot of the card is still the foot of something.
   Kept here rather than moved to either of them, because it is the shared
   card's own rule and two copies is how they would drift. */
.daymove-card-act { margin-top: auto; }
/* Nothing else in the application disables a `.btn`, so without this the only
   difference between "asking" and "press me" would be the word on it - and the
   spinner is a paragraph away from where the click lands. */
.daymove-card .btn[disabled] { opacity: .45; pointer-events: none; }
.daymove-card-blurb { font-size: var(--fs-xs); }
/* The wait and the failure, inside the card that owns them. Either card can be
   working while the other is idle, and a message under the pair would not say
   which call it belonged to. */
.daymove-card .hint, .daymove-card .advice-warn { margin: 0; font-size: var(--fs-xs); }
/* The card's own icon, on the title and again on the button. Three cards now,
   three different calls, and the picture is what tells them apart before a word
   is read - so it is sized to the text beside it and given the muted colour on
   the heading (where it is a marker) and the button's own colour on the control
   (where it is part of the label). */
.daymove-card-title .icon { vertical-align: -2px; margin-right: 6px; color: var(--muted); }
.daymove-card-head .btn .icon, .daymove-card-act .btn .icon {
  vertical-align: -3px;
  margin-right: 5px;
}
/* Each card's identity hue, on the rule down its left edge. The standing rule
   about colour, applied to the one place on this page where three controls that
   look identical buy three different things: the reader learns the position of
   the card and the colour together, and the colour is what survives a redraw
   that moved them. `--accent` for the move question, positive for the one that
   asks whether to add and warn for the one that asks whether to sell - which is
   the reading of the pair, not a judgement about the answer either returns. */
.daymove-card-move { border-left: 2px solid color-mix(in srgb, var(--accent) 55%, transparent); }
.daymove-card-advice { border-left: 2px solid color-mix(in srgb, var(--positive) 55%, transparent); }
.daymove-card-exit { border-left: 2px solid color-mix(in srgb, var(--warn) 55%, transparent); }
/* The adversary gets the negative hue outright rather than a warning tint,
   and it is the one card whose colour is a statement about the question
   rather than about the answer: the other three can come back saying the
   stock is fine, and this one is only ever asked to find what is wrong. */
.daymove-card-premortem {
  border-left: 2px solid color-mix(in srgb, var(--negative) 60%, transparent);
}

/* --- the runs under a card ------------------------------------------------
   Every answer already on file for one question, in boxes directly beneath the
   card that bought it. They were a table at the foot of the panel until now -
   the Reports tab's own, narrowed to this stock - and the arrangement asked a
   reader looking at "How is the stock?" to scroll past four cards and a due
   strip and then read a Question column to work out which rows were the ones
   they wanted. Four questions each heading their own answers says that with no
   column and no scroll. The table stays on the Reports tab, where the subject
   really is a list across every stock and every question.

   The STACK is the grid item, not the card: `.daymove-cards` is an auto-fit
   grid, and a card and its runs as two separate items would let the runs wrap
   into the next question's column - which is the one mistake this layout must
   not make, because a bear case listed under "How is the stock?" is a wrong
   answer drawn convincingly.

   The stack takes the PARENT's two rows rather than making its own, which is
   what makes every card in a row one height and every run block under them
   another - see the rule itself below. */
.daymove-stack {
  display: grid;
  /* The stack takes the parent's two rows rather than making its own, so every
     card in a row is the height of the tallest card and every run block is the
     height of the tallest run block. Without it the leftover height went to
     whichever column had the fewest answers on file, and the four boxes ended
     at four different places. */
  grid-template-rows: subgrid;
  grid-row: span 2;
}
/* Where subgrid is not understood the stack still has to be a column of two,
   and the row simply loses its shared baseline - which is a worse-looking
   panel, not a broken one. */
@supports not (grid-template-rows: subgrid) {
  .daymove-stack { grid-template-rows: 1fr auto; gap: 8px; }
}

/* Fills the row the subgrid gave it, and hands that height on to the runs
   inside - one answer stretches to the block, two share it. A box that sized
   itself to its own headline is what made the four columns end unevenly. */
.dmlog-runs { display: flex; flex-direction: column; gap: 6px; }
/* Reading, rather than nothing on file. The two are different sentences and the
   card's own state line only says the second - see `runCards`. */
.dmlog-runs-none { margin: 0; font-size: var(--fs-xs); }

/* One run. Quieter than the card above it - no border on three sides, a flat
   inset panel - because the card is the control and these are its receipts: a
   second box at the same weight directly underneath would read as a second
   thing to press. The left edge is the exception, and carries the same tone
   rail a row of the log does. */
.dmlog-run {
  display: flex;
  flex-direction: column;
  /* Grows into whatever height the row's tallest run block claimed - one answer
     fills the block, two share it. Without it a box sized itself to its own
     headline and the four columns ended in four different places. */
  flex: 1 1 auto;
  gap: 3px;
  padding: 6px 10px 7px;
  border-radius: var(--radius-sm);
  border-left: 2px solid transparent;
  background: var(--panel-2);
  cursor: pointer;
}
.dmlog-run:hover { background: var(--row-hover); }
/* The rail, painted from `data-dmlog-tone` for the reason the table's is:
   `.good` and `.bad` are worn by a dozen unrelated things in this stylesheet,
   and a tinted receipt is not something any of them should be able to turn on.
   `plain` gets neither, which is what makes the marked ones mean something. */
.dmlog-run[data-dmlog-tone="good"] {
  border-left-color: color-mix(in srgb, var(--positive) 55%, transparent);
  background: color-mix(in srgb, var(--positive) 5%, transparent);
}
.dmlog-run[data-dmlog-tone="bad"] {
  border-left-color: color-mix(in srgb, var(--negative) 55%, transparent);
  background: color-mix(in srgb, var(--negative) 5%, transparent);
}
/* Hover still wins, stated rather than relied on - the tone rules above tie
   with `.dmlog-run:hover` on specificity, and a tie goes to source order. */
.dmlog-run[data-dmlog-tone]:hover { background: var(--row-hover); }

/* When it was asked, what it is anchored to, who paid and what it cost - one
   line of small grey, because none of it is the answer. The cost is pushed to
   the far end, where the reader's eye already goes for money on this page. */
.dmlog-run-when {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 8px;
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-xs);
}
.dmlog-run-stamp { font-variant-numeric: tabular-nums; white-space: nowrap; }
.dmlog-run-about { font-variant-numeric: tabular-nums; }
/* Both are `display: block` on a table row, where they sit under the figure
   they qualify. Here the whole strip is one line, so they go back inline. */
.dmlog-run .dmlog-c-session, .dmlog-run .dmlog-today {
  display: inline;
  /* And the space `display: block` used to give them: without it "9 days ago"
     and "due a refresh" run together into one word. */
  margin-left: 5px;
}
.dmlog-run .advice-asked-by { display: inline; }
/* The cost, and the chevron after it. `margin-left: auto` on the cost is what
   splits the strip into a left half about the question and a right half about
   the bill. */
.dmlog-run-cost {
  margin-left: auto;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}
.dmlog-run-when > .icon { flex: none; color: var(--muted); align-self: center; }
/* The chevron says the box opens. Faint until the pointer is on it, for the
   reason the table's is: an arrow at full strength on five boxes is five
   arrows competing with the answers beside them. */
.dmlog-run > .dmlog-run-when > .icon { opacity: .45; }
.dmlog-run:hover > .dmlog-run-when > .icon { opacity: 1; color: var(--accent); }

/* What it concluded - the badge, and whatever the kind hangs beside it. The
   same marks the log's Answer column draws, from the same function. */
.dmlog-run-answer {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 8px;
  margin: 0;
}
/* The one-sentence answer. Clamped to three lines rather than left to run: five
   receipts each three sentences long would push the next question's card off
   the screen, and the whole answer is one click away in the popup. */
.dmlog-run-headline {
  margin: 0;
  font-size: var(--fs-sm);
  line-height: 1.4;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}
/* How many mechanisms are on file, beside the severity pill. Tabular so a
   reader scanning four cards is comparing digits rather than glyph widths. */
.premortem-count {
  color: var(--muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

/* --- the exit stance ----------------------------------------------------
   The three things the exit-and-trim answer draws that no other answer here
   has: the weight band with today's weight on it, the tripwire table, and the
   strip of what the stock has been valued at before. Scoped on their own
   prefix rather than added to `.advice-*`, because the Add-More verdict and
   the day-move report both draw inside `.advice-verdict` too and neither has
   any of these. See `frontend/js/exitadvice.js`. */

/* Two tones the pill has never needed before. `.pill` already carries `ok` and
   `bad`; the thesis state has four values across three colours, and the middle
   one - STRAINED - is genuinely neither. Declared beside the pill's own tones
   rather than as a local class, so a pill reads the same wherever it is drawn. */
.pill.good { color: var(--positive); border-color: color-mix(in srgb, var(--positive) 50%, transparent); }
.pill.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 50%, transparent); }
.pill.plain { color: var(--muted); }

.exit-band { margin: 6px 0; }
.exit-band-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin: 0 0 6px;
}
.exit-band-range { font-weight: 600; font-variant-numeric: tabular-nums; }
/* The bar is the whole point of drawing a band rather than printing two
   numbers: above the ceiling and below the floor are two different actions, and
   a reader should be able to see which one they are in without arithmetic.
   `position: relative` on the track, absolute on the two marks - so the span
   and the pin are placed by percentage against the same origin. */
.exit-band-bar {
  position: relative;
  height: 8px;
  margin: 0;
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  border: 1px solid var(--card-edge);
  overflow: hidden;
}
.exit-band-span {
  position: absolute;
  top: 0;
  bottom: 0;
  background: color-mix(in srgb, var(--positive) 28%, transparent);
}
/* Today's weight, as a pin rather than a fill: it is a point on the scale, and
   a bar growing from zero would read as a quantity to compare against the band
   rather than as a position inside it. Two pixels wide and full height so it
   stays visible where it lands inside the shaded span. */
.exit-band-now {
  position: absolute;
  top: -1px;
  bottom: -1px;
  width: 2px;
  margin-left: -1px;
  background: var(--muted);
}
.exit-band-now.good { background: var(--positive); }
.exit-band-now.bad { background: var(--negative); }
.exit-band-now.warn { background: var(--warn); }

/* The tripwires. It rides on `.advice-factors` for its frame and adds only the
   three column widths, for the reason the factor table is shared at all: two
   tables of evidence under one answer must not be two designs. */
.exit-wires .exit-wire-level { white-space: nowrap; font-variant-numeric: tabular-nums; }
.exit-wires .exit-wire-level .icon { vertical-align: -3px; margin-right: 4px; }
.exit-wires .exit-wire-col { white-space: nowrap; }
.exit-wires .exit-wire-col code {
  display: inline-block;
  margin-right: 6px;
  font-size: var(--fs-xs);
}

/* What it has been valued at. A scrolling strip rather than a wrapping one: the
   readings are in time order and a wrap would put the oldest under the newest,
   which reads as two series. `overflow-x` here rather than on the block, so the
   sentence under it stays put while the numbers scroll. */
.exit-val-strip {
  display: flex;
  gap: 10px;
  margin: 0 0 4px;
  overflow-x: auto;
  padding-bottom: 4px;
}
.exit-val-point { display: flex; flex-direction: column; white-space: nowrap; }
.exit-val-point b { font-variant-numeric: tabular-nums; }
.exit-val-point span { color: var(--muted); font-size: var(--fs-xs); }

/* The line under the two pills when the stance and the thesis disagree. Given
   the accent rather than a warning colour: it is the most interesting thing on
   the answer, not a fault in it. */
.exit-split { border-left: 2px solid var(--accent); padding-left: 8px; }

/* --- the pre-mortem ------------------------------------------------------
   The adversary's panel, and it is drawn deliberately unlike the two stance
   answers beside it. No factor table, no weight band, no case-for column -
   because the schema behind it has none of those, and a balanced-looking frame
   around a one-sided finding would hand the balance back in the layout after
   the prompt had spent a paragraph refusing to give it. */

/* Said in words on the panel and reinforced here by being the quietest line on
   it: the severity is a finding, not a stance. A reader who has learnt the two
   cards to the left will read a red pill as SELL, and this sentence is the only
   thing standing between them and that. Accent rail rather than a warning
   colour - it is a clarification, not a fault. */
.premortem-not-a-stance {
  border-left: 2px solid var(--accent);
  padding-left: 8px;
  margin: 4px 0 8px;
}

/* Whether the answer actually reached the web. Its own line above the finding
   rather than a footnote under it, because it changes what the finding is
   worth. The good case is a hint; the bad case renders as `.advice-warn` and
   inherits its colour from there. */
.premortem-reach .icon { vertical-align: -3px; margin-right: 4px; }

/* --- the kill paths, which are the body of the answer ------------------- */
.premortem-paths {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  counter-reset: none;
}
.premortem-path {
  border-left: 2px solid color-mix(in srgb, var(--negative) 45%, transparent);
  padding: 2px 0 2px 10px;
}
/* A path with nothing cited behind it. Dashed rather than a different colour:
   it is not a *worse* mechanism, it is one with no evidence under it, and the
   broken line says provisional in a way a red border would say dangerous. */
.premortem-path.unevidenced {
  border-left-style: dashed;
  border-left-color: color-mix(in srgb, var(--muted) 60%, transparent);
}
.premortem-path-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  margin: 0 0 4px;
}
/* The ordinal. The model returns these most-dangerous-first and that order is
   part of the answer, so it is numbered rather than bulleted - a reader
   quoting "the second kill path" has to be able to point at one. */
.premortem-path-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--negative) 18%, transparent);
  color: var(--negative);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.premortem-path-head .advice-point { flex: 1 1 260px; }
.premortem-damage { margin: 0 0 4px; }
.premortem-damage .icon,
.premortem-path .advice-weight-why .icon { vertical-align: -3px; margin-right: 4px; }
/* What is already visible behind the mechanism. Indented under it and marked
   with the same rule the evidence lists elsewhere use, so the eye reads it as
   support for the line above rather than as a second list of claims. */
.premortem-evidence {
  list-style: disc;
  margin: 0 0 4px;
  padding-left: 18px;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.premortem-evidence li { margin: 2px 0; }
/* The sentence in place of that list when there is nothing behind the path. It
   takes the warning colour because the absence is the finding. */
.premortem-nothing { margin: 0 0 4px; font-size: var(--fs-xs); }

/* What is already wrong, as opposed to what could go wrong. Rides on
   `.advice-points.bad` for its frame and adds only the room the kind pill needs
   on the same line as the finding. */
.premortem-visible li { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.premortem-visible .advice-point { flex: 1 1 240px; }
.premortem-visible .advice-evidence { flex: 1 1 100%; }

/* The receipt. Same treatment the why-it-moved report gives its citations - one
   line per page, the searches as inline code under them - because it is the
   same object produced by the same tool and two drawings of it would be two
   designs for one thing. */
.premortem-cites { margin: 0 0 6px; }
.premortem-searches code {
  display: inline-block;
  margin: 0 6px 4px 0;
  font-size: var(--fs-xs);
}

/* --- the due strip -------------------------------------------------------
   Under the four cards and ruled off from them, because it is the one block on
   this panel that is about the *book* rather than about the selected stock -
   and because what it offers is a place to click next, which belongs after the
   thing being clicked rather than above it.

   Deliberately not styled as a call to action. It is a suggestion of where the
   next spend is worth making, and a strip of primary buttons would read as a
   queue somebody is meant to work through - which is exactly the reading that
   would turn a considered feature into forty API calls. */
.premortem-due {
  margin: 10px 0 4px;
  padding: 10px 12px;
  border: 1px solid var(--card-edge);
  border-left: 2px solid color-mix(in srgb, var(--negative) 60%, transparent);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
/* The head is the drawer's own summary, so it is the whole click target rather
   than a paragraph above one. The browser's disclosure triangle is dropped for
   the chevron in the markup, exactly as the review drawer does it: the default
   marker cannot be coloured or aligned with the shield beside it, and two
   triangles would be worse than one. */
.premortem-due-head {
  display: flex;
  align-items: flex-start;
  gap: 7px;
  margin: 0;
  cursor: pointer;
  font-size: var(--fs-sm);
  list-style: none;
}
.premortem-due-head::-webkit-details-marker { display: none; }
.premortem-due-caret {
  display: inline-flex;
  margin-top: 1px;
  color: var(--muted);
  transition: transform 120ms ease;
}
.premortem-due[open] .premortem-due-caret { transform: rotate(90deg); }
.premortem-due-title { flex: 1; }
.premortem-due-head .icon { vertical-align: -3px; margin-right: 6px; color: var(--muted); }
.premortem-due-head .hint { display: block; margin-top: 2px; font-size: var(--fs-xs); }
.premortem-due-none { margin: 10px 0 4px; }
.premortem-due-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  /* Capped and scrolled rather than left to run. A real book puts eighty-odd
     names here, and opening the drawer should not push the actions under it
     four screens down the page. */
  max-height: 300px;
  overflow-y: auto;
}
.premortem-due-name {
  display: flex;
  flex-direction: column;
  min-width: 150px;
}
.premortem-due-name a { font-weight: 600; }
.premortem-due-name .advice-evidence { font-size: var(--fs-xs); }
/* The one already open. Marked rather than hidden: a reader who arrived at this
   stock from the strip should be able to see that is where they are. */
.premortem-due-name.here a { color: var(--accent); }
.premortem-due-name.here { border-left: 2px solid var(--accent); padding-left: 6px; }

/* --- one stock's answers, under the two cards ---------------------------
   What replaced the report that used to unroll into this panel. Both answers
   used to be drawn here in full - the why-it-moved report and, ruled off under
   it, the verdict - which pushed the two paid buttons off the top of the screen
   and could still only ever show the latest one of each. It is a list now, and
   the answer opens in a popup: the Reports table with one stock's rows in it.

   So it wears `.reports-panel`, and that is the whole point. The density, the
   header marks and the rail down a row's left edge are all scoped on that class
   one section away (see the note over it); a second set written here would be
   the copy that drifts, and a reader crossing between the two would be reading
   two people's work. What is only true here is where it sits: ruled off from
   the cards above, because those are where money is spent and this is the
   record of what spending it produced. */
.daymove-reports {
  margin-top: 18px;
  border-top: 1px solid var(--card-edge);
  padding-top: 12px;
}
/* Empty almost all of the time, and it must take up no room at all when it is
   - the rule above it would otherwise draw a line under the panel with nothing
   beneath it. The runs moved into the cards, so what is left in here is a
   failure line and the popup an opened run draws into; both are usually absent,
   and `StockReportsSection.markup` returns the empty string rather than a
   string of newlines so that this selector can actually match. */
.daymove-reports:empty { margin: 0; border-top: 0; padding: 0; }

/* The drivers table borrows `.advice-factors` whole; only the first column
   differs, because a share of one day's move is narrower than a factor weight. */
.daymove-drivers .advice-weight { width: 120px; }

/* Links inside a report. They go to pages a model chose, so they are marked as
   links properly rather than left to look like text - the whole argument for
   showing the receipt is that a doubting reader can go and read it. */
.daymove-news a, .daymove-cites a, .advice-weight-why a { color: var(--accent); text-decoration: none; }
.daymove-news a:hover, .daymove-cites a:hover, .advice-weight-why a:hover { text-decoration: underline; }
/* A cited URL can be a hundred characters of query string; the list must wrap
   inside the panel rather than push it sideways. */
.daymove-cites li { word-break: break-word; }
.daymove-searches { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 6px; }
.daymove-searches code {
  background: var(--panel-2);
  border-radius: var(--radius-xs);
  padding: 1px 6px;
  font-size: var(--fs-xs);
}
.daymove-sourcing { margin: 6px 0 0; }

/* --- Review Board: the whole book, read at once --------------------------
   The AI page's one section with no picker in it. Everything else on this page
   is a list of stocks with an answer hung on each; this is one book, one button
   and one answer.

   It is laid out as **five anchored sections under a sticky pill bar, with a
   jump rail down the right edge**, and that is a response to length rather than
   a taste for chrome. The answer alone is five scores with their reasoning, five
   actions with their evidence, the funding pairs, a blind spot and the gaps -
   and under it, always, today's book with however many groupings a forty-name
   portfolio produces. As one column it was a page you scrolled through hunting;
   the bar, the anchors and the rail make it one you navigate.

   Three colour systems meet here and are kept apart on purpose:

   * the **ticker** is a judgement - healthy / watch it / wrong - and uses the
     semantic pair, which is --positive (blue), --warn and --negative. Never a
     green-amber-red traffic light: this app's reader does not separate red from
     green, which is the whole reason --positive is blue;
   * the **category badges** are an identity - which bucket - and use the eight
     --cat-* hues, on the same licence the Fetch-menu group hues have;
   * the **action cards** are money in or money out, and reuse the semantic pair
     a third time because that is exactly what they mean.

   No cell anywhere relies on colour alone: a metric prints its number and the
   word under it, a badge prints its name and its mark, an action prints its
   verb. */
.review-panel { padding-top: 2px; }

/* --- the floating dock ---------------------------------------------------
   One navigation where there were two. This was a sticky row of pills across the
   top and, down the right edge, a second floating rail pointing at the five
   actions - one horizontal navigation and one vertical one for a single page,
   each holding a strip of screen the review itself could have used.

   Sticky against `main`, which is the element that actually scrolls this
   application (see the rule on `main` near the top of this file). `top: -10px`
   rather than 0 because `main` carries 10px of padding: at 0 the dock would park
   ten pixels below the top of the scroll port and let a sliver of content run
   under the gap.

   This is the one surface in the application that is genuinely glass, and it
   earns it rather than wearing it: it is the only thing here with content
   moving underneath, so a blur behind it says "the page is still going past"
   in a way an opaque panel cannot. `backdrop-filter` is a progressive
   enhancement - the browser without it gets a 76% panel over the page ground,
   which is a slightly translucent card and perfectly legible. The `-webkit-`
   line comes first, for the browser that shipped the prefix and kept it. */
.review-dock {
  position: sticky;
  top: -10px;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 7px;
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--panel) 76%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  backdrop-filter: blur(14px) saturate(1.2);
  /* Lifted off the wash, then rimmed in the accent. The rim is 10% - a hint
     that the panel is emitting rather than reflecting, not a halo competing
     with the lit item inside it. */
  box-shadow: var(--shadow-md),
    0 0 22px color-mix(in srgb, var(--accent) 10%, transparent);
  /* Taller than the port it sticks in and it would scroll its own bottom off
     the screen for good. Five sections plus five nested actions is close enough
     to that on a laptop to be worth the guard. */
  max-height: calc(100vh - 24px);
  overflow-y: auto;
}
.review-dock-item {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 9px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-base);
  text-align: left;
  cursor: pointer;
}
.review-dock-item:hover { background: var(--panel-2); color: var(--text); }
/* The section being read. Filled and glowing rather than merely bordered,
   because a column of six near-identical outlines gives a reader nothing to
   find themselves by at a glance. */
.review-dock-item.on {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  color: var(--accent);
  box-shadow: 0 0 16px color-mix(in srgb, var(--accent) 26%, transparent);
}
.review-dock-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.review-dock-item .icon { flex: 0 0 auto; }
.review-dock-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The five actions, hanging off the Action Queue item rather than floating on
   an edge of their own. The rule down their left is what says "inside that",
   and it is the only piece of hierarchy this dock draws. */
.review-dock-jumps {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin: 2px 0 4px 15px;
  padding-left: 8px;
  border-left: 1px solid var(--line-soft);
}
.review-dock-jump {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px 6px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-xs);
  text-align: left;
  cursor: pointer;
}
.review-dock-jump:hover { background: var(--panel-2); color: var(--text); }
.review-dock-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The verb's own colour on the mark alone, not on the label: a column of five
   coloured sentences is a column nobody can scan, and the mark is enough to
   tell a trim from a buy at this size. */
.review-dock-jump.good svg { color: var(--positive); }
.review-dock-jump.bad svg { color: var(--negative); }
.review-dock-jump.warn svg { color: var(--warn); }
.review-dock-jump span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the header and its ticker ------------------------------------------- */
.review-head { margin: 10px 0 4px; }
.review-headline { margin: 0; font-size: var(--fs-lg); line-height: 1.5; }
.review-byline { margin: 4px 0 0; color: var(--muted); font-size: var(--fs-xs); }

/* Five cards, one row, equal width. A grid rather than a flex row so a cell is
   sized by the row and not by its own content - read as a row of comparable
   scores, cells sized by their text would make the dimension with the longest
   reasoning look like the important one.

   The floor is 240px rather than the 160 it was, because a card holds a
   sentence now: the score and the reason it is that score used to be two
   screens apart, and 160px of it would have been six words per line. `auto-fit`
   still wraps them on a narrow window rather than squeezing five columns of
   prose into a phone. */
.review-ticker {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  gap: 7px;
  /* Each cell is as tall as its own disclosure now, not as tall as the tallest
     note in the row. That is the whole compaction: five shut cards are five
     two-line summaries, and one open one grows on its own. */
  align-items: start;
}
/* A `<details>` rather than a div, which is what makes the matrix compact: the
   scannable half - mark, name, meter, figure, word - is the summary and is
   always drawn, and the model's sentence is the body. Native, so it is
   keyboard-reachable, announced by a screen reader and open on print without a
   line of script, exactly as the long-tail drawer below is. What is open is the
   panel's own state rather than the markup's, because this markup is rebuilt on
   every refresh tick - see `ReviewSection.dims`. */
.review-metric {
  padding: 7px 9px;
  border: 1px solid var(--card-edge);
  border-left-width: 3px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--panel-2) 88%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  color: inherit;
  font: inherit;
  text-align: left;
  /* The glow. A tinted ring rather than a drop shadow, so it reads on both
     themes without a second value - and at 22% it is a hint at the edge of the
     card rather than a highlight competing with the number inside it. It is the
     FIFTH channel: the score, the word, the meter's own length and the left
     edge all say the same thing without it, which is what makes it safe to be
     a colour. */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--metric-tone, transparent) 22%, transparent);
  /* After the `border` shorthand above, which would otherwise reset it. The
     tone is one custom property read by the edge, the glow, the figure and the
     meter - four readers, one value, and no way for the glow to disagree with
     the number it surrounds. */
  border-left-color: var(--metric-tone);
}
.review-metric[open] {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--metric-tone, transparent) 30%, transparent),
    var(--shadow-sm);
}
/* One custom property per tone, set once and read by the edge, the glow and the
   figure. Three rules instead of nine, and no way for the glow to disagree with
   the number it surrounds. */
.review-metric.good { --metric-tone: var(--positive); }
.review-metric.warn { --metric-tone: var(--warn); }
.review-metric.bad { --metric-tone: var(--negative); }
.review-metric.plain { --metric-tone: var(--line); }
/* Two rows in one grid: the name and the figure across the top, the meter and
   the word beneath. A flex row would let the longest dimension name push the
   score out of line, and the score is the column the eye runs down. */
.review-metric-top {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 3px 6px;
  cursor: pointer;
  list-style: none;
}
.review-metric-top::-webkit-details-marker { display: none; }
.review-metric-top:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.review-metric-mark { display: inline-flex; color: var(--metric-tone, var(--muted)); }
/* The caret, which is the only thing on the summary saying it is a control. It
   turns rather than swaps, so the card that is open is legible in a row of six
   at a glance. */
.review-metric-caret {
  display: inline-flex;
  color: var(--muted);
  transition: transform .12s ease;
}
.review-metric[open] .review-metric-caret { transform: rotate(90deg); }
/* Ten steps of the score, as a second reading of the same number and never a
   replacement for it. The figure is printed beside it; this is what makes five
   scores comparable down a column without reading one of them. */
.review-metric-meter {
  grid-column: 1 / 3;
  height: 4px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--metric-tone, var(--line)) 20%, transparent);
  overflow: hidden;
}
.review-metric-fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--metric-tone, var(--line));
  box-shadow: 0 0 9px color-mix(in srgb, var(--metric-tone, transparent) 65%, transparent);
}
.review-metric-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
/* Monospaced, like every other figure on this board. A score is read against
   the four above and below it, and a proportional 8 beside a proportional 3
   does not line up in the column the eye is running down. */
.review-metric-score {
  justify-self: end;
  font-family: var(--mono);
  font-size: 19px;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--metric-tone);
}
/* The denominator, held down so the numerator is what the eye takes. A score
   out of ten printed at one size is a two-digit number to be read; printed like
   this it is a figure with a scale beside it. */
.review-metric-of { font-size: var(--fs-sm); color: var(--muted); }
.review-metric-word {
  grid-column: 3 / 5;
  justify-self: end;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--metric-tone);
}
.review-metric.plain .review-metric-score,
.review-metric.plain .review-metric-word { color: var(--muted); }
/* The model's own reasoning, in the card with the number it explains. This is
   the whole point of the card being a card: a score with no justification under
   it is the unauditable thing every other scorecard in this application refuses
   to be, and it used to live two screens down under a heading of its own.

   Not clamped to a line count, and not hidden either - it is one press away and
   the cards that failed arrive already open. A clamp would hide the end of
   exactly the sentence a reader opened this card for. */
.review-metric-note {
  margin: 6px 0 1px;
  padding-top: 6px;
  border-top: 1px dashed var(--line-soft);
  color: var(--muted);
  font-size: var(--fs-base);
  line-height: 1.5;
}
/* Every card's reasoning at once, parked at the end of the section heading. It
   is not the way in - the failures open themselves - it is the way to read the
   four that passed without pressing four times. */
.review-dims {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel);
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-xs);
  text-transform: none;
  letter-spacing: 0;
  cursor: pointer;
}
.review-dims:hover { border-color: var(--accent); color: var(--accent); }
.review-dims:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.review-dims.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }

/* --- an anchored section -------------------------------------------------- */
.review-section { margin: 20px 0; scroll-margin-top: 60px; }
.review-section-title, .review-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.review-sub { margin-top: 18px; }
/* The three sections that describe the book fold shut - and arrive that way, so
   the view opens on three headings and the Action Queue rather than on three
   screens of diagnosis. The two that say what to do never fold; see `FOLDABLE`
   in review.js. A native <details> again, for the third time on this page and
   for the same reasons: keyboard-reachable, announced as a disclosure, and open
   on print however it was left on screen.

   The heading is the whole hit area rather than the caret alone. It is one line
   of uppercase muted text with a chevron at its head, and a control that small
   is a control that gets missed. Since a folded heading is now the state a
   reader meets, it also carries the hover and focus treatment of something
   meant to be pressed. */
details.review-section > summary.review-section-title {
  cursor: pointer;
  /* The browser's own triangle, dropped for the chevron in the markup - the
     default marker cannot be coloured or lined up with the section's own icon,
     and two disclosure marks on one row is one too many. */
  list-style: none;
}
details.review-section > summary.review-section-title::-webkit-details-marker {
  display: none;
}
details.review-section > summary.review-section-title:hover { color: var(--text); }
details.review-section > summary.review-section-title:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}
/* Turned rather than swapped, so a folded section is legible in a column of
   headings at a glance - the same gesture the long-tail drawer and the rubric
   cards make. */
.review-section-caret {
  display: inline-flex;
  color: var(--muted);
  transition: transform 120ms ease;
}
details.review-section[open] > summary .review-section-caret {
  transform: rotate(90deg);
}
/* A shut section is its heading and nothing else, so the 10px reserved for the
   body it is no longer drawing would read as a gap somebody forgot to close -
   and with all three arriving shut, it would read as three of them. */
details.review-section:not([open]) > summary.review-section-title { margin-bottom: 0; }
/* The one badge on the board that says a figure is being recomputed rather than
   recalled. Every other section under it is a stored answer; the Pulse is
   arithmetic over today's holdings, redone on every draw.

   The dot pulses because a still dot is a status light and a moving one is a
   heartbeat, and the claim is the second. It is never the only channel - the
   badge prints the word "Live" beside it - which is what makes it safe to stop
   dead under `prefers-reduced-motion` below. */
.review-live {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: 4px;
  padding: 1px 8px 1px 7px;
  border: 1px solid color-mix(in srgb, var(--positive) 45%, transparent);
  border-radius: var(--radius-pill);
  background: var(--positive-soft);
  color: var(--positive);
  font-size: var(--fs-xs);
  letter-spacing: 0.06em;
}
.review-live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--positive);
  animation: review-pulse 2.4s ease-out infinite;
}
@keyframes review-pulse {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--positive) 55%, transparent); }
  70% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--positive) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--positive) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .review-live-dot { animation: none; }
  .review-metric-caret { transition: none; }
  .review-section-caret { transition: none; }
}
.review-blind { margin: 0; font-size: var(--fs-md); line-height: 1.6; }

/* --- the figures a free section leads with -------------------------------- */
/* Labelled cells rather than a run of dot-separated text: these are read
   against each other - top 1 against top 5 against top 10 - and a sentence
   makes that something the eye has to assemble. */
.review-figures {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  margin-bottom: 10px;
}
.review-figure { display: flex; flex-direction: column; gap: 1px; }
.review-figure-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
/* Monospaced, and every figure on this board with it: the value against the
   invested, the top-5 against the top-10, a cluster's weight against the one
   under it. These are read down a column rather than in a sentence, and a
   proportional font is the thing that stops them lining up. */
.review-figure-value {
  font-family: var(--mono);
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}
.review-figure.good .review-figure-value { color: var(--positive); }
.review-figure.bad .review-figure-value { color: var(--negative); }
/* Holdings with no live quote. --warn rather than --negative: nothing is wrong,
   but every percentage on this page is a share of what could be priced and a
   reader should know how much that leaves out. */
.review-figure.warn .review-figure-value { color: var(--warn); }
.review-conc { padding-top: 4px; border-top: 1px dashed var(--line-soft); }

/* --- the cluster map ------------------------------------------------------
   Category cards rather than list rows, because this list is scanned rather
   than read: which is the big one, and what kind of thing is it. The hue and
   the mark answer the second before the eye reaches the words. */
.review-clusters {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px;
}
.review-cluster {
  border: 1px solid var(--card-edge);
  border-left: 3px solid var(--cat, var(--line));
  border-radius: var(--radius-sm);
  /* Lifted off the wash rather than painted onto it, on the same argument the
     dock carries: a translucent panel over a gradient is a card with something
     behind it, which is what the depth here is for. The browser without
     `backdrop-filter` gets an 88% panel, which is a card. */
  background: color-mix(in srgb, var(--panel-2) 88%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  padding: 9px 11px;
}
.review-cluster-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 7px;
}
.review-cluster-mark { display: inline-flex; color: var(--cat, var(--muted)); }
.review-cluster-name { font-size: var(--fs-md); }
/* The pill saying where the grouping came from - the classifier's sector, one
   of the rubric's four flags, or the reader's own column. Tinted from the
   category's own hue rather than given a colour of its own, so a card is one
   colour and not two. */
.review-cluster-kind {
  padding: 0 7px;
  border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--cat, var(--line)) 45%, transparent);
  background: color-mix(in srgb, var(--cat, var(--line)) 12%, transparent);
  color: var(--cat, var(--muted));
  font-size: var(--fs-xs);
  white-space: nowrap;
}
/* Pushed right, because the weights are the column the eye runs down - "which
   of these is the big one" is the only question asked of this list, and a
   percentage after a variable-length name cannot be scanned. */
.review-cluster-weight {
  font-family: var(--mono);
  margin-left: auto;
  font-size: var(--fs-md);
  font-variant-numeric: tabular-nums;
}
.review-cluster-names { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px; }

/* The eight identity hues, as one custom property each. Set on the card and
   read by its edge, its mark, its kind pill and - in the drawer below - its
   row, so a category is one colour wherever it is drawn. */
.hue-indigo { --cat: var(--cat-indigo); }
.hue-emerald { --cat: var(--cat-emerald); }
.hue-amber { --cat: var(--cat-amber); }
.hue-rose { --cat: var(--cat-rose); }
.hue-sky { --cat: var(--cat-sky); }
.hue-violet { --cat: var(--cat-violet); }
.hue-teal { --cat: var(--cat-teal); }
.hue-slate { --cat: var(--cat-slate); }

/* --- the long tail, behind a drawer --------------------------------------
   Forty-odd groupings holding one name each. Not nothing - "which sectors am I
   in with one name" is a fair question - but not the finding either, so they
   are one line until asked for. A native <details>, which is keyboard-reachable
   and screen-reader-announced without a line of script. */
.review-minor {
  margin-top: 12px;
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.review-minor-summary {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 8px 11px;
  cursor: pointer;
  font-size: var(--fs-md);
  /* The browser's own disclosure triangle, dropped for the chevron in the
     markup: the default marker cannot be coloured or aligned with the row's
     other icons, and two triangles would be worse than one. */
  list-style: none;
}
.review-minor-summary::-webkit-details-marker { display: none; }
.review-minor-caret {
  display: inline-flex;
  color: var(--muted);
  transition: transform 120ms ease;
}
.review-minor[open] .review-minor-caret { transform: rotate(90deg); }
.review-minor-title { font-size: var(--fs-md); }
.review-minor-hint {
  margin-left: auto;
  color: var(--muted);
  font-size: var(--fs-xs);
}
.review-minor-list {
  list-style: none;
  margin: 0;
  padding: 0 11px 10px;
  /* Capped and scrolled rather than left to run: opening a drawer of forty rows
     should not push the Action Queue three screens down the page. The cap is
     about eight rows, which is enough to see that it is a list and enough to
     scan without leaving the section. */
  max-height: 260px;
  overflow-y: auto;
}
.review-minor-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 0;
  border-top: 1px solid var(--line-soft);
}
.review-minor-row:first-child { border-top: 0; }
.review-minor-mark { display: inline-flex; color: var(--cat, var(--muted)); }
.review-minor-name { font-size: var(--fs-base); }
.review-minor-weight {
  margin-left: auto;
  font-family: var(--mono);
  font-size: var(--fs-base);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

/* The scores are the first anchored section rather than a strip in the header,
   because once each card carries its own reasoning the row IS the Conviction
   Matrix - the separate list of notes it used to point at is gone. It keeps the
   section's own top margin at zero: it sits directly under the byline and a
   heading pushed away from what it heads reads as belonging to neither. */
.review-scores { margin-top: 14px; }

/* --- the detail layout: the dock, and the review beside it ----------------
   The dock leads rather than trails, because it is a navigation and a navigation
   on the right of what it navigates is one a reader finds after the thing they
   were looking for. 208px is the width at which "Conviction Matrix" fits on one
   line, which is the whole sizing argument: an item that ellipsises is an item
   you have to hover to read.

   `minmax(0, 1fr)` on the flow rather than `1fr`, because a grid track's
   implicit minimum is its content and one long unbroken evidence line would
   otherwise widen the column past the panel instead of wrapping inside it.

   The wash is what makes the glass in front of it glass. Two very faint pools -
   accent at the top left, the healthy hue at the top right - so the dock and the
   cards have something behind them to blur and lift off. Both are under a tenth
   of the token's own strength: at any more it is a coloured page, and this is
   supposed to read as depth rather than as decoration. It follows the theme's
   own two colours, so a custom palette washes in its own hues rather than in
   somebody else's blue. */
.review-detail {
  display: grid;
  grid-template-columns: 208px minmax(0, 1fr);
  gap: 18px;
  align-items: start;
  padding: 10px;
  margin: 8px -10px 0;
  border-radius: var(--radius-lg);
  background:
    radial-gradient(760px 300px at 6% -4%,
      color-mix(in srgb, var(--accent) 9%, transparent), transparent 68%),
    radial-gradient(620px 280px at 96% 2%,
      color-mix(in srgb, var(--positive) 7%, transparent), transparent 70%);
}
/* Nothing to put in the first track - a book with one section on it has nothing
   to navigate. The track is dropped rather than left empty, because a grid
   column is reserved whether or not anything lands in it and 208px of held
   space plus its gap is a visible margin down the side of the page. */
.review-detail.solo { grid-template-columns: minmax(0, 1fr); }

/* --- the five actions ----------------------------------------------------- */
/* One column, not a grid. These are ranked - the model is asked for them most
   valuable first - and side-by-side cards would throw that ordering away. */
.review-actions { display: flex; flex-direction: column; gap: 10px; }
.review-action {
  --tint: var(--line);
  border: 1px solid var(--card-edge);
  border-left: 3px solid var(--tint);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--panel-2) 88%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  padding: 10px 12px;
  scroll-margin-top: 60px;
  /* The verb's own colour bled in from the edge it is already drawn on. Inset
     and one-sided, so it reads as that border glowing rather than as a second
     border: the card is still a card, and the tone is still saying only what
     the left edge, the verb and its mark already say. */
  box-shadow: inset 14px 0 22px -20px var(--tint), var(--shadow-sm);
}
/* Colour follows money, not sentiment: in is positive, out is negative, and the
   three that move nothing today stay quiet. `warn` is SWITCH, which is both at
   once and should read as one decision rather than two.

   Held in `--tint` rather than set straight on the border, because the board's
   denser card washes the same colour across its own left edge as a gradient and
   two spellings of one tone is how they drift apart. */
.review-action.good { --tint: var(--positive); }
.review-action.bad { --tint: var(--negative); }
.review-action.warn { --tint: var(--warn); }
.review-action.plain { --tint: var(--line); }
.review-action-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
}
/* The tick-box that puts a card in the batch, first thing in the header and
   ahead of the rank: it is the one control on a board card that is pressed while
   SCANNING rather than after reading, so it sits where the eye enters the card.
   Board cards only - Detail draws no batch. */
.review-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px;
  border-radius: var(--radius-xs);
  cursor: pointer;
}
.review-tick:hover { background: var(--accent-soft); }
.review-tick input { margin: 0; cursor: pointer; }
/* A picked card is tellable from three columns away, because the batch is
   checked by looking at the board rather than by trusting the count: the accent
   ring says "this one goes" without touching the tone down the left edge, which
   is still saying what the action IS. */
.review-action.picked {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: inset 14px 0 22px -20px var(--tint),
    0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent), var(--shadow-sm);
}
/* The rank, drawn rather than implied. A numbered list would put the number
   outside the card and lose it against the border; here it is the first thing
   inside the card, which is where the eye lands. */
.review-action-rank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--tint) 45%, transparent);
  background: color-mix(in srgb, var(--tint) 15%, transparent);
  color: var(--tint);
  font-size: var(--fs-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* `plain` is the one tone the badge cannot borrow as text: `--line` is a hairline
   colour and a numeral drawn in it is unreadable. */
.review-action.plain .review-action-rank { color: var(--muted); }
.review-action-name {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 700;
  letter-spacing: 0.04em;
  font-size: var(--fs-md);
}
.review-action-name.good { color: var(--positive); }
.review-action-name.bad { color: var(--negative); }
.review-action-name.warn { color: var(--warn); }
.review-action-name.plain { color: var(--muted); }
/* How urgent an action is, as a pill beside the verb. Urgency is a fourth scale
   rather than a judgement of the stock, so it wears its own classes - but the
   ladder still avoids the red-green pair: red, amber, blue, slate, worst to
   mildest, and every pill prints its own word beside the mark. */
.review-severity {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  border: 1px solid color-mix(in srgb, var(--sev, var(--muted)) 45%, transparent);
  background: color-mix(in srgb, var(--sev, var(--muted)) 12%, transparent);
  color: var(--sev, var(--muted));
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
}
.review-severity.critical { --sev: var(--negative); }
.review-severity.high { --sev: var(--warn); }
.review-severity.medium { --sev: var(--cat-sky); }
.review-severity.low { --sev: var(--cat-slate); }
/* Pushed right, like the cluster weight and for the same reason: "how much" is
   the column the eye runs down across five cards. On the board the fold over the
   evidence rides in the same group, so the right end of every header is one
   place rather than two. */
.review-action-tools {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
}
.review-action-size {
  color: var(--muted);
  font-family: var(--mono);
  font-size: var(--fs-base);
}
/* The fold: a shield and a count. Lit rather than merely hovered when it is
   open, because a card showing its proof has to be tellable from one that is not
   at a glance down a column of thirty. */
.review-checks {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.review-checks .icon { color: var(--cat-teal); }
.review-checks:hover { border-color: var(--accent); color: var(--accent); }
.review-checks:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.review-checks.on {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: var(--accent-soft);
  color: var(--accent);
}
.review-action-reason { margin: 8px 0 0; font-size: var(--fs-md); line-height: 1.55; }
/* One figure per line, never a paragraph. This list is the whole defence
   against the generic-private-banker answer - each line is meant to be checked
   against the book above it, and a run-on sentence makes that impossible. */
.review-evidence { margin: 7px 0 0; padding-left: 18px; }
.review-evidence li {
  margin-bottom: 3px;
  color: var(--muted);
  font-size: var(--fs-base);
  line-height: 1.5;
}

/* --- the same card, at board density --------------------------------------
   Thirty of these stacked is what the Actions view is, and at Detail's density
   that is a page nobody reaches the bottom of. Every squeeze here is on the
   furniture rather than on the words: less padding, a tighter footer, and the
   tone washed across the left of the card as a gradient so a row of them is
   readable as colour before it is read as text. */
/* A column, so the footer can be pinned to the bottom of it - see the track
   rule below. */
.review-action.compact {
  display: flex;
  flex-direction: column;
  padding: 7px 10px;
  background:
    linear-gradient(100deg, color-mix(in srgb, var(--tint) 10%, transparent) 0%,
      transparent 40%),
    var(--panel-2);
}
.review-action.compact .review-action-reason { margin-top: 5px; font-size: var(--fs-base); }
.review-action.compact .review-evidence { margin-top: 5px; }
.review-action.compact .review-evidence li { margin-bottom: 2px; }
/* **The decision chips sit on the floor of the card, however tall it is.** The
   band is a grid and a grid row is as tall as its tallest card, so a card with
   three lines of evidence beside one with six had its chip row floating in the
   middle of its own box - three footers at three heights across one row, which
   is a set of controls that has to be found rather than one that is always in
   the same place. `margin-top: auto` in a column gives the footer the slack, so
   the empty space falls where nothing is drawn and every Open/Acked/Done/Dropped
   on a row lines up.

   The 7px that was this margin moves into the padding, so the distance from the
   last line of text to the chips is what it always was on a card with no slack
   to give - what changes is where the dashed rule sits, and the rule belongs to
   the footer rather than to the text above it. */
.review-action.compact .review-action-track { margin-top: auto; padding-top: 13px; }
.review-action.compact .review-split { margin-top: 6px; }
.review-action.compact .review-split-leg { padding: 5px 8px; }

/* --- the split panel: what leaves, what arrives ---------------------------
   The shape a SWITCH needs and a row of chips could not give it. The schema
   orders the two names - the one being sold first - and side by side the layout
   says which is which before a word is read. `auto` on the arrow column so the
   two legs are equal halves of whatever is left, which is what makes them read
   as a comparison rather than as a list of two.

   The same markup carries the funding pairs, because they are the same object:
   something sold, something bought, a reason underneath. Two shapes for one
   idea would be a reader learning it twice. */
.review-split {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: stretch;
  gap: 8px;
  margin-top: 9px;
}
.review-split-leg {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 8px 10px;
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel);
}
.review-split-leg.out { border-top: 2px solid var(--negative); }
.review-split-leg.into { border-top: 2px solid var(--positive); }
.review-split-role {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.review-split-leg.out .review-split-role { color: var(--negative); }
.review-split-leg.into .review-split-role { color: var(--positive); }
.review-split-what {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  font-size: var(--fs-md);
  line-height: 1.4;
}
.review-split-arrow {
  display: flex;
  align-items: center;
  color: var(--muted);
}

.review-pairs { display: flex; flex-direction: column; gap: 10px; }
.review-pair {
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  padding: 4px 12px 10px;
}
.review-pair .advice-evidence { margin-top: 7px; }

/* --- a stock, wherever it is named ---------------------------------------
   Clickable everywhere on this board, because the reader's next question is
   always about one of them and leaving them as text would mean copying a ticker
   into a search box. Quiet until hovered: five action cards and a cluster map of
   loud links would drown what they belong to. */
.review-chip {
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: transparent;
  color: var(--text);
  padding: 0 6px;
  font: inherit;
  font-size: var(--fs-base);
  cursor: pointer;
}
.review-chip:hover { border-color: var(--accent); color: var(--accent); }
.review-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* --- the heading row and the bar under it ---------------------------------
   Everything above the feed on this panel is one pinned block: what the board
   is, which of its three views is open, and - on Runs - the control that buys
   the next review.

   It was six stacked rows of card before, standing on top of the history, and
   on a laptop that put every run on file below the fold. The view whose whole
   point is the record opened showing none of it. */
.review-topbar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 6px 0 8px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
/* Pinned on Runs and Actions, static on Detail. `top: -10px` rather than 0 for
   the reason `.review-dock` carries the same number: `main` has 10px of padding,
   and at 0 the bar parks ten pixels down the port and lets a sliver of the feed
   run under the gap. Opaque and on the page's own ground - a translucent bar
   over scrolling text is the one thing that would make this harder to read.

   Detail is left static on purpose: it pins `.review-dock` already, and two
   sticky strips at one offset sit on top of each other. Of the two, the dock is
   the one worth the top of that screen. */
.review-topbar.stuck {
  position: sticky;
  top: -10px;
  z-index: 4;
  background: var(--bg);
}
/* Glass rather than a slab, where the engine can draw it: the board scrolls
   under this bar, and a translucent blur says so while keeping every chip
   legible on top of it. The solid --bg above stays as the fallback. */
@supports ((backdrop-filter: blur(9px)) or (-webkit-backdrop-filter: blur(9px))) {
  .review-topbar.stuck {
    background: color-mix(in srgb, var(--bg) 78%, transparent);
    -webkit-backdrop-filter: blur(9px);
    backdrop-filter: blur(9px);
  }
}
.review-head-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
}
.review-head-title {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 600;
}
.review-head-title .icon { color: var(--accent); }
/* The three views, as one enclosed segment group sitting beside the heading.
   `.subtabs` is a full-width rail with a rule under it, which is right for a
   page's own sections and wrong here: this is the third horizontal navigation on
   the screen, and three stacked rails read as a site with three levels rather
   than one page with three views of one list.

   It used to carry `margin-left: auto`, which threw it to the far right of the
   row - a whole screen-width of gap between the title and the control that
   switches what is under it, and the pills reading as furniture pinned to the
   corner rather than as the navigation for this panel. Left, against the title
   it belongs to, is where a reader's eye already is after reading the heading. */
.subtabs.review-seg {
  border-bottom: 0;
  margin: 0;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel-2);
}
.subtabs.review-seg .subtab {
  border-bottom: 0;
  margin-bottom: 0;
  border-radius: var(--radius-pill);
  padding: 3px 13px;
  font-size: var(--fs-sm);
}
.subtabs.review-seg .subtab:hover { background: var(--panel); }
/* The lit segment is filled rather than underlined. An underline inside a pill
   is a mark for a rail that is not there any more, and on a group this small the
   filled chip is the only reading that survives a glance. */
.subtabs.review-seg .subtab.on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.subtabs.review-seg .subtab.on:hover { background: var(--accent); }
/* The filters on Actions ride inside the same pinned block, so they lose the
   margin they carried when they were a bar of their own. */
.review-topbar .review-board-bar { margin-bottom: 0; }

/* --- the control that spends, and the states around it ---------------------
   One row: which book and the button that reads it, then what is on file for
   it and the disclosure. The picker and the button are adjacent because they
   are one gesture - pick, press - and the state line takes the slack after
   them rather than between them. The two
   paragraphs it used to stand on fold away behind `.review-why` - see the note
   on `askBar` for why they still open by default on a book nobody has read. */
.review-ask { display: flex; flex-direction: column; gap: 8px; }
.review-ask-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
}
.review-state { min-height: 22px; }
/* The state line takes the slack, which is what pins the picker and the button
   to the near end of the bar and the disclosure to the far one, and keeps all
   three in place as the sentence between them changes length. */
.review-ask-row .review-state { flex: 1 1 180px; margin: 0; }
/* The disclosure. Quiet - it is furniture next to the control it explains - and
   lit in the accent while it is open, so a reader who folded the sentence away
   can see that there is one to fold back. */
.review-why {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.review-why:hover { border-color: var(--accent); color: var(--accent); }
.review-why:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.review-why.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.review-why .icon { flex: 0 0 auto; }
/* What the button buys, above the button rather than under it. The panel opens
   this by itself until a profile has been read once: this control spends money,
   and the sentence a hesitating reader needs has to be readable BEFORE the
   click. Set on its own ground so the fold reads as a panel opening rather than
   as the bar growing two stray paragraphs. */
.review-ask-why {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 9px 11px;
  border: 1px solid var(--card-edge);
  border-left: 2px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.review-ask-why > p { margin: 0; }
/* The button, and the one rule it actually needed: `inline-flex`.
   `.icon` is `display: block` app-wide - that is what kills the inline baseline
   gap everywhere else - so inside a plain `.btn` the sparkle took a line of its
   own and STACKED above the label. The `vertical-align: -3px` that used to sit
   here was arguing with `display: block`, which ignores it, so what the bar drew
   was a two-line slab: mark on top, four words under it, twice the height of the
   picker beside it. A row and a gap, and it is one line again.

   Pill-ended and padded wider than a stock `.btn` because of where it now sits:
   right of the picker, which is itself a pill. Two controls that are one gesture
   should read as a matched pair - a 4px-cornered rectangle against a lozenge
   reads as two unrelated things - and the extra side padding is what keeps the
   most consequential control on the panel from looking like the smallest. */
.review-ask-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  padding: 6px 14px;
  border-radius: var(--radius-pill);
  line-height: 1.25;
  white-space: nowrap;
}
.review-ask-btn .icon { flex: none; }
/* Hover goes DARKER, not lighter. The fill is already `--accent` mixed 78%
   toward black precisely so white text clears 4.5:1 on it; lightening on hover
   would walk that back toward the 3.20:1 raw accent carries. */
.review-ask-btn:hover {
  background: color-mix(in srgb, var(--accent) 66%, #000);
  border-color: color-mix(in srgb, var(--accent) 66%, #000);
}
/* Dead until a profile is picked, and it has to LOOK dead. It carried this from
   `.daymove-card .btn[disabled]` while it was a card; as a bar it stopped
   matching that selector, so the one state this control spends most of its life
   in was drawing at full strength - a saturated button beside a sentence saying
   it cannot be pressed yet. */
.review-ask .btn[disabled] { opacity: .45; pointer-events: none; }
/* The wait and the failure, under the row that caused them. */
.review-ask > .hint, .review-ask > .advice-warn { margin: 0; font-size: var(--fs-xs); }
.review-ask .review-empty { margin: 0; }
/* --- whose book to read ---------------------------------------------------
   The one control on this bar that is not the button, and it is drawn to look
   like a decision rather than like a filter. Violet is its identity hue - not
   `--accent`, which on this panel already means "standing", and not a semantic
   colour, because naming a person is not a judgement about them. The same hue
   carries onto the owner chip down the history below, so the question and the
   answers to it are recognisably one thing.

   Pill-shaped since it stopped being a row of its own: on a bar of four
   controls the rounded end is what says this one is a choice rather than
   another chip in the sentence beside it. */
.review-profile {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
  padding: 3px 8px 3px 10px;
  border: 1px solid color-mix(in srgb, var(--cat-violet) 40%, transparent);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--cat-violet) 9%, transparent);
}
.review-profile-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--cat-violet);
  font-size: var(--fs-sm);
}
.review-profile select { font-size: var(--fs-sm); }
.review-positions { font-size: var(--fs-md); }
.review-when { color: var(--muted); font-size: var(--fs-xs); }
.review-none { color: var(--muted); font-size: var(--fs-sm); }
/* Two different reasons the standing answer may not describe today, drawn the
   same way on purpose: from where the reader sits they mean one thing - press
   the button again. */
.review-stale { color: var(--negative); font-size: var(--fs-xs); }
/* No portfolio at all. An instruction rather than an error: nothing has gone
   wrong, there is simply nothing to read yet. */
.review-empty {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 14px 0 4px;
  font-size: var(--fs-md);
}

/* --- what was decided about an action ------------------------------------
   The half of this feature that is not the model's. An action arrives as advice
   and becomes a decision - acknowledged now, executed on Thursday, reconsidered
   next month - and the footer under each card is where that happens.

   Four chips rather than a menu, because this is a one-click decision made
   dozens of times and a `<select>` would make every one of them two clicks and
   a read. They are drawn in the order a decision travels, so the row reads as a
   progression rather than as a list of options. */
.review-action-track {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px dashed var(--line-soft);
}
/* The decision chips sit at the RIGHT end of the foot and the note bar at the
   left: the chips are the card's exit, and the eye leaves a card bottom-right.
   Ordered here rather than in the markup, so the reading order for a screen
   reader stays "what was decided, then the notes about it". The open thread
   takes the full width below them both - a composer is a paragraph, not a
   column. */
.review-action-track .review-statuses { order: 2; margin-left: auto; }
.review-action-track .review-thread-bar { order: 1; margin-top: 0; }
.review-action-track .review-thread { order: 3; flex: 1 1 100%; margin-top: 0; }
/* Drawn as ONE enclosed track rather than four loose outlines, which is the
   same bargain `.review-seg` makes over the view strip: four separately
   bordered pills on a card that already has a border and a left spine is a
   third rectangle inside a second, and the eye reads boxes before it reads
   words. Enclosed, the row says "one control, four positions" - which is what a
   progression is - and costs a border instead of four. */
.review-statuses {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel);
}
.review-status {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-xs);
  cursor: pointer;
}
.review-status:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.review-status:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Muted while a write is in flight. A double-press sends two PATCHes for one
   decision - harmless against an upsert, and still the wrong thing to draw. */
.review-status:disabled { opacity: .45; pointer-events: none; }
/* The state an action is in. Filled rather than merely outlined: a row of four
   near-identical outlines gives the eye nothing to land on, and this chip is
   the answer to the only question the footer asks. */
.review-status.on {
  --chip: var(--line);
  border-color: color-mix(in srgb, var(--chip) 60%, transparent);
  background: color-mix(in srgb, var(--chip) 20%, transparent);
  color: var(--chip);
  /* A lit segment reads as raised rather than merely filled. The board is
     scanned for what is still outstanding, and that scan is over four
     near-identical rows per card. */
  box-shadow: 0 2px 10px -5px color-mix(in srgb, var(--chip) 80%, transparent);
}
.review-status.on.good { --chip: var(--positive); }
.review-status.on.warn { --chip: var(--warn); }
.review-status.on.bad { --chip: var(--negative); }
.review-status.on.plain { --chip: var(--muted); }

.review-thread-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
  margin-top: 8px;
}
.review-thread-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-base);
  cursor: pointer;
}
.review-thread-toggle:hover { background: var(--panel); color: var(--text); }
.review-thread-toggle.on { background: var(--panel); color: var(--text); }
/* When the decision was made, and by whom. Muted and to the right of the
   toggle: it is provenance rather than a control, and it is the line that makes
   a board of thirty acknowledgements readable a month later. */
.review-decided {
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.review-thread { margin-top: 8px; }
.review-comments { list-style: none; margin: 0 0 8px; padding: 0; }
.review-comment {
  padding: 7px 10px;
  margin-bottom: 6px;
  border-left: 2px solid var(--line);
  background: var(--panel);
  border-radius: var(--radius-xs);
}
.review-comment-body {
  margin: 0;
  font-size: var(--fs-md);
  line-height: 1.5;
  /* A note is prose the reader typed, newlines and all. Without this a
     paragraph break becomes a space and a list becomes one long line. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.review-comment-by {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 4px 0 0;
  color: var(--muted);
  font-size: var(--fs-xs);
}
/* Pushed to the right edge and quiet until the comment is hovered. Deleting a
   note is rare and irreversible, and a delete button at full strength on every
   row invites exactly the mis-click it cannot undo. */
.review-comment-drop {
  margin-left: auto;
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 0 2px;
  cursor: pointer;
  opacity: 0;
}
.review-comment:hover .review-comment-drop,
.review-comment-drop:focus-visible { opacity: 1; }
.review-comment-drop:hover { color: var(--negative); }

.review-composer { display: flex; gap: 6px; align-items: flex-start; }
.review-note {
  flex: 1;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-md);
  line-height: 1.45;
  resize: vertical;
}
.review-note:focus { outline: none; border-color: var(--accent); }

/* --- the Runs view --------------------------------------------------------
   A card per run rather than a table row, because a run is not one line of
   figures: it is a date, a headline sentence, five scores and a progress strip
   over its own actions. Squeezed into a table the headline would be the column
   that had to be truncated, and it is the one that says what the run found.

   Raised off the page rather than fenced in. It used to carry a 1px box, a 3px
   rule down its left edge and a fill one step off the ground - three separate
   pieces of furniture per card, and a feed of them read as a stack of borders
   with sentences trapped inside. So the card sits on `--panel` with a hairline
   and a shadow: the elevation is what separates one run from the next, and the
   ink is spent on the three bands inside instead. */
/* --- the lanes ------------------------------------------------------------
   One column per portfolio, its runs under it. The history used to be a single
   column with every profile's runs interleaved by date, which answers "what
   happened last" and nothing else: consecutive cards were about different books,
   so comparing a book with ITSELF over time - the whole reason the runs are kept
   - meant reading past two other people's between every pair.

   `auto-fit` with a floor rather than a fixed column count: the roster decides
   how many lanes there are, and at three of them on a laptop a hard
   `repeat(3, 1fr)` would squeeze each below the width its headline needs.
   Below the floor the lanes wrap and become sections stacked down the page,
   which is the right reading on a phone. `align-items: start` so a lane with one
   run does not stretch its header to the height of a lane with nine. */
.review-lanes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  align-items: start;
  gap: 12px;
}
.review-lane {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
}
/* Whose column this is. Violet, the same identity hue the picker and the owner
   chip carry, so "Rahul" at the head of a lane and "Rahul" in the bar above are
   recognisably one fact rather than two marks that happen to share a word. It is
   a heading rather than a chip: this names the column, and a pill here would
   read as one more filter over the cards under it. */
.review-lane-head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 10px;
  padding: 0 2px 6px;
  border-bottom: 2px solid color-mix(in srgb, var(--cat-violet) 35%, transparent);
}
.review-lane-who {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--cat-violet);
  font-size: var(--fs-md);
  font-weight: 600;
}
/* The reader's own book. Marked on the LANE rather than on each card in it,
   which is the whole economy of this layout: one mark per column instead of one
   per run. */
.review-lane.yours .review-lane-head {
  border-bottom-color: var(--cat-violet);
}
.review-lane.yours .review-lane-who::after {
  content: "yours";
  padding: 0 6px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--cat-violet) 16%, transparent);
  font-size: var(--fs-xs);
  font-weight: 400;
}
.review-lane-count { color: var(--muted); font-size: var(--fs-base); }
/* What this book's reviews have cost, added up. Pushed to the far end, because
   it is the one figure on the head that is a running total rather than a label,
   and it wants to line up down the row of lanes. */
.review-lane-spend {
  margin-left: auto;
  font-size: var(--fs-base);
  font-variant-numeric: tabular-nums;
}
/* An unread lane is an instruction, not a gap. Dashed rather than solid: the
   border says "nothing here yet" in the one way a reader does not have to read.
   The lane keeps its full width so the board still reads as a row of
   portfolios rather than as two columns and a hole.

   The modifier on the lane is `unread`, NOT `empty` - `.empty` is a global
   utility a few thousand lines up (40px of padding, centred grey text) and a
   lane that took the name took the padding with it, dropping its head 40px
   below the lanes either side of it. */
.review-lane-none {
  margin: 0;
  padding: 14px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: var(--fs-base);
  line-height: 1.5;
}
/* Equal rows rather than a flex column, so every card in a lane is the height
   of the tallest one in it. The headline below is clamped to a fixed three
   lines, which is what makes the cards match ACROSS lanes as well - this rule
   is what keeps them matching when something else on a card wraps. */
.review-lane-runs {
  display: grid;
  grid-auto-rows: 1fr;
  gap: 10px;
}
.review-run {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--card-edge);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
  padding: 0;
  overflow: hidden;
}
.review-run:hover { box-shadow: var(--shadow-md); }
/* The newest run, and the one currently open in Detail. Two different marks
   because they are two different facts and a run is often both. The standing
   one keeps a rule, but at the TOP of the card rather than down its side: on a
   header band that already runs edge to edge it reads as that band's own line
   rather than as a fourth border. */
.review-run.standing .review-run-head {
  border-top: 2px solid var(--accent);
  margin-top: -1px;
}
.review-run.on {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent), var(--shadow-md);
}
/* The identity band: when, whose, and what it cost. Its own ground and its own
   hairline, so the sentence below it starts on a clean field. */
.review-run-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 5px 9px;
  padding: 7px 12px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line-soft);
}
/* The two measures, pushed to the far end as one group rather than as two
   things that each happened to be last. */
.review-run-meta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}
.review-run-open {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 0;
  background: transparent;
  color: var(--text);
  padding: 0;
  font: inherit;
  font-size: var(--fs-md);
  cursor: pointer;
}
.review-run-open:hover { color: var(--accent); }
.review-run-badge {
  padding: 0 7px;
  border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  font-size: var(--fs-xs);
}
/* Whose book a run read. Violet, the picker's own hue, because it is the same
   fact answered rather than a second kind of mark - and outlined rather than
   filled when it is the reader's own, so a mixed list can be skimmed for one's
   own book without reading every chip. */
.review-run-owner {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0 7px;
  border-radius: var(--radius-lg);
  border: 1px solid color-mix(in srgb, var(--cat-violet) 45%, transparent);
  color: var(--cat-violet);
  font-size: var(--fs-xs);
}
.review-run-owner.yours { background: color-mix(in srgb, var(--cat-violet) 14%, transparent); }
.review-run-badge.stale {
  border-color: color-mix(in srgb, var(--warn) 45%, transparent);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  color: var(--warn);
}
.review-run-cost { font-size: var(--fs-base); font-variant-numeric: tabular-nums; }
/* The one part of a run a reader can actually read at list size, so it gets the
   card to itself: full width, no chip beside it, and a line height meant for a
   sentence rather than for a row of figures. */
.review-run-headline {
  margin: 0;
  padding: 10px 12px 2px;
  font-size: var(--fs-md);
  line-height: 1.55;
  /* Three lines, filled or not: the headline is the one part of a card whose
     length the model decides, and it was the only reason a row of cards came
     out at three different heights. `min-height` holds the short ones open,
     the clamp holds the long ones in, and the whole sentence is on the card's
     own tooltip so the clamp costs a reader nothing. `-webkit-box` is what the
     clamp acts on - see `.cmp-clip`, which does the same for a thesis. */
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  min-height: calc(1.55em * 3);
  overflow: hidden;
}
/* Pinned to the floor of the card and kept to ONE row. Both are about the same
   promise: a footer that wrapped onto a second line made its card taller than
   the one beside it, which is the whole thing this layout is trying not to do.
   The figures are what gives when the lane is narrow - they ellipsis, and the
   two strips and the note count keep their widths. */
.review-run-foot {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 6px 12px;
  padding: 6px 12px 9px;
  margin-top: auto;
}
.review-run-figs {
  color: var(--muted);
  font-size: var(--fs-base);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* What the answer asked for, next to the figures it was reasoned against: the
   count in words, and the mix beside it flex-weighted so one CRITICAL among ten
   LOWs does not read the same as ten CRITICALs and one LOW. The ladder is the
   chips' - red, amber, sky, slate - rather than the tally's good/warn/bad,
   because urgency is a different scale from progress; and it is narrower than
   the tally and sits at the other end of the footer so two strips on one line
   cannot be mistaken for one measure drawn twice. */
.review-run-mix { display: inline-flex; align-items: center; gap: 7px; }
.review-run-acts {
  color: var(--muted);
  font-size: var(--fs-base);
  white-space: nowrap;
}
.review-mix {
  display: flex;
  flex: 0 0 auto;
  width: 108px;
  height: 16px;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
/* Mark and count, not count alone. Four hues at 16px is more than colour can
   carry on its own - and the mark is the same one the chip beside the action
   wears, so a red cell up here and a CRITICAL chip down there are recognisably
   one fact. `min-width` is what the mark costs: the cells still weight by
   count, but none of them may shrink below its own drawing. */
.review-mix-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 25px;
  padding: 0 3px;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--bg);
  background: var(--sev, var(--muted));
}
.review-mix-cell.critical { --sev: var(--negative); }
.review-mix-cell.high { --sev: var(--warn); }
.review-mix-cell.medium { --sev: var(--cat-sky); }
.review-mix-cell.low { --sev: var(--cat-slate); }
.review-run-notes {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--muted);
  font-size: var(--fs-base);
}

/* Five dots: a whole review's health at list size. The ticker's four channels
   collapse to one here, which is honest - this is a glance, not a reading, and
   each dot carries its own dimension and score in a tooltip. */
.review-dots { display: inline-flex; gap: 3px; }
.review-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--line);
}
.review-dot.good { background: var(--positive); }
.review-dot.warn { background: var(--warn); }
.review-dot.bad { background: var(--negative); }

/* How far through a run's five actions the reader got. Flex-weighted segments,
   with the count printed on each: five segments at list size are too small to
   read by area alone. */
.review-tally {
  display: flex;
  margin-left: auto;
  min-width: 120px;
  height: 16px;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.review-tally-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--bg);
  background: var(--muted);
}
.review-tally-cell.good { background: var(--positive); }
.review-tally-cell.warn { background: var(--warn); }
.review-tally-cell.bad { background: var(--negative); }
.review-tally-cell.plain { background: var(--line); color: var(--text); }

/* --- picking which run is on screen ---------------------------------------
   Two shapes for one question, because the two views ask it differently:
   Detail wants exactly one run and gets a real `<select>`, Actions wants a set
   and gets tick-boxes behind a dropdown. They share this bar so that "which
   answers am I looking at" sits in the same place on both. */
.review-board-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin-bottom: 14px;
}
/* The chips carry a bottom margin for when they stand on their own; inside the
   bar that margin is a flex item's, so it pushed the whole row 7px above centre
   and made the pinned strip taller than anything in it. */
.review-board-bar .review-filters { margin-bottom: 0; }
/* The board's stock search: the shared picker - `stockpicker.js` - and not a
   box of this view's own, because "which stock" is a question the application
   answers in one place. `flex: 0 0 auto` for the reason the run menu beside it
   carries the same: this is a wrapping row with five status chips and two bulk
   buttons in it, and at the default shrink the box was squeezed until a symbol
   no longer fitted inside it. It sits narrower than the 240px run menu on
   purpose - a ticker is eight characters where a run is a date, a position
   count and sometimes an owner. */
.review-board-bar .stock-pick { flex: 0 0 auto; }
.review-board-bar .stock-pick .input-icon { display: flex; }
.review-board-bar .stock-pick input[type="text"] { min-width: 190px; }
.review-pick { display: inline-flex; align-items: center; gap: 7px; }
.review-pick-label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.review-pick-select {
  padding: 4px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--text);
  font-size: var(--fs-base);
  /* The dates are the column the eye runs down when a history is long, and a
     select sized to its longest option jumps as the choice changes. */
  min-width: 240px;
}
.review-pick-select:focus { outline: none; border-color: var(--accent); }

/* The board's own, which is a set. `position: relative` so its panel hangs off
   it rather than off the bar, whose wrapping would drag it sideways. */
/* `position: relative` so the panel hangs off the button rather than off the
   bar, whose wrapping would drag it sideways. The left-anchoring is
   `.dropdown-left` in the shared rules - see the note there.

   `flex: 0 0 auto` because this sits in a wrapping flex row beside five status
   chips: at the default `flex-shrink: 1` the button was squeezed until "All
   runs ▾" broke onto a second line under its own icon. It is a label, not a
   column - it gets the width it needs and the chips wrap instead. */
.review-runpick { position: relative; flex: 0 0 auto; }
/* Drawn at the select's width, not at its label's. Sized to "All runs" the
   button was a sliver beside the chips, and it grew or shrank as the choice
   changed - the same jump the Detail tab's run select pins with its own
   min-width, so the one control keeps one width on every view. The caret sits
   at the right edge the way a select's does, and a long chosen-run label
   ellipses instead of stretching the bar. */
.review-runpick > .btn {
  display: flex;
  align-items: center;
  gap: 5px;
  min-width: 240px;
  max-width: 340px;
  white-space: nowrap;
}
.review-runpick-text { overflow: hidden; text-overflow: ellipsis; }
.review-runpick-caret { margin-left: auto; color: var(--muted); }
/* Wider than the shared 330px, because every row in it is a date, a position
   count and sometimes an owner - "28/Aug/26 06:22 · 69 positions · Rahul" - and
   at 330 that wrapped onto two lines per run, which turns a list you scan into
   a list you read. Capped against the viewport so a phone still gets a panel
   that fits on it. */
.review-runpick-panel { width: 400px; max-width: calc(100vw - 32px); }
.review-run-checks {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 8px;
  /* Capped and scrolled: a book reviewed weekly for a year is fifty runs, and a
     panel that grew with the history would run off the bottom of the screen. */
  max-height: 260px;
  overflow-y: auto;
}
.review-run-check {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 6px;
  border-radius: var(--radius-xs);
  font-size: var(--fs-base);
  cursor: pointer;
}
.review-run-check:hover { background: var(--panel-2); }
.review-run-check.on { background: var(--accent-soft); color: var(--accent); }
.review-run-check span:first-of-type { flex: 1; min-width: 0; }
/* How many actions that run carries - the figure that says whether ticking it
   will change what is on screen. */
.review-run-check-count {
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.review-run-check.on .review-run-check-count { color: inherit; }

/* --- the Actions view -----------------------------------------------------
   Grouped by run under a dated heading rather than run as one flat list: an
   action's evidence quotes figures from the book it was reasoned against, and a
   row torn from that context is a number with no date on it. */
.review-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 14px;
}
.review-filter {
  --chip: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--panel);
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-base);
  cursor: pointer;
}
/* **The mark is coloured whether or not the chip is pressed.** Five outlined
   grey pills in a row is the wall this house style exists to prevent, and the
   colour is not decoration here: it is the same key the tally strip, the run
   band and every status chip on the cards below are drawn in, so a reader
   learns amber-is-outstanding once and reads it everywhere.

   Keyed off the value rather than off the tone class, because the tones are
   about the *decision* - OPEN and DROPPED are both `plain`, correctly - and two
   states sharing a mood should still be tellable apart at a glance. */
.review-filter[data-review-filter=""] .icon { color: var(--cat-violet); }
.review-filter[data-review-filter="OPEN"] .icon { color: var(--accent); }
.review-filter[data-review-filter="ACKED"] .icon { color: var(--warn); }
.review-filter[data-review-filter="DONE"] .icon { color: var(--positive); }
.review-filter[data-review-filter="DROPPED"] .icon { color: var(--muted); }
.review-filter:hover { border-color: var(--accent); color: var(--accent); }
.review-filter.on {
  border-color: color-mix(in srgb, var(--chip) 60%, transparent);
  background: color-mix(in srgb, var(--chip) 16%, transparent);
  color: var(--chip);
  box-shadow: 0 2px 12px -5px color-mix(in srgb, var(--chip) 85%, transparent);
}
.review-filter.on .icon { color: var(--chip); }
.review-filter.on.good { --chip: var(--positive); }
.review-filter.on.warn { --chip: var(--warn); }
.review-filter.on.bad { --chip: var(--negative); }
/* A state nobody is in, drawn muted rather than dropped: a board where nothing
   is Done should say so rather than quietly having one fewer chip. */
.review-filter.empty { opacity: .55; }
/* The count is the reason to press the chip, so it is set in its own well
   rather than run on as a second word. */
.review-filter-count {
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--panel-2);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
}
.review-filter.on .review-filter-count {
  background: color-mix(in srgb, var(--chip) 22%, transparent);
}

/* --- the run band ---------------------------------------------------------
   Which run these cards came from, and it is **pinned**: this is the one list on
   the page long enough that a reader scrolling it loses the answer, and an
   action's evidence quotes figures from the book of the day it was read. A
   heading that scrolls away turns every figure below it into a number with no
   date on it.

   `top` clears the bar above it, which is `.review-topbar.stuck` sitting at -10
   and about 74 tall. Below the band's own z-index, so the two never fight: if
   the number drifts the band tucks under the bar rather than covering it. */
.review-group { margin-bottom: 18px; }
.review-group-head {
  position: sticky;
  top: 64px;
  z-index: 3;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 5px 9px;
  margin: 0 0 8px;
  padding: 5px 10px;
  border: 1px solid var(--card-edge);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  /* Opaque, because it is pinned over scrolling cards - and washed with the
     accent from the left so the band reads as a header rather than as one more
     card in the stack. */
  background:
    linear-gradient(100deg, var(--accent-soft) 0%, transparent 45%),
    var(--panel-2);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.review-group-open {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 0;
  background: transparent;
  color: var(--text);
  padding: 0;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.review-group-open .icon { color: var(--accent); }
.review-group-open:hover { color: var(--accent); }
.review-group-note {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  text-transform: none;
  letter-spacing: 0;
}
.review-group-note .icon { color: var(--cat-teal); }
/* Whose book it was, in the violet the history's own owner chip uses - the same
   fact answered in the same colour rather than a second kind of mark. */
.review-group-note.owner { color: var(--cat-violet); }
.review-group-note.owner .icon { color: var(--cat-violet); }
/* How far through this run's rows on screen the reader is. Shorter than the
   history's, because there it is a card's headline figure and here it rides on
   a heading. */
.review-group-head .review-tally { height: 14px; min-width: 96px; }

/* --- the severity bands ----------------------------------------------------
   One run's actions, banded CRITICAL → LOW with the cards three to a row.
   The head is a full-width bar in the band's own hue - the same red, amber,
   blue, slate ladder the chips wear - so a fold that shuts it leaves a
   coloured line saying what is folded and how much of it. The count rides on
   the head for the reason the filter chips carry theirs: it is what decides
   whether unfolding is worth the click. */
.review-sev { margin: 0 0 10px; }
.review-sev-head {
  --sev: var(--cat-slate);
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 3px 9px;
  border: 1px solid color-mix(in srgb, var(--sev) 40%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--sev) 10%, transparent);
  color: var(--text);
  cursor: pointer;
  font-size: var(--fs-sm);
  font-weight: 700;
  letter-spacing: 0.05em;
  box-shadow: 0 0 12px -8px var(--sev);
  transition: box-shadow 0.15s, background 0.15s;
}
.review-sev-head:hover {
  background: color-mix(in srgb, var(--sev) 16%, transparent);
  box-shadow: 0 0 14px -6px var(--sev);
}
.review-sev-head .icon, .review-sev-name { color: var(--sev); }
.review-sev-count {
  padding: 0 7px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--sev) 18%, transparent);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-weight: 400;
}
.review-sev-caret { margin-left: auto; color: var(--muted); transition: transform 0.15s; }
.review-sev.folded .review-sev-caret { transform: rotate(-90deg); }
.review-sev.critical .review-sev-head { --sev: var(--negative); }
.review-sev.high .review-sev-head { --sev: var(--warn); }
.review-sev.medium .review-sev-head { --sev: var(--cat-sky); }
.review-sev.low .review-sev-head { --sev: var(--cat-slate); }
/* Three to a row, not a column: thirty cards one under another is a page
   nobody reaches the bottom of, and at board width a card is a third of it.
   `minmax(0, 1fr)` so a long symbol chip squeezes its own card rather than
   widening the track; the steps down to two and one match the width at which
   a third of the board stops holding a readable card. */
.review-sev-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-top: 8px;
}
@media (max-width: 1200px) {
  .review-sev-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  .review-sev-cards { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  .review-bulk, .review-sev-head { transition: none; }
}

/* --- the batch moves -------------------------------------------------------
   Ack and Done, at the right end of the pinned bar, acting on the cards the
   reader has TICKED and nothing else - there is no "all", by design: a press
   over 55 rows three screens down cannot be checked before it is made. DROPPED
   stays per-row: declining is exactly the judgement a batch button exists to
   skip. */
.review-bulkbar { display: inline-flex; gap: 6px; margin-left: auto; }
.review-bulk {
  --chip: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid color-mix(in srgb, var(--chip) 45%, transparent);
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--chip) 10%, transparent);
  color: var(--chip);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
  transition: box-shadow 0.15s, background 0.15s;
}
.review-bulk.good { --chip: var(--positive); }
.review-bulk.warn { --chip: var(--warn); }
.review-bulk:hover:not(:disabled) {
  background: color-mix(in srgb, var(--chip) 18%, transparent);
  box-shadow: 0 0 12px -5px var(--chip);
}
.review-bulk:disabled { cursor: default; }
.review-bulk.empty { opacity: 0.55; }
.review-bulk-count {
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--chip) 20%, transparent);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-xs);
}
/* What is in the batch, and the way out of it. Quiet - it is an undo, not a
   third move - and drawn only while something is ticked. */
.review-bulk-clear {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.review-bulk-clear:hover { border-color: var(--accent); color: var(--accent); }
.review-bulk-clear:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* Nothing to show, said with the mark of the filter that emptied it rather than
   as a bare line of grey text. */
.review-board-empty {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 14px 12px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-sm);
  color: var(--muted);
}
.review-board-empty .icon { color: var(--accent); }

/* Where the ask card used to be on Detail. A line, not a control: the card that
   spends is on Runs, and what a reader landing here on a book with no review
   needs is to be told why the page is thin and where the button is. */
.review-gap { display: flex; align-items: center; gap: 7px; margin: 12px 0; }

/* The banner over a run that is not the standing one. Its actions are still
   worth ticking off - that is why the runs are kept - but its figures describe
   a book that has since moved, and nothing else on screen would say so. */
.review-older { display: flex; align-items: flex-start; gap: 7px; }

/* --- narrow windows -------------------------------------------------------
   The dock lies down: one column becomes one row, scrolling sideways, still
   stuck to the top of the port. The nested action jumps go rather than lie down
   with it - they are an alternative route to something the page already shows in
   full, and inline they would double the strip's height to save a scroll. The
   split panel stacks rather than shrinking, because two 90px columns stop being
   a comparison and start being a squeeze. */
@media (max-width: 900px) {
  .review-detail {
    grid-template-columns: minmax(0, 1fr);
    padding: 8px;
    margin-left: -8px;
    margin-right: -8px;
  }
  .review-dock {
    flex-direction: row;
    gap: 4px;
    max-height: none;
    overflow-x: auto;
    overflow-y: hidden;
  }
  .review-dock-item { white-space: nowrap; }
  .review-dock-jumps { display: none; }
  /* The bar above wraps onto two and three rows down here, so the one number
     the band's `top` is measured against stops being one number. Static is
     honest; a band pinned at the wrong offset either hides under the bar or
     floats a strip of scrolling cards above itself. */
  .review-group-head { position: static; }
}
@media (max-width: 560px) {
  .review-split { grid-template-columns: minmax(0, 1fr); }
  /* The status chips lose their words and keep their marks: four labelled chips
     wrap onto three lines on a phone, and the mark plus the lit state is enough
     to say which of four an action is in. */
  .review-status span { display: none; }
  .review-status { padding: 4px 8px; }
  .review-tally { min-width: 90px; }
  .review-mix { width: 100px; }
  /* The lanes are stacked down here, so no two cards are ever side by side and
     there is nothing left for a matched height to line up with. The footer gets
     its second row back rather than ellipsing the figures on a 360px screen. */
  .review-run-foot { flex-wrap: wrap; }
  .review-run-figs { overflow: visible; white-space: normal; }
  /* Pointing down rather than across once the legs are stacked - an arrow that
     still read left-to-right would be describing a layout that is no longer
     there. */
  .review-split-arrow { justify-content: center; transform: rotate(90deg); }
  /* Marks only. Six labelled items scroll sideways for a screen and a half on a
     phone, and the mark plus the lit state is enough to say which of six a
     reader is in - the tooltip still carries the name. */
  .review-dock-label { display: none; }
  .review-dock-item { padding: 6px 10px; }
  /* The heading's expand-all drops its word for the same reason the status
     chips below drop theirs. */
  .review-dims span { display: none; }
}

/* --- Scoreboard: what every verdict was actually worth -------------------
   The AI page's one section that draws an OUTCOME rather than an opinion, and
   the one that costs nothing. Both of those have to be visible before anything
   is read, because a reader arriving here has been trained by the three tabs in
   front of it to expect a bill and a paragraph of prose.

   So the layout inverts the others': the sections above lead with a card that
   spends, this one leads with a frame of figures that are already true and puts
   its (free) button under them. Everything below the button is tables, which is
   the right shape for the first time on this page - the answer is arithmetic,
   and prose would be dressing a number up as a judgement.

   Two things this stylesheet is doing more work for than usual, both because
   the honest reading of this table is mostly absences:

   * an unmeasured cell must read as *not yet* rather than as *zero*. `.score-none`
     and `.score-pending` are muted em dashes with a tooltip each, and they are
     deliberately not the same colour as a real 0.0%.
   * a right verdict and a rising stock are different facts. Colour follows the
     MOVE (green up, red down) exactly as it does everywhere else in the app, and
     whether the verdict was borne out is an underline instead - so a TRIM that
     was right reads as a red number that is marked correct, which is what it is. */
.score-panel { padding-top: 4px; }

/* The frame of figures at the top. Same object as `.review-today` and styled
   with it in mind: always current, always free, and boxed so it does not read as
   the first paragraph of the report underneath. */
.score-head {
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  padding: 12px 14px;
}
.score-head-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.score-head-what { margin: 0 0 10px; }
.score-coverage {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin: 0;
}
.score-coverage-figs { font-size: var(--fs-lg); }
/* A gap between what is on file and what has been scored, or a window that
   closed with no price behind it. `--warn` rather than `--negative`: nothing is
   broken, something is missing and pressing the button usually fixes it. */
.score-warn {
  color: var(--warn);
  font-size: var(--fs-base);
  border: 1px solid var(--warn);
  border-radius: var(--radius-xs);
  padding: 0 5px;
}

/* When the first reading lands. Drawn large and above the grid while it is
   true, because for the first month it is the only sentence on this page with
   information in it - a table of dashes under nothing reads as a failure. */
.score-waiting {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 10px 0 0;
  padding: 8px 10px;
  border-left: 3px solid var(--accent);
  background: var(--panel);
  border-radius: var(--radius-xs);
  font-size: var(--fs-md);
}
.score-waiting svg { color: var(--accent); flex: 0 0 auto; }

/* The bill for the answers being scored, and the nil bill for scoring them.
   Side by side deliberately: the whole argument for this feature is the ratio
   between those two numbers. */
.score-spend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
}
.score-spend-cell { display: flex; flex-direction: column; gap: 1px; }
.score-spend-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.score-spend-value { font-size: var(--fs-md); font-variant-numeric: tabular-nums; }
.score-spend-total .score-spend-value { font-size: var(--fs-lg); }
.score-spend-free .score-spend-value { color: var(--positive); }

/* The card. `.daymove-card` whole, borrowed like the Review Board's is, with
   one change: the button is not the expensive thing on this page, so it does not
   need the visual weight the two paid ones carry. */
.score-card { margin-top: 12px; }
.score-state { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.score-positions { font-variant-numeric: tabular-nums; }
.score-when, .score-none { color: var(--muted); }

/* --- the grid: one block per question, one row per window ---------------- */
.score-block { margin-top: 16px; }
.score-source { margin-bottom: 18px; }
.score-source:last-child { margin-bottom: 0; }
.score-source-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 2px;
  font-size: var(--fs-md);
}
.score-source-what { margin: 0 0 8px; }

.score-grid, .score-stances, .score-verdicts {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-base);
  font-variant-numeric: tabular-nums;
}
.score-grid th, .score-stances th, .score-verdicts th {
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  font-weight: 600;
  padding: 4px 8px 4px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.score-grid td, .score-stances td, .score-verdicts td {
  padding: 5px 8px 5px 0;
  border-bottom: 1px solid var(--line-soft);
  white-space: nowrap;
}
.score-grid tbody tr:last-child td,
.score-stances tbody tr:last-child td,
.score-verdicts tbody tr:last-child td { border-bottom: 0; }
.score-grid th[scope="row"] { font-size: var(--fs-base); text-transform: none;
  letter-spacing: 0; color: var(--text); border-bottom: 1px solid var(--line-soft); }
/* Numbers right, and the same tabular figures as the grid pages, so a column of
   percentages can be read down rather than across. */
.score-num { text-align: right; }
th.score-num { text-align: right; }
.score-num.good { color: var(--positive); }
.score-num.bad { color: var(--negative); }
.score-num.plain { color: var(--muted); }
/* The one column the whole table is for. */
.score-excess { font-weight: 600; }

/* --- the record: one line per verdict, one cell per window --------------- */
/* Nine columns of figures on a phone is not a table anybody can read, so it
   scrolls inside its own box rather than pushing the page sideways - the same
   bargain the Jobs tab's result table makes. */
.score-table-scroll { overflow-x: auto; }
.score-verdicts { min-width: 640px; }
.score-source-cell { color: var(--muted); }
.score-source-cell svg { vertical-align: -2px; }
.score-when-cell { color: var(--muted); font-size: var(--fs-base); }

/* A stock, as a button: the reader's next question is always about the name,
   and making it text would mean copying a ticker into a search box. Quiet until
   hovered - the same treatment the review's action chips get. */
.score-symbol {
  border: 0;
  background: transparent;
  color: var(--text);
  padding: 0;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.score-symbol:hover { color: var(--accent); text-decoration: underline; }

/* What the model said, and what came of the advice. One pill shape for both,
   because they are the same kind of object - a short word carrying a colour. */
.score-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 0 5px;
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  font-weight: 600;
}
.score-pill.good { color: var(--positive); border-color: var(--positive); }
.score-pill.bad { color: var(--negative); border-color: var(--negative); }
.score-pill.plain { color: var(--muted); }
.score-pill svg { width: 11px; height: 11px; }

/* Not yet, and could not be. Two different absences and two different marks:
   an em dash for a window still open, a question mark for one that closed with
   no price behind it. Both muted, and neither is a zero. */
.score-pending { color: var(--muted); }
.score-missing { color: var(--warn); }

/* A window this source is not read at, as against one it has not reached yet.
   Only the exit advisor is scored at eighteen months, and drawing "not measured
   here" the same way as "not scored yet" would invent a fault on three quarters
   of the table. Fainter than `.score-missing`, which is a real gap. */
.score-unread { color: var(--line); }

/* --- the cut-the-winner tally -------------------------------------------
   The one outcome the plan behind this feature calls unforgivable, and the one
   an average hides: a single 3x among forty rows moves an excess median by
   almost nothing. Drawn at zero as well as at one, because "none yet" is the
   number this block exists to keep saying - a counter that appeared only on
   failure would leave a reader unsure whether it was clean or simply not
   running. */
.score-clean {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 0;
  color: var(--muted);
}
.score-clean svg { color: var(--positive); flex: 0 0 auto; margin-top: 2px; }
.score-cuts { list-style: none; margin: 10px 0 0; padding: 0; }
.score-cut {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  padding: 6px 0;
  border-top: 1px solid var(--line-soft);
}
.score-cut:first-child { border-top: 0; padding-top: 0; }
/* The move itself, loudest thing on the row: it is the size of the mistake. */
.score-cut-move {
  font-size: var(--fs-md);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Whether the verdict was borne out, drawn as an underline rather than a colour.
   The colour on that cell already means something - which way the stock went -
   and a TRIM that was right is a red number that is nonetheless correct. */
.score-right { text-decoration: underline; text-decoration-color: var(--positive);
  text-decoration-thickness: 2px; text-underline-offset: 3px; }
.score-wrong { text-decoration: underline; text-decoration-color: var(--negative);
  text-decoration-thickness: 2px; text-underline-offset: 3px;
  text-decoration-style: dotted; }

/* The two caveats under the record. Hints rather than warnings: neither is a
   fault, and both are things a figure above has to be read with. */
.score-bench, .score-ledger {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin-top: 10px;
}
.score-bench svg, .score-ledger svg { flex: 0 0 auto; margin-top: 2px; }

/* --- Reports: the log of what has been asked, both questions ------------- */
/* Its own AI section now rather than a sub-tab of Eval Profile, and it carries
   the What-to-Do-Now verdicts beside the why-it-moved reports - so a row here
   is one of two shapes and the cells that differ say so per row.

   The table reuses `.advice-table` whole - the Add-More tab already solved a
   dense list of stocks with a verdict, a timestamp, an asker and a cost on each
   row, which is exactly this list with different words in it. What is added
   here is the column widths, the marks a row can carry that no Add-More row can
   (the session it explains, and the stock having left the book), and the opened
   report sitting in a modal over the list. */

/* The toolbar is the What-to-Do-Now one whole - `.toolbar.advice-toolbar`,
   which already wraps and already caps a select at 190px. One exception: the
   Answer menu carries the longest option on either page, because its words are
   the ones the cells beneath it use - "Nothing found accounts for it" rather
   than `UNEXPLAINED`. Widened rather than given a shorter vocabulary of its
   own, since two names for one attribution is exactly how a filter and the
   column it filters stop agreeing. */
.toolbar.advice-toolbar select[data-reports-filter="answer"] { max-width: 250px; }

/* The whole row opens its report, so it has to look like it does. Add-More
   puts the link in a cell because a row there has two actions; a row here has
   one, and a link inside a row where everything is clickable is a smaller
   target for no reason. */
.dmlog-table tbody tr { cursor: pointer; }
.dmlog-table tbody tr:hover { background: var(--row-hover); }

.dmlog-c-when { white-space: nowrap; font-variant-numeric: tabular-nums; }
.dmlog-c-move { white-space: nowrap; font-variant-numeric: tabular-nums; text-align: right; }
.dmlog-c-move.good { color: var(--positive); }
.dmlog-c-move.bad { color: var(--negative); }
.dmlog-c-move.plain { color: var(--muted); }
/* The session a move report explains, under the move it explains rather than in
   a column of its own. It had one while every row was a move report; half of
   them are position verdicts now, which have no session at all, and a column
   that is blank on half a table is a column paying no rent. */
.dmlog-c-session {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}
/* A verdict's answer to the same question - not a percentage on a day but an
   age, so it is the one cell here that is words rather than figures. */
.dmlog-c-age { color: var(--muted); }
/* Which question was asked. Muted because it is the row's category rather than
   its content: the eye should land on the headline, not on the twenty rows
   reading "Why it fell". */
.dmlog-c-kind, .dmlog-c-tab { white-space: nowrap; color: var(--muted); }
/* The answer itself, so it gets whatever width is left and wraps rather than
   being clipped - a headline cut off mid-sentence is a row worth nothing. */
.dmlog-c-headline { line-height: 1.5; min-width: 220px; }
.dmlog-c-stock .advice-name { display: block; }

/* "today" beside the session date. A pill would be too loud on every second
   row; this is the same quiet accent the Add-More table marks a due row with. */
.dmlog-today {
  display: block;
  color: var(--accent);
  font-size: var(--fs-xs);
}
/* A report on a stock that has left the book. Marked rather than hidden: it is
   the report most likely to be looked for and the one the other two tabs can
   never show. */
.dmlog-gone {
  display: inline-block;
  margin-left: 6px;
  padding: 0 6px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-xs);
  white-space: nowrap;
}

/* --- Reports: density, marks and the rail ------------------------------- */
/* Everything below is scoped on `.reports-panel`, which only this section wears
   (see `markup` in `reports.js`). The table it draws is `.advice-table`, which
   is ALSO the What-to-Do-Now list on Eval Profile and the stock popup's own
   list - so a header mark or a tinted row written against that class would turn
   up on two tabs nobody asked about. The scope is the point, not decoration.

   What this tab needs that those two do not: it is the only one that is a *log*
   rather than a working list. It grows without limit, it is read by scanning
   rather than by acting, and every row on it is money already spent. So it gets
   the density, the marks that let an eye find a column without reading the
   header, and one channel - a rail down the left edge - that says which way the
   row leans before a word of it is read. */

/* The card's own title. 14px and bold against the 13px `.panel h3` everywhere
   else: this is a section head above a wall of 12px figures, and the step is
   what makes it read as the top of a hierarchy rather than as the first row of
   it. The margin under it comes in with the density below. */
.reports-panel h3 { font-size: var(--fs-lg); font-weight: 700; margin: 0 0 6px; }
/* The three lines between the title and the toolbar - the count, the spend and
   the "showing n of m". Direct children only: `.hint` is also every explanatory
   line inside an OPENED report, which sits in a popup inside this same panel
   and is prose to be read rather than chrome to be skimmed. */
.reports-panel > .hint { margin: 0 0 5px; }
/* The filter chips. Tighter than the 8px `.toolbar` gap they inherit, because
   there are seven controls here and the wrap they cause is what pushes the
   first row of the table below the fold on a laptop. */
.reports-panel .toolbar.advice-toolbar { gap: 4px 6px; margin: 6px 0 6px; }

/* The rows. 6px rather than the 7px `.advice-table` gives every other list, and
   the leading of the stacked sub-lines with it: a row here is two or three
   lines tall in three of its seven cells - the company under the ticker, the
   session under the move, the conviction under the call - so the LEADING is
   what actually sets the row height, and shaving the padding alone would have
   moved it by 2px. Together they are about 8px a row, which is a couple more
   reports above the fold on every screen. */
.reports-panel .dmlog-table td { padding: 6px 10px 6px 0; line-height: 1.35; }
.reports-panel .dmlog-table th { padding: 4px 10px 5px 0; }
.reports-panel .dmlog-table .advice-name,
.reports-panel .dmlog-table .advice-age,
.reports-panel .dmlog-table .advice-tokens,
.reports-panel .dmlog-table .advice-asked-by,
.reports-panel .dmlog-table .dmlog-c-session,
.reports-panel .dmlog-table .dmlog-today { line-height: 1.3; }
/* The answer itself still wraps to as many lines as it needs - it is the one
   cell that is a sentence - but at the table's own leading rather than at the
   1.5 it carried while the rows around it were looser. */
.reports-panel .dmlog-c-headline { line-height: 1.4; }

/* The ticker, one step up from the body text at 13px. It is what a reader
   scanning this list is looking FOR - the row is found by its stock and then
   read - and it was the same 12px as the timestamp beside it. The company name
   under it stays at --muted and its own size: that is the confirmation, not the
   target. */
.reports-panel .dmlog-c-stock .advice-sym { font-size: var(--fs-md); }

/* The head, and the mark in it. `.icon` is `display: block` by default - it is
   drawn inside flex buttons nearly everywhere - so the label and its mark are
   wrapped in one inline-flex rather than left to sit on two lines.
   The mark is quiet on purpose: it is a finding aid for a column, not a second
   label, and at full strength seven of them would read as a row of buttons. */
.dmlog-th { display: inline-flex; align-items: center; gap: 4px; }
.dmlog-th .icon { opacity: .65; }
/* The two heads over right-aligned columns. `.advice-cost` and `.dmlog-c-move`
   right-align the CELLS and always did; the heads over them did not, so the
   word "Cost" sat at the left of a column of figures stacked at the right. */
.reports-panel .dmlog-table th.dmlog-c-cost,
.reports-panel .dmlog-table th.dmlog-c-move { text-align: right; }
/* The head used to be sticky here, and is not any more. Two things took the
   argument away, and one of them made it impossible.
   The argument was that this is a log with no natural end, so a reader is
   reliably a long way from the labels by the time they need them. It is paged
   at fifteen rows now, which is the end it did not have.
   The impossibility is the resize: a table whose columns the reader can widen
   has to sit in a horizontal scroller or it bursts the card and gives the whole
   application a sideways scrollbar. `overflow-x: auto` on that wrapper computes
   `overflow-y` to `auto` as well, which makes the wrapper the nearest scrolling
   ancestor - and a `top: 0` measured against a box that never scrolls vertically
   pins the head to a wrapper that is itself scrolling away up the page. The head
   would be sticky to nothing, which looks like a bug and is worse than not doing
   it at all. The scroller wins: a page that scrolls sideways is a broken page,
   and a header you scroll past is an inconvenience. */
.reports-panel .table-scroll { margin-top: 2px; }

/* The call, as a badge rather than as a coloured word.
   The tint is mixed from `currentColor`, which is the whole trick:
   `.advice-action` already resolves to --positive, --negative or --muted
   through its tone class, so one declaration paints all three states, follows
   both themes, and follows a custom theme's own two colours - where three
   hard-coded backgrounds would be the one thing on the screen that did not.
   The same shape as `.delta-pill` above: a 4px badge, not a 999px lozenge, and
   tinted rather than filled. A saturated fill on every row of a long list reads
   as a wall of stickers - see the note there.
   NOT green for the up side, and this is not an oversight: see --positive at
   the top of this file. The reader is red/green colour-blind and blue-vs-red is
   the pair that separates for them. */
.dmlog-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  line-height: 1.5;
  background: color-mix(in srgb, currentColor 12%, transparent);
}
/* The triangle inside a SELL, or inside a report that says the thesis is
   broken. It inherits the badge's colour, so it is never a second hue - it is
   the shape channel doing the work colour cannot do on its own. */
.dmlog-badge .icon { flex: none; }

/* The rail down a row's left edge - which way this row leans, before a word of
   it is read. Two channels rather than one: a 2px edge that survives greyscale
   as a dark or a pale line, and a tint under the row so faint it reads as a
   change of paper rather than as a colour.
   `plain` gets neither, which is what makes the marked rows mean something.
   Painted from `data-dmlog-tone` rather than from a class, because `.good` and
   `.bad` are worn by a dozen unrelated things in this stylesheet and a tinted
   TABLE ROW is not something any of them should be able to turn on. */
.reports-panel .dmlog-table tbody tr { border-left: 2px solid transparent; }
.reports-panel .dmlog-table tbody tr[data-dmlog-tone="good"] {
  border-left-color: color-mix(in srgb, var(--positive) 55%, transparent);
  background: color-mix(in srgb, var(--positive) 5%, transparent);
}
.reports-panel .dmlog-table tbody tr[data-dmlog-tone="bad"] {
  border-left-color: color-mix(in srgb, var(--negative) 55%, transparent);
  background: color-mix(in srgb, var(--negative) 5%, transparent);
}
/* Hover still wins. The tone rules above and `.dmlog-table tbody tr:hover` tie
   on specificity, and a tie is decided by source order - which would have left
   every tinted row the one row on the table that did not light up under the
   pointer. Stated rather than relied upon. */
.reports-panel .dmlog-table tbody tr[data-dmlog-tone]:hover { background: var(--row-hover); }
/* The rail needs somewhere to sit. `.advice-table` cells pad on the right only,
   so without this the 2px edge would be flush against the timestamp. */
.reports-panel .dmlog-table td:first-child,
.reports-panel .dmlog-table th:first-child { padding-left: 8px; }

/* The chevron at the end of a row. The whole row opens its report and has since
   this list was built, but nothing on the row ever SAID so - the only sign was
   the cursor, which is a thing you find by accident. Not a column: it carries
   no data, it is not in `LOG_COLUMNS`, and its head is a screen-reader label
   rather than a word. Faint at rest and full strength under the pointer, which
   is the affordance rather than the decoration. */
.reports-panel .dmlog-table .dmlog-c-open {
  width: 20px;
  /* Enough to keep the chevron off the table's own edge. It used to be zero,
     which was right while this was a 20px column of its own and wrong once it
     became the filler as well: the filler is as wide as the slack, `text-align`
     pins the chevron to its right edge, and with no padding that edge is the
     last pixel of the card. */
  padding-right: 4px;
  text-align: right;
  color: var(--muted);
  opacity: .4;
}
.reports-panel .dmlog-table .dmlog-c-open .icon { display: inline-block; vertical-align: -1px; }
.reports-panel .dmlog-table tbody tr:hover .dmlog-c-open { opacity: 1; color: var(--accent); }

/* Figures line up here too. The stamp, the move and the cost already asked for
   tabular digits; the age ("3 days ago", "17h ago") and the token count under a
   cost are figures in a column and never did. */
.dmlog-c-age, .advice-tokens { font-variant-numeric: tabular-nums; }

/* --- Reports: the reader's own columns, and the pages under them --------- */
/* The head is draggable to reorder and its right edge is a resize handle, both
   the way the grid's is - `.dragging`, `.drop-before` and `.drop-after` are
   painted by the shared rule up beside the grid's own, and `.rz-edge` and
   `.resizable` come from the width engine untouched. What is only true here is
   below: how the table behaves once that engine has pinned its columns, and
   what the pager under it looks like. */

/* `width: max-content; min-width: 100%` is the pair that lets a pinned table
   still fill its panel - the same two the grid states. `applyWidths` sets an
   explicit width equal to the sum of the seven real columns; without the
   minimum the table would stop at that sum and leave a gap down the right of
   the card the moment anybody narrowed a column. */
.dmlog-table.resizable { width: max-content; min-width: 100%; }
/* A head being dragged must not also be selectable text, or the drag reads as
   a botched double-click on half the attempts. */
.dmlog-table th[data-dmlog-col] { user-select: none; }
.dmlog-table th[data-dmlog-col]:hover { cursor: grab; }
.dmlog-table th[data-dmlog-col].rz-edge:hover { cursor: col-resize; }

/* The chevron column does two jobs and is one cell, which is why it carries
   `rz-filler` as well as its own class: it is the affordance saying the row
   opens, AND it is the column the width engine leaves unsized so it can absorb
   whatever the seven real columns do not fill. Both jobs want it last and want
   it fluid, so making it two cells would be a second empty column for nothing.
   `.rz-filler`'s own rule is scoped to `table.grid`, so the padding reset it
   makes there is restated here rather than inherited. */
.dmlog-table .dmlog-c-open.rz-filler { border-right: 0; cursor: pointer; }

/* The pager's numbers. The arrows either side are plain `.btn sm` and need
   nothing; these are square, quiet, and one of them is lit. */
.dmlog-pages { display: flex; align-items: center; gap: 3px; flex-wrap: wrap; }
.dmlog-page {
  min-width: 24px;
  padding: 2px 5px;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}
.dmlog-page:hover { color: var(--text); }
/* Where you are. Lit rather than merely bold: the strip is a row of near
   identical squares and weight alone is not enough to find yourself in it at
   11px. `aria-current` says the same thing to a reader who cannot see it. */
.dmlog-page.on {
  color: var(--text);
  border-color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}
/* Where the run of numbers was cut. Not a button, and drawn as not one. */
.dmlog-gap { color: var(--muted); padding: 0 2px; user-select: none; }

/* The opened report is the Add-More popup - `.advice-modal`, `.advice-modal-box`
   and `.advice-modal-head`, defined once above and reused whole. Nothing is
   restyled here: a report drawn one way on the AI page's first tab and another
   way on its second would be two designs for one object, and the second would
   drift. The list stays rendered underneath, so closing puts the reader back
   exactly where they were.

   The one addition is letting the head wrap: it carries a name, a ticker, the
   direction and - for a stock since sold - a "no longer a position" mark, which
   is one item more than the Add-More head ever holds. It is a `flex: 0 0 auto`
   row in a fixed-height box now, so wrapping costs the body a line rather than
   pushing anything out of the popup. */
.advice-modal-head { flex-wrap: wrap; }

/* --- the AI tab's Why It Fell half ------------------------------------ */
/* The evals on file for one stock, and the report behind whichever is open. It
   reuses `.advice-table` for the list and `.report-split` for the report, both
   defined above and both unchanged - the point of this half is that a reader who
   has learned the AI tab's report already knows this one. What is added here is
   the two columns this table has that no other does, and the mark on the row
   whose report is open. */

.whyfall-panel { max-width: 880px; margin: 0 auto; }
.whyfall-table tbody tr { cursor: pointer; }
.whyfall-table tbody tr:hover { background: var(--row-hover); }
/* The row whose report is being read. A left rule rather than a background
   alone, matching `.report-jump.on` in the rail beside it: both are saying "this
   is the one you are on", and saying it two different ways in one panel would
   read as two different meanings. */
.whyfall-table tbody tr.on {
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}
.whyfall-table tbody tr.on td:first-child {
  box-shadow: inset 2px 0 0 var(--accent);
}

.whyfall-report { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 4px; }
.whyfall-loading { margin-top: 14px; }

/* The report as this tab and the AI page both draw it. The rail is sticky
   against whichever box scrolls it - the popup's body here, the modal on the AI
   page - and `.report-split` already says so; this only keeps the head above it
   from being dragged into the grid. */
.daymove-report .advice-head { margin-top: 4px; }
.daymove-report .report-split { margin-top: 14px; }

/* --- the job queue --------------------------------------------------------
   The Jobs tab and the popup's Fetch menu. Built from the same tokens as the
   provenance tables above: a job row is a status and a sentence, not a figure,
   so nothing here is coloured by value. */
.job-pill {
  display: inline-block; padding: 1px 8px; border-radius: var(--radius-lg);
  font-size: var(--fs-xs); font-weight: 600; border: 1px solid var(--line);
  color: var(--muted);
  /* Wide enough for the longest of the six, so the column of pills has one
     left edge and one right edge rather than six of each. `Cancelled` is the
     longest and sets the figure; anything shorter centres in it. */
  min-width: 62px; text-align: center;
}
/* Queued is deliberately the quietest of the six: it is the state a job spends
   most of its life in and the one nobody needs to look at. */
.job-pill.queued { color: var(--muted); }
.job-pill.leased,
.job-pill.running {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: var(--accent-soft);
}
.job-pill.succeeded {
  color: var(--positive);
  border-color: color-mix(in srgb, var(--positive) 50%, transparent);
}
.job-pill.failed {
  color: var(--negative);
  border-color: color-mix(in srgb, var(--negative) 50%, transparent);
}
.job-pill.cancelled { color: var(--muted); font-style: italic; }
/* A job that is running is the one thing on this panel that changes while it
   is being read, and a table that has to be re-read to find out is a table
   that gets re-read. The halo is the refresh dot's, not an opacity blink -
   same argument as `.dot.busy`: a mark that vanishes for half of every second
   reads as a rendering fault where an expanding ring reads as work. */
.job-pill.leased::before,
.job-pill.running::before {
  content: ''; display: inline-block; width: 5px; height: 5px;
  margin-right: 5px; vertical-align: 1px;
  border-radius: 50%; background: currentColor;
  animation: dot-pulse 1.8s ease-out infinite;
}
/* The dot still marks the pill, it just stops moving. Same trade the spinner
   makes: the information is in the mark, not in the animation. */
@media (prefers-reduced-motion: reduce) {
  .job-pill.leased::before,
  .job-pill.running::before { animation: none; }
}

/* The log is read as a list and was drawn as one with none of a list's
   furniture: no band over the headings, no rule under a row, and nothing under
   the eye tracking fifteen near-identical lines out to the far column. What is
   below is the shape `.fs-table` already uses one sub-tab away - micro-caps
   heading on the panel's second surface, a hairline under each row, a stripe
   under every second one and the accent under the hovered one - so a reader
   crossing from one to the other is reading the same table rather than two
   people's work. `separate` and not `collapse`, because the left-edge status
   rule below is an inset shadow and a collapsed border eats it.

   Said once for all four tables under the Jobs tab - the log, Run Jobs,
   Queue stats' per-lane counts and Fetch Theory's cadence - because they are
   four sub-tabs
   of one strip and a reader moves between them in a keypress. Only the log has
   a state to mark down its left edge; the shape above it is the same. */
.job-table,
.queue-table,
.run-jobs-table,
.checklist-table,
.theory-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--fs-sm);
}
.job-table th,
.queue-table th,
.run-jobs-table th,
.checklist-table th,
.theory-table th {
  padding: 5px 10px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
  white-space: nowrap;
}
/* 5px and not the 8 an untouched row would take: a hairline and a stripe are
   already separating these rows, and padding on top of both buys nothing but a
   page of fifteen that no longer fits a laptop screen without scrolling. */
.job-table td,
.queue-table td,
.run-jobs-table td,
.checklist-table td,
.theory-table td {
  padding: 5px 10px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
  line-height: 1.35;
}
/* The frame owns the last edge. A rule under the final row and a border under
   it is the same line drawn twice. */
.job-table tbody tr:last-child td,
.queue-table tbody tr:last-child td,
.run-jobs-table tbody tr:last-child td,
.checklist-table tbody tr:last-child td,
.theory-table tbody tr:last-child td { border-bottom: 0; }
/* Before the hover rule on purpose: the two carry the same specificity, so
   source order is the whole of what decides which paints a hovered stripe. */
.job-table tbody tr:nth-child(even) td,
.queue-table tbody tr:nth-child(even) td,
.run-jobs-table tbody tr:nth-child(even) td,
.checklist-table tbody tr:nth-child(even) td,
.theory-table tbody tr:nth-child(even) td { background: var(--row-stripe); }
.job-table tbody tr:hover td,
.queue-table tbody tr:hover td,
.run-jobs-table tbody tr:hover td,
.checklist-table tbody tr:hover td,
.theory-table tbody tr:hover td { background: var(--row-hover); }
/* Which rows are moving, and which broke, off the left edge - the 3px rule the
   Data Fetch Stats table marks a held stock with. These are the two questions
   the panel is opened for, and both were carried only by a pill in the middle
   of a very wide row, which is the one place the eye is not when it starts
   down a list. Three states go unmarked: done is the answer nobody is looking
   for, and queued and cancelled are both "nothing is happening". */
.job-table tbody tr[data-status="running"] td:first-child,
.job-table tbody tr[data-status="leased"] td:first-child {
  box-shadow: inset 3px 0 0 var(--accent);
}
.job-table tbody tr[data-status="failed"] td:first-child {
  box-shadow: inset 3px 0 0 var(--negative);
}
/* The row whose popup is up, tinted rather than outlined - and it has to beat
   both the stripe and the hover, which is why it carries a class more than
   either. `--panel-2` was the old mark and is now the heading band's colour;
   against a striped body it said nothing at all. */
.job-table tbody tr.job-row.open td,
.job-table tbody tr.job-row.open:hover td {
  background: color-mix(in srgb, var(--panel) 82%, var(--accent));
}

/* Every column but Fetch is sized by what is in it - `width: 1%` with
   `nowrap` is what asks a cell for exactly that - so the slack lands in the
   one column that can use it and the meta columns close up into a block
   at the right. They were spread across the panel by fixed pixel widths that
   the longest value in each overran anyway. */
.job-c-id {
  width: 1%; white-space: nowrap;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.job-c-what { width: 100%; }
/* The stock, in a column of its own rather than chipped onto the end of the
   fetch. It is the part somebody scanning a page is actually looking for, and
   a column is the only thing the eye can run straight down - a chip whose left
   edge moved with the length of the fetch beside it could not be scanned at
   all. It is also half of why a row is now one line. */
.job-c-stock { width: 1%; white-space: nowrap; }
.job-c-status { width: 1%; white-space: nowrap; }
.job-c-when {
  width: 1%; white-space: nowrap;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.job-c-by { width: 1%; white-space: nowrap; color: var(--muted); }

/* The widest column, and until now one grey run-on line that read the same on
   every row. Three parts: what was run, what it was run over, and the sentence
   under them. The middle one is the symbol - the part somebody scanning a page
   of fifteen is actually looking for - so it is the part set apart. */
.job-what { font-weight: 600; }
.job-where {
  display: inline-block; margin-left: 7px; padding: 0 5px;
  border: 1px solid var(--line); border-radius: var(--radius-xs);
  background: var(--panel-2); color: var(--muted);
  font-size: var(--fs-xs); letter-spacing: .02em; white-space: nowrap;
}
/* A pass over everything is not a stock and must not read as one: no chip, and
   set as the phrase it is. */
.job-where.all {
  border-color: transparent; background: none; padding: 0;
  font-style: italic; letter-spacing: 0;
}
/* The one expander, hard right and sized to its label. The row itself does not
   open anything: it opens a modal, and a row that did that on any click through
   it would take the whole log away from under a mis-aimed press. */
.job-c-open { width: 1%; white-space: nowrap; text-align: right; }
/* Over `.job-table th`, which sets every heading left: the column is as narrow
   as its button, so a heading anywhere but over it reads as a stray label. */
.job-table th.job-c-open { text-align: right; }
/* The log, at the density a log wants to be read at.
   `.job-table-tight` and not the shared `.job-table` rules above, because those
   are also Run Jobs, Queue stats, Checklist and Fetch Theory - four tables of a
   dozen rows each that are read one row at a time, where 5px of breathing room
   is worth what it costs. This one is fifteen near-identical rows scanned as a
   block, and it is the only one on the page with a pager under it: every pixel
   of row height is a row that did not fit.

   Most of the doubling is not here. It came from the row losing its second
   line - the grey "waiting for a worker" that every queued row carried, and the
   two clamped lines of traceback that every failed one did - which is now the
   pill's tooltip and the mark beside it. A page of fifteen was between fifteen
   and forty-five lines tall and no two pages were the same height; it is
   fifteen now. The padding below is the rest of it. */
.job-table-tight th { padding: 3px 8px; }
.job-table-tight td {
  padding: 2px 8px;
  font-size: var(--fs-xs);
  line-height: 1.25;
}
/* The pill sets the row's height, not the text beside it, so shrinking one
   without the other buys nothing. Same colours, same six words, tighter box. */
.job-table-tight .job-pill {
  padding: 0 6px;
  min-width: 54px;
}
/* The button too, for the same reason - it is the tallest thing in its cell. */
.job-table-tight .job-open { padding: 0 7px; }

/* Job execution is one working surface with two different reading modes. The
   controls and queue-health figures form a stable rail on the left; the log is
   the wide, scannable surface on the right. `minmax(0, 1fr)` is important here:
   it lets the table's own scroll box contain a widened or user-resized table
   instead of making the whole page grow sideways. */
.job-execution-layout {
  display: grid;
  grid-template-columns: minmax(350px, 390px) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}
.job-execution-sidebar {
  min-width: 0;
  padding-right: 14px;
  border-right: 1px solid var(--line);
}
.job-execution-sidebar > .hint { margin-bottom: 10px; }
.job-execution-sidebar .job-bar { align-items: stretch; }
.job-execution-sidebar .toolbar-gap { display: none; }
.job-execution-table { min-width: 0; }
.job-execution-table .tk-bar { margin-top: 0; }

/* The rail is useful only while it leaves the log enough room to scan. Below
   that point the same semantic groups stack in their DOM order: explanation,
   controls and statistics, then the table. */
@media (max-width: 1050px) {
  .job-execution-layout { grid-template-columns: minmax(0, 1fr); }
  .job-execution-sidebar {
    padding-right: 0;
    padding-bottom: 12px;
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }
}

/* The marks beside the pill: why it failed, and how many workers have died
   holding it. Both are exceptions - most rows carry neither - which is why they
   are marks rather than a column that would be empty on fourteen rows in
   fifteen. The whole message is on the tooltip, and the whole of *that* is in
   the Details popup. */
.job-flag {
  display: inline-flex;
  align-items: center;
  margin-left: 5px;
  vertical-align: middle;
  font-size: var(--fs-xs);
  font-weight: 600;
  cursor: help;
}
.job-flag svg { display: block; }
.job-flag.bad { color: var(--negative); }
.job-flag.warn { color: var(--warn); }

/* The list as a card. Without a frame the rows floated on the panel with the
   heading band hanging in mid-air over them; with one the table reads as the
   single object it is, and the band has something to sit inside. `.table-scroll`
   is shared, so this is said on the four panels that own one of the tables
   above and nowhere else. */
.job-panel .table-scroll,
.queue-panel .table-scroll,
.run-jobs-panel .table-scroll,
.stock-checklist .table-scroll,
.theory-panel .table-scroll {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}
/* Fifteen outlined buttons down the right edge were the loudest column on a
   panel whose subject is everywhere else. Ghosted to a word until the row is
   under the pointer, where the frame comes back and says it is pressable - the
   same trade `.row-btn` makes in the grid, except the word stays legible
   throughout rather than fading out, because this is the row's only way in. */
.job-open {
  border-color: transparent;
  background: transparent;
  color: var(--muted);
}
.job-row:hover .job-open {
  border-color: var(--line);
  background: var(--panel-2);
  color: var(--text);
}
.job-row:hover .job-open:hover,
.job-open:focus-visible { border-color: var(--accent); }
/* Open is marked softly rather than with the filled `.btn.on` used on chart
   toggles: a page of fifteen rows carries fifteen of these, and one of them
   filled in the accent would be the loudest thing on the panel. The row behind
   it is already tinted, which is where the eye actually lands. */
.job-open.on,
.job-open.on:hover,
.job-row:hover .job-open.on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: var(--accent-soft);
}
/* The three filters over the log. The stock one is a box to type in rather
   than a menu - the log carries hundreds of symbols - so it is sized the way
   the Data Fetch Stats search box is, wide enough for the longest symbol and
   its magnifier without being the widest thing on the toolbar. The two menus
   beside it are capped: the fetch menu's longest entry is `BSE filings (all
   stocks)`, and a select left to size itself off that would sit wider than
   everything else on the bar put together. */
.job-bar .input-icon > input[type="text"] { min-width: 150px; }
.job-bar select { max-width: 170px; }
/* The status filter is the one of the three that is not a menu of single
   choices: the questions asked of the log are sets - the three states that mean
   "still going", the two that mean "did not finish" - so it is a panel of
   tick-boxes behind a button that says what is ticked. The button wears the
   accent while anything is, because a filter that is on and looks exactly like
   a filter that is off is how a reader ends up believing the log is empty. */
.job-status-btn { display: inline-flex; align-items: center; gap: 5px; }
.job-status-btn svg { flex: none; opacity: 0.75; }
.job-status-btn.on {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: var(--accent-soft);
}
.job-status-btn.on svg { opacity: 1; }
/* Narrower than the standard 330px: six pills and three shortcuts, none of them
   long. Capped to the viewport so the phone rule further up has nothing left to
   correct. */
.job-status-panel { width: min(260px, calc(100vw - 24px)); }
.job-status-opts { display: flex; flex-direction: column; gap: 1px; margin-bottom: 8px; }
/* The pill the rows themselves wear, so the control and the column it filters
   read as one vocabulary - the tick is the only thing this adds to it. */
.job-status-opt .job-pill { pointer-events: none; }
.job-status-opt input { margin: 0; cursor: pointer; }
.job-status-opt.on { background: var(--accent-soft); }
.job-status-opt input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The two sets worth a press of their own, and the way back to every job. A row
   under the boxes rather than three more boxes in them: they are not a seventh
   and eighth state, they are shortcuts to combinations of the six above. */
.job-status-panel .picker-actions { margin: 0; border-top: 1px solid var(--line); padding-top: 8px; }

/* --- the metrics bar over the log ----------------------------------------
   Queue health, which the log itself cannot answer. Fifteen rows of a table
   that only grows say what became of fifteen jobs; they cannot say whether the
   backlog is falling, or whether this morning was worse than the last hour.
   Five states down, five windows across, and every cell in it is the filter it
   counts - press one and the list below is narrowed to exactly those rows.

   Drawn as a table rather than as a row of chips because it is one: the value
   in it is the comparison down a column and across a row, and a wrapped ribbon
   of thirty "Failed 1 Hr 0" chips has neither axis. It is also why the whole
   block is about six compact rows tall - roughly what the table below it just
   got back by dropping its second line. */
.job-metrics {
  /* Sized to its own numbers, not to the panel. Stretched to a 1400px window,
     five counts sit a hand's width apart and the comparison across a row - the
     one thing the bar is read for - stops being readable at a glance. Capped at
     the panel so a narrow window scrolls the bar rather than the page. */
  width: fit-content;
  max-width: 100%;
  margin: 8px 0 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  overflow-x: auto;
}
.job-metrics-table {
  width: auto;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}
/* The heading band, matching every other table under this tab - said again
   rather than shared, because those are `.job-table`'s and this one is tighter
   than they are. */
.job-metrics-table thead th {
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}
/* The window headings are buttons that fill their cell - the press target is
   the column, not the word - so the padding is on the button and the cell has
   none. The corner is plain text and keeps its own. */
.job-metric-col { padding: 0; }
.job-metric-corner {
  padding: 4px 10px;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: .05em;
}
/* A column heading is a button - it puts the list under that window and leaves
   the states alone - so it fills its cell rather than sitting in it, or the
   press target would be the word and not the column. */
.job-metric-head,
.job-metric-label,
.job-metric {
  display: block;
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.job-metric-head {
  padding: 4px 10px;
  text-align: right;
  text-transform: uppercase;
  letter-spacing: .05em;
}
.job-metric-head:hover,
.job-metric-label:hover,
.job-metric:hover { background: var(--row-hover); }
.job-metric-head:focus-visible,
.job-metric-label:focus-visible,
.job-metric:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The window that is on, marked on the heading and down the column. Softer than
   the cell mark below it, because a whole column tinted as loudly as one cell
   would leave nothing to find. */
.job-metric-head.on { color: var(--accent); }
.job-metric-col.on { box-shadow: inset 0 -2px 0 var(--accent); }

.job-metrics-table tbody th,
.job-metrics-table tbody td {
  padding: 0;
  border-bottom: 1px solid var(--line-soft);
}
.job-metrics-table tbody tr:last-child th,
.job-metrics-table tbody tr:last-child td { border-bottom: 0; }
.job-metric-name { width: 1%; white-space: nowrap; text-align: left; }
.job-metric-label {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  font-weight: 600;
  text-align: left;
}
.job-metric-label svg { flex: none; }
/* The count. Right-aligned and tabular so a column of them lines up on the
   units digit, which is the whole point of reading down one. */
.job-metric {
  /* Wide enough that three digits and one digit share a left edge, so a column
     of counts reads as a column rather than as five ragged numbers. */
  min-width: 52px;
  padding: 3px 10px;
  text-align: right;
  font-weight: 600;
}
/* A nought is still pressable - "nothing failed in the last hour" is a real
   answer, and a control that disappears when its count reaches nought
   disappears at the exact moment somebody is watching it - but it is not worth
   the eye stopping on. */
.job-metric.zero { color: var(--muted); font-weight: 400; opacity: .55; }
/* The cell the list below is actually under. Filled rather than outlined: it is
   one cell in thirty and it has to be findable without hunting. */
.job-metric.on,
.job-metric.on:hover {
  background: var(--accent-soft);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent);
}
.job-metric.on.zero { opacity: 1; font-weight: 600; }
.job-metric-row.on .job-metric-label { color: var(--accent); }

/* The row hues, borrowed from `.job-pill` above so the bar and the column of
   pills it filters are one vocabulary rather than two palettes that happen to
   sit on one page. Only the label is tinted; the counts stay body text, because
   thirty coloured numbers is a heat map of nothing. */
.job-metric-row[data-metric-row="running"] .job-metric-label { color: var(--accent); }
.job-metric-row[data-metric-row="succeeded"] .job-metric-label { color: var(--positive); }
.job-metric-row[data-metric-row="failed"] .job-metric-label { color: var(--negative); }
.job-metric-row[data-metric-row="cancelled"] .job-metric-label {
  color: var(--muted); font-style: italic;
}
/* Queued keeps the body colour: it is the state a job spends most of its life
   in and the one nobody needs to be drawn to - the same argument
   `.job-pill.queued` makes for being the quietest of the six. */

/* The bar's own read failing, said where the bar is. The counts already in hand
   stay on the screen behind it - a bar blanked on a failed read would say every
   state is empty, which is a worse lie than a count a few seconds old. */
.job-metrics-error {
  max-width: 46ch;
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 0;
  padding: 5px 10px;
  border-bottom: 1px solid var(--line-soft);
  color: var(--warn);
}
.job-metrics-error svg { flex: none; margin-top: 1px; }
.job-metrics.stale .job-metrics-table { opacity: .7; }
/* The shared stock picker - `stockpicker.js`, drawn wherever "which stock" is
   asked. It lives here rather than under the Jobs page it grew out of because
   it is not the Jobs page's control any more.

   The list is drawn by the page rather than left to the browser for one reason:
   a `datalist` or a `select` opens a menu the page has no say in, one symbol
   per full-height row, and several hundred stocks that way is a scroll however
   short the names are. A stock symbol is eight characters in a row twenty-two
   wide, so the list is a grid instead - three columns of tight rows put roughly
   forty on screen where the browser's list managed a dozen. Wider than the
   standard 330px panel to fit those columns, and capped to the viewport so the
   phone rule further up has nothing to correct. */
.stock-pick-panel { width: min(420px, calc(100vw - 24px)); padding: 6px; }
.stock-pick-count { margin: 0 0 5px; font-size: var(--fs-xs); }
/* The two tick-boxes over the list: what else to offer. A row of their own
   above the count, because they change what the count is counting - reading
   "42 stocks you hold" with the control that widens it sitting underneath
   would be reading the answer before the question. */
.stock-pick-scopes {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 0 0 6px;
  margin: 0 0 6px;
  border-bottom: 1px solid var(--line);
}
/* A label rather than a box with text beside it, so the word is part of the hit
   area - the same trade the grid's toolbar toggles make. Not `.toolbar .toggle`
   itself: that rule is scoped to a toolbar and this panel is not one, and
   widening it to reach here would put the grid's tick-boxes at the mercy of a
   dropdown's styling. */
.stock-pick-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px 2px 5px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
}
.stock-pick-toggle:hover { border-color: var(--accent); color: var(--text); }
.stock-pick-toggle input { margin: 0; cursor: pointer; }
.stock-pick-toggle svg { flex: none; opacity: 0.75; }
/* Ticked reads as the stronger state, because it is: the list is showing more
   than the box beside it would. */
.stock-pick-toggle.on {
  color: var(--text);
  border-color: var(--accent);
  background: var(--accent-soft);
}
.stock-pick-toggle.on svg { opacity: 1; }
.stock-pick-toggle input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* What ticking the box would add - "+312". Dimmer than the label, because it is
   a fact about the list rather than part of the control's name. */
.stock-pick-note { color: var(--muted); font-variant-numeric: tabular-nums; }
.stock-pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
  gap: 1px 4px;
  /* The widened list is the whole research universe - several hundred names -
     and a panel that tall would run off the bottom of the screen with its own
     tick-boxes scrolled past. Capped and scrolled here rather than on the panel
     so the ticks and the count stay in view while the stocks move. */
  max-height: min(46vh, 340px);
  overflow-y: auto;
}
/* A row of the list, not a button on a toolbar: no frame, no fill, and the
   hover is the only thing that says it is pressable - the same trade the job
   log's own Details buttons make, and for the same reason. Forty outlined
   buttons in a grid would be the loudest thing on the page. */
.stock-pick-opt {
  border: 0;
  background: none;
  padding: 2px 5px;
  border-radius: var(--radius-xs);
  font: inherit;
  font-size: var(--fs-sm);
  color: var(--text);
  text-align: left;
  cursor: pointer;
  /* A symbol never wraps: a two-line row would break the grid's rhythm, and
     the whole symbol is in the box the moment it is picked anyway. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* The hue on the left edge says which of the three lists a name came off.
     Transparent on a plain holding, which is the default scope: a list that is
     all one kind needs no key, and the marks appear exactly when a tick has
     made the list mean more than one thing. */
  border-left: 2px solid transparent;
}
.stock-pick-opt:hover { background: var(--panel-2); }
.stock-pick-opt:focus-visible { outline: 1px solid var(--accent); outline-offset: -1px; }
.stock-pick-opt.on {
  color: var(--accent);
  background: var(--accent-soft);
}
/* Watched-not-held and deleted, the two a tick brings in. Held keeps the plain
   text colour because it is the base of every list this control draws. */
.stock-pick-opt.watched { border-left-color: color-mix(in srgb, var(--accent) 60%, transparent); }
.stock-pick-opt.gone { border-left-color: var(--negative); color: var(--muted); }
/* The way back to no stock in particular. Spans the grid rather than sitting as
   one cell among the symbols, because it is not one of them - it is the row
   that undoes the rest. */
.stock-pick-opt.clear { grid-column: 1 / -1; color: var(--muted); }
.stock-pick-opt.clear:hover { color: var(--text); }
/* One job, in a box of its own: what it changed and then what it printed, on
   one scroll. It borrows `.modal` for the frame the way `.notes-modal` does and
   overrides only what a box of output needs - both panels inside it are capped
   scrollers where they sit in a table row, and a scroller inside a scroller is
   exactly what a popup this size exists to avoid. `.modal-body` is the one
   thing that scrolls. */
.job-modal .job-log,
.job-modal .job-result-table { max-height: none; }
/* The output half, under a heading of its own now that it is stacked under the
   figures rather than behind a tab of its own. Same size as `.job-result h4`
   over the table above it, so the two halves read as two halves. */
.job-output { margin: 2px 0 0; }
.job-output h4 { margin: 0 0 6px; font-size: var(--fs-sm); }
.job-modal .job-result-table { display: table; overflow: visible; }
/* The reason under the name of the fetch, on its own line: it is the sentence
   that says why a job failed or what it is waiting for, and inline it would
   push the columns about. */
/* The pager under the log. Nothing on the left but which rows these are, the
   two buttons hard right - the log is read top-down, so the control that moves
   it belongs after the last row rather than above the first. */
/* Two logs page, and they page the same way. Stated as one rule for the reason
   the reorder marks below the grid are: the Jobs log turns pages against a SQL
   offset and the Reports log against an array it already holds, which is a
   difference in where the rows come from and no difference at all in what the
   footer under them looks like. */
.job-pager, .dmlog-pager {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
.job-pager-range { color: var(--muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
/* The sentence that was under the name of the fetch - why it failed, or what it
   is waiting for - is not drawn on the row any more. It cost every one of
   fifteen rows a second line, and every queued row carried one ("waiting for a
   worker") that said nothing the pill beside it had not. It is the pill's
   tooltip now, and where it is a failure it is also a mark beside the pill:
   `.job-flag`, above. The whole of it is still in the Details popup, which is
   what the row's one button is for. */
.job-command code { font-size: var(--fs-sm); word-break: break-all; }
.job-error { color: var(--negative); }
/* What the fetch actually brought in, above the output rather than below it:
   it is what the job was for, and the log is what you read when it is wrong.
   Old value beside new in two columns - a figure alone cannot say whether the
   run changed anything, which is the whole question. */
.job-result { margin: 2px 0 10px; }
.job-result h4 { margin: 0 0 6px; font-size: var(--fs-sm); }
.job-result-source {
  margin: 8px 0 3px; font-size: var(--fs-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
}
.job-result-count {
  margin-left: 8px; font-weight: 400; text-transform: none; letter-spacing: 0;
  color: var(--muted); font-size: var(--fs-xs);
}
/* Capped and scrolled for the reason `.job-log` is: a first fetch of a stock
   nothing has been run for moves every field it has. */
.job-result-table {
  width: 100%; max-width: 720px; border-collapse: collapse;
  display: block; max-height: 300px; overflow: auto;
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums;
}
.job-result-table th {
  text-align: left; font-weight: 400; color: var(--muted);
  padding: 1px 10px 1px 0; white-space: nowrap;
}
.job-result-table td { padding: 1px 10px 1px 0; }
/* The old value is context, the new one is the answer - so only one of them
   is set in the body colour. The arrow is drawn rather than written so the
   two columns still line up when a value is missing. */
.job-result-was { color: var(--muted); }
.job-result-now::before { content: '→'; margin-right: 6px; color: var(--muted); }
.job-transport { color: var(--muted); }
/* Said only when something is live AND following is switched off, which is the
   one combination where a still page means nothing is wrong. It has to read as
   a state rather than a failure - so `--warn` and a left rule, the shape every
   other "this is on purpose" line in the app uses, rather than the red of
   `.job-error`. The icon is the same one the Refresh button carries, because
   pressing that is the whole of what this line asks for. */
/* Two flex items and no more, which is why the sentence is wrapped in a span in
   the markup: bare text beside an element becomes an anonymous flex item of its
   own, so the `<strong>` in the middle would have cut the line into three boxes
   laid out side by side instead of one paragraph that wraps. */
.job-paused {
  display: flex; align-items: baseline; gap: 7px;
  margin: 2px 0 8px;
  padding: 6px 9px;
  border-left: 2px solid var(--warn);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: color-mix(in srgb, var(--warn) 9%, transparent);
  color: var(--text); line-height: 1.5;
}
.job-paused svg { flex: none; color: var(--warn); transform: translateY(2px); }
/* The child's output. Capped and scrolled rather than allowed to grow: a
   screener pass prints thousands of lines and an expanded row that pushed the
   rest of the table off the screen would be unusable. */
.job-log {
  max-height: 320px; overflow: auto; margin: 6px 0;
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg); color: var(--muted);
  font-size: var(--fs-sm); line-height: 1.45; white-space: pre-wrap; word-break: break-word;
}

/* The popup's Fetch menu. Wider than the Export panel beside it because its
   buttons carry utility names rather than two verbs. */
.fetch-panel { min-width: 260px; }
.fetch-group + .fetch-group { margin-top: 8px; }
/* The heading takes its group's hue, and the rule after it fades out of the
   same one. Unlike `.sc-card-head`, where the mark is coloured and the label
   stays in the body colour: there the hue competes with a delta pill that
   carries the verdict, and here there is nothing to compete with - the heading
   IS the group, and five coloured words down a menu are what let a reader find
   Scrapers without reading Market, Filings and Models on the way. The ten steps
   in the token block clear 4.5:1 on both panel surfaces, which is what makes
   colouring text rather than a graphic honest.

   The rule is `flex: 1` on a pseudo-element rather than a border under the
   heading, so it starts where the word ends: a full-width line reads as a
   divider between groups, and this is a label with its group's colour trailing
   off it. 35% because a hairline at full strength would be louder than the
   buttons under it. */
.fetch-group h5 {
  display: flex; align-items: center; gap: 6px;
  margin: 0 0 4px; font-size: var(--fs-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--fetch-mark, var(--muted));
}
.fetch-group h5::after {
  content: ''; flex: 1; height: 1px;
  background: color-mix(in srgb, var(--fetch-mark, var(--line)) 35%, transparent);
}
/* Each button is a mark and two lines: the utility, and when its data was last
   brought in for this stock. The two lines are stacked rather than set beside
   each other, so the labels stay in one column and the dates in another - the
   menu is read down the dates ("what here is stale") far more often than
   across - and the mark stands outside that column, centred on both. */
.fetch-btn {
  display: inline-flex; align-items: center;
  gap: 6px; line-height: 1.25; padding: 3px 8px;
}
.fetch-btn-text {
  display: inline-flex; flex-direction: column; align-items: flex-start;
  gap: 1px; min-width: 0;
}
/* The mark on a tinted ground of its own group's hue - the same chip
   `.sc-card-head .icon` wears, at the same 14%, and for the same reason: a bare
   13px stroke is a shape the eye has to look for, and a filled chip is one it
   lands on. `content-box` because the size rides on the svg's own width and
   height attributes, so the padding has to sit outside that box.

   The hover step is what makes the chip read as part of the button rather than
   as a sticker on it: the whole control lights, not just its border. */
.fetch-btn .icon {
  flex: none;
  color: var(--fetch-mark, var(--muted));
  box-sizing: content-box;
  padding: 3px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--fetch-mark, var(--muted)) 14%, transparent);
}
.fetch-btn:hover .icon {
  background: color-mix(in srgb, var(--fetch-mark, var(--muted)) 28%, transparent);
}
/* One hue per group, carried on the wrapper so the heading and every mark under
   it read the same `--fetch-mark`. Slugged from the group's own name by
   `bandSlug` - the same bargain the size bands strike, and the reason a group
   added to `jobs.JOB_TYPES` draws in the muted base rather than unstyled. */
.fetch-mark-market { --fetch-mark: var(--fetch-market); }
.fetch-mark-filings { --fetch-mark: var(--fetch-filings); }
.fetch-mark-models { --fetch-mark: var(--fetch-models); }
.fetch-mark-pipeline { --fetch-mark: var(--fetch-pipeline); }
.fetch-mark-scrapers { --fetch-mark: var(--fetch-scrapers); }
.fetch-when { font-size: var(--fs-xs); font-weight: 400; color: var(--muted); }
/* The bracketed word after the label - (LLM), (API), (Local) - which says what
   pressing the button reaches for. `jobs.JobType.call` chooses it; see the note
   there for why it is declared per type rather than read off the group.

   Hue and weight together, because the button already has two channels working
   - the group's colour on the chipped mark, one of three age tints on the date
   - and a third that is only colour would be read as a fourth group. So the
   three call hues are backed by weight as well: the model call is violet AND
   the heaviest of the three, the free fetch is teal at the base weight, and the
   one that reaches nothing recedes into muted at 400. Read down a menu the
   ladder is what shows first; read across one button, the word settles it. */
.fetch-call { font-size: var(--fs-xs); font-weight: 500; color: var(--muted); }
.fetch-call.call-llm { color: var(--call-llm); font-weight: 600; }
.fetch-call.call-api { color: var(--call-api); }
.fetch-call.call-local { color: var(--call-local); font-weight: 400; }
/* The same tag on the Jobs tab's Run menu, where it trails a plain one-line
   label instead of sitting over a date. A word space alone runs the bracket
   into the label there, so it gets 2px more; the hue needs nothing, the panel
   is the same surface. */
[data-job-run-panel] .fetch-call { margin-left: 2px; }
/* The whole-menu press, above the groups. Full width and ruled off underneath
   because it is not one more fetch in a row of them - it is the alternative to
   reading the rows at all, and a button the same size as its neighbours sitting
   in the same flow would be pressed by accident by somebody aiming at Kite
   market. The rule is what says the two halves are different questions. */
.fetch-all-group {
  /* 2px, not 8: `.picker-actions` already carries 6 under the button, and
     the two together are the 8 that matches the gap below the rule. */
  margin: 0 0 8px; padding-bottom: 2px; border-bottom: 1px solid var(--line);
  /* The accent, not one of the five group hues: this press is not a sixth kind
     of work, it is the alternative to choosing one - and the accent is what the
     rest of the application already spends on "the thing to press". */
  --fetch-mark: var(--accent);
}
/* Its own bottom margin is the gap; the sibling rule above would stack a
   second one on top of it. */
.fetch-all-group + .fetch-group { margin-top: 0; }
.fetch-all-btn { width: 100%; }
.fetch-all-btn .fetch-btn-label { font-weight: 600; }
/* The same three tints the Data Fetch Stats table uses, and the same thresholds
   behind them - 30 days old, 90 days stale. `never` is not a warning: plenty of
   these have simply never been asked for, and colouring it red would cry wolf
   on every stock nobody has run the model over. */
.fetch-when.old { color: var(--warn); }
.fetch-when.stale { color: var(--negative); }
.fetch-when.never { color: var(--muted); font-style: italic; }

/* The popup's AI Analytics menu, the head's third. It borrows the Fetch menu's
   whole button idiom above - `.fetch-btn`, its stacked two lines, its chipped
   mark and its three date tints - because the two menus answer the same shape of
   question about the same stock, and a reader who has learned to scan one for
   "what here has never been run" should not have to learn a second vocabulary
   for it. What it adds is only what is genuinely different about spending money.

   Wider than the Fetch menu because its labels are whole questions - "How does
   this go to zero?" against "Kite market" - and a question that wraps mid-clause
   is one the eye has to reassemble. Capped against the viewport as well, because
   the phone rule further down sets `.dropdown-panel` a width rather than a
   max-width - so a bare `min-width` would win that fight and hang the panel off
   the side of a narrow screen. */
.ai-menu-panel { min-width: min(320px, calc(100vw - 24px)); }
/* One hue for every question that spends, rather than one per question. Violet
   because that is what this application already spends on work a machine did on
   its own - `--fetch-models` on the Fetch menu, `--an-model` on the Analytics
   tab, `--stance-queue` on the AI page - so the mark says "a model answers this"
   before the label is read. Four separate identity hues would be four colours
   saying nothing the four icons do not already say, and would spend the one
   distinction that matters here on the one that does not. */
.ai-menu-group { --fetch-mark: var(--fetch-models); }
/* And the one that does not spend, in the muted base. This is the only colour
   decision on the panel that carries information: the group under this heading
   is free, and it has to be visibly not the group above it before a button is
   pressed rather than after. */
.ai-menu-group.ai-menu-free { --fetch-mark: var(--muted); }
/* The line under each heading - what a press in this group costs. One line, at
   the size of the group headings rather than of the buttons, so it reads as a
   caption on the heading and not as a first item in the list. */
.ai-menu-group .hint { margin: 0 0 6px; font-size: var(--fs-xs); line-height: 1.4; }
/* Full width, stacked. The Fetch menu's buttons sit two and three to a row
   because a utility name is two words; a question is a sentence, and four of
   them in a wrapping row would put the state stamps in four different columns -
   which is the one column this menu is read down. */
.ai-menu-btn { width: 100%; justify-content: flex-start; text-align: left; }
/* Drawn dead over a call that is already queued or running, and on a deployment
   with no API key. The same treatment the AI tab's own Ask button gets, and for
   the same reason it is not merely greyed: a button that still depresses under
   the pointer is one somebody presses twice, and here the second press is money.
   `pointer-events` rather than only opacity, so the tooltip explaining why does
   not follow the cursor onto a control that cannot act. */
.ai-menu-btn[disabled] { opacity: .45; pointer-events: none; }

/* The Details tab's Freshness card, which draws the same twelve stamps down a
   card instead of across a menu - so it borrows `.fetch-when` and its tints
   rather than growing a second set that would drift out of step with them.
   What it adds is the group headings: the card is read to find a gap, and
   "never" means something different under Scrapers than under App - model. */
.fresh-group + .fresh-group { margin-top: 8px; }
.fresh-group h5 {
  margin: 0 0 2px; font-size: var(--fs-xs); font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted);
}
/* Size only. The stamp is the row's whole answer here, not a second line under
   a button, so it reads at the row's size - and the three tints are left
   untouched, which is the only reason this rule can be one line. */
.fresh-group .fetch-when { font-size: var(--fs-sm); }

/* One card per distinct queue. A grid rather than a stack because the
   deployment can point both lanes at separate queues, and two backlogs are read
   against each other - "the browser lane is 40 deep and the api lane is empty"
   is the sentence, and it is not one a reader should have to scroll to build. */
.queue-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 12px;
}
.queue-card h4 { display: flex; align-items: center; gap: 8px; text-transform: none;
  font-size: var(--fs-base); letter-spacing: 0; color: var(--text); }
/* FIFO is worth a mark and standard is not: standard is the default everywhere
   and a badge on every queue would be a badge that says nothing. */
.queue-kind {
  padding: 1px 6px; border-radius: var(--radius-lg); border: 1px solid var(--line);
  font-size: var(--fs-2xs); font-weight: 600; letter-spacing: .06em; color: var(--muted);
  text-transform: uppercase;
}

/* Waiting / In flight / Delayed. Equal thirds, so the three are compared by
   height of digit rather than by position - the eye is looking for which one is
   not zero. */
.queue-figures {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin: 8px 0 6px;
}
.queue-figure {
  padding: 8px 10px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); text-align: center;
}
.queue-figure-n {
  display: block; font-size: 20px; font-weight: 600;
  font-variant-numeric: tabular-nums; line-height: 1.2;
}
.queue-figure-l {
  display: block; margin-top: 2px; font-size: var(--fs-xs); color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
}

/* The settings under the figures: label left, value right, one per line. A
   definition list because that is what it is - and two columns rather than a
   table because none of these is compared against the row above it. */
.queue-settings { margin: 8px 0 0; }
.queue-setting {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 3px 0; border-top: 1px solid var(--line);
}
.queue-setting:first-child { border-top: 0; }
.queue-setting dt { color: var(--muted); font-size: var(--fs-sm); }
.queue-setting dd {
  margin: 0; font-size: var(--fs-sm); text-align: right;
  font-variant-numeric: tabular-nums;
}

/* What the jobs table says, per lane. Narrow number columns and a wide first
   one; the frame, the band and the rules are `.job-table`'s, said once above. */
.queue-c-lane { width: 140px; }
/* Over the shared band, which sets every heading left: a count set right under
   a heading set left reads as a column that has come loose from its name, and
   the band the three now sit in is what made that visible. */
.queue-c-n,
.queue-table th.queue-c-n {
  width: 90px; text-align: right; font-variant-numeric: tabular-nums;
}
.queue-c-when { width: 140px; color: var(--muted); }

/* What Empty the queue did. Left as a line under the toolbar rather than as a
   toast: it reports two counts a reader may well want to still be there after
   the refresh it triggers. */
.queue-notice { margin: 2px 0 8px; }

/* --- Run Jobs ---------------------------------------------------------- */

/* Eight deliberately detailed columns. The panel scrolls horizontally on a
   narrow screen rather than collapsing cost, output and next-step prose into
   an unreadable stack under each job. */
.run-jobs-table { min-width: 1980px; }
.run-jobs-table td { vertical-align: top; }
.run-c-action { width: 190px; }
.run-c-purpose { width: 260px; }
.run-c-last { width: 250px; }
.run-c-cadence { width: 210px; }
.run-c-cost { width: 290px; }
.run-c-output { width: 260px; }
.run-c-view { width: 220px; }
.run-c-next { width: 285px; }
.run-c-action .btn {
  display: inline-flex; align-items: center; gap: 5px; margin: 0 8px 5px 0;
}
.run-c-action > strong { display: inline; }
.run-job-latest { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.run-job-last { font-weight: 600; font-variant-numeric: tabular-nums; }
.run-job-never { color: var(--muted); font-style: italic; }
.run-job-counts, .run-job-rate, .run-job-sub {
  display: block; margin-top: 3px; color: var(--muted); font-size: var(--fs-xs);
}
.run-job-counts strong { color: var(--text); font-variant-numeric: tabular-nums; }
.run-job-sub.loose { font-style: italic; }
.run-jobs-notice { margin: 0 0 8px; color: var(--accent); }
.run-outcome-link { display: inline-flex; align-items: center; gap: 5px; }

/* --- Stock evaluation Checklist -------------------------------------- */

/* A deliberately wide operational table. The stock popup already owns a
   horizontal scroller; keeping scope, freshness, cadence, cost and outcome in
   stable columns is much easier to scan than stacking thirteen miniature
   forms. */
.stock-checklist { min-height: 240px; }
.stock-checklist .panel-head { align-items: flex-start; margin-bottom: 8px; }
.stock-checklist .panel-head h3 { margin-bottom: 2px; }
.stock-checklist .panel-head .hint { max-width: 900px; margin: 0; }
.checklist-table { min-width: 1680px; }
.checklist-table td { vertical-align: top; }
.check-c-step { width: 118px; }
.check-c-job { width: 430px; }
.check-c-scope { width: 145px; }
.check-c-last { width: 190px; }
.check-c-cadence { width: 270px; }
.check-c-run { width: 330px; }
.check-c-outcome { width: 135px; }
.check-step {
  display: inline-grid; place-items: center; width: 25px; height: 25px;
  margin-right: 6px; border-radius: 50%; background: var(--accent); color: #fff;
  font-size: var(--fs-xs); font-weight: 700; font-variant-numeric: tabular-nums;
}
.check-phase {
  display: block; margin-top: 5px; color: var(--muted); font-size: var(--fs-xs);
  font-weight: 600; line-height: 1.25;
}
.check-c-job > strong { display: flex; align-items: center; gap: 6px; font-size: var(--fs-md); }
.check-meta, .check-why, .check-next, .check-c-cadence > .hint,
.check-c-run > .hint, .check-data-at {
  display: block; margin-top: 4px; font-size: var(--fs-xs); line-height: 1.38;
}
.check-meta { color: var(--muted); text-transform: capitalize; }
.check-why { color: var(--muted); }
.check-next { color: var(--text); }
.check-scope {
  display: inline-block; padding: 2px 7px; border: 1px solid var(--line);
  border-radius: var(--radius-lg); font-size: var(--fs-xs); font-weight: 600;
  white-space: nowrap;
}
.check-scope.stock { color: var(--accent); border-color: var(--accent); }
.check-scope.universe { color: var(--warn); border-color: var(--warn); }
.check-run { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.check-job-id { color: var(--muted); font-size: var(--fs-xs); }
.check-never, .check-data-at { color: var(--muted); }
.check-data-at.missing { font-style: italic; }
.check-c-run .btn { display: inline-flex; align-items: center; gap: 5px; }
.check-c-outcome { white-space: nowrap; }
.check-row.open td, .check-row.open:hover td { background: var(--row-hover); }
.check-outcome-row td, .check-outcome-row:hover td {
  padding: 0; background: var(--panel-2) !important;
}
.check-outcome { padding: 14px 18px; border-left: 3px solid var(--accent); }
.check-outcome-head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-bottom: 10px;
}
.check-outcome .job-output { max-height: 360px; overflow: auto; }
.check-outcome .job-result-table { display: table; max-height: none; overflow: visible; }
.check-notice { margin: 0 0 8px; color: var(--accent); }
.check-paused { margin: 0 0 8px; }

/* --- Fetch Theory ------------------------------------------------------- */

/* Which lane a fetch runs in, as a badge. Both lanes get one, unlike `.queue-kind`
   above where only FIFO is marked: there is no default lane, and "api" is as much
   a fact about where a job can run as "browser" is. Colourless on purpose - it is
   a category, not a warning. */
.job-lane {
  padding: 1px 6px; border-radius: var(--radius-lg); border: 1px solid var(--line);
  font-size: var(--fs-2xs); font-weight: 600; letter-spacing: .06em; color: var(--muted);
  text-transform: uppercase; white-space: nowrap;
}
.job-lane.browser { border-color: var(--warn); color: var(--warn); }

/* The cadence table. Wide prose columns and one narrow count, so the eye runs
   down "Runs" - which is the column the tab exists for. The frame, the band and
   the rules are `.job-table`'s, said once above; `top` is this table's own,
   because its cells hold sentences of uneven length and a row centred on the
   longest of them leaves the short ones floating. */
.theory-table td { vertical-align: top; }
.theory-c-what { width: 180px; }
.theory-c-lane { width: 84px; }
.theory-c-every { width: 190px; }
.theory-c-gate { width: 118px; }
.theory-c-writes { color: var(--muted); font-size: var(--fs-sm); }
/* A gate the code enforces reads as a fact; one that is nobody's but the
   reader's reads as a caveat, because running early there is not free. */
.theory-c-gate.loose { color: var(--muted); font-style: italic; }

/* The name jumps to the card. A button rather than an anchor - see `bind()` -
   so it has to be dressed as a link by hand. */
.theory-jump {
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--accent); font: inherit; text-align: left;
}
.theory-jump:hover { text-decoration: underline; }
/* The group under the name - Scrapers, Models, Pipeline - which is what says
   what a press will cost before the row is read across. */
.theory-group { display: block; color: var(--muted); font-size: var(--fs-xs); }

/* One card per fetch, stacked rather than in a grid: unlike the queue cards
   these are read one at a time, jumped to from the table, and their field lists
   run to fifty chips. Two columns of that is a wall. */
.theory-cards { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.theory-card { scroll-margin-top: 8px; }
.theory-card h4 {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  text-transform: none; font-size: var(--fs-base); letter-spacing: 0; color: var(--text);
}
/* The cadence, on the card's own heading. Pushed right so the twelve of them
   line up when the cards are scrolled past. */
.theory-every {
  margin-left: auto; color: var(--muted); font-size: var(--fs-sm); font-weight: 400;
  text-transform: none; letter-spacing: 0;
}

/* Why / what enforces it / what it costs / where it lands. A definition list
   with the label in a fixed left column, because all four are read as answers
   to the same four questions on every card and a ragged label edge makes them
   look like different questions. */
.theory-facts { margin: 8px 0 0; }
.theory-facts > div {
  display: grid; grid-template-columns: 132px 1fr; gap: 12px;
  padding: 4px 0; border-top: 1px solid var(--line);
}
.theory-facts > div:first-child { border-top: 0; }
.theory-facts dt { color: var(--muted); font-size: var(--fs-sm); }
.theory-facts dd { margin: 0; font-size: var(--fs-sm); }

/* The fields themselves. Chips with the meaning in the tip rather than a second
   table: screener writes forty-nine of them, and forty-nine rows of
   `name | sentence` is a page nobody reads to the end. */
.theory-fields { margin-top: 10px; }
.theory-fields h5 {
  margin: 0 0 4px; color: var(--muted); font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: .05em;
}
.theory-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.theory-chip {
  padding: 1px 6px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); font-size: var(--fs-xs); color: var(--text);
  cursor: help;
}

/* --- AI ▸ Summary: the price list ---------------------------------------
   The one panel on the AI page with no control on it at all, so everything
   below is about making a deck of money readable rather than about state.

   It was a table with a stack of explanations under it, and neither half was
   much use alone — the figures needed the prose to be believed and the prose
   needed the figures to be worth reading. So each analysis is now ONE card,
   three to a row, carrying its own numbers and its own reasons.

   Three things carry the meaning and each has its own device:
   * the LEVEL — stock, portfolio, universe — is a coloured pill, and the deck
     is banded by it, because a reader scanning for "which of these are about my
     book" should not have to read words to find it;
   * a total that is a FLOOR is marked in the warn colour wherever it appears,
     on the strip above and on the card's own Spent cell, because an understated
     bill that looks whole is the one way this panel can mislead;
   * FREE and NOT RECORDED are drawn differently from each other on purpose. One
     is a fact and one is a gap, and a shared dash would merge them.

   What the card layout costs, stated rather than hidden: the figures no longer
   line up in columns, so comparing cost-per-run across the deck is a scan. The
   totals strip carries the cross-cutting numbers for exactly that reason. */

.ais-panel h3 { display: flex; align-items: center; gap: 8px; }

/* --- the four faces -----------------------------------------------------
   Six cards in one scroll was a page nobody reached the bottom of. The split is
   the one the panel was already grouped by - what the analysis is said about -
   so the three level tabs are the payload's own level keys and Overview holds
   the figures that cross them.

   The nested-strip look: no rule of its own under it, so the page strip's line
   and this one do not stack into a ladder. Same trade `.subtabs.users` makes. */
.subtabs.ais-subtabs { border-bottom: 0; margin: -2px 0 10px; }
.ais-subtabs .subtab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px; font-size: var(--fs-base);
}
/* How many cards are behind a tab. On the tab rather than inside it, because
   the levels are wildly uneven and a reader who cannot see that from the strip
   clicks through all three to find out. */
.ais-subtabs .count {
  font-family: var(--mono); font-size: var(--fs-2xs); line-height: 1;
  padding: 2px 4px; border-radius: var(--radius);
  background: var(--panel-2); color: var(--muted);
}
.ais-subtabs .subtab.on .count { background: var(--accent-soft); color: inherit; }

/* The Overview index: one row per level, and the second way onto its tab. A
   button, because that is the whole of what it does - a reader who has just
   read that Portfolio cost the most should not have to go back up to the strip
   to open it. */
.ais-index { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 0; }
.ais-index-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  width: 100%; padding: 8px 10px; text-align: left;
  border: 1px solid var(--card-edge); border-radius: var(--radius-lg);
  background: var(--panel-2); color: var(--text); font: inherit; cursor: pointer;
}
.ais-index-row:hover { border-color: var(--accent); background: var(--row-hover); }
.ais-index-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.ais-index-what {
  flex: 1 1 220px; min-width: 0; color: var(--muted); font-size: var(--fs-sm);
}
.ais-index-figs {
  display: flex; gap: 4px 16px; flex-wrap: wrap;
  color: var(--muted); font-size: var(--fs-sm);
}
.ais-index-figs b { color: var(--text); }
.ais-index-figs .ais-floor b { color: var(--warn); }
.ais-index-row > .icon { color: var(--muted); flex: 0 0 auto; }

/* --- the operating map on Overview -------------------------------------
   Two diagrams, because they answer two different questions. The lifecycle is
   one stock moving from recommendation to a monitored position; the pipeline
   lanes are shared jobs moving stored data into the screens that read it.
   Both use real anchors, so a node is also the shortest path to doing the work.

   Connectors are text arrows rather than lines painted behind the cards. They
   survive the vertical mobile layout, remain legible in forced colours and do
   not turn a wrapped row into a line pointing at the wrong node. */
.ais-flow {
  margin: 18px 0 20px; padding-top: 14px; border-top: 1px solid var(--line);
}
.ais-flow-title {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; margin-bottom: 10px;
}
.ais-flow-title h4 {
  display: flex; align-items: center; gap: 7px;
  margin: 0; color: var(--text); font-size: var(--fs-md);
  text-transform: none; letter-spacing: 0;
}
.ais-flow-title h4 .icon { color: var(--accent); }
.ais-flow-title p { margin: 3px 0 0; max-width: 920px; }
.ais-flow-key {
  flex: 0 0 auto; padding: 2px 8px; border: 1px solid var(--accent);
  border-radius: 999px; background: var(--accent-soft); color: var(--accent);
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .05em;
}
.ais-flow-open {
  display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto;
  color: var(--accent); font-size: var(--fs-sm); font-weight: 600;
}

/* The six numbered moments. Kept on one line while there is genuinely room;
   below that they become one vertical path rather than six squeezed columns. */
.ais-life-track {
  display: flex; align-items: stretch; gap: 7px;
  margin: 0; padding: 0; list-style: none;
}
.ais-life-stage {
  flex: 1 1 0; min-width: 0; padding: 9px 10px;
  border: 1px solid var(--card-edge); border-top: 3px solid var(--accent);
  border-radius: var(--radius-lg); background: var(--panel-2);
}
.ais-life-stage[data-life-stage="1"],
.ais-life-stage[data-life-stage="5"] { border-top-color: var(--positive); }
.ais-life-stage[data-life-stage="3"] { border-top-color: var(--warn); }
.ais-life-head { display: flex; align-items: center; gap: 6px; }
.ais-life-head .icon { color: var(--muted); flex: 0 0 auto; }
.ais-life-number {
  display: inline-grid; place-items: center; width: 20px; height: 20px;
  border-radius: 50%; background: var(--accent); color: var(--panel);
  font-family: var(--mono); font-size: var(--fs-xs); font-weight: 600;
}
.ais-life-head h5 {
  margin: 0; color: var(--text); font-size: var(--fs-sm);
  text-transform: none; letter-spacing: 0;
}
.ais-life-stage > p { margin: 7px 0; color: var(--muted); font-size: var(--fs-xs); }
.ais-life-links { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.ais-life-links a {
  display: inline-flex; align-items: center; gap: 3px;
  color: var(--accent); font-size: var(--fs-xs); font-weight: 600;
}
.ais-life-links a .icon { flex: 0 0 auto; }
.ais-flow-arrow {
  align-self: center; flex: 0 0 auto; color: var(--accent);
  font-size: var(--fs-lg); font-weight: 600;
}

/* The loop under the path. Cadence is six visible triggers rather than one
   vague "review periodically" label: when and what changed are the two inputs
   to the decision to spend another fetch or model call. */
.ais-reeval {
  display: grid; grid-template-columns: minmax(190px, .8fr) minmax(0, 3.2fr);
  gap: 14px; margin-top: 10px; padding: 10px 12px;
  border: 1px solid var(--card-edge); border-left: 3px solid var(--positive);
  border-radius: var(--radius-lg); background: var(--panel);
}
.ais-reeval-head { display: flex; align-items: flex-start; gap: 8px; }
.ais-reeval-head > .icon { margin-top: 1px; color: var(--positive); }
.ais-reeval h5 { margin: 0; color: var(--text); font-size: var(--fs-sm); }
.ais-reeval p { margin: 3px 0 0; color: var(--muted); font-size: var(--fs-xs); }
.ais-reeval-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 12px;
}
.ais-reeval-grid > div { min-width: 0; }
.ais-reeval-grid strong { display: block; color: var(--text); font-size: var(--fs-xs); }
.ais-reeval-grid span { display: block; margin-top: 2px; color: var(--muted); font-size: var(--fs-xs); }

/* A lane is a sentence read left to right: purpose, executable jobs, stored
   output, reader. Jobs are compact nodes rather than a table because order and
   convergence are the information here. */
.ais-pipeline-map { display: flex; flex-direction: column; gap: 8px; }
.ais-pipe-lane {
  display: grid;
  grid-template-columns: minmax(155px, .85fr) minmax(330px, 2.25fr) 18px
    minmax(190px, 1.25fr) 18px minmax(155px, .9fr);
  align-items: center; gap: 8px; padding: 8px 10px;
  border: 1px solid var(--card-edge); border-radius: var(--radius-lg);
  background: var(--panel-2);
}
.ais-pipe-name { display: flex; align-items: flex-start; gap: 7px; }
.ais-pipe-name .icon { flex: 0 0 auto; margin-top: 1px; color: var(--accent); }
.ais-pipe-name h5 {
  margin: 0; color: var(--text); font-size: var(--fs-sm);
  text-transform: none; letter-spacing: 0;
}
.ais-pipe-jobs { display: flex; align-items: stretch; gap: 5px; min-width: 0; }
.ais-pipe-job {
  display: flex; flex: 1 1 0; min-width: 0; flex-direction: column; gap: 2px;
  padding: 6px 7px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel); color: var(--text);
}
.ais-pipe-job:hover { border-color: var(--accent); background: var(--row-hover); }
.ais-pipe-job strong { font-size: var(--fs-xs); }
.ais-pipe-job span { color: var(--muted); font-size: var(--fs-2xs); }
.ais-pipe-job.dependency { border-style: dashed; }
.ais-pipe-job.action { border-color: var(--warn); background: var(--warn-soft); }
.ais-pipe-arrow { align-self: center; color: var(--muted); font-weight: 600; }
.ais-pipe-arrow.output { text-align: center; color: var(--accent); }
.ais-pipe-output {
  min-width: 0; color: var(--text); font-size: var(--fs-xs);
}
.ais-pipe-output > span {
  display: block; margin-bottom: 2px; color: var(--muted); font-size: var(--fs-2xs);
  text-transform: uppercase; letter-spacing: .05em;
}
.ais-pipe-screen {
  display: flex; align-items: center; justify-content: space-between; gap: 5px;
  min-width: 0; padding: 6px 8px; border-left: 3px solid var(--positive);
  border-radius: var(--radius); background: var(--positive-soft);
  color: var(--positive); font-size: var(--fs-xs); font-weight: 600;
}
.ais-flow-divider {
  display: flex; align-items: center; gap: 10px; margin: 22px 0 10px;
  color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: .06em;
}
.ais-flow-divider::before, .ais-flow-divider::after {
  content: ""; flex: 1 1 auto; border-top: 1px solid var(--line);
}

/* The sentence at the top of a level's own face, where the group heading used
   to be. The tab is the heading now, so this is left saying only what the tab
   name cannot: what the level actually means. */
.ais-level-tip { display: flex; align-items: center; gap: 6px; margin: 0 0 4px; }

/* The totals strip. Auto-fit rather than a fixed count: the gap tile only
   appears when there is something the total does not cover, and a five-column
   grid with a hole in it reads as a rendering fault. */
.ais-tiles {
  display: grid; gap: 10px; margin: 12px 0 14px;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.ais-tile {
  padding: 10px 12px; border: 1px solid var(--card-edge);
  border-radius: var(--radius-lg); background: var(--panel-2);
  display: flex; flex-direction: column; gap: 2px; cursor: help;
}
.ais-tile-label {
  color: var(--muted); font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: .05em;
}
.ais-tile-value {
  font-size: var(--fs-lg); font-weight: 600; color: var(--text);
  display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
}
.ais-tile-inr { font-size: var(--fs-sm); font-weight: 400; color: var(--muted); }
.ais-tile-sub { color: var(--muted); font-size: var(--fs-xs); }
/* The spend tile when the figure is a floor. Tinted rather than merely
   annotated: the caveat has to reach somebody who only looks at the number. */
.ais-tile.warn { border-color: var(--warn); background: var(--warn-soft); }
.ais-tile.warn .ais-tile-sub { color: var(--warn); }
/* What the total does not cover, spelled out. Its own tile rather than a
   footnote under the deck — a sentence below the cards is not read by somebody
   looking at a number above them. */
.ais-tile-gap { grid-column: span 2; cursor: default; }
.ais-tile-gaps { display: flex; flex-direction: column; gap: 2px; }
.ais-tile-gaps span { color: var(--muted); font-size: var(--fs-xs); }

/* The deck. Three to a row, and the cards stretch to the tallest in their row
   so a band of them reads as one shelf rather than as a ragged edge. */
.ais-cards {
  display: grid; gap: 12px; margin-top: 16px;
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
/* `.panel .panel.sub` carries a top margin for the stacked case, which inside a
   grid would push one card off the line its neighbours sit on. */
.ais-cards .ais-card {
  margin: 0; display: flex; flex-direction: column;
  scroll-margin-top: 8px;
}
.ais-card h4 {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  text-transform: none; font-size: var(--fs-base); letter-spacing: 0; color: var(--text);
}
.ais-card-name { font-weight: 600; }
/* The level pill goes to the right edge of the heading, which lines the three
   pills of a row up with each other. */
.ais-card h4 .ais-level { margin-left: auto; }
.ais-card > .hint { margin: 0 0 2px; }

/* The level heading, spanning the full width of the grid beneath it. A band
   rather than a column, because the deck is grouped on it. */
.ais-cards .ais-group {
  grid-column: 1 / -1;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 6px 2px 4px; border-bottom: 1px solid var(--line);
}
.ais-cards .ais-group:first-child { padding-top: 0; }
.ais-group-name {
  display: inline-flex; align-items: center; gap: 6px;
  font-weight: 600; color: var(--text);
}
.ais-group-tip { color: var(--muted); font-size: var(--fs-xs); }

/* The three figures that used to be columns, now a strip inside the card. Its
   own recessed block so the numbers separate from the prose above and below
   them without a third border weight. */
.ais-stats {
  display: grid; gap: 8px; margin: 10px 0 0;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  padding: 8px; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--panel-2);
}
.ais-stat { display: flex; flex-direction: column; gap: 1px; cursor: help; min-width: 0; }
.ais-stat-label {
  color: var(--muted); font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: .05em;
}
.ais-stat-value { font-size: var(--fs-md); font-weight: 600; color: var(--text); }
/* The Spent cell when its figure is a floor — tinted for the same reason the
   tile above is, so the caveat reaches somebody who only reads the number. */
.ais-stat.warn { color: var(--warn); }
/* The second line under a figure — the run breakdown, the mean's sample size,
   what is missing from a total. Always the quiet one. */
.ais-sub { display: block; color: var(--muted); font-size: var(--fs-xs); }
.ais-sub.ais-floor, .ais-figures .ais-floor { color: var(--warn); }

/* Stock / portfolio / universe. A hue each, because this is what the deck is
   banded by and the reader is scanning for one of the three rather than reading
   all of them. */
.ais-level {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 7px; border-radius: 999px;
  border: 1px solid transparent; font-size: var(--fs-xs); white-space: nowrap;
}
.level-stock {
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent);
}
.level-portfolio {
  background: var(--positive-soft); color: var(--positive); border-color: var(--positive);
}
.level-universe {
  background: var(--warn-soft); color: var(--warn); border-color: var(--warn);
}
.level-other {
  background: var(--ghost); color: var(--muted); border-color: var(--line);
}
/* Whether a second reader gets it free. In the footer beside the run span
   rather than in the heading: it is a consequence of the level, not a fact
   competing with the name. */
.ais-shared { color: var(--muted); cursor: help; }
.ais-shared.own { color: var(--warn); }

/* What stops a run, in two words, beside the cadence it qualifies. No tooltip
   on it: the sentence it summarises is two rows below, in full. */
.ais-gate {
  display: inline-block; margin-left: 6px;
  padding: 0 6px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--ghost); color: var(--muted);
  font-size: var(--fs-xs);
}

/* The four states a cost-per-run figure can be in, drawn as four different
   things on purpose. `free` is a fact and reads as one; `unknown` is a gap and
   is marked; `none` is simply nothing yet and stays quiet. */
.ais-measured .ais-stat-value { color: var(--text); }
.ais-free { color: var(--positive); font-weight: 600; }
.ais-unknown .ais-stat-value { color: var(--warn); }
.ais-none .ais-stat-value { color: var(--muted); }

/* Why that often, what stops a run, what it costs, where it lands. Stacked
   rather than a label column: a third of a panel is too narrow for two columns
   of prose, and the label is the shorter half. */
.ais-facts { margin: 10px 0 0; }
.ais-facts > div { padding: 5px 0; border-top: 1px solid var(--line); }
.ais-facts > div:first-child { border-top: 0; padding-top: 0; }
.ais-facts dt {
  color: var(--muted); font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: .05em;
}
.ais-facts dd { margin: 2px 0 0; font-size: var(--fs-sm); }
/* The footer: who pays, the span of runs, and what the bill is missing. Pushed
   to the bottom so the three cards of a row end on the same line. */
.ais-figures {
  display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: auto;
  padding-top: 8px; border-top: 1px solid var(--line);
  color: var(--muted); font-size: var(--fs-xs);
}
.ais-figures b { color: var(--text); }

@media (max-width: 1200px) {
  /* Two to a row. Three thirds of a laptop panel is narrower than the cadence
     sentence, and a card whose every fact wraps to four lines is a column of
     prose with a heading on it. */
  .ais-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ais-life-track { flex-direction: column; }
  .ais-flow-arrow { transform: rotate(90deg); line-height: 1; }
  .ais-life-stage { width: 100%; }
  .ais-life-links { flex-direction: row; flex-wrap: wrap; gap: 4px 14px; }
  .ais-pipe-lane {
    grid-template-columns: minmax(145px, .8fr) minmax(300px, 2fr) 16px minmax(180px, 1fr);
  }
  .ais-pipe-lane > .ais-pipe-arrow.output:nth-of-type(2) { display: none; }
  .ais-pipe-screen { grid-column: 4; grid-row: 2; }
}

@media (max-width: 900px) {
  .ais-cards { grid-template-columns: minmax(0, 1fr); }
  .ais-tile-gap { grid-column: span 1; }
  /* The index row's figures go under its name rather than off the edge. */
  .ais-index-what { flex-basis: 100%; }
  .ais-flow-title { flex-direction: column; }
  .ais-reeval { grid-template-columns: 1fr; }
  .ais-reeval-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ais-pipe-lane { display: flex; flex-direction: column; align-items: stretch; }
  .ais-pipe-jobs { flex-direction: column; }
  .ais-pipe-arrow { align-self: center; transform: rotate(90deg); line-height: 1; }
  .ais-pipe-lane > .ais-pipe-arrow.output:nth-of-type(2) { display: block; }
  .ais-pipe-screen { width: 100%; grid-column: auto; grid-row: auto; }
}

@media (max-width: 560px) {
  .ais-reeval-grid { grid-template-columns: 1fr; }
}

/* --- Universe: the stocks this deployment does NOT hold (F9) ---------------

   The AI page's one section drawn over the outside rather than the inside. Its
   shape follows from that: a pipeline strip at the top, three cards in the
   order the stages run, then a shortlist of proposals and a record of what has
   been decided.

   Two things this stylesheet is doing deliberate work for:

   * the three stages cost genuinely different things, and the cards say so
     without making the two free actions look secondary. All three buttons use
     one primary treatment; the paid card is distinguished by its border, cost
     ticker and explicit copy rather than by making its neighbours look weak.
   * a proposal is a decision, not a report, so the two buttons that resolve it
     are the heaviest thing on the card and are drawn apart from the prose. The
     approve is `--positive`-weighted and the reject is quiet: the asymmetry is
     the point, because approving is the only control on this page that changes
     the universe. */
.scout-panel { padding-top: 4px; }

/* The Universe workspace is an instrument panel inside the application theme.
   These are its dark defaults; the light translation below keeps the glass,
   status colours and cyan/blue identity without turning into a black island on
   a light page. */
.universe-shell {
  --scout-bg: #090a0f;
  --scout-cyan: #00f2fe;
  --scout-violet: #4facfe;
  --scout-emerald: #10b981;
  --scout-yellow: #f59e0b;
  --scout-crimson: #ef4444;
  --scout-blue: #3b82f6;
  --panel: rgba(255, 255, 255, .035);
  --panel-2: rgba(255, 255, 255, .065);
  --line: rgba(79, 172, 254, .28);
  --line-soft: rgba(255, 255, 255, .09);
  --card-edge: rgba(79, 172, 254, .22);
  --text: #eafcff;
  --muted: #8ea6b8;
  --accent: var(--scout-cyan);
  --positive: var(--scout-emerald);
  --negative: var(--scout-crimson);
  --warn: var(--scout-yellow);
  position: relative;
  min-height: 100%;
  padding: 10px;
  border: 1px solid rgba(79, 172, 254, .18);
  border-radius: var(--radius-lg);
  color: var(--text);
  background:
    radial-gradient(circle at 8% -10%, rgba(0, 242, 254, .11), transparent 28%),
    radial-gradient(circle at 92% 0%, rgba(79, 172, 254, .12), transparent 30%),
    var(--scout-bg);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .035),
    0 18px 50px rgba(0, 0, 0, .24);
}

/* Glass surfaces shared by the pipeline, its action cards, and its record. */
.universe-shell .scout-head,
.universe-shell .scout-card,
.universe-shell .scout-block {
  border: 1px solid rgba(79, 172, 254, .2);
  background: rgba(255, 255, 255, .03);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .04),
    0 10px 30px rgba(0, 0, 0, .18);
  backdrop-filter: blur(12px);
}
.universe-shell .scout-head { border-radius: var(--radius); }
.universe-shell .scout-head-title,
.universe-shell .scout-block-title { color: #dffcff; }
.universe-shell .scout-head-title .icon,
.universe-shell .scout-block-title .icon { color: var(--scout-cyan); }

/* The funnel across the top. The chart itself is the shared `.flow-*` block;
   what is here is only the box it sits in - and it needs very little, because
   the shell above redefines --accent, --positive, --warn, --panel and
   --card-edge to this tab's own palette, so the diagram arrives wearing it. */
.universe-shell .scout-funnel {
  margin-top: 10px;
  padding: 8px 10px 4px;
  border: 1px solid rgba(0, 242, 254, .16);
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, .2);
}

/* Three operational cards: compact, measurable, and still explicit about the
   very different cost of each pass. */
.universe-shell .scout-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: stretch;
  gap: 10px;
}
.universe-shell .scout-card {
  position: relative;
  min-height: 0;
  border-radius: var(--radius);
  transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease;
}
.universe-shell .scout-card:hover {
  transform: translateY(-2px);
  border-color: rgba(0, 242, 254, .42);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .055),
    0 12px 32px rgba(0, 0, 0, .26), 0 0 20px rgba(0, 242, 254, .08);
}
.universe-shell .scout-card-paid {
  border-color: rgba(79, 172, 254, .5);
  background: linear-gradient(145deg, rgba(59, 130, 246, .09),
    rgba(255, 255, 255, .025));
}
.universe-shell .scout-card-kicker {
  display: inline-flex;
  align-items: center;
  align-self: flex-start;
  gap: 5px;
  margin: -2px 0 1px;
  padding: 2px 7px;
  border: 1px solid rgba(0, 242, 254, .18);
  border-radius: var(--radius-pill);
  color: var(--scout-cyan);
  background: rgba(0, 242, 254, .055);
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  letter-spacing: .035em;
}
.universe-shell .scout-card-kicker .icon { width: 12px; height: 12px; }
.universe-shell .scout-card-meter {
  height: 4px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .07);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .35);
}
.universe-shell .scout-card-meter > span {
  display: block;
  width: var(--scout-progress);
  min-width: 3px;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--scout-violet), var(--scout-cyan));
  box-shadow: 0 0 10px rgba(0, 242, 254, .55);
  transition: width .35s ease;
}
.universe-shell .scout-card-active {
  border-color: rgba(0, 242, 254, .62);
  animation: scout-card-pulse 1.8s ease-in-out infinite;
}
.universe-shell .scout-card-active .scout-card-meter > span {
  width: 42%;
  background-size: 200% 100%;
  animation: scout-meter-run 1.25s linear infinite;
}
.universe-shell .scout-card .btn {
  border-color: rgba(0, 242, 254, .28);
  background: rgba(0, 242, 254, .07);
  color: #dcfbff;
}
.universe-shell .scout-card .btn:hover:not([disabled]) {
  border-color: var(--scout-cyan);
  box-shadow: 0 0 15px rgba(0, 242, 254, .18);
}
.universe-shell .scout-card .btn.primary {
  border-color: rgba(79, 172, 254, .72);
  background: linear-gradient(135deg, rgba(59, 130, 246, .82),
    rgba(0, 242, 254, .55));
  color: #fff;
  box-shadow: 0 0 18px rgba(59, 130, 246, .19);
}

/* Semantic pills: every state remains written in full and also takes a glow. */
.universe-shell .scout-pill-good {
  color: var(--scout-emerald);
  background: rgba(16, 185, 129, .09);
  box-shadow: 0 0 10px rgba(16, 185, 129, .12);
}
.universe-shell .scout-pill-mid {
  color: var(--scout-blue);
  background: rgba(59, 130, 246, .09);
  box-shadow: 0 0 10px rgba(59, 130, 246, .12);
}
.universe-shell .scout-pill-low {
  color: var(--scout-crimson);
  background: rgba(239, 68, 68, .08);
  box-shadow: 0 0 10px rgba(239, 68, 68, .1);
}

/* Light-mode translation of the same design. Cyan #00f2fe is an illuminated
   edge on #090a0f, but cannot carry small text on white; the deeper steps below
   preserve the hue while restoring contrast. Glass becomes white over a pale
   slate/cyan canvas, and shadows become elevation rather than black glow. */
html[data-theme="light"] .universe-shell {
  --scout-bg: #f4f9fc;
  --scout-cyan: #0e7490;
  --scout-violet: #2563eb;
  --scout-emerald: #047857;
  --scout-yellow: #a16207;
  --scout-crimson: #c2414b;
  --scout-blue: #1d4ed8;
  --panel: rgba(255, 255, 255, .78);
  --panel-2: rgba(241, 245, 249, .84);
  --line: rgba(14, 116, 144, .24);
  --line-soft: rgba(71, 85, 105, .12);
  --card-edge: rgba(14, 116, 144, .2);
  --text: #102033;
  --muted: #526579;
  --accent: var(--scout-cyan);
  --positive: var(--scout-emerald);
  --negative: var(--scout-crimson);
  --warn: var(--scout-yellow);
  border-color: rgba(14, 116, 144, .2);
  color: var(--text);
  background:
    radial-gradient(circle at 8% -10%, rgba(6, 182, 212, .14), transparent 30%),
    radial-gradient(circle at 92% 0%, rgba(37, 99, 235, .1), transparent 32%),
    linear-gradient(180deg, rgba(255, 255, 255, .74), rgba(244, 249, 252, .94)),
    var(--scout-bg);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9),
    0 12px 34px rgba(15, 23, 42, .09);
}
html[data-theme="light"] .universe-shell .scout-head,
html[data-theme="light"] .universe-shell .scout-card,
html[data-theme="light"] .universe-shell .scout-block {
  border-color: rgba(14, 116, 144, .2);
  background: rgba(255, 255, 255, .66);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .95),
    0 8px 24px rgba(15, 23, 42, .075);
}
html[data-theme="light"] .universe-shell .scout-head-title,
html[data-theme="light"] .universe-shell .scout-block-title { color: #183044; }
html[data-theme="light"] .universe-shell .scout-head-title .icon,
html[data-theme="light"] .universe-shell .scout-block-title .icon {
  color: var(--scout-cyan);
}
html[data-theme="light"] .universe-shell .scout-funnel {
  border-color: rgba(14, 116, 144, .18);
  background: rgba(248, 250, 252, .78);
}
html[data-theme="light"] .universe-shell .scout-card:hover {
  border-color: rgba(14, 116, 144, .4);
  box-shadow: inset 0 1px 0 #fff, 0 12px 28px rgba(15, 23, 42, .11),
    0 0 16px rgba(14, 116, 144, .06);
}
html[data-theme="light"] .universe-shell .scout-card-paid {
  border-color: rgba(37, 99, 235, .34);
  background: linear-gradient(145deg, rgba(219, 234, 254, .62),
    rgba(255, 255, 255, .7));
}
html[data-theme="light"] .universe-shell .scout-card-kicker {
  border-color: rgba(14, 116, 144, .24);
  color: #0e657d;
  background: rgba(6, 182, 212, .075);
}
html[data-theme="light"] .universe-shell .scout-card-meter {
  background: rgba(15, 23, 42, .1);
  box-shadow: inset 0 1px 2px rgba(15, 23, 42, .12);
}
html[data-theme="light"] .universe-shell .scout-card-meter > span {
  background: linear-gradient(90deg, #2563eb, #0891b2);
  box-shadow: 0 0 8px rgba(14, 116, 144, .24);
}
html[data-theme="light"] .universe-shell .scout-card .btn {
  border-color: rgba(14, 116, 144, .3);
  color: #0c4a6e;
  background: rgba(255, 255, 255, .72);
}
html[data-theme="light"] .universe-shell .scout-card .btn:hover:not([disabled]) {
  border-color: var(--scout-cyan);
  background: rgba(6, 182, 212, .08);
  box-shadow: 0 3px 12px rgba(14, 116, 144, .12);
}
html[data-theme="light"] .universe-shell .scout-card .btn.primary {
  border-color: #2563eb;
  color: #fff;
  background: linear-gradient(135deg, #2563eb, #0891b2);
  box-shadow: 0 4px 14px rgba(37, 99, 235, .16);
}
html[data-theme="light"] .universe-shell .scout-pill-good {
  background: rgba(4, 120, 87, .07);
  box-shadow: none;
}
html[data-theme="light"] .universe-shell .scout-pill-mid {
  background: rgba(29, 78, 216, .07);
  box-shadow: none;
}
html[data-theme="light"] .universe-shell .scout-pill-low {
  background: rgba(194, 65, 75, .07);
  box-shadow: none;
}
html[data-theme="light"] .scout-toast {
  border-color: rgba(4, 120, 87, .42);
  color: #065f46;
  background: rgba(240, 253, 250, .96);
  box-shadow: 0 10px 30px rgba(15, 23, 42, .16),
    0 0 14px rgba(4, 120, 87, .08);
}

/* Local confirmations for queued passes and proposal decisions. */
.scout-toast {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 320;
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: min(380px, calc(100vw - 28px));
  padding: 9px 13px;
  border: 1px solid rgba(16, 185, 129, .62);
  border-radius: var(--radius-sm);
  color: #d9fff2;
  background: rgba(9, 24, 22, .94);
  box-shadow: 0 12px 36px rgba(0, 0, 0, .42),
    0 0 20px rgba(16, 185, 129, .13);
  backdrop-filter: blur(14px);
  animation: scout-toast-in .22s ease-out;
}
.scout-toast .icon { color: var(--scout-emerald, #10b981); }

@keyframes scout-card-pulse {
  0%, 100% { box-shadow: 0 0 0 rgba(0, 242, 254, 0); }
  50% { box-shadow: 0 0 24px rgba(0, 242, 254, .15); }
}
@keyframes scout-meter-run {
  from { transform: translateX(-115%); }
  to { transform: translateX(260%); }
}
@keyframes scout-toast-in {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@media (max-width: 1050px) {
  .universe-shell .scout-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 680px) {
  .universe-shell { padding: 7px; }
  .universe-shell .scout-cards { grid-template-columns: minmax(0, 1fr); }
  .scout-toast { right: 14px; bottom: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .universe-shell .scout-card,
  .universe-shell .scout-card-meter > span { transition: none; }
  .universe-shell .scout-card-active,
  .universe-shell .scout-card-active .scout-card-meter > span,
  .scout-toast { animation: none; }
}

/* The frame at the top. Same object as `.score-head` and styled with it in
   mind: always current, always free, and boxed so it does not read as the first
   paragraph of the shortlist underneath. */
.scout-head {
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  padding: 12px 14px;
}
.scout-head-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.scout-head-what { margin: 0 0 10px; }
.scout-empty {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 8px 10px;
  border-left: 3px solid var(--accent);
  background: var(--panel);
  border-radius: var(--radius-xs);
  font-size: var(--fs-md);
}
.scout-empty svg { color: var(--accent); flex: 0 0 auto; }

/* The funnel, outside the Universe shell's own palette. The diagram is the
   shared `.flow-*` block; this is only the box around it. */
.scout-funnel { margin: 0; }
.scout-pool { margin: 8px 0 0; }

/* The acceptance rate and the bill, side by side - the two figures this
   feature's own review turns on. */
.scout-totals {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line-soft);
}
.scout-total-cell { display: flex; flex-direction: column; gap: 1px; }
.scout-total-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.scout-total-value { font-size: var(--fs-md); font-variant-numeric: tabular-nums; }
/* Nothing decided yet is an em dash, not a 0% - see `pct` in scout.js. */
.scout-total-nil .scout-total-value { color: var(--muted); }
/* The money cell. Tinted because it is the one figure on this header that is
   an actual cost rather than a count, and the whole reason the strip below it
   exists is that this number used to be wrong. */
.scout-total-spend .scout-total-value { color: var(--accent); font-weight: 600; }

/* What the bill is made of: the calls that proposed, the calls that passed
   over, the calls that failed - and the confidence spread beside them, because
   a shortlist whose every row claims the same confidence is a filter worth
   looking at and nothing else on the page would say so. */
.scout-spend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  margin-top: 8px;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.scout-spend-leg, .scout-spend-conf, .scout-spend-gap {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-variant-numeric: tabular-nums;
}
.scout-spend-leg b, .scout-spend-conf b { color: var(--text); font-weight: 600; }
.scout-spend-leg svg, .scout-spend-conf svg, .scout-spend-gap svg {
  width: 12px;
  height: 12px;
}
.scout-spend-usd { color: var(--accent); }
.scout-spend-conf {
  padding: 1px 7px;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  gap: 7px;
}
/* Every proposal at the same confidence. Warned rather than hidden: this is
   exactly the state the field spent thirty proposals in without anybody
   noticing, and a quiet header is what let it. */
.scout-spend-conf-flat { border-color: var(--warn); color: var(--warn); }
.scout-spend-conf-flat b { color: var(--warn); }
.scout-spread-leg { display: inline-flex; gap: 2px; }
.scout-spread-high b { color: var(--positive); }
.scout-spread-medium b { color: var(--accent); }
.scout-spread-low b { color: var(--muted); }
.scout-spread-nil, .scout-spread-nil b { color: var(--muted); opacity: .55; }
.scout-spend-gap { color: var(--warn); }

/* The three passes. `.daymove-card` whole, borrowed as the Scoreboard's is. */
.scout-cards { margin-top: 12px; }
.scout-card { min-width: 0; }
/* The one that spends. A tinted edge rather than a badge: the card's own text
   already says it costs money, and a second announcement would be shouting. */
.scout-card-paid { border-color: var(--accent); }
.scout-state { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
.scout-count { font-variant-numeric: tabular-nums; }
.scout-when, .scout-none { color: var(--muted); }

/* --- the shortlist ------------------------------------------------------- */
.scout-block { margin-top: 18px; }
.scout-block-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 2px;
  font-size: var(--fs-md);
}
.scout-block-what { margin: 0 0 10px; }

/* Six columns - who, when, how well it was seen, why, on what, and the decision
   - because ten proposals are a list to be worked down rather than ten documents
   to be read. Wider than a phone, so it scrolls inside its own box like the
   record below it. */
.scout-open {
  width: 100%;
  min-width: 1120px;
  border-collapse: collapse;
  font-size: var(--fs-base);
}
.scout-open th {
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  font-weight: 600;
  padding: 4px 12px 4px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.scout-open td {
  padding: 10px 12px 10px 0;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.scout-open tbody tr:last-child td { border-bottom: 0; }
.scout-open tbody tr:hover { background: var(--row-hover); }

/* The name column, stacked: symbol, company, and the one mark that has to be
   read beside the name. The date and the confidence used to be crammed in here
   too and are columns of their own now - a fifty-row shortlist is triaged on
   both, and neither can be compared down a column while it is a grey fourth
   line in somebody else's cell. */
.scout-stock-cell { width: 1%; white-space: nowrap; }
.scout-stock-cell .scout-symbol { display: block; }
.scout-proposal-name {
  display: block;
  color: var(--muted);
  font-size: var(--fs-sm);
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
}
.scout-proposal-marks { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 2px; }
/* When it was proposed: narrow, unwrapped, and read down the column rather than
   across the row - which is the whole reason it left the name cell. */
.scout-proposed-cell { width: 1%; }
/* What the confidence CLAIMS, printed rather than hovered for. The pill alone
   reads as a grade on the company; the sentence is the only thing that says it
   is a statement about how well the model could see the business. */
.scout-conf-cell {
  white-space: normal;
  min-width: 190px;
  max-width: 260px;
}
.scout-conf-what {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1.35;
}
/* A proposal with no entry price can never be scored, which is the one thing
   this feature is measured by - so it is marked rather than left to be noticed
   as a blank column three months later. */
.scout-unpriced {
  color: var(--warn);
  font-size: var(--fs-xs);
  border: 1px solid var(--warn);
  border-radius: var(--radius-xs);
  padding: 0 5px;
  cursor: help;
}
.scout-thesis { margin: 0 0 8px; font-size: var(--fs-md); }

/* The reasoning behind the sentence, folded. A row carrying four facts, four
   named risks and a checks note is twenty lines tall, and sixty of those is a
   document rather than a list - so the pane arrives shut and the reader opens
   the one row they stopped at. See `thesisBody` in scout.js for why the
   sentence itself stays outside the fold.

   A native <details>, as three other places on this page: keyboard-reachable,
   announced as a disclosure, and open on print however it was left on screen.
   The browser's triangle is dropped for the chevron in the markup, which can be
   coloured and lined up with the sentence's first line. */
.scout-fold > summary {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  cursor: pointer;
  list-style: none;
}
.scout-fold > summary::-webkit-details-marker { display: none; }
.scout-fold > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
/* Turned rather than swapped, so an open row is legible at a glance down a
   column of shut ones - the same gesture the review sections make. */
.scout-fold-caret {
  display: inline-flex;
  flex: 0 0 auto;
  margin-top: 3px;
  color: var(--accent);
  transition: transform 120ms ease;
}
.scout-fold[open] > summary .scout-fold-caret { transform: rotate(90deg); }
/* `min-width: 0` or the sentence refuses to wrap inside the flex row and drags
   the cell - and with it the whole table - sideways. */
.scout-fold-line { min-width: 0; flex: 1 1 auto; }
/* Two lines while shut. A thesis sentence runs to three or four, and the row
   height a fold exists to buy back is spent again by the third of them. */
.scout-fold:not([open]) .scout-thesis {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  margin-bottom: 0;
}
/* What is behind the fold, counted. A shut pane that does not admit it is
   hiding four risks is a shut pane that gets left shut - and the risks are the
   half of a thesis this desk is here to read. Gone once it is open, where the
   things themselves are on screen and a count of them is noise. */
.scout-fold-hints {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  margin-top: 3px;
}
.scout-fold[open] .scout-fold-hints { display: none; }
.scout-fold-hint {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  color: var(--muted);
  font-size: var(--fs-xs);
}
.scout-fold-warn { color: var(--warn); }
.scout-fold-body { margin-top: 8px; }

.scout-facts { margin: 0 0 8px; }
.scout-facts dt {
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-top: 6px;
}
.scout-facts dd { margin: 0; font-size: var(--fs-sm); }

.scout-concerns { margin: 8px 0; }
.scout-concerns h5 {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 0 0 3px;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  font-weight: 600;
}
.scout-concerns svg { color: var(--warn); }
.scout-concerns ul { margin: 0; padding-left: 18px; font-size: var(--fs-sm); }

/* What this build thought of the model's own answer. Warn-coloured because
   every one of them is a reason to read the proposal harder, and none of them
   is a fault in the application. */
.scout-checks {
  margin: 8px 0;
  padding: 6px 10px 6px 24px;
  border-left: 3px solid var(--warn);
  background: var(--panel);
  border-radius: var(--radius-xs);
  color: var(--warn);
  font-size: var(--fs-sm);
}
.scout-checks li { margin: 2px 0; }

/* The scraped lines, as a strip. Verbatim strings from screener.in - a blank
   cell is a row the page did not have, and it is simply absent here rather than
   drawn as a zero. */
.scout-figs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 8px 0;
  padding-top: 8px;
  border-top: 1px solid var(--line-soft);
  font-size: var(--fs-sm);
}
.scout-fig { display: flex; gap: 4px; align-items: baseline; }
.scout-fig-label {
  color: var(--muted);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.scout-fig-value { font-variant-numeric: tabular-nums; }

/* The same strip, inside one table cell. A dozen pairs laid across a card is a
   strip; laid down a column it is a small grid of stacked label-over-value
   pairs - which is the only shape in which twelve numbers fit beside a
   paragraph of thesis without becoming twelve columns. */
.scout-figs-cell { min-width: 208px; max-width: 300px; }
.scout-figs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
  gap: 6px 12px;
  margin: 0;
  padding-top: 0;
  border-top: 0;
}
.scout-figs-grid .scout-fig { flex-direction: column; gap: 0; align-items: flex-start; }

/* The decision. The heaviest row on the card, and set apart from the prose
   above it, because this is the only control in the application that puts a
   stock on the watch list. */
.scout-decide {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}
.scout-note {
  flex: 1 1 220px;
  min-width: 0;
  padding: 4px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--panel);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-sm);
}
.scout-note:focus { outline: none; border-color: var(--accent); }
.scout-decide .btn { display: inline-flex; align-items: center; gap: 4px; }
.scout-decide .btn[disabled] { opacity: .45; pointer-events: none; }
/* In the table it stacks inside its own column, and needs no rule above it -
   the cell edge is already the separation the card drew a line for. */
.scout-decide-cell { min-width: 208px; }
/* Both tables draw the same thesis block, so both need the width a paragraph
   with a definition list and a bullet list under it actually reads at. The
   record used to hold one summary sentence and 240px was enough for that. */
.scout-open .scout-thesis-cell,
.scout-decided .scout-thesis-cell { min-width: 300px; }
.scout-open .scout-thesis,
.scout-decided .scout-thesis { margin-bottom: 6px; }
.scout-open .scout-decide {
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.scout-open .scout-note { flex: 0 0 auto; }
.scout-open .scout-decide .btn { justify-content: center; }

/* --- the record ---------------------------------------------------------- */
/* Seven columns is not a table a phone can hold, so it scrolls inside its own
   box rather than pushing the page sideways - the same bargain the Scoreboard's
   verdict table makes. */
.scout-table-scroll { overflow-x: auto; }
.scout-decided {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
  font-size: var(--fs-base);
}
.scout-decided th {
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  font-weight: 600;
  padding: 4px 8px 4px 0;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.scout-decided td {
  padding: 5px 8px 5px 0;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
/* This is an audit record, so its evidence must be copyable. The shared table
   engine deliberately disables selection on draggable headers; state the body
   side explicitly so a drag/select never inherits that control treatment. */
.scout-decided tbody,
.scout-decided tbody td,
.scout-decided tbody td * {
  user-select: text;
  -webkit-user-select: text;
}
.scout-decided tbody tr:last-child td { border-bottom: 0; }
.scout-thesis-cell { white-space: normal; min-width: 240px; }
.scout-decided-note {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  margin-top: 2px;
}
.scout-when-cell { color: var(--muted); white-space: nowrap; }
.scout-num { text-align: right; font-variant-numeric: tabular-nums; }
.scout-decided th.scout-num { text-align: right; }

/* The record's heading and the control over it on one line. The count in the
   heading is the landmark a reader scans for, so the picker sits beside it
   rather than above the paragraph explaining why rejections are kept. */
.scout-block-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
}
/* The panel hangs off it, and `.dropdown` on the same element already says
   `position: relative`. Kept anyway, because the record's heading is the one
   place that host is not a bare `.dropdown` - it is a flex item on a heading
   row, and a positioning context that depended on a class somebody could drop
   in a markup tidy-up would put the panel at the top of the page. */
.scout-colhost { position: relative; }
.scout-colhint { margin: 0 0 6px; font-size: var(--fs-xs); line-height: 1.45; }
/* Wider than the grid's own panel by ten pixels and for one reason: this list
   carries the whole column registry under group headings that say where each
   column came from, and "Watch list · My inputs" is the longest of them. */
.scout-colpanel { width: 340px; }
.scout-colpanel .picker-row .icon { opacity: .75; flex: none; }
/* No watch-list row behind this cell at all - the name was rejected and never
   watched, so nothing was ever scraped for it. Muted, because an em dash drawn
   in the weight of a figure reads as a value rather than as an absence. */
.scout-decided .scout-blank { color: var(--muted); }

/* A stock, as a button: the reader's next question is always about the name.
   The same treatment `.score-symbol` gets. */
.scout-symbol {
  border: 0;
  background: transparent;
  color: var(--text);
  padding: 0;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
}
.scout-symbol:hover { color: var(--accent); }
.scout-symbol:hover .scout-symbol-code,
.scout-symbol:hover .scout-proposal-name { color: var(--accent); text-decoration: underline; }

/* How sure the model is, and what a human decided. One pill shape for both,
   because they are the same kind of object - a short word carrying a colour. */
.scout-pill {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0 6px;
  border-radius: 999px;
  border: 1px solid currentColor;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 600;
  cursor: help;
}
.scout-pill-good { color: var(--positive); }
.scout-pill-mid { color: var(--accent); }
.scout-pill-low { color: var(--muted); }

@media (max-width: 900px) {
  /* The two the reader did not come for - When and By. Dropped by CLASS rather
     than by position: they used to be the sixth and seventh columns and now
     they are wherever the reader's column choice puts them, so an nth-child
     rule would hide whichever two happened to land there. */
  .scout-decided th.scout-when-cell,
  .scout-decided td.scout-when-cell { display: none; }
  .scout-decided { min-width: 420px; }
  /* The shortlist keeps all six - none of them is the one the reader did not
     come for - and scrolls instead, a little tighter than on the desktop. */
  .scout-open { min-width: 940px; }
  .scout-figs-cell, .scout-decide-cell { min-width: 180px; }
  .scout-open .scout-thesis-cell,
  .scout-decided .scout-thesis-cell { min-width: 240px; }
  .scout-conf-cell { min-width: 150px; max-width: 200px; }
}

/* --- Universe / List of Stocks: the pool as companies ----------------------

   The second of Universe's three halves, and the one place in this application
   where a table of a thousand unfamiliar tickers has to be scannable. Two
   decisions carry that:

   * the stage is a toned pill rather than a word, so "approved" and "barred" do
     not look alike down a long column - but it is a WORD in a pill, never a
     colour alone, so the table still reads in greyscale;
   * a row that is out - barred, passed over, rejected - is dimmed rather than
     hidden. The pool's dead ends are most of it, and a list that quietly showed
     only the live names would be the same lie as a pipeline strip with no
     zeroes on it.

   The furniture is the Universe block's above and the News feed's next door:
   `.scout-head` for the frame, the chip idiom for the stage filter, the same
   table metrics. Three tables in three dialects would make one page look like
   three features. */
.pool-panel { padding-top: 4px; }
.pool-bar { margin: 10px 0 0; }
.pool-sort { display: inline-flex; align-items: center; gap: 5px; }
.pool-sort-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.pool-bar .btn { display: inline-flex; align-items: center; gap: 4px; }

/* The stage filter. `.news-chip`'s shape, without the tone dot: the stage is
   already a coloured pill on every row of the table below, and a second colour
   for the same fact on the control above it would be two vocabularies. */
.pool-chips { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0 0; }
.pool-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px 9px;
  border: 1px solid var(--line);
  border-radius: 11px;
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: var(--fs-sm);
  cursor: pointer;
}
.pool-chip:hover { border-color: var(--accent); color: var(--text); }
.pool-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pool-chip .n { font-variant-numeric: tabular-nums; opacity: .7; font-size: var(--fs-xs); }
.pool-chip.on {
  color: var(--text);
  border-color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}
/* A stage nothing has reached yet. Dimmed and unpressable rather than hidden,
   for the reason the pipeline strip draws its zeroes. */
.pool-chip[disabled] { opacity: .4; cursor: default; }
.pool-count { margin: 10px 0 6px; }

.pool-scroll { overflow-x: auto; }
.pool-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.pool-table th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 5px 8px;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
.pool-table td {
  padding: 5px 8px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.pool-table tbody tr.pool-row:hover { background: var(--row-hover); }
.pool-row.on td { background: var(--accent-soft); }
/* Barred, passed over, rejected: the pool's dead ends, which are most of it. */
.pool-row-closed .pool-sym, .pool-row-closed .pool-fig,
.pool-row-closed .pool-mcap { opacity: .62; }

.pool-open { width: 22px; }
.pool-toggle {
  display: inline-flex;
  border: 0;
  background: transparent;
  color: var(--muted);
  padding: 0;
  cursor: pointer;
}
.pool-toggle:hover { color: var(--accent); }
.pool-sym { white-space: nowrap; font-weight: 600; }
.pool-name {
  display: block;
  font-weight: 400;
  color: var(--muted);
  font-size: var(--fs-xs);
  white-space: normal;
  max-width: 220px;
}
.pool-mcap, .pool-fig {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.pool-table th.pool-mcap, .pool-table th.pool-fig { text-align: right; }

/* The stage. A word in a pill, and the tone says which of three things has
   happened: still moving, on the watch list, out. */
.pool-stage {
  display: inline-block;
  padding: 0 7px;
  border-radius: 999px;
  border: 1px solid currentColor;
  font-size: var(--fs-xs);
  white-space: nowrap;
}
.pool-stagecell { white-space: nowrap; }
.pool-stage-good { color: var(--positive); }
.pool-stage-mid { color: var(--accent); }
.pool-stage-low { color: var(--muted); }

/* The threshold a name tripped, or the model's line on it. The widest column
   and the one worth the width: it is the only text this pool ever produces. */
.pool-said { width: 100%; min-width: 220px; }
.pool-said-text { display: inline; line-height: 1.5; }
.pool-said .scout-pill { margin-right: 5px; }

/* The opened row: the other sixteen scraped figures, and where the name came
   from. A row rather than a popup because the comparison a reader is making is
   with the rows above and below it. */
.pool-detail td { background: var(--panel); }
.pool-detail-body { padding: 4px 2px 8px; }
.pool-detail-said { margin: 0 0 8px; font-size: var(--fs-md); }
.pool-detail-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  margin: 8px 0 0;
  font-size: var(--fs-xs);
}
.pool-detail-facts dt { color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.pool-detail-facts dd { margin: 0 8px 0 0; font-variant-numeric: tabular-nums; }

.pool-more { display: flex; align-items: center; gap: 8px; margin: 10px 0 0; }
.pool-more .btn { display: inline-flex; align-items: center; gap: 4px; }

@media (max-width: 900px) {
  /* The four the reader did not come for. What survives is the name, the size,
     the stage and what was said about it - which is the whole question this
     list answers. */
  .pool-table th:nth-child(4), .pool-table td:nth-child(4),
  .pool-table th:nth-child(5), .pool-table td:nth-child(5),
  .pool-table th:nth-child(6), .pool-table td:nth-child(6),
  .pool-table th:nth-child(7), .pool-table td:nth-child(7) { display: none; }
  .pool-table { min-width: 420px; }
  .pool-name { max-width: 140px; }
}

/* --- Universe / Criteria: the rubric, drawn ------------------------------

   Four stages down the page in the order a company passes through them, each
   carrying what it costs - free, a page view, a model call, a person - because
   those four costs are the reason the pipeline is shaped as it is.

   The one piece of real information design here is the bar's table: a threshold
   with the number of names it has actually barred beside it. Four numbers in
   prose is a paragraph nobody rereads; four numbers each carrying how often it
   bit is a review document, and it is the only argument there will ever be for
   moving one of them. */
.crit-panel { padding-top: 4px; }
.crit-coverage { margin: 8px 0 0; }
.crit-steps { display: flex; flex-direction: column; gap: 12px; margin-top: 14px; }
.crit-step {
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  padding: 12px 14px;
}
.crit-step-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* The number, because the four are a sequence and not a menu. */
.crit-step-n {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-xs);
  font-weight: 700;
  flex: 0 0 auto;
}
.crit-step-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--fs-md);
}
.crit-step-title svg { color: var(--accent); }
/* What the stage costs, on the stage. The free ones say so quietly; the two
   that spend something are the ones worth the ink. */
.crit-cost {
  margin-left: auto;
  padding: 1px 8px;
  border: 1px solid var(--warn);
  border-radius: 999px;
  color: var(--warn);
  font-size: var(--fs-xs);
  white-space: nowrap;
}
.crit-cost-free { border-color: var(--line); color: var(--muted); }
.crit-step-what { margin: 6px 0 10px; }

.crit-facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 3px 12px;
  margin: 0 0 10px;
  font-size: var(--fs-sm);
}
.crit-facts dt {
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--fs-xs);
  align-self: center;
}
.crit-facts dd { margin: 0; }

.crit-notes { margin: 0; padding: 0; list-style: none; font-size: var(--fs-sm); }
.crit-notes li {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 5px 0;
  line-height: 1.5;
}
.crit-notes svg { color: var(--muted); flex: 0 0 auto; margin-top: 2px; }

.crit-scroll { overflow-x: auto; }
.crit-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.crit-table th {
  padding: 5px 8px;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
.crit-table td {
  padding: 6px 8px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.crit-table tbody tr:last-child td { border-bottom: 0; }
.crit-rule { white-space: nowrap; font-weight: 600; }
.crit-value { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--accent); }
.crit-hits { text-align: right; }
.crit-hit { font-variant-numeric: tabular-nums; font-weight: 600; }
/* A threshold that has never bitten. Not an error and not a boast - most likely
   the sweep has not reached enough of the pool yet, which the line above the
   table says in as many words. */
.crit-hit-nil { color: var(--muted); font-weight: 400; }
.crit-why { line-height: 1.5; min-width: 260px; }
.crit-blank { display: block; color: var(--muted); font-size: var(--fs-xs); margin-top: 3px; }
.crit-missing { margin: 8px 0 0; }
/* What the bar deliberately does not test. A footnote to the table rather than a
   row in it - it is not a rule and must not be counted as one - but on the page
   all the same: an unexplained absence is the one the next person "fixes". */
.crit-absent { margin: 8px 0 0; }
.crit-absent li { align-items: flex-start; color: var(--muted); }
.crit-absent strong { color: var(--text); font-weight: 600; }
.crit-absent svg { color: var(--warn); }

.crit-model { margin: 0 0 10px; }
.crit-line { display: flex; align-items: flex-start; gap: 6px; margin: 5px 0; }
.crit-line svg { color: var(--accent); flex: 0 0 auto; margin-top: 2px; }
.crit-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 14px 0 4px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.crit-sub svg { color: var(--accent); }

.crit-asks {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 4px 12px;
  margin: 6px 0 0;
  font-size: var(--fs-sm);
}
.crit-asks dt {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-xs);
  color: var(--accent);
  white-space: nowrap;
}
.crit-asks dd { margin: 0; line-height: 1.5; }
.crit-enum {
  display: block;
  color: var(--muted);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.crit-confs { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.crit-conf { display: flex; align-items: flex-start; gap: 8px; font-size: var(--fs-sm); }
.crit-conf .scout-pill { flex: 0 0 auto; cursor: default; }
.crit-conf-what { line-height: 1.5; }

.crit-checks { margin: 6px 0 0; padding-left: 18px; font-size: var(--fs-sm); }
.crit-checks li { margin: 4px 0; line-height: 1.5; }
.crit-checks code {
  margin-right: 6px;
  color: var(--accent);
  font-size: var(--fs-xs);
}

.crit-fold { margin-top: 12px; }
.crit-fold > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--muted);
}
.crit-fold > summary:hover { color: var(--text); }
.crit-fold > summary svg { color: var(--accent); }
/* The instruction as it is sent. Monospaced and wrapped rather than scrolled:
   it is prose written for a model, and a reader checking what was asked should
   not have to drag a horizontal bar to read a sentence. */
.crit-prompt {
  margin: 6px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  background: var(--panel);
  color: var(--text);
  font-size: var(--fs-xs);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.crit-figs { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0 0; }
.crit-fig {
  padding: 1px 7px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  color: var(--muted);
  font-size: var(--fs-xs);
}

/* The flow chart: one company's journey as a rail of gates, each carrying the
   exit its failures take.

   A list rather than an SVG, deliberately. This has to survive a phone, a
   screen reader and a copy into an email, and a diagram survives none of the
   three; what makes it read as a chart is the rail down the left and the branch
   off each node. The branch is the content - a chart of the happy path would be
   four boxes and an arrow, and the question a reader brings here is always "why
   is this name not in the pool", which only an exit answers. So an exit is
   indented off the rail and tinted --negative, and never drawn in the same
   weight as the gate above it. */
.crit-map { margin-top: 14px; }
.crit-flow {
  position: relative;
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
}
/* The rail, behind the numbers and stopped short of the last one so the chart
   ends rather than trailing off the bottom of itself. */
.crit-flow::before {
  content: '';
  position: absolute;
  top: 12px;
  bottom: 24px;
  left: 10px;
  width: 2px;
  border-radius: 2px;
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--accent) 55%, transparent),
    color-mix(in srgb, var(--accent) 12%, transparent));
}
.crit-node {
  position: relative;
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr);
  gap: 10px;
  padding: 0 0 12px;
}
.crit-node:last-child { padding-bottom: 0; }
.crit-node-n {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 1px solid color-mix(in srgb, var(--accent) 52%, transparent);
  border-radius: 50%;
  background: var(--scout-bg, var(--panel-2));
  color: var(--accent);
  font-size: var(--fs-xs);
  font-weight: 700;
  box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 22%, transparent);
}
.crit-node-body {
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel);
  padding: 9px 11px;
}
/* The two ends of the journey are terminals rather than gates - nothing is
   tested at either - and drawing them like the six that do test something would
   make the chart claim there are eight gates. */
.crit-node[data-flow="listed"] .crit-node-body,
.crit-node[data-flow="watch"] .crit-node-body {
  border-style: dashed;
  background: transparent;
}
.crit-node-title {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: 600;
}
.crit-node-title svg { color: var(--accent); flex: 0 0 auto; }
/* The stage a survivor is written to, in the pool filter's own word. It is the
   join between this chart and the counts on the panel next door. */
.crit-node-stage {
  padding: 0 6px;
  border: 1px solid color-mix(in srgb, var(--positive) 45%, transparent);
  border-radius: var(--radius-pill);
  color: var(--positive);
  background: color-mix(in srgb, var(--positive) 10%, transparent);
  font-size: var(--fs-xs);
  cursor: help;
}
.crit-node-cost {
  margin-left: auto;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 400;
  white-space: nowrap;
}
.crit-node-what { margin: 4px 0 0; }
.crit-gates { margin: 7px 0 0; padding: 0; list-style: none; }
.crit-gate {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 3px 0;
  font-size: var(--fs-sm);
  line-height: 1.45;
}
.crit-gate svg { color: var(--positive); flex: 0 0 auto; margin-top: 2px; }
.crit-node-note {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 7px 0 0;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1.5;
}
.crit-node-note svg { color: var(--warn); flex: 0 0 auto; margin-top: 2px; }
.crit-exit {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 8px 0 0 10px;
  padding: 5px 8px;
  border-left: 2px solid color-mix(in srgb, var(--negative) 60%, transparent);
  border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
  background: color-mix(in srgb, var(--negative) 7%, transparent);
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1.5;
}
.crit-exit svg { color: var(--negative); flex: 0 0 auto; margin-top: 2px; }
/* What the one stage that spends tokens actually spends them on. Numbered,
   because the order is the argument: the figures are laid out before the call,
   the call is one name, and the bill is written after it whatever came back. */
.crit-calls-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 9px 0 0;
  color: var(--accent);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
}
.crit-calls-head svg { color: var(--accent); flex: 0 0 auto; }
.crit-calls {
  margin: 5px 0 0;
  padding-left: 20px;
  font-size: var(--fs-sm);
}
.crit-call { margin: 3px 0; line-height: 1.45; }
.crit-call::marker { color: var(--accent); font-weight: 700; }
/* Where the data comes from and where it lands. Drawn on every node rather than
   only the interesting ones: the reader has to be able to see that six of the
   eight stages reach nothing at all, which is what makes the two that do reach
   something legible as the expensive ones. */
.crit-io {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 6px 0 0;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1.5;
}
.crit-io svg { flex: 0 0 auto; margin-top: 2px; }
.crit-io[data-io="pull"] svg { color: var(--accent); }
.crit-io[data-io="write"] svg { color: var(--scout-violet, var(--accent)); }
.crit-io-k {
  margin-right: 6px;
  padding: 0 5px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  color: var(--text);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
}

@media (max-width: 900px) {
  .crit-facts, .crit-asks { grid-template-columns: minmax(0, 1fr); }
  .crit-facts dt, .crit-asks dt { margin-top: 6px; }
  .crit-cost, .crit-node-cost { margin-left: 0; }
  .crit-table { min-width: 420px; }
}

/* ── Flow charts, shared ──────────────────────────────────────────────────
   Two tabs draw a pipeline as a diagram - the Screener's "how this is fed" and
   the Universe's funnel from the exchange list to the watch list - and they are
   drawn out of one set of shapes on purpose. Two charts a fortnight apart, each
   with its own idea of what a node is and how an arrow ends, would be two
   visual languages a reader has to learn separately, and the second would be
   the one that never quite matched. See `flowchart.js`.

   An SVG because the arrows are the content, and one that scrolls inside its
   own box rather than shrinking: a diagram scaled down to a phone is a diagram
   nobody can read, and a horizontal scrollbar is at least honest about what it
   is asking.

   Every colour here is a token, and every kind a chart uses is named in words
   in the legend above it. Nothing in either picture is knowable only by hue. */
.flow-wrap { overflow-x: auto; padding: 4px 0 2px; }
.flow { display: block; max-width: none; height: auto; }
.flow-lane {
  fill: var(--muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .08em;
}
.flow-node rect {
  fill: var(--panel);
  stroke: var(--card-edge);
  stroke-width: 1;
}
.flow-t { fill: var(--text); font-size: 12.5px; font-weight: 600; }
.flow-s { fill: var(--muted); font-size: 10.5px; }
/* The five kinds. The first three are the three costs this application keeps
   distinguishing and names in words wherever it draws a button; `main` is the
   chain itself and `out` is what leaves it. */
.flow-fetch rect { stroke: var(--accent); stroke-width: 2.5; }
.flow-paid rect { stroke: var(--warn); stroke-width: 2.5; }
.flow-free rect { stroke: var(--positive); stroke-width: 2.5; }
.flow-out rect { stroke: var(--line); stroke-dasharray: 4 3; }
.flow-main rect {
  fill: var(--panel-2);
  stroke: var(--accent);
  stroke-width: 2;
}
.flow-edge {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.4;
  marker-end: url(#flow-tip);
}
/* A pass that PRODUCES a stage rather than the flow through it. Dashed, so a
   reader following the funnel does not count it as a step in it. */
.flow-dash { stroke-dasharray: 3 3; opacity: .8; }
.flow-spine { fill: none; stroke: var(--muted); stroke-width: 1.4; opacity: .75; }
.flow-tip { fill: var(--muted); }
.flow-key { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 4px; }
.flow-key-item {
  padding: 1px 8px;
  border-left: 3px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--control-bg);
  font-size: var(--fs-xs);
}
.flow-key-fetch { border-left-color: var(--accent); }
.flow-key-paid { border-left-color: var(--warn); }
.flow-key-free { border-left-color: var(--positive); }
.flow-key-main { border-left-color: var(--accent); }
.flow-key-out { border-left-color: var(--muted); }

/* ── The Screener tab (F12) ───────────────────────────────────────────────
   The watch list ranked against itself. It shares the head and stage idiom of
   the Universe block above deliberately: the two tabs are one pair of questions
   about membership, and furniture that agreed on nothing would be the fastest
   way to make them look unrelated.

   Three things here carry meaning rather than decoration, and none of them is
   reduced to hue alone: the composite's band, the coverage bar, and the arrow
   marking an inverted factor. Each pairs its colour with a figure or a shape -
   the bar prints its own percentage inside it, the composite prints its number,
   the arrow is a glyph, an unfillable factor is dashed as well as dimmed - so
   the page still reads in greyscale and to anybody who cannot separate the two
   ends of the ramp. */
.screen-head {
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  padding: 12px 14px;
}
.screen-head-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.screen-head-what { margin: 0 0 10px; }
.screen-empty {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0;
  color: var(--muted);
}

/* The run's counts. The same shape as the scout's stage strip next door. */
.screen-stages { display: flex; flex-wrap: wrap; gap: 6px; }
.screen-stage {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 78px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--panel);
}
.screen-stage-n { font-size: var(--fs-lg); font-variant-numeric: tabular-nums; }
.screen-stage-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.screen-stage-nil { opacity: .5; }
.screen-stage-nil .screen-stage-n { color: var(--muted); }
.screen-when { margin: 8px 0 0; }
.screen-when code { font-size: var(--fs-xs); opacity: .8; }

/* What the run could NOT do. Warn-tinted, and above the ranking rather than
   below it: this block exists to make the table less convincing, and a reader
   who has already scrolled past it has believed the list. */
.screen-honesty {
  margin: 10px 0 0;
  padding: 8px 10px 8px 12px;
  list-style: none;
  border-left: 3px solid var(--warn);
  border-radius: var(--radius-xs);
  background: var(--warn-soft);
  font-size: var(--fs-sm);
}
.screen-honesty li {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: 0 0 6px;
}
.screen-honesty li:last-child { margin-bottom: 0; }
.screen-honesty svg { flex: none; margin-top: 2px; }
.screen-gap-list { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.screen-gap-list code {
  padding: 0 4px;
  border-radius: var(--radius-xs);
  background: var(--control-bg);
  font-size: var(--fs-xs);
}

/* Before you run it. The readiness list, above the operations, because it is
   what somebody reads to decide which of them to press.

   A table rather than cards: seven rows that differ only in their numbers are
   exactly what a table is for, and the one thing a reader does here is compare
   the state column down the page. The state is a word in a pill, never a bare
   colour - the whole point of the block is to be readable at a glance by
   somebody who is about to spend two hours of page views. */
.screen-need-block {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.screen-need-scroll { overflow-x: auto; }
.screen-need-table { margin-top: 8px; min-width: 720px; }
.screen-need-table td { vertical-align: top; }
.screen-pill {
  display: inline-block;
  padding: 1px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  white-space: nowrap;
}
.screen-pill-good { color: var(--positive); background: var(--positive-soft); }
.screen-pill-mid { color: var(--accent); background: var(--accent-soft); }
.screen-pill-low { color: var(--warn); background: var(--warn-soft); }
.screen-pill-bad { color: var(--negative); background: var(--negative-soft); }
.screen-need-name { white-space: nowrap; }
/* The one row that can stop a run, said on the row rather than in a footnote. */
.screen-need-req {
  margin-left: 6px;
  padding: 0 5px;
  border: 1px solid var(--warn);
  border-radius: var(--radius-xs);
  color: var(--warn);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.screen-need-n { white-space: nowrap; font-variant-numeric: tabular-nums; }
.screen-need-age { white-space: nowrap; color: var(--muted); font-size: var(--fs-xs); }
.screen-need-stale {
  display: inline-block;
  margin-left: 6px;
  padding: 0 5px;
  border-radius: var(--radius-xs);
  color: var(--warn);
  background: var(--warn-soft);
}
.screen-need-feeds { color: var(--muted); font-size: var(--fs-xs); }
.screen-need-act { text-align: right; white-space: nowrap; }
/* The two paid passes have a card of their own below, with the figure on it.
   One press per operation, so the row points rather than duplicating it. */
.screen-need-below { color: var(--muted); font-size: var(--fs-xs); }
.screen-need-act .btn .icon { vertical-align: -2px; margin-right: 4px; }
/* A press that went somewhere else. Announced, because the job it queued is
   followed on another tab and nothing on this one would otherwise change. */
.screen-toast {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 12px;
  padding: 8px 12px;
  border-left: 3px solid var(--positive);
  border-radius: var(--radius-xs);
  background: var(--positive-soft);
  font-size: var(--fs-sm);
}

.screen-flow-fold > summary { font-size: var(--fs-base); }

/* The operations. Three cards on the overview half, in the card idiom the rest
   of the AI page uses for a control that starts something - the scout's three
   passes next door are the same object with a progress meter on it.

   The one thing carrying meaning here is the tag on each heading: two of the
   three spend money and one does not, and that difference is drawn as a word
   and an icon before it is drawn as a colour, so it survives greyscale. The
   paid pair also carry the warn hue on their left edge - not because spending
   is a mistake, but because it is the one property of a button on this tab that
   somebody must not have to hover to learn. */
.screen-ops {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--card-edge);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.screen-ops-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}
.screen-ops-what { margin: 0 0 10px; }
.screen-op-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px;
}
.screen-op {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px;
  border: 1px solid var(--card-edge);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--panel);
}
.screen-op > p { margin: 0; }
.screen-op-name {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--fs-base);
}
.screen-op-name .icon { color: var(--muted); }
/* Free or spends, said in a word with a mark beside it. The whole reason this
   block can carry a paid button at all: nothing here costs money without
   saying so above the fold, in text rather than in a hue. */
.screen-op-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.screen-op-tag .icon { width: 12px; height: 12px; }
.screen-op-tag-free { color: var(--positive); background: var(--positive-soft); }
.screen-op-tag-paid { color: var(--warn); background: var(--warn-soft); }
.screen-op-tag-paid .icon, .screen-op-tag-free .icon { color: inherit; }
.screen-op-paid { border-left-color: var(--warn); }
.screen-op-active { border-color: var(--accent); }
.screen-op-what { font-size: var(--fs-sm); }
.screen-op-state {
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.screen-op-cost { font-size: var(--fs-xs); }
.screen-op-act { margin-top: auto; padding-top: 4px; }
.screen-op-act .btn .icon { vertical-align: -3px; margin-right: 5px; }
.screen-op .btn[disabled] { opacity: .45; pointer-events: none; }
.screen-op .hint { margin: 0; font-size: var(--fs-xs); }
.screen-busy { display: flex; align-items: center; gap: 8px; margin: 12px 0 0; }
.screen-job {
  padding: 1px 6px;
  border-radius: var(--radius-xs);
  background: var(--control-bg);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
}

/* The ranking. */
.screen-table {
  width: 100%;
  margin-top: 14px;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}
.screen-table th {
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  text-transform: uppercase;
  letter-spacing: .05em;
  white-space: nowrap;
}
.screen-table td {
  padding: 5px 8px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
.screen-table tbody tr:hover { background: var(--row-hover); }
.screen-rank {
  width: 34px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.screen-sym { white-space: nowrap; }
.screen-name {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  max-width: 26ch;
  overflow: hidden;
  text-overflow: ellipsis;
}
.screen-score { width: 64px; }
/* The band, always beside its own figure. The colour qualifies the number; it
   never replaces it. */
.screen-composite {
  display: inline-block;
  min-width: 46px;
  padding: 1px 6px;
  border-radius: var(--radius-xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.screen-composite-good { background: var(--positive-soft); color: var(--positive); }
.screen-composite-mid { background: var(--accent-soft); color: var(--accent); }
.screen-composite-low { background: var(--control-bg); color: var(--text); }
.screen-composite-bad { background: var(--negative-soft); color: var(--negative); }
.screen-star { width: 56px; color: var(--muted); white-space: nowrap; }

/* Coverage: a bar with its own percentage inside it, so the fill is a second
   reading of the figure rather than the only one. */
.screen-covcell { width: 92px; }
.screen-cov {
  position: relative;
  display: block;
  height: 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--control-bg);
  overflow: hidden;
  cursor: help;
}
.screen-cov-fill { position: absolute; inset: 0 auto 0 0; }
.screen-cov-good .screen-cov-fill { background: var(--positive-soft); }
.screen-cov-mid .screen-cov-fill { background: var(--accent-soft); }
.screen-cov-low .screen-cov-fill { background: var(--warn-soft); }
.screen-cov-n {
  position: relative;
  display: block;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  line-height: 14px;
  text-align: center;
}

.screen-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.screen-chip {
  padding: 1px 6px;
  border-radius: 999px;
  font-size: var(--fs-xs);
  white-space: nowrap;
  cursor: help;
}
.screen-chip-good { background: var(--positive-soft); color: var(--positive); }
.screen-chip-bad { background: var(--negative-soft); color: var(--negative); }
.screen-foot { margin: 8px 0 0; }

/* The two folded buckets and the rubric. Folded rather than absent: a name that
   is not in the ranking has to be findable, or the ranking is the only thing
   anybody can argue with. */
.screen-fold {
  margin-top: 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel);
  padding: 8px 12px;
}
.screen-fold > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.screen-fold[open] > summary { margin-bottom: 8px; }
.screen-gate-n {
  padding: 0 6px;
  border-radius: 999px;
  background: var(--control-bg);
  font-variant-numeric: tabular-nums;
}
.screen-gates { display: flex; flex-direction: column; gap: 10px; }
.screen-gate-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 4px;
  font-size: var(--fs-sm);
}
.screen-gate-names { display: flex; flex-wrap: wrap; gap: 4px 10px; margin: 0; }
.screen-table-thin { margin-top: 4px; }

.screen-fams {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 10px;
}
.screen-fam {
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  padding: 8px 10px;
  background: var(--panel-2);
}
.screen-fam-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 4px;
  font-size: var(--fs-sm);
}
.screen-fam-w {
  margin-left: auto;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--control-bg);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}
.screen-fam-factors { display: flex; flex-wrap: wrap; gap: 4px; margin: 6px 0; }
.screen-factor {
  padding: 1px 6px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  font-size: var(--fs-xs);
  cursor: help;
}
/* A factor nothing can fill yet. Dimmed AND dashed - a reader who cannot see
   the opacity difference still gets the border. */
.screen-factor-off {
  border-style: dashed;
  opacity: .55;
  color: var(--muted);
}
.screen-inv { margin-left: 2px; color: var(--warn); font-weight: 700; }
.screen-fam-live { margin: 0; }

@media (max-width: 720px) {
  /* The rating and the chips go first: on a narrow screen the ranking is what
     is being read, and those two qualify it rather than carry it. */
  .screen-table th:nth-child(5), .screen-table td:nth-child(5),
  .screen-table th:nth-child(6), .screen-table td:nth-child(6) { display: none; }
  .screen-fams { grid-template-columns: 1fr; }
  /* One operation per row rather than a column of squeezed cards. The button is
     the point of each and it has to be reachable with a thumb. */
  .screen-op-cards { grid-template-columns: 1fr; }
}

/* The tailwind family's own block. Sectors rather than stocks - the only grid on
   this page whose unit is an industry - and an unscored one is dashed as well as
   dimmed, the same second channel `.screen-factor-off` uses. */
.screen-sectors {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 6px;
}
.screen-sector {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  background: var(--panel-2);
  cursor: help;
}
.screen-sector-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--fs-sm);
}
.screen-sector-score {
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm);
}
.screen-sector-n {
  min-width: 2ch;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.screen-sector-off {
  border-style: dashed;
  opacity: .55;
}
.screen-sector-off .screen-sector-score { color: var(--muted); }

/* The one line on this page that is waiting for time rather than for a button:
   the order-inflow comparison cannot exist until the filing ledger is two years
   old. Marked as a note rather than as a warning - nothing is wrong. */
.screen-short {
  border-left: 2px solid var(--accent-2, var(--line));
  padding-left: 8px;
}

/* --- the Sectors page ---------------------------------------------------- */
/* One row per industry, joining the per-stock classifier to the per-sector
   tailwind score. The unit is an industry rather than a company, which is the
   one thing this page has in common with the tailwind block on the Screener tab
   and the reason it borrows that block's vocabulary - a dashed, dimmed edge for
   an answer that does not exist - without borrowing its classes: that block is a
   fold of chips inside another page, this is a table with a detail under it.

   The four classification flags each take an identity hue and keep it in both
   themes and in every place they are drawn - the row's chip, the open sector's
   stock list, the filter. A flag is a fact about a company that never changes
   meaning, so it is exactly the kind of thing that should be recognisable
   without reading it. */
.sector-top h3 { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.sector-top .hint { margin: 0 0 10px; max-width: 92ch; }

.sector-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 8px;
  margin: 0 0 10px;
}
.sector-stat {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-lg);
  background: var(--panel-2);
  color: var(--muted);
  font-size: var(--fs-sm);
  cursor: help;
}
.sector-stat .icon { color: var(--accent); }
.sector-stat b { color: var(--text); font-variant-numeric: tabular-nums; }

/* The explanation of the page's most conspicuous column of dashes. A fold
   rather than a paragraph because it is read once and then never again - but it
   has to be *there*, because "63 of 404 scored" reads as a broken pass to
   anybody who does not know the sector labels are free text. */
.sector-reach { margin: 0 0 10px; }
.sector-reach > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.sector-reach > summary .icon { color: var(--warn); }
.sector-reach .advice-evidence { margin: 6px 0 0; max-width: 96ch; }

.sector-n {
  padding: 0 6px;
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
}

.sector-bar { flex-wrap: wrap; row-gap: 6px; margin-bottom: 8px; }
.sector-bar .btn .icon { margin-right: 4px; }

/* --- the table --- */
.sector-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.sector-table th {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 5px 6px;
  border-bottom: 1px solid var(--line);
  background: var(--panel-2);
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
.sector-table td {
  padding: 4px 6px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
}
.sector-row { cursor: pointer; }
.sector-row:hover { background: var(--row-hover); }
/* The open sector. A left edge as well as a fill, because the fill is the same
   colour family as the hover it has to be told apart from. */
.sector-row.selected { background: var(--accent-soft); }
.sector-row.selected .sector-c-name { box-shadow: inset 2px 0 0 var(--accent); }

.sector-c-name { min-width: 200px; max-width: 340px; }
.sector-c-members { min-width: 120px; max-width: 220px; }
.sector-members { display: flex; flex-wrap: wrap; gap: 2px 7px; }
.sector-member { font-size: var(--fs-xs); }
.sector-c-n { width: 56px; }
.sector-c-flags { width: 150px; }
.sector-c-conf,
.sector-c-when,
.sector-c-asked { color: var(--muted); white-space: nowrap; }
.sector-c-conf { font-variant-numeric: tabular-nums; cursor: help; }
.sector-c-when, .sector-c-asked { font-variant-numeric: tabular-nums; }
.sector-c-score { width: 64px; }
.sector-c-tconf, .sector-c-state { white-space: nowrap; }

/* A note on the row rather than a column of its own: "stale", "empty" and
   "human" are each true of a handful of rows and would be four-fifths blank as
   columns. Dashed, so the marker is not mistaken for a state pill. */
.sector-tag {
  margin-left: 5px;
  padding: 0 5px;
  border: 1px dashed var(--line);
  border-radius: var(--radius-xs);
  color: var(--muted);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: .04em;
  cursor: help;
}

/* --- the four flags --- */
.sector-flags { display: inline-flex; gap: 3px; }
.sector-flag {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 0 4px;
  border: 1px solid color-mix(in srgb, var(--sector-hue, var(--accent)) 45%, transparent);
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--sector-hue, var(--accent)) 14%, transparent);
  color: var(--sector-hue, var(--accent));
  font-size: var(--fs-2xs);
  font-variant-numeric: tabular-nums;
  cursor: help;
}
.sector-flag-financial { --sector-hue: var(--accent); }
.sector-flag-commodity_cyclical { --sector-hue: var(--negative); }
.sector-flag-tailwind_cyclical { --sector-hue: var(--warn); }
.sector-flag-tailwind { --sector-hue: var(--positive); }
/* Nobody in this sector carries the flag. Kept in place rather than dropped so
   the four sit in the same order on every row and can be read down a column;
   dimmed and dashed, the same two channels `.screen-factor-off` uses. */
.sector-flag-off {
  border-style: dashed;
  border-color: var(--line);
  background: none;
  color: var(--muted);
  opacity: .6;
}

/* --- the score --- */
.sector-score {
  display: inline-block;
  min-width: 44px;
  padding: 1px 6px;
  border-radius: var(--radius-xs);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.sector-score-good { background: var(--positive-soft); color: var(--positive); }
.sector-score-mid { background: var(--accent-soft); color: var(--accent); }
.sector-score-low { background: var(--panel-2); color: var(--text); }
.sector-score-bad { background: var(--negative-soft); color: var(--negative); }
/* No score at all, which is not a low one. Flat and muted, with no fill to be
   read as a band. */
.sector-score-plain { color: var(--muted); }

.pill.sector-state.tone-good {
  color: var(--positive);
  border-color: color-mix(in srgb, var(--positive) 50%, transparent);
}
.pill.sector-state.tone-bad {
  color: var(--negative);
  border-color: color-mix(in srgb, var(--negative) 50%, transparent);
}
.pill.sector-state.tone-warn {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 50%, transparent);
}
.pill.sector-state.tone-plain { color: var(--muted); }

/* --- the open sector --- */
.sector-detail {
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--card-edge);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
/* Nothing open yet. No border and no fill: an empty card the size of a full one
   is a hole in the page, and this is a prompt, not a panel. */
.sector-detail-empty {
  margin-top: 10px;
  padding: 0;
  border: 0;
  background: none;
}
.sector-detail-empty .hint { display: flex; align-items: center; gap: 6px; margin: 0; }
.sector-detail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  margin-bottom: 8px;
}
.sector-detail-head h4 {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: var(--fs-lg);
  color: var(--text);
  text-transform: none;
  letter-spacing: 0;
}
.sector-detail-head .icon { color: var(--accent); }
.sector-detail-meta {
  margin-left: auto;
  color: var(--muted);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

/* The case and the case against it, side by side on a wide screen. Deliberately
   equal columns: the bear case is not a footnote to the narrative, it is the
   half of the answer the validator refuses to store without. */
.sector-case {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 10px;
}
.sector-case section {
  padding: 8px 10px;
  border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs);
  background: var(--panel);
}
.sector-case h5 {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 0 0 4px;
  font-size: var(--fs-sm);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.sector-case section:nth-child(1) h5 .icon { color: var(--positive); }
.sector-case section:nth-child(2) h5 .icon { color: var(--negative); }
.sector-case section:nth-child(3) h5 .icon { color: var(--warn); }
.sector-case p { margin: 0; line-height: 1.6; }
.sector-case .hint { margin-top: 5px; }

.sector-drivers { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0 0; }
.sector-driver {
  padding: 1px 7px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: var(--radius-lg);
  color: var(--accent);
  font-size: var(--fs-xs);
}

.sector-sources { margin-top: 8px; }
.sector-sources > summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  color: var(--muted);
  font-size: var(--fs-sm);
}
.sector-src-list { margin: 6px 0 0; padding-left: 20px; }
.sector-src-list li { margin-bottom: 3px; line-height: 1.5; }
.sector-src-date {
  margin-right: 6px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.sector-src-note { display: block; color: var(--muted); font-size: var(--fs-sm); }

.sector-stocks-title {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 12px 0 6px;
  font-size: var(--fs-sm);
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: .05em;
}
.sector-stocks { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.sector-stocks th {
  padding: 4px 6px;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
}
.sector-stocks td {
  padding: 3px 6px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
}
.sector-s-sym, .sector-s-flags, .sector-s-conf, .sector-s-when { white-space: nowrap; }
.sector-s-name { max-width: 200px; }
.sector-s-when { color: var(--muted); font-variant-numeric: tabular-nums; }
.sector-s-why { min-width: 300px; width: 100%; color: var(--muted); line-height: 1.5; }
.sector-symbol {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}
.sector-symbol:hover { text-decoration: underline; }

@media (max-width: 900px) {
  /* The classifier's date and the ask's date go first: on a narrow screen the
     question is which sectors are scored and how strongly, and when the label
     was last written qualifies that rather than carrying it. */
  .sector-table .sector-c-when,
  .sector-table .sector-c-asked { display: none; }
  .sector-detail-meta { margin-left: 0; width: 100%; }
}
@media (max-width: 640px) {
  .sector-table .sector-c-conf,
  .sector-table .sector-c-tconf { display: none; }
  .sector-case { grid-template-columns: 1fr; }
}

/* --- selling: the popup's Sell menu, and the Sold tab -------------------- */
/* Two surfaces, one feature. The menu is where a sale is recorded, on the stock,
   in the middle of reading it; the tab is where sales are read back, later, as a
   list. See `sellForm` in detail.js and `sold.js`.

   The identity hue is the money green rather than the accent. Everything else
   in this popup's head is either neutral or the accent, and Sell is the one
   control on it that moves a position out of the book - it has earned a colour
   of its own, and green is the one a realised figure is already drawn in. */
.btn.sell { --sell-mark: var(--rf-green); }
.btn.sell svg { color: var(--sell-mark); }
.btn.sell:hover { border-color: var(--sell-mark); }
.sell-panel { width: 300px; }
.sell-panel h4 { margin-top: 0; }
/* Label above box rather than beside it. Four fields at 300px wide is not a
   two-column form, and a label column wide enough for "Sale price" would leave
   the boxes too narrow to read a grouped rupee figure in. */
.sell-form { display: flex; flex-direction: column; gap: 6px; margin: 6px 0; }
.sell-field { display: flex; flex-direction: column; gap: 2px; }
.sell-label {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--muted);
}
.sell-field input[type="text"] { width: 100%; }
/* The date picker's button already fills its column and starts its text at the
   left - `.date-pick` sets both, for the popup's own date fields - so there is
   nothing to add here beyond saying that this form uses it. */
/* The cost basis the realised figure will be struck against. Ruled off above,
   because it is not a fifth field - it is what the four above are measured
   from, and a reader checking a gain that came out wrong starts here. */
.sell-cost {
  margin: 2px 0 0;
  padding-top: 6px;
  border-top: 1px solid var(--line);
}

/* The Sold tab. One card per sleeve across the top, the ledger under it. */
.sold-head-row { align-items: center; gap: 8px; }
.sold-search { display: inline-flex; align-items: center; gap: 5px; }
.sold-search svg { color: var(--muted); flex: none; }
.sold-search input {
  width: 210px;
  max-width: 40vw;
  background: var(--panel-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 3px 7px;
  font-size: var(--fs-sm);
  color: var(--text);
}
.sold-cards { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
/* A card, and a filter. Pressed it narrows the table under it, which is why it
   is a button and why `.on` is a fill rather than a border - the same bargain
   the Errors tab's status chips strike. */
.sold-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 168px;
  padding: 6px 10px;
  text-align: left;
  border: 1px solid color-mix(in srgb, var(--sold-hue, var(--line)) 45%, transparent);
  border-left: 3px solid var(--sold-hue, var(--line));
  border-radius: var(--radius);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
}
.sold-card:hover { border-color: var(--sold-hue, var(--line)); }
.sold-card.on {
  background: color-mix(in srgb, var(--sold-hue) var(--rf-tint, 22%), var(--panel));
  border-color: var(--sold-hue);
}
.sold-card-head { display: flex; align-items: center; gap: 5px; font-size: var(--fs-sm); }
.sold-card-head svg { color: var(--sold-hue, var(--muted)); flex: none; }
.sold-card-head .count { color: var(--muted); font-variant-numeric: tabular-nums; }
/* The one figure the card exists for, set large enough to be read across the
   row of them without stopping at each. */
.sold-card-gain {
  font-size: var(--fs-lg);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.sold-card-gain small { font-size: var(--fs-xs); font-weight: 400; color: var(--muted); }
.sold-card-gain.pos { color: var(--positive); }
.sold-card-gain.neg { color: var(--negative); }
.sold-card-foot { font-size: var(--fs-xs); color: var(--muted); }

.sold-table { margin-top: 4px; }
.sold-table th {
  padding: 3px 4px;
  border-bottom: 1px solid var(--line);
  text-align: left;
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--muted);
  white-space: nowrap;
}
.sold-table th.num { text-align: right; }
.sold-row { border-left: 3px solid transparent; }
.sold-row.open { background: var(--panel-2); border-left-color: var(--sold-hue, var(--line)); }
.sold-table td.pos { color: var(--positive); }
.sold-table td.neg { color: var(--negative); }
.sold-when { color: var(--muted); font-variant-numeric: tabular-nums; }
/* The stock name is the row's own handle: a button, so opening the snapshot is
   reachable by keyboard, dressed down to look like the cell it sits in. */
.sold-open {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.sold-open svg { color: var(--muted); flex: none; }
.sold-open:hover svg { color: var(--accent); }
.sold-sleeve svg { color: var(--sold-hue, var(--muted)); }
/* The reason, and the one cell on the row that may wrap: everything else is a
   figure and a figure that wraps is a figure misread. Capped so a long note
   cannot push the actions off the right of a narrow window. */
.sold-note { white-space: normal; max-width: 260px; color: var(--muted); }
.sold-actions { text-align: right; white-space: nowrap; }
.sold-actions .btn { padding: 1px 5px; }
/* The opened row's body, in a cell spanning the table. Tinted and ruled on the
   left in its row's own sleeve hue, so a long digest still reads as belonging
   to the line above it. */
.sold-detail > td {
  padding: 8px 10px;
  background: var(--panel-2);
  border-left: 3px solid var(--sold-hue, var(--line));
  white-space: normal;
}
.sold-snap { display: flex; flex-direction: column; gap: 6px; }
.sold-snap-rating { display: flex; align-items: center; gap: 6px; margin: 0; flex-wrap: wrap; }
.sold-snap-rating svg { color: var(--rf-amber); flex: none; }
.sold-snap-biz { margin: 0; }
/* The figures the stock carried that day, as a wrapping row of small facts
   rather than a table: there are never more than nine and a table of nine
   two-cell rows is a lot of furniture for eighteen values. */
.sold-facts { display: flex; flex-wrap: wrap; gap: 4px 14px; }
.sold-fact { display: flex; flex-direction: column; gap: 0; min-width: 92px; }
.sold-fact span { font-size: var(--fs-xs); color: var(--muted); }
.sold-fact strong { font-variant-numeric: tabular-nums; }
.sold-fact small { font-weight: 400; color: var(--muted); }
/* When each source behind those figures was last published. The gaps between
   them are the signal, which is why they are set side by side. */
.sold-stamps { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.sold-stamps svg { color: var(--muted); flex: none; }
.sold-empty, .sold-error { display: flex; align-items: center; gap: 6px; }
.sold-error { color: var(--negative); }
/* A part sale, marked on its row. The rest of the holding stayed in the sleeve,
   so the row is what went rather than the whole position - and without the chip
   a trim and a close-out are the same line with a smaller number on it. Amber
   rather than a sleeve hue: it qualifies the row, it does not classify it. */
.sold-trim {
  margin-left: 4px;
  border-color: color-mix(in srgb, var(--rf-amber) 45%, transparent);
  color: var(--rf-amber);
}
