/* ────────────────────────────────────────────────────────────────
   Sinux Signals — design tokens
   Neutral slate base + a single blue accent (light & dark).
   Chrome stays neutral so the semantic colours — green/red P&L, amber
   tickers, action pills — read clearly on top.
   ──────────────────────────────────────────────────────────────── */

:root {
  /* Brand anchors */
  --sinux-emerald: #10B981;
  --sinux-ink:     #0B0C0E;
  --sinux-pale:    #F2F3F5;

  /* Type — display (Conthrax substitute) / body / mono */
  --f-display: "Orbitron", "Conthrax", system-ui, sans-serif;
  --f-sans:    "Poppins", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-mono:    "JetBrains Mono", "IBM Plex Mono", ui-monospace, Menlo, monospace;

  /* Row density. */
  --row-h: 44px;
  --pad-x: 16px;
  --pad-y: 12px;

  /* Text-size scale (overridden by [data-fontscale]) — multiplies the rem root.
     Default = 1 (today's sizes exactly). Only TEXT scales, not spacing/borders. */
  --fs: 1;
  /* The type scale. Every size text is set in, in rem, so the Text size preference (--fs) and the
     browser's own font size scale all of it. At Small, the smallest step: 10, 11, 12, 13, 14, 17, 22
     and 32px. The half steps the file used to have (10.5, 11.5 and 12.5px) are snapped onto it, except
     where a step either way would cut or re-wrap what they size (a measured table fit, a one-line label):
     those stay literals, each with its reason beside it. */
  --t-2xs: 0.625rem;
  --t-xs: 0.6875rem;
  --t-sm: 0.75rem;
  --t-md: 0.8125rem;
  --t-base: 0.875rem;
  --t-lg: 1.0625rem;
  --t-xl: 1.375rem;
  --t-hero: 2rem;

  /* Misc */
  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 10px;

  /* "Needs attention" that is not a result: a feed that stopped updating, a stale mark.
     An ALIAS of the trim hue (.val-warn's colour), so it follows the theme and is the same on
     every brand: never --c-down (red is a loss) and never an action colour by name. */
  --c-warn: var(--c-trim);
  /* Placeholder fill for the boot skeleton AND the in-app loading rows, mixed from --fg-2
     so it reads on every theme and brand. It used to live on #boot only, which left the
     in-app placeholders transparent. */
  --boot-fill: color-mix(in oklab, var(--fg-2) 20%, transparent);
}

/* ── DARK theme (default) — neutral slate field, single blue accent */
:root,
[data-theme="dark"] {
  --bg-0: #0A0D0C;
  --bg-1: #131714;
  --bg-2: #191E1A;
  --bg-3: #212823;
  --bg-4: #2A312C;

  --border-1: #1F241F;
  --border-2: #2E342F;
  --border-3: #383F3A;

  --fg-0: #F3F5F1;
  --fg-1: #C7CCD3;
  /* --fg-2 and --fg-3 are TEXT: the quiet tiers that label every figure ("22 of 22 marked",
     "08:59 ET", "avg", "4h ago"). Both clear 4.5:1 on --bg-0 to --bg-2 (fg-3 4.5 to 5.2, fg-2 5.3 to
     6.1); --fg-3 used to measure 2.6 to 3.0. They stay quiet through size, capitals and spacing,
     not through faint ink. --fg-deco is the old faint value, for marks that carry no words: a
     separator dot, an arrow between two prices, a chevron, a timeline dot. A control (an icon
     button, a clear ×) is never --fg-deco: it needs 3:1 to be found. */
  --fg-2: #8C9289;
  --fg-3: #80867D;
  --fg-deco: #5A5F57;
  --fg-mono: #D8DCD4;

  --accent:      #10B981;
  --accent-dim: #0D9C6D;
  --accent-glow: rgba(18, 196, 137, 0.14);

  /* Semantic trade actions — distinct hues that read on neutral slate */
  --c-open:   #34C281;
  --c-add:    #2DB9C4;
  --c-trim:   #F2B53D;
  --c-cut:    #FF7A45;
  --c-close:  #E06A8B;
  --c-hold:   #8A92A2;
  --c-watch:  #9B7BFF;
  --c-expire: #565C66;
  /* Ticker symbols: high-contrast amber, on EVERY tab and every brand. Every ticker
     style reads --c-ticker, never --accent: a brand accent can sit next to a P&L or
     action colour (Saucy's orange is the CUT orange, the default emerald is the win
     green), and a symbol must not change colour from tab to tab. */
  --c-ticker: #F5B845;

  --c-up:     #34C281;
  --c-down:   #F2585B;

  --conf-high:   #34C281;
  --conf-medium: #F2B53D;
  --conf-low:    #8A92A2;

  /* Analyst identity slots: an analyst's dot, avatar ring and chart line (stored as "var(--a-N)",
     given out by add order, so a new value recolours every analyst at once). None wears a P&L or
     action colour: six of these used to BE them (a-2 the loss red, a-3 the win green, a-4 the watch
     purple, a-5 the ticker amber, a-6 the add teal, a-7 the close pink). Blue, khaki, orchid, sky,
     chartreuse, plum and stone: muted, at least 4.2:1 on every brand's --bg-1, and apart from each
     other for a deuteranope or a protanope too. tools/brand-check.js tests a brand's own --a-N. */
  --a-1: #5B8CF5;
  --a-2: #9A9461;
  --a-3: #C58DCE;
  --a-4: #7DD0FA;
  --a-5: #BAD074;
  --a-6: #AF5F9E;
  --a-7: #C4B4A3;

  --shadow-1: 0 1px 2px rgba(0,0,0,0.30);
  --shadow-2: 0 16px 40px rgba(0,0,0,0.46);
  --grid-line: rgba(255,255,255,0.05);

  --body-bg: linear-gradient(180deg, #0F130F 0%, #0A0D0C 340px);
}

/* ── LIGHT theme — neutral light field, blue accent (darker for white bg) */
[data-theme="light"] {
  --bg-0: #F7F8F4;
  --bg-1: #FFFFFF;
  --bg-2: #EDF0EA;
  --bg-3: #E3E5DE;
  --bg-4: #D6DAD1;

  --border-1: #E6E8E1;
  --border-2: #D4D8CE;
  --border-3: #BBC1B6;

  --fg-0: #14171C;
  --fg-1: #3A3F48;
  /* One quiet text tier in light: --fg-3 is --fg-2. #60655B clears 4.5:1 on --bg-0 to --bg-2 of
     every light field, the brands' accent-tinted ones included (4.55 on WarRoom's --bg-2, the
     darkest); #686E63 measured 4.0 there and the old --fg-3 #9CA295 2.0 to 2.6. --fg-deco keeps
     the old faint value for marks that carry no words (see the dark block). */
  --fg-2: #60655B;
  --fg-3: #60655B;
  --fg-deco: #9CA295;
  --fg-mono: #21262E;

  /* #047857 / #065F46: the old #059669 measured 3.3 to 3.8:1 as text on this field.
     Every brand sets its own light accent, so this one is the unbranded view only. */
  --accent:      #047857;
  --accent-dim:  #065F46;
  --accent-glow: rgba(4, 120, 87, 0.12);

  /* Same hue and chroma as before, only darker, so each one clears 4.5:1 as text on
     bg-0, bg-1 and bg-2 of EVERY light field, branded ones included (the worst case is
     WarRoom's bg-2). At AA a yellow cannot stay bright on white, so trim is an ochre,
     close to the ticker amber; the two are told apart by shape (pill or symbol), as in
     dark. Dark mode keeps its own values. The --a-* identity slots below are not text: they
     need 3:1 as an analyst's dot (see the dark block). Hold and watch went a step deeper in
     v2.9.0 (#5E6675, #6A52D0 measured 4.3 to 4.5:1 on the accent-tinted bg-2 of six brands);
     tools/brand-check.js tests every one of these against every brand's fields. */
  --c-open:   #00733C;
  --c-add:    #006E79;
  --c-trim:   #895A00;
  --c-cut:    #B63700;
  --c-close:  #AB3B5F;
  --c-hold:   #5B6372;
  --c-watch:  #664DCB;
  --c-expire: #5F646F;
  --c-ticker: #8E5400;   /* at least 4.6:1 on every brand's light bg-2, where hovered and expanded rows sit */

  --c-up:     #007342;
  --c-down:   #C21E37;

  --conf-high:   #007342;
  --conf-medium: #895A00;
  --conf-low:    #5B6372;

  /* The same seven identity hues, deeper for a light field (at least 3.8:1 on every brand's --bg-1,
     and 3:1 on --bg-2, where a hovered or opened row and a popover put the dot). */
  --a-1: #3D6AE0;
  --a-2: #5A5422;
  --a-3: #804C88;
  --a-4: #3286AD;
  --a-5: #728426;
  --a-6: #702562;
  --a-7: #867867;

  --shadow-1: 0 1px 2px rgba(20,23,28,0.06);
  --shadow-2: 0 16px 40px rgba(20,23,28,0.13);
  --grid-line: rgba(20,23,28,0.06);

  --body-bg: linear-gradient(180deg, #FFFFFF 0%, #F7F8F4 340px);
}

/* Branded LIGHT mode — backgrounds, borders and the page field pick up a faint
   wash of the brand accent, so light mode echoes the brand (cyan for Charan,
   gold for Market Magicians, …) instead of being plain neutral grey. Automatic
   for any brand: it derives from --accent, which each brand sets per theme.
   Two attribute selectors → wins over the base light theme; the injected brand
   <style> only sets the accent vars, so these background tints apply on top. */
:root[data-brand][data-theme="light"] {
  --bg-0: color-mix(in oklab, var(--accent) 6%,  #F6F8FA);
  --bg-1: color-mix(in oklab, var(--accent) 3.5%, #FFFFFF);
  --bg-2: color-mix(in oklab, var(--accent) 8%,  #EBEEF1);
  --bg-3: color-mix(in oklab, var(--accent) 11%, #E0E4E8);
  --bg-4: color-mix(in oklab, var(--accent) 14%, #D4D9DF);
  --border-1: color-mix(in oklab, var(--accent) 13%, #E4E7EB);
  --border-2: color-mix(in oklab, var(--accent) 17%, #D3D7DD);
  --border-3: color-mix(in oklab, var(--accent) 24%, #BAC0C8);
  --grid-line: color-mix(in oklab, var(--accent) 18%, transparent);
  --body-bg: linear-gradient(180deg,
    color-mix(in oklab, var(--accent) 8%, #FBFCFD) 0%,
    color-mix(in oklab, var(--accent) 5%, #F6F8FA) 340px);
}

/* Accessibility text-size steps (set on <html> via data-fontscale). New users
   default to "md"; "sm" is the original compact size, "lg" the largest. Scales
   every rem-based font platform-wide. */
[data-fontscale="sm"] { --fs: 1; }
[data-fontscale="md"] { --fs: 1.125; }
[data-fontscale="lg"] { --fs: 1.25; }

/* ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  background: var(--bg-0);
  color: var(--fg-0);
  font-family: var(--f-sans);
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
/* rem root = the browser's own font size × the text-size scale. Every font-size is rem, so the whole
   platform's type scales from this one line; spacing/borders stay px (no overflow). 100% is the
   member's own default size: 16px unless they set a larger one in their browser (Chrome's Large is
   20px, Very large 24px), and then every step is larger by the same factor. At 16px it is exactly what
   a fixed 16px gave (16 / 18 / 20px for Small / Default / Larger). Only the sidebar's brand lockup is
   capped (below); the text a member reads never is. */
html { font-size: calc(100% * var(--fs, 1)); }

body {
  min-height: 100vh;
  background-image: var(--body-bg);
  font-size: var(--t-base);   /* = 14px at the default scale */
}
/* The installed iPhone app in the light theme while iOS still draws its status bar in white over
   the page (the app launched dark, or iOS kept the style it saw at Add to Home Screen; app.html
   asks for dark icons in the light theme): a dark band behind the clock and battery so they stay
   readable. Its height is the top inset, which is 0 whenever iOS draws its own bar above the page,
   so it disappears by itself. iOS only (-webkit-touch-callout): on Android the icons follow
   theme-color, and a dark band would hide dark icons. Chrome, not a brand or semantic colour. */
@supports (-webkit-touch-callout: none) {
  @media (display-mode: standalone) {
    :root[data-theme="light"] body::after {
      content: ""; position: fixed; top: 0; left: 0; right: 0;
      height: env(safe-area-inset-top, 0px);
      background: #0B0C0E; z-index: 1000; pointer-events: none;
    }
  }
}

button { font-family: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
input, select, textarea { font-family: inherit; color: inherit; }
::selection { background: var(--accent-glow); color: var(--fg-0); }

.mono { font-family: var(--f-mono); }
.kbd {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  padding: 2px 5px;
  border-radius: 3px;
  background: var(--bg-3);
  border: 1px solid var(--border-2);
  color: var(--fg-1);
}

/* ── scrollbars */
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--border-3); background-clip: padding-box; border: 2px solid transparent; }
*::-webkit-scrollbar-track { background: transparent; }

/* ── Shell (sidebar + main) — full width */
/* The sidebar is 224px, or 11.2rem where that is wider: the same 224px at every text size with the
   browser's default 16px font (11.2rem is 224px at Larger), and room for the nav's names and counts
   when a member's own larger browser font makes the root bigger (at a 27px root the nav needed about
   300px, and a 224px rail scrolled sideways and cut its counts: "35" for 350). */
.shell {
  display: grid;
  grid-template-columns: max(224px, 11.2rem) minmax(0, 1fr);
  /* Two rows: sidebar+main share the first; the footer spans the full width of
     the second, running under the sidebar instead of stopping at its edge. */
  grid-template-rows: minmax(0, 1fr) auto;
  height: 100vh;          /* lock to the viewport — content scrolls, page doesn't */
  overflow: hidden;
  width: 100%;
}

/* Mobile reading-progress rail — hidden on desktop (the viewport is locked, so
   there is no page-level scroll to track). Made visible + fixed under 900px. */
.scroll-rail { display: none; }
/* Its fill, driven by the page's own scroll where the browser supports scroll timelines (the
   900px block below), so scrolling runs no script at all. app.jsx writes the same transform
   itself everywhere else (older iOS Safari). */
@keyframes scrollRailFill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ── Sidebar */
.sidebar {
  position: sticky;
  top: 0;
  height: 100%;   /* fill the first grid row — the footer owns the strip below */
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 14px;
  border-right: 1px solid var(--border-1);
  background: var(--bg-1);
}
/* Brand lockup: [mark | name] vertically centered as a row; the white-label
   "Powered by Sinux Signals" line spans full-width beneath the lockup. */
.sidebar .brand {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 6px 8px 20px;
}
.sidebar .brand .brand-row {
  display: flex;
  align-items: center;   /* mark stays centered against the name, even wrapped */
  gap: 11px;
}
.sidebar .brand .mark {
  width: 30px; height: 30px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--bg-0);
  display: grid; place-items: center;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: min(1rem, 16px);   /* capped per step, as the name below: the letter stays inside its 30px tile */
  flex-shrink: 0;
}
[data-fontscale="md"] .sidebar .brand .mark { font-size: min(1rem, 18px); }
[data-fontscale="lg"] .sidebar .brand .mark { font-size: min(1rem, 20px); }
/* Real brand logo in the mark slot — the image brings its own background */
.sidebar .brand .mark.mark-img { background: none; overflow: hidden; }
.sidebar .brand .mark img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: inherit; }
.sidebar .brand .brand-name {
  font-family: var(--f-display);
  font-size: min(0.9375rem, 15px);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--fg-0);
  line-height: 1.18;
  /* Break BETWEEN words, and only split a word that could never fit on its own.
     `anywhere` split brand names mid-letter ("OPTIONALIT / Y"), which reads as a
     broken lockup rather than a wrapped one. */
  overflow-wrap: break-word;
}
/* A brand lockup is an identity, not body copy. The sidebar is 224px with the browser's default
   font (~155px once the 30px mark and gaps are taken), so at the larger text steps a
   one-word name like OPTIONALITY outgrows the rail and splits mid-word. Let the
   name grow LESS than the rest of the UI — it still scales up (16px at "lg" vs
   15px at "sm"), it just stops out-running its container.
   Each step is also capped (min()) at the size it has with the browser's default 16px font, and so are
   the letter mark (above) and the powered-by line (below): the rem root follows the browser's own font
   size, and with a larger one (24px makes the root 27px at the default step) the name and the line
   under it ran up to 30px past the rail. Capped, they look exactly as they do at that step with the
   default font, whole in the rail (which only grows with a larger font, .shell). */
[data-fontscale="md"] .sidebar .brand .brand-name { font-size: min(var(--t-base), 15.75px); letter-spacing: 0.04em; }
[data-fontscale="lg"] .sidebar .brand .brand-name { font-size: min(0.8rem, 16px);   letter-spacing: 0.03em; }
/* White-label sub-line — the Sinux Signals reference under a brand lockup.
   Always Sinux emerald (NOT the brand accent): it names Sinux, so it keeps
   Sinux's colour no matter whose brand is active. */
.sidebar .brand .brand-powered {
  font-family: var(--f-mono);
  font-size: min(0.5625rem, 9px);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--sinux-emerald);
  white-space: nowrap;
  padding-left: 1px;
}
[data-fontscale="md"] .sidebar .brand .brand-powered { font-size: min(0.5625rem, 10.125px); }
[data-fontscale="lg"] .sidebar .brand .brand-powered { font-size: min(0.5625rem, 11.25px); }
.sidebar nav { display: flex; flex-direction: column; gap: 2px; }
.nav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 11px;
  border-radius: var(--radius);
  color: var(--fg-2);
  font-size: 0.84375rem;
  font-weight: 500;
  text-align: left;
  transition: background 120ms, color 120ms;
}
.nav-item .icon { flex-shrink: 0; }
.nav-item:hover { background: var(--bg-2); color: var(--fg-0); }
.nav-item.active { background: var(--accent-glow); color: var(--accent); }
.nav-label { flex: 1; }
.nav-badge {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--fg-2);
  background: var(--bg-2);
  padding: 1px 7px;
  border-radius: 99px;
}
.nav-item.active .nav-badge { background: color-mix(in oklab, var(--accent) 18%, transparent); color: var(--accent); }
/* Light: the count in the accent wash read about 3.6:1 on every brand; ink reads 11:1. */
[data-theme="light"] .nav-item.active .nav-badge { color: var(--fg-0); }
.sidebar-foot {
  margin-top: auto;
  padding-top: 14px;
  border-top: 1px solid var(--border-1);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.sidebar-ver { font-size: var(--t-xs); letter-spacing: 0.1em; color: var(--fg-3); padding-left: 2px; }
/* Feedback + the collapse toggle share the last row. */
.side-foot-row { display: flex; gap: 6px; }
.side-foot-row .bug-report-btn { flex: 1; min-width: 0; }
.side-toggle {
  flex-shrink: 0; width: 34px;
  display: grid; place-items: center;
  border: 1px solid var(--border-1); border-radius: var(--radius);
  color: var(--fg-3);
  transition: color 120ms, background 120ms, border-color 120ms;
}
.side-toggle:hover { color: var(--fg-0); background: var(--bg-2); border-color: var(--border-2); }
.nav-dot { display: none; }
.user-avatar { display: grid; place-items: center; flex-shrink: 0; }

/* ── Collapsed sidebar: an icon rail (desktop only — the phone drawer ignores it).
   <html data-side="rail"> is stamped before first paint (app.html) and kept in step by
   App, so the boot skeleton and the real shell agree on the width from frame one.
   No width animation: the labels would wrap mid-slide, and a snap reads cleaner. */
@media (min-width: 901px) {
  :root[data-side="rail"] .shell,
  :root[data-side="rail"] .boot-shell { grid-template-columns: 64px minmax(0, 1fr); }
  :root[data-side="rail"] .sidebar { padding: 18px 10px; }
  :root[data-side="rail"] .sidebar .brand { padding: 6px 0 20px; align-items: center; }
  :root[data-side="rail"] .sidebar .brand .brand-name,
  :root[data-side="rail"] .sidebar .brand .brand-powered,
  :root[data-side="rail"] .nav-label,
  :root[data-side="rail"] .nav-badge,
  :root[data-side="rail"] .user-chip .user-meta,
  :root[data-side="rail"] .bug-label { display: none; }
  :root[data-side="rail"] .sidebar nav { scrollbar-width: none; }
  :root[data-side="rail"] .sidebar nav::-webkit-scrollbar { display: none; }
  :root[data-side="rail"] .nav-item { position: relative; justify-content: center; padding: 10px 0; }
  :root[data-side="rail"] .nav-dot {
    display: block; position: absolute; top: 6px; right: 8px;
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--accent); box-shadow: 0 0 0 2px var(--bg-1);
  }
  :root[data-side="rail"] .user-chip { flex-direction: column; gap: 4px; padding: 6px 0; border-color: transparent; background: none; }
  :root[data-side="rail"] .side-foot-row { flex-direction: column; }
  :root[data-side="rail"] .bug-report-btn { justify-content: center; padding: 9px 0; }
  :root[data-side="rail"] .side-toggle { width: auto; height: 34px; }
  :root[data-side="rail"] .boot-side { padding: 18px 10px; }
  :root[data-side="rail"] .boot-side .boot-brand { justify-content: center; }
  :root[data-side="rail"] .boot-side .boot-name,
  :root[data-side="rail"] .boot-side .boot-wake { display: none; }
  :root[data-side="rail"] .boot-nav i { width: 100%; }
}
@media (max-width: 900px) { .side-toggle { display: none; } }
/* The rail's hover label. Fixed + portalled to <body>, so the nav's overflow can't clip it. */
.rail-tip {
  position: fixed; z-index: 400; transform: translateY(-50%);
  display: flex; align-items: baseline; gap: 8px;
  padding: 6px 10px;
  border: 1px solid var(--border-2); border-radius: var(--radius-sm);
  background: var(--bg-2); color: var(--fg-0);
  box-shadow: var(--shadow-2);
  font-size: var(--t-md); font-weight: 500; white-space: nowrap;
  pointer-events: none;
  animation: fadeIn 90ms ease-out;
}
.rail-tip-sub { font-family: var(--f-mono); font-size: var(--t-xs); font-weight: 400; color: var(--fg-3); }
@media (prefers-reduced-motion: reduce) { .rail-tip { animation: none; } }

/* ── Main column + top bar */
.main { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  /* Reserve the iOS status-bar / notch inset so the header never sits under it
     (we draw full-screen via viewport-fit=cover + black-translucent). env()
     resolves to 0 off-PWA, so this is a no-op on desktop/Android. */
  padding: calc(16px + env(safe-area-inset-top, 0px)) calc(28px + env(safe-area-inset-right, 0px)) 16px calc(28px + env(safe-area-inset-left, 0px));
  border-bottom: 1px solid var(--border-1);
}
/* The frosted glass lives on a layer BEHIND the header, never on .topbar itself: an
   element with a backdrop-filter becomes the containing block of its position:fixed
   children, which shrank the phone bell's full-screen backdrop (fixed, inset 0) to the
   header's own box, so a tap below the bell sheet closed it AND landed on the page. */
.topbar::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: color-mix(in oklab, var(--bg-0) 86%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.topbar-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.nav-toggle { display: none; }      /* hamburger — shown on mobile */
.nav-backdrop { display: none; }    /* drawer backdrop — shown on mobile */
.topbar-title { display: flex; flex-direction: column; gap: 7px; min-width: 0; }
.topbar-title h1 {
  margin: 0;
  font-size: 1.1875rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--fg-0);
}
/* The status line under the title is words, so it is set in the sans face (JetBrains Mono is kept for
   figures: tickers, prices, P&L, times in tables, counts), at 500 so small type keeps its weight. */
.topbar-sub {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: var(--t-sm);
  color: var(--fg-2);
  flex-wrap: wrap;
}
.topbar-sub .sep { color: var(--fg-deco); }
/* The facts after the status pill (server, channels, last alert), each with its separator. On a
   phone both wrappers step aside and the line wraps as it always has; a desktop width keeps them
   to one line (below, min-width: 901px). */
.topbar-facts, .topbar-fact { display: contents; }
@media (min-width: 901px) {
  /* One line at every desktop width. The status words change ("connecting", "offline · updated
     9:05 PM", "reconnecting"), and a line that rewrapped on each change pushed the whole page
     down 26 px and back up, between about 1024 and 1280 px wide depending on the server's name.
     The facts wrap inside a box one line tall instead, so a fact that does not fit is left off
     whole, never cut: the channel count first, then the last alert (app.jsx puts them in that
     order from the end). The pill stays outside that box, so its live ring is never clipped. */
  .topbar-sub { flex-wrap: nowrap; }
  .topbar-sub > .status { flex: none; }
  .topbar-facts {
    display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
    min-width: 0; height: calc(1.45 * var(--t-sm)); overflow: hidden;
  }
  /* Alone on the line and still too wide (a long server name in a narrow window): an ellipsis. */
  .topbar-fact { display: block; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .topbar-fact .sep { margin-right: 8px; }
}
.topbar-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* Content fills the space between top bar and footer and is the scroll owner —
   so the footer stays pinned and never needs a page scroll to reach. */
.content { padding: 22px 28px 24px; display: flex; flex-direction: column; gap: 16px; min-width: 0; min-height: 0; flex: 1 1 auto; overflow-y: auto; }
/* Don't let the flex column squeeze a tall child (e.g. the Settings panel) down
   and clip it — keep natural height so the content area scrolls instead. The
   positions split opts back in to filling via its own `flex` below. */
.content > * { flex-shrink: 0; }

/* Signal Events and the Watchlist (and their recycle bins) on a computer: the feed's panel fills
   the window below its filter bar, however many rows the bar wraps to, so the box and its sideways
   scrollbar end at the bottom of the screen. The feed's own min-height (400px, inline) still holds
   on a short window, where the page scrolls instead. A phone scrolls the page (max-width 900px). */
@media (min-width: 901px) {
  .content > .feed-page { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
  .feed-page > .panel { flex: 1 1 0; min-height: calc(400px + 3.5rem); }
  .feed-page > .panel > .feed-scroll { flex: 1 1 0; }
}

/* Positions split fills the viewport; each panel scrolls inside its own box.
   `.solo` is the same geometry without the events rail (rail is owner-only /
   granted), so hiding the rail keeps the pinned header + in-panel scrolling. */
.split.cols, .split.solo { grid-template-rows: minmax(0, 1fr); flex: 1 1 auto; min-height: 0; }
.split.cols > .panel, .split.solo > .panel { min-height: 0; }
.split.cols .scroll-y, .split.solo .scroll-y { flex: 1 1 auto; min-height: 0; }

/* ── App footer — a full-width strip across BOTH grid columns (under the
   sidebar too), so the version/copyright reads once, edge to edge. */
.app-footer {
  grid-column: 1 / -1;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 28px; border-top: 1px solid var(--border-1);
  background: var(--bg-0);
  font-family: var(--f-sans); font-weight: 500;
  font-size: var(--t-xs); letter-spacing: 0.02em; color: var(--fg-3); flex-shrink: 0;
}
.app-footer-mid { text-align: center; flex: 1; }
.app-footer-ver { white-space: nowrap; }
.app-footer a { color: var(--accent); text-decoration: none; font-weight: 600; }
.app-footer a:hover { text-decoration: underline; }

/* ── Status pill
   Its look says the feed's state, never the brand (the brand accent pulsed the same way for
   "live", "connecting" and an outage, and on two brands it matched an action colour):
   solid --c-up only while live, the token .lv-chip-live and the open-market dot already use
   for "live"; a hollow ring otherwise, grey while connecting or paused, --c-warn while the
   feed is failing (offline, reconnecting, signed out). The word beside it always names the
   state, so it is never colour alone.
   The live ring is a transform + opacity animation on its own layer. The old box-shadow
   pulse could not run off the main thread and repainted the page every frame: about 60% of
   all idle main-thread work. No animation under reduced motion. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: var(--t-sm);
  color: var(--fg-1);
}
.status .status-word { text-transform: lowercase; }
.status .status-meta { color: var(--fg-2); }
.status .dot {
  position: relative;
  flex: none;
  width: 8px; height: 8px; border-radius: 99px;
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--fg-3);
}
.status.is-live .dot { background: var(--c-up); box-shadow: none; }
.status.is-error .dot { box-shadow: inset 0 0 0 1.5px var(--c-warn); }
.status.is-live .dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: color-mix(in oklab, var(--c-up) 45%, transparent);
  animation: statusRing 2.4s ease-out infinite;
  will-change: transform, opacity;
}
@keyframes statusRing {
  0%        { transform: scale(1); opacity: .8; }
  70%, 100% { transform: scale(3); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .status.is-live .dot::after { animation: none; opacity: 0; }
}

/* ── Stat tiles */
.statgrid {
  display: grid;
  /* Auto-fit: 4-up on wide screens, wrapping to 2×2 on narrower ones instead
     of crushing the cards. (Mobile pins an explicit 2-col layout.) */
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 10px;
}
.stat {
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 16px 18px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  position: relative;
  overflow: hidden;
}
.stat .lbl {
  font-family: var(--f-sans);
  font-size: var(--t-sm);
  letter-spacing: 0.01em;
  color: var(--fg-2);
  display: flex;
  align-items: center;
  gap: 8px;
}
.stat .val {
  font-family: var(--f-mono);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
}
.stat .delta {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--fg-3);
}
.stat .delta.up { color: var(--c-up); }
.stat .delta.down { color: var(--c-down); }
/* Under the Positions strip on a computer: how much of the open book has gone quiet (positions.jsx
   PositionsSummary). Set like the tiles' footnotes but in --fg-2, neutral, never amber: words a member
   reads, and --fg-3 measured 2.5 to 3:1 at this size; tucked up under the tiles (.content spaces its
   children 16px apart). A phone says it in its line under the row. A sentence, so the sans face. */
.pos-quiet { margin-top: -8px; font-size: var(--t-xs); color: var(--fg-2); }
@media (max-width: 900px) { .pos-quiet { display: none; } }
/* Positions summary strip: colored values + green/red inline splits */
.stat .val.up { color: var(--c-up); }
.stat .val.down { color: var(--c-down); }
.stat .val.val-sm { font-size: 1.1875rem; }
.stat .val.val-sm .ticker { font-size: 1rem; }
.stat .delta .up { color: var(--c-up); }
.stat .delta .down { color: var(--c-down); }
.stat .delta .even { color: var(--fg-2); }
.stat .icon {
  width: 14px; height: 14px;
  color: var(--fg-2);
  flex-shrink: 0;
}

/* ── Filter bar */
.filterbar {
  display: grid;
  grid-template-columns: 1fr auto auto auto;
  gap: 10px;
  align-items: center;
  /* The feed panel used to sit flush against this row. `.content` puts a 16px gap
     between ITS children, but a tab renders the bar and the panel inside one
     wrapper, so nothing separated them. */
  margin-bottom: 14px;
}
/* One height for every control in the bar. They come from four different families
   — .input (10px padding), .select, .seg/.seg-btn (9px) and the analyst popover's
   .btn — so each sat at its own height and the row never lined up. Fixing the
   height and centring the contents makes it read as a single strip. */
/* Control heights are in REM, not px, on purpose.
   `html { font-size: calc(100% * var(--fs)) }` scales every rem with the reader's
   text-size setting (and their browser's own font size), so a px height pins the BOX while the text inside it keeps
   growing — at the largest step a 0.8125rem control needs ~42px and a 38px box clips
   the descenders clean off (reported on the Analyst select in Log position).
   2.375rem is exactly 38px at the smallest step, so nothing moves for anyone there,
   and the row still reads as one strip at every size. */
.filterbar .input,
.filterbar .select,
.filterbar .btn,
.filterbar .seg { height: 2.375rem; }
.filterbar .input,
.filterbar .select { padding-top: 0; padding-bottom: 0; }
.filterbar .btn,
.filterbar .seg-btn { display: inline-flex; align-items: center; padding-top: 0; padding-bottom: 0; }
.filterbar .analyst-filter { display: inline-flex; }
/* On mobile the bar wraps to its own rows — keep the rhythm there too. */
@media (max-width: 900px) { .filterbar { gap: 8px; margin-bottom: 12px; } }
/* Each bar's columns, as classes (an inline grid template beat every phone rule). */
.rc-filterbar { grid-template-columns: 1fr auto; }
/* The Events and Watchlist bars carry up to seven controls (Add event and Live / Deleted for those
   who can use them). As a grid, their auto tracks shrank below the segments' labels on a laptop
   ("Chat" and "Analysis" vanished, "Shares" read "Shar", and Events' seventh control dropped onto a
   track of its own). On a computer they wrap instead: whole controls move to a second row, the
   ticker field takes what is left on the first. A phone has its own bar (.feed-bar-m). */
@media (min-width: 901px) {
  .filterbar.ev-filterbar, .filterbar.wl-filterbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
  .ev-filterbar > .input.search, .wl-filterbar > .input.search { flex: 1 1 220px; min-width: 180px; }
  .ev-filterbar > :not(.input), .wl-filterbar > :not(.input) { flex: none; }
  .ev-filterbar .seg-btn, .wl-filterbar .seg-btn { white-space: nowrap; }
}

/* ── The ticker search (shared.jsx SearchField). The clear button inside it is the phone's: a
   computer keeps its Clear buttons beside the field. */
.search-field { position: relative; display: flex; min-width: 0; }
.search-field > .input { flex: 1 1 auto; min-width: 0; width: 100%; }
.search-clear { display: none; }

/* ── Filter chips: one per filter that is set, each removes itself (shared.jsx FilterChips). */
.fchips { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; }
.fchip {
  display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
  min-height: 32px; padding: 0 8px 0 11px;
  border: 1px solid var(--border-2); border-radius: var(--radius-sm);
  background: var(--bg-1); color: var(--fg-1); font-size: var(--t-sm); line-height: 1.2;
}
.fchip-l { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fchip .icon { flex: none; color: var(--fg-3); }
.fchip:hover { border-color: var(--border-3); }
.fchip:hover .icon, .fchip:focus-visible .icon { color: var(--fg-0); }

/* ── The phone filter sheet (shared.jsx FilterSheet): a bottom sheet over a dim backdrop. Above the
   drawer, the bell and the corner buttons; below dialogs, toasts and the tour. */
.fsheet-back {
  position: fixed; inset: 0; z-index: 260;
  background: rgba(2, 4, 8, 0.55);
  animation: fadeIn 150ms ease-out;
}
.fsheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 261;
  max-height: 86vh;
  max-height: calc(100dvh - 40px - env(safe-area-inset-top, 0px));
  display: flex; flex-direction: column;
  background: var(--bg-1); border-top: 1px solid var(--border-2);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: var(--shadow-2);
  padding: 0 env(safe-area-inset-right, 0px) env(safe-area-inset-bottom, 0px) env(safe-area-inset-left, 0px);
  outline: none;
  animation: fsheetUp 220ms cubic-bezier(.22, 1, .36, 1);
}
@keyframes fsheetUp { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .fsheet, .fsheet-back { animation: none; } }
.fsheet-hd {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 8px 8px 8px 18px; border-bottom: 1px solid var(--border-1);
}
.fsheet-title { font-size: 0.9375rem; font-weight: 600; color: var(--fg-0); }
.fsheet-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 2px 18px 6px; }
.fsheet-sec { padding: 14px 0; border-bottom: 1px solid var(--border-1); }
.fsheet-sec:last-child { border-bottom: 0; }
.fsheet-lbl {
  margin-bottom: 9px;
  font-family: var(--f-sans); font-weight: 500; font-size: var(--t-2xs); letter-spacing: .06em; text-transform: uppercase; color: var(--fg-2);
}
.fsheet-sec .seg { display: flex; width: 100%; }
/* Equal widths while they fit, and a label never narrower than itself: an equal share cut "Chat" at
   320 px and broke "This week" over two lines. Each keeps one line; the rest share what is left. */
.fsheet-sec .seg-btn {
  flex: 1 1 0; min-width: max-content; justify-content: center; text-align: center; min-height: 40px;
  padding-left: 8px; padding-right: 8px; white-space: nowrap;
}
/* Many choices (the actions): three to a row. */
.fsheet-sec .seg.fsheet-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.fsheet-grid .seg-btn { border-right: 1px solid var(--border-1); }
.fsheet-grid .seg-btn:nth-child(3n) { border-right: 0; }
.fsheet-grid .seg-btn:nth-child(n+4) { border-top: 1px solid var(--border-1); }
.fsheet-sec .af-opt { min-height: 40px; }
/* Two buttons side by side while both labels fit on one line each ("Show 2,335 events" wrapped at
   320 px and at the largest text size); when they do not, the main one takes its own row below,
   under the thumb. */
.fsheet-ft {
  display: flex; flex-wrap: wrap; gap: 10px; padding: 12px 18px;
  border-top: 1px solid var(--border-1);
}
.fsheet-ft .btn { flex: 1 1 0; min-width: max-content; justify-content: center; min-height: 44px; white-space: nowrap; }
/* Clear all with nothing set does nothing, and looks it: the shared .btn:disabled. */

@media (max-width: 900px) {
  /* The search's own clear: it empties the field only. */
  .search-clear {
    display: grid; place-items: center;
    position: absolute; right: 1px; top: 50%; transform: translateY(-50%);
    width: 40px; height: 40px; border-radius: var(--radius-sm);
    color: var(--fg-2); background: transparent;
  }
  .search-clear:hover, .search-clear:focus-visible { color: var(--fg-0); }
  .search-field.has-value > .input { padding-right: 42px; }

  /* Open Positions: the field and the window on one row, Clear an icon between them once something
     is set (app.jsx puts it there in the markup). The field's column is the one that gives up the
     room, so the window buttons never move under the finger. While a ticker is typed the window and
     Clear fold away (the field's own clear takes over, and a narrowed window shows as a chip in the
     list header). */
  .filterbar.pos-filterbar {
    display: grid; grid-auto-flow: column;
    grid-template-columns: minmax(0, 1fr); grid-auto-columns: auto;
  }
  .pos-filterbar .pos-range { width: auto; }
  .pos-filterbar .pos-range .seg-btn { flex: 0 0 auto; white-space: nowrap; padding: 0 9px; font-size: var(--t-sm); }
  .pos-range .lbl-full { display: none; }
  .pos-range .lbl-short { display: inline; }
  .pos-filterbar .pos-reset { width: 2.375rem; padding: 0; justify-content: center; color: var(--fg-1); }
  .pos-filterbar .pos-reset-l { display: none; }
  .pos-filterbar.is-querying .pos-range,
  .pos-filterbar.is-querying .pos-reset { display: none; }
  /* The page title already says Open Positions. */
  .panel-hdr .pos-title-plain { display: none; }
  /* The analyst roster's header with only the instrument switch and the count: one row, not two
     (it wraps back to two when a narrow phone has no room). */
  .pos-hdr-lean { flex-wrap: wrap; align-items: center; }
  .panel-hdr-tools.pos-hdr-lean .seg { flex: 1 1 auto; order: 1; }
  .panel-hdr-tools.pos-hdr-lean .panel-meta { order: 2; }
  /* Each choice at least as wide as its word: the equal thirds below (.panel-hdr-tools .seg-btn)
     gave "Options" less room than its word on 320 to 390px phones (57px of the 71 it needs at 320px),
     its letters against the dividers. Where the three do not fit beside the count, the count takes
     the next row. */
  .panel-hdr-tools.pos-hdr-lean .seg-btn { min-width: max-content; }
  /* The header's controls never run past the panel: the quick filter's four buttons with their
     counts ("Losers 139") wanted 352 px of a 332 px header, and took the count and the Log position
     button over the panel's edge with them. Tighter buttons, and the row may shrink. */
  .panel-hdr-tools.pos-hdr-tools { min-width: 0; }
  .panel-hdr-tools .pos-seg .seg-btn { flex: 1 1 auto; gap: 4px; padding-left: 5px; padding-right: 5px; }
  .panel-hdr-tools .pos-seg .seg-count { padding: 1px 4px; }
  /* The sort menu (positions.jsx) ends the instrument switch's row (All / Options / Shares), the
     last row above the list, as tall as the switch: on the analyst filter's row it did not fit
     beside the count and took a row of its own, pushing the first position down 42px. The
     switch's buttons size to their words here and are padded like the quick filter's above, so a
     phone from 360px keeps both on one row; where they do not fit (320px, the largest text on a
     narrow phone) the menu takes the next row. It wears the analyst filter's button (the brand's
     own .btn tokens, never an action or P&L colour) with a short name for the order; the native
     select over it is invisible and opens the phone's own picker, with the full names. Its focus
     shows on the button. `> .btn.pos-sort` outranks the later `.panel-hdr-tools > .btn`, which
     gives a header's own button (Log position) a full row of its own, first. */
  .panel-hdr-tools > .seg.pos-inst { flex: 1 1 auto; }
  .panel-hdr-tools > .seg.pos-inst .seg-btn { flex: 1 1 auto; padding-left: 5px; padding-right: 5px; }
  .panel-hdr-tools > .btn.pos-sort { order: 4; align-self: stretch; flex: 0 1 auto; justify-content: flex-start; position: relative; min-width: 0; max-width: 100%; }
  .pos-sort .af-label { overflow: hidden; text-overflow: ellipsis; padding-bottom: 0.1em; margin-bottom: -0.1em; }   /* room for a descender ("Expiry") inside the clip */
  .pos-sort .icon { flex: none; }
  .pos-sort select {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer; border: 0; margin: 0;
  }
  .pos-sort:has(select:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
  /* While a ticker is typed: chips (what still narrows the list) and the count, one row. */
  .pos-hdr-q .panel-hdr-tools { flex-wrap: nowrap; align-items: center; }
  .pos-hdr-q .fchips { flex: 1 1 auto; }
  .pos-hdr-q .panel-meta { margin-left: auto; }
  /* Recaps: the field's own clear replaces the full-width Clear row. */
  .rc-filterbar .rc-clear { display: none; }

  /* Events and the Watchlist: the field, Filters and (for analysts) Add event, one row; the chips
     under it. */
  .filterbar.feed-bar-m {
    display: grid; grid-auto-flow: column;
    grid-template-columns: minmax(0, 1fr); grid-auto-columns: auto;
  }
  .feed-bar-m .feed-filters-btn { gap: 7px; padding: 0 12px; white-space: nowrap; color: var(--fg-1); }
  .feed-bar-m .feed-filters-btn.on { border-color: var(--accent-dim); color: var(--fg-0); }
  .feed-filters-n { color: var(--accent); font-family: var(--f-mono); }
  .feed-bar-m .feed-add-btn { width: 2.375rem; padding: 0; justify-content: center; }
  .feed-chips { margin: -4px 0 12px; }
  /* The Watchlist's Analysts / My list switch keeps its own row above the search, in the same
     place in both views (My list's line follows it). */
  .wl-source-m { display: flex; margin-bottom: 8px; }
  .wl-source-m .seg { flex: 1 1 auto; display: flex; }
  .wl-source-m .seg-btn { flex: 1 1 0; justify-content: center; }
  .wl-mine-note { margin: 4px 0 12px; }
}


.input, .select, .btn {
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  color: var(--fg-0);
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: var(--t-md);
  /* Explicit, and deliberately tighter than the body's 1.45. A control with a pinned
     height must not inherit a body line-height it was never measured against: the text
     box then wants more room than the box has and the descenders get cut. Stating it
     here makes the height arithmetic deterministic instead of dependent on what the
     browser decides a <select> inherits. */
  line-height: 1.2;
  outline: none;
  transition: border-color 120ms, background 120ms;
}
.input:focus, .select:focus { border-color: var(--border-3); }
.input::placeholder { color: var(--fg-3); }
.input.search { padding-left: 38px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236d7787' stroke-width='2'><circle cx='11' cy='11' r='7'/><path d='m21 21-4.3-4.3'/></svg>"); background-repeat: no-repeat; background-position: 12px 50%; }
.btn { cursor: pointer; display: inline-flex; align-items: center; gap: 8px; }
/* Every hover rule on a button skips a disabled one: `:hover:where(:not(:disabled))` adds no
   specificity, so a later contextual rule still wins (the sign-in page's Discord button keeps
   its own blurple hover). `:hover:not(:disabled)` would outrank it. */
.btn:hover:where(:not(:disabled)) { background: var(--bg-2); border-color: var(--border-2); }
/* Three tiers. Primary (the action the screen is for) is the accent tint; plain .btn is
   secondary; danger is the QUIET tier, an outline in the loss red, so a Delete or a Close never
   outweighs the action beside it. Only the confirm dialog's last step is solid (.danger.solid). */
.btn.primary {
  background: var(--accent-glow);
  border-color: var(--accent-dim);
  color: var(--accent);
}
/* .22, not denser: at .28 the accent text fell under 4.5:1 on its own hover tint in dark (4.0 on
   the default theme's --bg-2, where the modal footers sit). So the tint alone is a small step (on
   Optionality's dark lime it all but vanished), and the border steps up to the accent itself. */
.btn.primary:hover:where(:not(:disabled)) { background: oklch(from var(--accent) l c h / 0.22); border-color: var(--accent); }
/* Light: the accent as text measured just under 4.5:1 on its own tint ("Add event" 4.47), and
   --accent-dim alone fell to 3.8 on the hover tint (Saucy Swings on --bg-2). A step deeper than
   --accent-dim clears 5:1 at rest and on hover on all seven light themes. :where() keeps this at
   the specificity of .btn.primary, so a button that sets its own colours still wins (the
   white-on-blurple Discord sign-in button). */
:where([data-theme="light"]) .btn.primary { color: color-mix(in oklab, var(--accent-dim) 85%, black); }
/* --c-down is never rebranded, so danger reads the same on every brand: 5.4:1 or better on every
   dark panel (4.5 on the hover tint); in light the deeper ink keeps 6.9:1 on every field. */
.btn.danger { background: transparent; border-color: color-mix(in oklab, var(--c-down) 45%, transparent); color: var(--c-down); }
:where([data-theme="light"]) .btn.danger { color: color-mix(in oklab, var(--c-down) 80%, black); }
.btn.danger:hover:where(:not(:disabled)) { background: color-mix(in oklab, var(--c-down) 10%, transparent); border-color: color-mix(in oklab, var(--c-down) 60%, transparent); }
/* The last step of a destructive confirm (confirm.jsx): solid, white on a deeper red (5.6:1 dark,
   9.1:1 light; white on the dark theme's own --c-down was 3.3). */
.btn.danger.solid { background: color-mix(in oklab, var(--c-down) 80%, black); border-color: transparent; color: #fff; }
.btn.danger.solid:hover:where(:not(:disabled)) { background: color-mix(in oklab, var(--c-down) 70%, black); border-color: transparent; }
.btn-sm { padding: 6px 10px; font-size: var(--t-sm); gap: 5px; }
.btn .icon { width: 14px; height: 14px; }
.btn.ghost { background: transparent; border-color: transparent; }
.btn.ghost:hover:where(:not(:disabled)) { background: var(--bg-2); border-color: var(--border-1); }
/* A disabled button looks off: dimmed, with a not-allowed cursor, and (above) no hover. The
   browser's own greying never showed, because .btn sets its colours. A reason it is off is said
   in words beside it (Manage, the Ask composer, the alert card): a title never shows on a phone. */
.btn:disabled, .icon-btn:disabled { opacity: .5; cursor: not-allowed; }
/* That reason: plain text, readable and calm (Manage's .manage-blocked reads the same). */
.btn-why { font-size: var(--t-sm); line-height: 1.4; color: var(--fg-1); }

/* ── Icon-only buttons (delete, options, etc.) */
.icon-btn {
  width: 30px; height: 30px;
  display: inline-grid;
  place-items: center;
  border-radius: var(--radius-sm);
  color: var(--fg-3);
  background: transparent;
  border: 1px solid transparent;
  transition: color 120ms, background 120ms, border-color 120ms;
  flex-shrink: 0;
}
.icon-btn:hover:where(:not(:disabled)) { color: var(--fg-1); background: var(--bg-3); }
/* Row actions sat on --fg-3, the dimmest foreground token, against --bg-1: legible if you
   knew where to look and easy to miss if you didn't. One step up to --fg-2 at rest, and the
   whole set lifts to --fg-1 when the row is hovered, so the row you are pointing at tells you
   what can be done to it. Nothing appears or moves on hover - only the contrast changes. */
.cell-actions .icon-btn { color: var(--fg-2); }
.tbl tbody tr:hover .cell-actions .icon-btn { color: var(--fg-1); }
.cell-actions .icon-btn:hover:where(:not(:disabled)) { background: var(--bg-3); border-color: var(--border-2); }
.cell-actions .icon-btn.danger:hover:where(:not(:disabled)) { color: var(--c-down); }
/* Button text label — hidden on desktop (icon-only), shown in mobile card footers */
.btn-label { display: none; }
.icon-btn.active { color: var(--accent); background: var(--accent-glow); }
/* Manage (admin manual update) — accent-tinted so it reads as the primary row action */
.icon-btn.manage-btn { color: var(--accent); }
.icon-btn.manage-btn:hover:where(:not(:disabled)) { color: var(--accent); background: var(--accent-glow); }
.icon-btn.danger:hover:where(:not(:disabled)) { color: var(--c-down); background: color-mix(in oklab, var(--c-down) 12%, transparent); border-color: color-mix(in oklab, var(--c-down) 30%, transparent); }
.numinput {
  display: inline-flex; align-items: center; gap: 8px;
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 6px 14px;
  font-family: var(--f-mono);
  font-size: var(--t-sm);
}
.numinput label { color: var(--fg-2); font-size: var(--t-2xs); letter-spacing: 0.14em; text-transform: uppercase; }
.numinput input { width: 50px; background: transparent; border: 0; outline: 0; color: var(--fg-0); font-family: var(--f-mono); font-size: var(--t-md); padding: 4px 0; text-align: right; }

/* ── Positions filter row: timeframe segment + min-trades stepper + reset */
.pos-filterbar { align-items: stretch; }
.pos-range { flex-shrink: 0; }
.pos-range .seg-btn { padding: 0 13px; display: inline-flex; align-items: center; }
.pos-reset { flex-shrink: 0; color: var(--fg-2); font-size: var(--t-sm); }
.pos-reset:hover { color: var(--fg-0); }

/* ── Panels */
.panel {
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.panel-hdr {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-1);
}
.panel-title {
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.panel-hdr-tools { display: flex; align-items: center; gap: 12px; }
.panel-meta {
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: var(--t-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-2);
}
/* On a computer a header whose controls do not fit beside its title wraps them, whole, onto a row
   of their own at the right, instead of squeezing: a non-wrapping row cut segment buttons ("Shar")
   and broke the title, the count and a button over two lines each. Where they fit, nothing moves.
   A phone has its own header rules (the max-width 900px block). */
@media (min-width: 901px) {
  .panel-hdr { flex-wrap: wrap; row-gap: 10px; column-gap: 12px; }
  .panel-hdr-tools { flex-wrap: wrap; justify-content: flex-end; row-gap: 8px; margin-left: auto; min-width: 0; }
  .panel-hdr-tools .seg { flex-shrink: 0; }
  .panel-hdr .seg-btn, .panel-title, .panel-meta, .panel-hdr-tools > .btn { white-space: nowrap; }
}

/* ── Tables */
.tbl { width: 100%; border-collapse: collapse; }
/* Fixed layout: columns get defined widths (via <colgroup>), the flex column
   absorbs the rest, and cells clip with ellipsis — so the table always fills
   its container instead of overflowing. */
.tbl-fixed { table-layout: fixed; }
/* The row checkbox column of Events and the Watchlist (a touch screen widens it, below). */
.tbl col.col-check { width: 3%; }
.tbl-fixed th, .tbl-fixed td { overflow: hidden; text-overflow: ellipsis; }
/* A column's name is a label, not a figure: the sans face at 500, in capitals. Poppins' capitals are
   wider than the mono's, so the tracking is lighter (0.14em in mono). */
.tbl thead th {
  text-align: left;
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: var(--t-2xs);
  letter-spacing: 0.08em;
  color: var(--fg-2);
  text-transform: uppercase;
  padding: 10px var(--pad-x);
  border-bottom: 1px solid var(--border-1);
  background: var(--bg-1);
  position: sticky;
  top: 0;
  z-index: 1;
  white-space: nowrap;
}
.tbl thead th.sortable { cursor: pointer; user-select: none; }
.tbl thead th.sortable:hover { color: var(--fg-1); }
.tbl thead th .sort-ind {
  display: inline-block;
  margin-left: 4px;
  opacity: 0.5;
}
/* A sortable head keeps its arrow when its label is cut short: the label ellipsises in its own span
   and leaves the arrow room ("Last analyst update" lost the arrow showing the list's own order). */
.tbl thead th .th-l { display: inline-block; max-width: calc(100% - 1.4em); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
.tbl thead th.active .sort-ind { color: var(--accent); opacity: 1; }
.tbl tbody td {
  padding: var(--pad-y) var(--pad-x);
  border-bottom: 1px solid var(--border-1);
  font-size: var(--t-md);
  vertical-align: middle;
  height: var(--row-h);
}
.tbl tbody tr { transition: background 120ms; }
.tbl tbody tr:hover { background: var(--bg-2); }
.tbl tbody tr.expanded { background: var(--bg-2); }
.tbl tbody tr.expanded + tr.expand-row > td { background: var(--bg-2); }

.tbl tbody tr.row-selected,
.tbl tbody tr.row-selected:hover { background: var(--accent-glow); }

/* ── Bulk action bar (multi-select) */
.bulk-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px;
  background: var(--accent-glow);
  border: 1px solid var(--accent-dim);
  border-radius: var(--radius);
  font-size: var(--t-md); color: var(--fg-0);
}
.bulk-bar strong { color: var(--accent); }
/* "…, including 6,470 not shown": Select all reaches rows the long list has not drawn yet. */
.bulk-bar .bulk-note { color: var(--fg-1); }

/* ── Recycle bin (Events / Watchlist "Deleted" view) ── */
.trash-wrap { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; }
.trash-note { padding: 24px 14px; font-size: var(--t-md); }
.trash-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 2px 8px; font-size: var(--t-md);
  border-bottom: 1px solid var(--border-1); margin-bottom: 4px;
}
.trash-row {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  background: var(--bg-1); border: 1px solid var(--border-1);
  border-radius: var(--radius);
}
.trash-row-main { min-width: 0; flex: 1; }
.trash-label {
  font-size: 0.84375rem; color: var(--fg-0); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.trash-sub { font-size: var(--t-sm); color: var(--fg-3); margin-top: 5px; }
.trash-row-acts { display: flex; gap: 6px; flex-shrink: 0; }
/* Pick rows to restore together: a checkbox per row, Select all in the bar. */
.trash-cb { flex: none; width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; }
/* Its tap area: 32 px around the box (44 px on a touch screen), reaching into the row's
   padding and gap through the negative margin, so the row itself does not move. */
.trash-pick { flex: none; display: grid; place-items: center; width: 32px; height: 32px; margin: -8px; cursor: pointer; }
@media (pointer: coarse) { .trash-pick { width: 44px; height: 44px; margin: -14px; } }
.trash-all { display: inline-flex; align-items: center; gap: 6px; color: var(--fg-1); cursor: pointer; white-space: nowrap; }
.trash-all input { accent-color: var(--accent); }
.trash-row.is-picked { border-color: var(--border-3); background: var(--bg-2); }
@media (max-width: 900px) { .trash-bar { flex-wrap: wrap; } }

/* ── To-Do tab (reconciliation + parse-quality queue) ──
   Deliberately the same restrained card-list as the recycle bin: a flat stack of
   rows, one accent-bearing kind pill, actions on the right. No status dashboard. */
.todo-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.todo-wrap { display: flex; flex-direction: column; gap: 14px; padding: 12px 14px; }
.todo-group { display: flex; flex-direction: column; gap: 8px; }
.todo-group-head {
  display: flex; align-items: baseline; gap: 8px;
  font-size: var(--t-sm); letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-3); padding-bottom: 6px; border-bottom: 1px solid var(--border-1);
}
.todo-row {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 12px;
  --todo-bg: var(--bg-1);
  background: var(--todo-bg); border: 1px solid var(--border-1);
  border-radius: var(--radius);
  box-shadow: inset 3px 0 0 var(--todo-tone, var(--border-3));
}
/* Status reads off the card itself — a left stripe plus a faint wash — so a
   glance down the list separates settled from outstanding without reading a
   word. Open rows take the stripe from their KIND (severity); settled rows
   override it with green/grey and recede. Status rules must stay AFTER the
   tone rules: equal specificity, so source order decides the winner. */
.todo-row.tone-warn { --todo-tone: var(--c-watch); }
.todo-row.tone-bad  { --todo-tone: var(--c-down); }
.todo-row.tone-act  { --todo-tone: var(--c-up); }
.todo-row.td-resolved {
  --todo-tone: var(--c-up);
  --todo-bg: color-mix(in oklab, var(--c-up) 7%, var(--bg-1));
  border-color: color-mix(in oklab, var(--c-up) 24%, var(--border-1));
}
.todo-row.td-dismissed { --todo-tone: var(--fg-3); opacity: .68; }
.todo-row.td-resolved .todo-detail,
.todo-row.td-dismissed .todo-detail { color: var(--fg-2); }
.todo-row-main { min-width: 0; flex: 1; }
.todo-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.todo-contract { font-size: var(--t-sm); color: var(--fg-2); }
.todo-detail { font-size: var(--t-md); color: var(--fg-1); margin-top: 6px; line-height: 1.45; }
.todo-sub { font-size: var(--t-sm); color: var(--fg-3); margin-top: 6px; }
/* Done stays bordered; Ask and Find are ghosts with their icon, Dismiss a ghost word: four
   identical boxes on every row read as four equal choices. */
.todo-row-acts { display: flex; gap: 6px; flex-shrink: 0; }
/* Kind pill — tone carries the meaning, never the brand accent (semantics stay
   readable across white-label brands, same rule as the action pills). */
.todo-kind {
  font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .04em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 999px;
  border: 1px solid currentColor; white-space: nowrap;
}
.tk-warn { color: var(--c-watch, #d9a441); }
.tk-bad  { color: var(--c-down, #e2555b); }
.tk-act  { color: var(--c-up, #3fbf7f); }
/* Settled state, spelled out — the stripe says it at a glance, this says it
   exactly (colour alone must never be the only carrier of meaning). */
.todo-status {
  font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .05em;
  text-transform: uppercase; padding: 1px 6px; border-radius: 999px;
  border: 1px solid currentColor; white-space: nowrap;
}
.ts-resolved  { color: var(--c-up); }
.ts-dismissed { color: var(--fg-3); }
@media (max-width: 640px) {
  .todo-row { flex-direction: column; align-items: stretch; gap: 8px; }
  /* Ask, Find, Done and Dismiss share one line on a phone at the default text size: the ghosts
     need no 14 px of padding to look like a button, having no box. A row with Approve too, or
     Larger text, still wraps. */
  .todo-row-acts { justify-content: flex-end; flex-wrap: wrap; gap: 4px; }
  .todo-row-acts .btn { padding-left: 12px; padding-right: 12px; }
  .todo-row-acts .btn.ghost { padding-left: 8px; padding-right: 8px; gap: 5px; }
}
/* A 360 or 375 px phone (most Androids, an iPhone mini or SE): the row is 278 px there, and the
   four took 301 at the padding above, so Dismiss sat alone on a second line. */
@media (max-width: 380px) {
  .todo-row-acts .btn { padding-left: 10px; padding-right: 10px; }
  .todo-row-acts .btn.ghost { padding-left: 4px; padding-right: 4px; gap: 4px; }
}

/* ── Questions to analysts (To-Do tab, owner only). Same restrained card stack as the
   to-dos: a status stripe, one mono pill, the thread in plain blocks (the question and
   follow-ups muted, their replies marked by a left rule), actions at the foot. */
.q-viewseg { margin: 12px 14px 0; }
.q-pill, .q-chip {
  font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .05em;
  text-transform: uppercase; padding: 1px 7px; border-radius: 999px;
  border: 1px solid currentColor; white-space: nowrap;
}
.qp-act  { color: var(--c-up); }
.qp-warn { color: var(--c-watch, #d9a441); }
.qp-bad  { color: var(--c-down); }
.qp-done { color: var(--fg-3); }
.q-chip {
  display: inline-flex; align-items: center; gap: 5px; margin-top: 7px;
  background: transparent; cursor: pointer;
}
.q-chip:hover, .q-chip:focus-visible { background: color-mix(in oklab, currentColor 10%, transparent); }
.q-card {
  display: flex; flex-direction: column; gap: 8px;
  padding: 10px 12px; background: var(--bg-1); border: 1px solid var(--border-1);
  border-radius: var(--radius); box-shadow: inset 3px 0 0 var(--q-tone, var(--border-3));
}
.q-card.qc-act  { --q-tone: var(--c-up); }
.q-card.qc-warn { --q-tone: var(--c-watch); }
.q-card.qc-bad  { --q-tone: var(--c-down); }
.q-card.qc-done { --q-tone: var(--fg-3); }
.q-card.focused { border-color: color-mix(in oklab, var(--accent) 55%, var(--border-1)); }
.q-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.q-subject { font-weight: 600; color: var(--fg-0); font-size: 0.84375rem; }
.q-where { font-size: var(--t-sm); }
.q-meta { font-size: var(--t-xs); color: var(--fg-3); }
.q-msg {
  font-size: var(--t-md); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere;
  padding: 8px 10px; border-radius: var(--radius); display: flex; flex-direction: column; gap: 3px;
}
.q-mine { background: var(--bg-2); color: var(--fg-1); }
.q-theirs { border-left: 2px solid var(--c-up); background: color-mix(in oklab, var(--c-up) 6%, var(--bg-1)); color: var(--fg-0); }
.q-fu { font-size: var(--t-md); color: var(--fg-2); }
.q-msg-when { font-size: var(--t-xs); color: var(--fg-3); white-space: normal; }
.q-att { font-size: var(--t-sm); color: var(--accent); overflow-wrap: anywhere; }
.q-note { font-size: var(--t-sm); }
.q-err { font-size: var(--t-md); color: var(--c-down); line-height: 1.45; }
.q-more { align-self: flex-start; background: none; border: 0; padding: 0; color: var(--accent); font-size: var(--t-sm); cursor: pointer; }
.q-acts { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
.q-fu-box { display: flex; flex-direction: column; gap: 6px; }
.q-fu-acts { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.q-fu-acts .q-count { margin-right: auto; }
.q-msg-flag { font-size: var(--t-xs); color: var(--fg-3); font-style: italic; }
.q-msg.q-gone { opacity: .62; }
.q-msg.q-gone .q-msg-body { text-decoration: line-through; }
/* A plain message sent while several questions were open: shown under the one it was filed with,
   and under the others as "possibly about this", each with a way to move it. */
.q-maybe { display: flex; flex-direction: column; gap: 6px; padding-top: 2px; }
.q-maybe-head { font-size: var(--t-xs); color: var(--fg-3); }
.q-maybe .q-msg { border-left: 2px dashed var(--border-3); background: var(--bg-1); color: var(--fg-1); }
.q-maybe-acts { display: flex; justify-content: flex-end; }
.q-open-with { font-size: var(--t-sm); line-height: 1.45; color: var(--fg-2); padding: 8px 10px; border-radius: var(--radius); background: var(--bg-2); }
/* The composer */
.q-modal { max-width: 560px; }
.q-modal-body { padding: 10px 20px 16px; display: flex; flex-direction: column; gap: 10px; }
.q-pick { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.q-field { display: flex; flex-direction: column; gap: 4px; font-size: var(--t-sm); color: var(--fg-2); }
.q-field .input, .q-field select { width: 100%; }
.q-text { font-family: inherit; font-size: var(--t-md); line-height: 1.5; resize: vertical; min-height: 96px; }
.q-to { font-size: var(--t-md); color: var(--fg-1); }
.q-earlier, .q-hint, .q-loading { font-size: var(--t-sm); line-height: 1.45; }
.q-count { font-size: var(--t-xs); color: var(--fg-3); text-align: right; }
.q-count.over { color: var(--c-down); }
/* Why Send is off: at the start of the footer, beside the buttons it explains. Access's user
   editor says why its Save is off the same way (no server picked). */
.q-modal .modal-actions, .user-modal .modal-actions { align-items: center; }
.q-send-why, .user-edit-why { margin-right: auto; min-width: 0; }
@media (max-width: 640px) {
  .q-pick { grid-template-columns: 1fr; }
  /* A phone: the reason takes its own line over the buttons. */
  .q-modal .modal-actions, .user-modal .modal-actions { flex-wrap: wrap; }
  .q-send-why, .user-edit-why { flex: 1 1 100%; }
}

/* Mobile-only "Select all" bar (the table header checkbox is hidden in card mode) */
.cards-selectall { display: none; }

.tbl td.mono { font-family: var(--f-mono); }
.tbl td.muted { color: var(--fg-2); }
.tbl td.dim { color: var(--fg-3); }
.tbl td.num { text-align: right; font-family: var(--f-mono); }

.ticker {
  font-family: var(--f-mono);
  font-weight: 600;
  color: var(--c-ticker);
  font-size: var(--t-md);
}
.contract {
  font-family: var(--f-mono);
  font-size: var(--t-sm);
  color: var(--fg-1);
}

/* ── Instrument (shares vs option) cell */
.inst { display: inline-flex; align-items: center; gap: 8px; font-family: var(--f-mono); font-size: var(--t-sm); white-space: nowrap; }
.inst-tag {
  font-size: 0.59375rem; font-weight: 600; letter-spacing: 0.08em;
  padding: 2px 6px; border-radius: 4px;
  border: 1px solid currentColor;
  background: color-mix(in oklab, currentColor 12%, transparent);
}
.inst-tag.call   { color: var(--c-up); }
.inst-tag.put    { color: var(--c-down); }
.inst-tag.opt    { color: var(--accent); }
.inst-tag.shares { color: var(--fg-2); }
.inst-detail { color: var(--fg-1); }

/* ── Segmented control (reusable) */
.seg { display: inline-flex; border: 1px solid var(--border-1); border-radius: var(--radius); overflow: hidden; }
/* ONE rule for every segment button. There used to be two: this one, and a later one written for the
   Access role filter that silently won everywhere (padding, radius, capitalisation, the .on look).
   This is the look that rule gave every segment, now in one place; the Access filter's own frame is
   .seg-filter, below. Its words are in the sans face, its count (.seg-count) in the mono. A button's
   line height is its font's own ("normal"), and Poppins' is taller than the mono's (a segment whose
   height nothing pins grew 3px), so it is stated: 1.265 of 0.75rem is the 1.32 of 0.71875rem the mono
   gave, so a segment keeps its height. */
.seg-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px; border-radius: 5px;
  font-family: var(--f-sans); font-weight: 500; font-size: var(--t-sm); letter-spacing: 0.01em; text-transform: capitalize;
  line-height: 1.265;
  color: var(--fg-2); background: var(--bg-1);
  border-right: 1px solid var(--border-1);
  transition: color 120ms, background 120ms;
}
.seg-btn:last-child { border-right: 0; }
.seg-btn:hover { color: var(--fg-0); }
/* The choice in use: more than a colour (a hue step alone measured 1.3:1 against its neighbours, and
   grey on grey on WarRoom). A 2px underline in its own colour, the motif of the active Settings tab,
   and it takes up no room, so no segment changes width. In Manage's action picker the inline action
   colour is currentColor, so the underline takes it. Never a weight change: once segments are set in
   the sans face, bold would reflow a content-sized strip on every click. aria-pressed says it to a
   screen reader. */
.seg-btn.on { background: var(--bg-0); color: var(--accent); box-shadow: inset 0 -2px 0 currentColor, var(--shadow-1); }
/* --fg-1: the pill is --bg-3, where --fg-3 measured 4.0:1 (dark) and --fg-2 4.0 on a brand's light field.
   A count is a figure, so it stays in the mono face inside the sans button. */
.seg-count {
  font-family: var(--f-mono);
  font-size: var(--t-2xs); min-width: 16px; text-align: center;
  padding: 1px 5px; border-radius: 99px;
  background: var(--bg-3); color: var(--fg-1);
}
.seg-btn.on .seg-count { background: var(--accent-glow); color: var(--accent); }

/* ── Analyst filter dropdown (reusable across feeds) */
.analyst-filter { position: relative; display: inline-block; }
/* A menu's label, like the server picker beside it: the sans face. */
.af-btn { padding: 8px 12px; display: inline-flex; align-items: center; gap: 7px; font-family: var(--f-sans); font-weight: 500; font-size: var(--t-sm); color: var(--fg-1); }
.af-label { white-space: nowrap; }
.af-menu {
  /* Desktop: anchor to the button's right (it usually sits right-aligned).
     Mobile overrides to left:0 (the trigger sits on the left there). Width is
     capped so it can never spill off either edge. */
  position: absolute; top: calc(100% + 4px); right: 0; left: auto; z-index: 60;
  min-width: 220px; max-width: min(300px, calc(100vw - 28px)); max-height: 320px; overflow-y: auto;
  background: var(--bg-1); border: 1px solid var(--border-2);
  border-radius: var(--radius); box-shadow: var(--shadow-2); padding: 4px;
}
.af-menu-hd {
  display: flex; justify-content: space-between; align-items: center;
  padding: 6px 8px 8px; font-family: var(--f-sans); font-weight: 500; font-size: var(--t-2xs);
  letter-spacing: .06em; text-transform: uppercase; color: var(--fg-2);
}
.af-menu-hd button { color: var(--accent); font-size: var(--t-2xs); letter-spacing: .06em; }
.af-opt {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 8px; border-radius: 7px; font-size: var(--t-md); color: var(--fg-1);
  text-align: left;
}
.af-opt:hover { background: var(--bg-2); }
.af-opt.on { color: var(--fg-0); }
.af-check { width: 14px; display: inline-flex; align-items: center; justify-content: center; color: var(--accent); flex-shrink: 0; }

/* ── Pills / badges */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--f-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.10em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 99px;
  border: 1px solid currentColor;
  background: color-mix(in oklab, currentColor 12%, transparent);
}
.pill.open    { color: var(--c-open); }
.pill.add     { color: var(--c-add); }
.pill.trim    { color: var(--c-trim); }
.pill.cut     { color: var(--c-cut); }
.pill.close   { color: var(--c-close); }
.pill.hold    { color: var(--c-hold); }
.pill.watch   { color: var(--c-watch); }
.pill.expire  { color: var(--c-expire); }
.pill.high    { color: var(--conf-high); }
.pill.medium  { color: var(--conf-medium); }
.pill.low     { color: var(--conf-low); }
/* Light theme: a self-tinted chip is OPAQUE, its tint mixed onto the card surface
   (--bg-1) instead of a transparent wash over whatever row is behind it, so its text
   keeps 4.5:1 on hovered, selected and expanded rows too. Dark keeps the transparent
   tint. The DTE, roster, recap-publish and category chips follow below. */
[data-theme="light"] .pill,
[data-theme="light"] .inst-tag:not(.sold),
[data-theme="light"] .wl-kind:not(.analysis) { background: color-mix(in oklab, currentColor 10%, var(--bg-1)); }

.dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 99px;
  background: currentColor;
  margin-right: 6px;
  vertical-align: middle;
}

/* ── Analyst chip */
/* A name, not a figure: the sans face, untracked. */
.analyst {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--f-sans);
  font-weight: 500;
  font-size: var(--t-sm);
  color: var(--fg-0);
}
.analyst .swatch {
  width: 8px; height: 8px;
  flex-shrink: 0;   /* a name that wraps must never squash the dot into a bar */
  border-radius: 99px;
  background: var(--col, var(--accent));
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--col, var(--accent)) 18%, transparent);
}
/* Analyst avatar — ringed in that analyst's colour so the colour-coding that
   ties rows together survives, and the face reads as identity rather than
   decoration. Falls back to the plain swatch when no picture resolves. */
.analyst .analyst-pic {
  box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--col, var(--accent)) 70%, transparent);
  background: var(--bg-2);
  flex-shrink: 0;
}
/* A picture is a wider object than the 8px dot — tighten the gap so the name
   still reads as one unit with it rather than drifting away. */
.analyst:has(.analyst-pic) { gap: 7px; }
/* The handle (shared.jsx AnalystHandle). In a card a name wraps, and a sub-account book's label
   ("Ansh Stocks") sits on its own line under the trader's name, small, in the neutral --fg-2 (never
   a brand or semantic colour), so a trader's two books never read alike. */
.an-handle { min-width: 0; }
/* Outside a table or a card (Recaps, Charts, a profile) a book reads on one line as it did before
   2.8.0, "CHARAN · 20k Challenge", its label in the book style. The two-line layouts below are for
   phone cards and desktop table cells only: as the base rule they stacked every book in Recaps'
   leaderboard and group headers and in Charts. */
.an-book { display: inline; }
.an-book-l { font-size: var(--t-2xs); font-weight: 500; letter-spacing: 0.04em; color: var(--fg-2); }
.an-book-l::before { content: "\00a0·\00a0"; }
/* A computer's tables keep a handle to TWO lines at most: a longer name ends in "…" (the whole of
   it is the chip's title), so rows stay one even height (a name ran to five lines, and a row to
   123 px; the rows beside it are two lines tall anyway: a mark under its fill, a gap under its
   market price). A book's label trails the trader's name where both fit on one line, and takes the
   second line where they don't, so neither is cut down to nothing ("… 20k Challenge" was CHARAN's
   book): the trader's name ends in "…" only when it alone is wider than the column, the label only
   as a last resort, on its own line. */
@media (min-width: 901px) {
  .tbl-fixed td .analyst { max-width: 100%; min-width: 0; vertical-align: middle; }
  .tbl-fixed td .an-handle:not(.an-book) { min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
  .tbl-fixed td .an-book { display: inline-flex; flex-direction: row; flex-wrap: wrap; align-items: baseline; column-gap: 6px; row-gap: 1px; min-width: 0; max-width: 100%; }
  .tbl-fixed td .an-book-l::before { content: none; }
  .tbl-fixed td .an-trader, .tbl-fixed td .an-book-l { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
/* Phone cards: a name stops at two lines (it ran to five); a book's trader line ends in "…" and its
   label keeps the line under it. */
.mpos .an-handle:not(.an-book) { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
.mpos .an-book { display: inline-flex; flex-direction: column; align-items: flex-start; max-width: 100%; }
.mpos .an-book-l::before { content: none; }
.mpos .an-trader { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Splits */
.split { display: grid; gap: 14px; }
.split.cols { grid-template-columns: 1fr 420px; }
.split.cols-narrow { grid-template-columns: 280px 1fr; }

/* Charts page fills the viewport like the positions split; each region scrolls
   inside its own box so the panels extend into the available vertical space. */
.split.cols-charts {
  grid-template-columns: 280px 1fr;
  grid-template-rows: minmax(0, 1fr);
  flex: 1 1 auto; min-height: 0;
}
.split.cols-charts > .panel { min-height: 0; }
.charts-right { display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 14px; min-height: 0; }
/* Focused charts view: ticker list + one big chart that fills its column. */
.charts-side { display: flex; flex-direction: column; }
.charts-tk-search { padding: 10px 12px; border-bottom: 1px solid var(--border-1); }
.charts-tk-search .input.search { width: 100%; padding: 8px 14px 8px 38px; }
.charts-right-solo { grid-template-rows: minmax(0, 1fr); }
.chart-wrap-fill { height: 100%; min-height: 0; box-sizing: border-box; }
/* In fill mode the rendered svg height must track the (responsive) viewBox
   height, else preserveAspectRatio letterboxes + centers the chart. */
.chart-wrap-fill .chart-svg { height: auto; }
.charts-subgrid > .panel { min-height: 0; }
.charts-sources { max-height: 280px; }
.charts-fill { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.charts-fill-cap { flex: 1 1 auto; min-height: 140px; overflow-y: auto; }

/* ── Event rail card */
.event {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 4px 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-1);
  cursor: pointer;
  transition: background 120ms;
}
.event:hover { background: var(--bg-2); }
/* The name in its analyst's colour (--col, positions.jsx), a third of the way to --fg-0: the identity
   slots are tuned for 3:1 as a dot, and as this small name the raw slot measured 4.0:1 (the plum, a
   hovered row, dark) to 3.1:1 (the sky, light). Mixed, every slot reads 5.5:1 or better on every brand's
   --bg-1 and --bg-2, dark and light, and keeps its hue. */
.event .e-analyst { font-family: var(--f-sans); font-weight: 500; font-size: var(--t-xs); color: color-mix(in oklab, var(--col, var(--fg-1)) 65%, var(--fg-0)); }
.event .e-time   { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-2); justify-self: end; }
.event .e-ticker { font-family: var(--f-mono); font-weight: 600; color: var(--c-ticker); font-size: var(--t-base); }
.event .e-pill   { justify-self: end; }
.event .e-note   { grid-column: 1 / -1; color: var(--fg-1); font-size: var(--t-sm); margin-top: 4px; }
.event.slide-in { animation: slideIn 380ms cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes slideIn {
  from { opacity: 0; transform: translateY(-8px); background: var(--accent-glow); }
  to   { opacity: 1; transform: translateY(0); }
}
/* Fresh event in the rail — a quiet left accent so "new" reads without shouting. */
.event.event-fresh { box-shadow: inset 2px 0 0 var(--accent); }

/* ── Recency cues — the at-a-glance "new vs old" language, shared by the
   Positions, Events and rail feeds. Subtle on purpose: a "NEW" tag for the
   last ~2h, a softly tinted relative time, and a quiet left accent on the row. */
.rcy-new {
  display: inline-block;
  font-family: var(--f-mono);
  font-size: 0.53125rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-glow);
  border: 1px solid var(--accent-dim);
  border-radius: 4px;
  padding: 1px 4px;
  margin-right: 6px;
  vertical-align: middle;
  line-height: 1.35;
}
.rcy-live  { color: var(--accent); }
.rcy-today { color: var(--fg-1); }
/* The Events feed's time: one line ("Tue 4:51 AM"), with the NEW tag on its own line above it,
   so a fresh row is no taller than the analyst beside it and the stamp is never broken in two. */
.tbl td.ev-time { white-space: nowrap; }
.ev-time .rcy-new { display: table; margin: 0 0 3px; }

/* ── The Events table's columns (events.jsx colgroup). Identity and figures in rem, so they grow
   with the text size, measured against the dense cells every computer gets for this table (7px
   padding, 0.71875rem; the 901px+ rules at the end of this file). Each holds the longest value in
   the whole book (6,887 events, 2026-10-16) at the SMALL size, the tightest one (a cell's 14px of
   padding doesn't grow with the text): Time "Fri 12:59 AM", Ticker "$MAYBANK", Instrument its own
   header (SOLD and CALL stack under each other), Expiry "Oct 15 2026", Strike "1127.5", Stop
   "350.00", Price a stated "→ 1715.00" (61.5px at Small in a 62.8px box) and the tags under a price
   (CORRECTED and STOP FILL 61px; the market tag reads MKT FILL in this table, 55px, where MARKET
   FILL's 73px would not fit), Mkt "1718.95", Action EXPIRE, Conf MEDIUM. Analyst gives a name two
   lines (a book's label takes the second where it doesn't fit beside the trader). Channel is the flex
   column and keeps 4rem; actions is --act-w, as many 30px buttons as a row of yours can carry and the
   cell's padding: three for the owner (104px; 130px for a touch screen's 36px buttons), two for a
   moderator or an analyst with a book of their own (74px; 86px), none for an analyst with no row of
   their own (no column). Where they don't fit, the panel scrolls sideways (.feed-scroll), the row
   buttons with it: they are the last column, so nothing ever sits over a value, and on a narrow screen
   they are what you scroll to. Measured with the app's 10px scrollbars, the owner's table (select
   column on) fits from 1344px wide at Small, 1458px at the default size and 1572px at Larger (with a
   touch screen's buttons 1372, 1486 and 1600px); a moderator's from 1314, 1428 and 1542px (1328, 1442,
   1556), an analyst's with a book of their own from 1280, 1394 and 1508px (1292, 1406, 1520), one with
   none from 1206, 1320 and 1434px. At 1366px and the default size only the buttons are out of view
   until it is scrolled (the owner's 92px of them), at 1280px Channel too.
   From 1720px a wide screen gives the names and the market gap more room. */
.ev-tbl { --chk-w: 0px; --act-w: 104px; }
.ev-tbl.ev-sel { --chk-w: 34px; }   /* the select column is drawn only for a moderator (events.jsx) */
/* As many buttons as a row of yours can carry (events.jsx rowActs): Parser is the owner's alone, so a
   moderator's rows have two (74px), and an analyst with no row of their own in view has no column. */
.ev-tbl.ev-act-2 { --act-w: 74px; }
.ev-tbl.ev-act-1 { --act-w: 44px; }
.ev-tbl.ev-act-0 { --act-w: 0px; }
.ev-tbl col.col-check { width: var(--chk-w); }   /* the 13px box doesn't grow with the text */
.ev-tbl col.col-actions { width: var(--act-w); }
.ev-tbl col.evc-time { width: 6.1rem; }
.ev-tbl col.evc-an { width: 7rem; }
.ev-tbl col.evc-tk { width: 4.5rem; }
.ev-tbl col.evc-inst { width: 4.6rem; }
.ev-tbl col.evc-exp { width: 5.8rem; }
.ev-tbl col.evc-strike { width: 3.5rem; }
.ev-tbl col.evc-stop { width: 3.5rem; }
.ev-tbl col.evc-price { width: 4.8rem; }
.ev-tbl col.evc-mkt { width: 3.9rem; }
.ev-tbl col.evc-act { width: 4.7rem; }
.ev-tbl col.evc-conf { width: 4.7rem; }
/* The gap between the market price and the stated fill ("+1.2%", events.jsx EventMktPrice): muted,
   never a P&L colour. Beside the price on a phone card. In the table it follows the price where the
   column holds both (a wide screen) and drops under it where it doesn't, like the stated mark under
   the fill, so the column stays narrow; a gap of four figures ends in "…" (the price's title has it). */
.ev-gap { color: var(--fg-3); margin-left: 4px; font-size: var(--t-xs); }
.ev-tbl .ev-mkt-v { display: inline-flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; column-gap: 4px; row-gap: 2px; max-width: 100%; vertical-align: middle; }
.ev-tbl .ev-gap { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* A stated mark on its own line under the fill; a 0DTE chip under the date; a SOLD contract's two
   tags side by side where they fit, else one under the other (never cut). */
.ev-tbl .ev-mark { display: block; margin-top: 2px; color: var(--fg-2); }
.ev-tbl .ev-exp .dte-chip { display: table; margin: 3px 0 0; }
.ev-tbl td .inst { max-width: 100%; flex-wrap: wrap; row-gap: 3px; column-gap: 4px; }
.ev-tbl td .inst .inst-tag.sold { margin-right: 0; }
/* The price's tags (corrected, mkt fill...) a step tighter than the timeline's, each one whole on one
   line and inside the column at every text size: the widest, CORRECTED and STOP FILL, are 61 / 67.6 /
   74.3px in 62.8 / 72.4 / 82px boxes. The market tag reads MKT FILL here (events.jsx EventPriceTags,
   its title unchanged): MARKET FILL, the timeline's label, is 73 to 89px, ran past the price above it
   into the cell's padding, and a column wide enough for it pushed Action and Conf further out of
   view on a laptop. Two tags rarely fit side by side, so the second goes under the first: a gap
   spaces them, not a margin, so the one underneath ends at the same right edge as the price and the
   tag above it. */
.ev-tbl .ev-price .ev-tags { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 2px 3px; }
.ev-tbl .ev-price .ev-tags .manual-tag { margin: 0; padding: 0 3px; letter-spacing: 0.02em; white-space: nowrap; }
/* The row checkbox's cell never shows an ellipsis (its box overflowed by a pixel and drew "."). */
.tbl-fixed td.cell-check, .tbl-fixed thead th:first-child:has(> input) { text-overflow: clip; }
@media (min-width: 901px) {
  /* the rem columns, Channel's 4rem, the check box and the actions */
  .ev-tbl { min-width: calc(57.1rem + var(--chk-w) + var(--act-w)); }
}
/* A wide screen: the names get some of the room Channel would otherwise take, and a market gap fits
   beside its price rather than a line under it. From 1720px (the 16-inch MacBook Pro's 1728px screen
   included) the table fits whole at every text size, with a touch screen's buttons too (at Larger,
   1424px in a 1428px view at 1720px). */
@media (min-width: 1720px) {
  .ev-tbl col.evc-an { width: 10rem; }
  .ev-tbl col.evc-mkt { width: 6.7rem; }
  .ev-tbl { min-width: calc(62.9rem + var(--chk-w) + var(--act-w)); }
}
.tbl tbody tr.row-fresh > td { background: color-mix(in oklab, var(--accent) 5%, transparent); }
.tbl tbody tr.row-fresh > td:first-child { box-shadow: inset 2px 0 0 var(--accent); }
.tbl tbody tr.row-fresh:hover > td { background: color-mix(in oklab, var(--accent) 9%, transparent); }
.mpos.row-fresh { box-shadow: inset 2px 0 0 var(--accent); }

/* Notification deep-link landing: briefly flash the row you were sent to, so the
   eye finds it after the scroll. One pass, then it settles — never a persistent
   highlight competing with row-fresh / selection. */
@keyframes rowFlash {
  0%   { background: var(--accent-glow); }
  15%  { background: var(--accent-glow); }
  100% { background: transparent; }
}
@keyframes rowFlashBar {
  0%, 70% { box-shadow: inset 3px 0 0 var(--accent); }
  100%    { box-shadow: inset 0 0 0 transparent; }
}
.tbl tbody tr.row-flash > td { animation: rowFlash 2.4s ease-out; }
.tbl tbody tr.row-flash > td:first-child { animation: rowFlash 2.4s ease-out, rowFlashBar 2.4s ease-out; }
.mpos.row-flash { animation: rowFlashBar 2.4s ease-out; }
/* The same landing on a recap line or a recap day. A line the landing opened fades back to its
   open colour, so it never ends clear and then snaps to it. */
@keyframes rowFlashOpen {
  0%, 15% { background: color-mix(in oklab, var(--accent) 14%, var(--bg-2)); }
  100%    { background: var(--bg-2); }
}
.recap-line.row-flash { animation: rowFlash 2.4s ease-out, rowFlashBar 2.4s ease-out; }
.recap-line.open.row-flash { animation: rowFlashOpen 2.4s ease-out, rowFlashBar 2.4s ease-out; }
.recap-period.row-flash { animation: rowFlashBar 2.4s ease-out; }
/* A to-do card keeps its own colour and its severity stripe through the landing: the card is
   washed with the accent and fades back to itself, and a ring marks it. */
@keyframes todoFlash {
  0%, 15% { background-color: color-mix(in oklab, var(--accent) 14%, var(--todo-bg)); outline-color: var(--accent); }
  70%     { outline-color: var(--accent); }
  100%    { background-color: var(--todo-bg); outline-color: transparent; }
}
.todo-row.row-flash { outline: 2px solid transparent; outline-offset: -1px; animation: todoFlash 2.4s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .tbl tbody tr.row-flash > td, .tbl tbody tr.row-flash > td:first-child, .mpos.row-flash,
  .recap-line.row-flash, .recap-line.open.row-flash, .todo-row.row-flash, .recap-period.row-flash { animation: none; outline: 2px solid var(--accent); outline-offset: -2px; }
}
/* What an opened row shows (the Events message, a Watchlist post, a position's detail and timeline)
   spans the whole table, so where the table scrolls sideways its lines ran on past the panel's edge,
   and a position's Override buttons sat out of sight. It keeps to the part of the feed in view
   instead (--feed-w, the scroller's own width without its scrollbar: shared.jsx useFeedWidthRef) and
   stays there while the columns above it scroll. Until that width is known, it is as wide as the row. */
@media (min-width: 901px) {
  /* The cell must not clip (.tbl-fixed's overflow: hidden): a clipping cell would be the box the
     content sticks inside, and it would scroll away with the table. */
  .ev-tbl tbody tr.wl-raw-row > td, .wl-tbl tbody tr.wl-raw-row > td, .pos-tbl tbody tr.expand-row > td { overflow: visible; }
  .ev-tbl tr.wl-raw-row .wl-raw, .wl-tbl tr.wl-raw-row :is(.wl-raw, .wl-raw-title) { position: sticky; left: var(--pad-x); box-sizing: border-box; width: calc(var(--feed-w) - 2 * var(--pad-x)); }
  .pos-tbl tr.expand-row .position-detail, .pos-tbl tr.expand-row .parser-detail { position: sticky; left: 0; box-sizing: border-box; width: var(--feed-w); }
  /* The owner's parser panel under it keeps to the same width. Its two parts, the message with the
     parser's reasoning and the extracted JSON, sit side by side only where the JSON fits whole beside
     at least 12rem of the message; in a narrower view the JSON goes under the message (as two grid
     columns it ran up to 400px past the panel's edge from 901 to 1280px, out of reach). A JSON line
     wider than the view scrolls inside its own box, and a long word in the message wraps rather than
     leaving its box. */
  .pos-tbl tr.expand-row .parser-detail { display: flex; flex-wrap: wrap; }
  .pos-tbl tr.expand-row .parser-detail > div:first-child { flex: 1 1 12rem; min-width: 0; }
  .pos-tbl tr.expand-row .parser-detail > div:last-child { flex: 0 1 auto; min-width: 0; }
  .pos-tbl tr.expand-row .parser-detail :is(.raw-msg, .reasoning, .meta-row > span) { overflow-wrap: anywhere; }
  /* A day's heading likewise keeps its date in view (its cell must not clip either). It is one line
     and never wider than the view: wider than the part of the row left to scroll, it could stick
     only part of the way and lost the start of its date (the Watchlist at 901px, Larger text), and
     on two lines it showed under the next day's heading when both were stuck to the top. Where the
     view is that narrow, the count after the date ends in "…". Stuck to the top it still covers the
     column heads, as it always has. */
  .ev-tbl.tbl tbody tr.day-sep > td, .wl-tbl.tbl tbody tr.day-sep > td { overflow: visible; }
  .ev-tbl tr.day-sep .day-sep-in, .wl-tbl tr.day-sep .day-sep-in { display: inline-block; position: sticky; left: var(--pad-x); max-width: calc(var(--feed-w) - 2 * var(--pad-x)); vertical-align: top; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* ── Positions: P&L as the visual anchor ─────────────────────────────────── */
.pos-pnl { line-height: 1.3; }
.pnl-big { font-family: var(--f-mono); font-size: var(--t-base); font-weight: 600; display: block; }
.pnl-big.up { color: var(--c-up); }
.pnl-big.down { color: var(--c-down); }
/* fg-2, not fg-3: these are the two dollar figures the big percentage derives
   from — they measured 2.9:1 on the panel background, below the AA floor,
   while the derived number above them shouted at full strength. */
.pnl-sub { display: block; font-size: var(--t-xs); color: var(--fg-2); margin-top: 4px; }
/* Under it, only when there is something to say: an option's live underlying, the market
   price at ENTRY (or the analyst's entry on a market-scored row) and a borrowed basis. A line
   of their own, so none of it reads as today's price beside the mark. In the Positions table
   they share it where the column holds them all and each moves to a line of its own where it
   doesn't, so nothing in it is cut (the live underlying was, at the end of a long line), and a
   row only grows where it must. */
.pnl-alt { display: block; font-size: var(--t-xs); color: var(--fg-2); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pos-tbl .pnl-alt { display: flex; flex-wrap: wrap; justify-content: flex-end; column-gap: 10px; row-gap: 2px; }
.pos-tbl .pnl-alt > * { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
/* The Positions table (positions.jsx): identity and figures in rem, P&L the flex column. It never
   shrinks below the width that leaves P&L 12.2rem, which holds the widest entry → mark line's prices
   whole ("A$8.47avg → A$8.54 · 4h ago" is 12.1rem at Small; a longer age such as "10h ago" or
   "Yesterday" can end in "…"); below that the panel scrolls
   sideways (.feed-scroll) rather than squeezing. One analyst's own table has no Analyst column.
   Each P&L line keeps to one line and ends in "…" when the column is short of it, never a figure
   cut in half (the whole line is its title). The actions column is --act-w, exactly three 30px
   buttons and the cell's padding (104px; 130px for a touch screen's 36px ones). */
.pos-tbl { --act-w: 104px; }
.pos-tbl col.col-actions { width: var(--act-w); }
@media (min-width: 901px) {
  .pos-tbl { min-width: calc(48.5rem + var(--act-w)); }
  .pos-tbl.pos-tbl-drill { min-width: calc(39.5rem + var(--act-w)); }
  .pos-tbl .pnl-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* The NEW tag on its own line above the stamp, so the stamp is never cut short. */
  .pos-tbl td.pos-upd .rcy-new { display: table; margin: 0 0 3px; }
}
.pnl-arr { color: var(--fg-deco); }
/* The 0.65625rem pieces of the entry -> mark line (.pnl-avg here, .mark-age and .mark-src) stay off the
   type scale: the Positions table keeps P&L at least 12.2rem because that line measured 12.1rem at Small. */
.pnl-avg { font-size: 0.65625rem; color: var(--fg-2); margin-left: 4px; text-transform: uppercase; letter-spacing: .04em; }
/* Green/red left accent bar by current P&L */
.tbl tbody tr.pnl-up > td:first-child { box-shadow: inset 3px 0 0 var(--c-up); }
.tbl tbody tr.pnl-down > td:first-child { box-shadow: inset 3px 0 0 var(--c-down); }
.mpos.pnl-up { box-shadow: inset 3px 0 0 var(--c-up); }
.mpos.pnl-down { box-shadow: inset 3px 0 0 var(--c-down); }
/* Details moved out of the row, surfaced in the expand panel */
/* Cap the tracks — 1fr let 5-8 fields sprawl the full table width (the panel is
   a colSpan cell), so Entry/Stop/Mark sat hundreds of px apart on a wide
   monitor. 160px absorbs the longest "Last stated" value without wrapping. */
.pos-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 160px)); justify-content: start; gap: 10px 20px; padding: 14px 16px 6px; margin: 0; }
.pos-detail-grid dt { font-size: 0.59375rem; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); margin-bottom: 5px; }
.pos-detail-grid dd { margin: 0; font-size: var(--t-md); color: var(--fg-0); }
.pos-seg { flex-shrink: 0; }

/* ── Parser detail */
.parser-detail {
  padding: 18px 24px;
  background: var(--bg-2);
  border-top: 1px dashed var(--border-2);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}
.parser-detail h4 {
  font-family: var(--f-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-2);
  margin: 0 0 10px;
  font-weight: 500;
}
.parser-detail .raw-msg {
  background: var(--bg-0);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 12px 14px;
  font-size: var(--t-md);
  color: var(--fg-1);
  line-height: 1.55;
}
.parser-detail .reasoning {
  font-size: var(--t-sm);
  color: var(--fg-1);
  line-height: 1.55;
}
.parser-detail .reasoning .highlight {
  background: var(--accent-glow);
  color: var(--accent);
  padding: 1px 4px;
  border-radius: 2px;
}
.parser-detail pre {
  font-family: var(--f-mono);
  font-size: var(--t-sm);
  color: var(--fg-mono);
  background: var(--bg-0);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin: 0;
  overflow-x: auto;
  line-height: 1.6;
}
.parser-detail pre .k { color: oklch(0.78 0.12 290); }
.parser-detail pre .s { color: var(--accent); }
.parser-detail pre .n { color: var(--c-trim); }
.parser-detail .meta-row {
  display: flex;
  gap: 16px;
  margin-top: 12px;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--fg-2);
  flex-wrap: wrap;
}
.parser-detail .meta-row span strong { color: var(--fg-0); font-weight: 500; }

/* ── Trade interaction timeline (position expand + recaps) */
.position-detail { padding: 16px 24px; background: var(--bg-2); border-top: 1px dashed var(--border-2); }
/* A section's name ("All interactions · 5"): a label, so the sans face at 500 as the tables' heads and
   the field names beside it (a phone card's .mpos-lbl says the same in the same face). */
.pd-title {
  font-family: var(--f-sans); font-size: var(--t-2xs); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--fg-2); margin: 0 0 10px; font-weight: 500;
}
.trade-timeline { display: flex; flex-direction: column; }
.tl-row {
  display: flex; align-items: center; gap: 10px;
  /* Staircase indent: each step nests the follow-up under the entry (--tl-level
     0..2, set per row). Easier to read; capped so it never runs off-screen. */
  padding: 9px 0 9px calc(var(--tl-level, 0) * 20px);
  border-bottom: 1px solid var(--border-1);
  font-size: var(--t-sm);
}
.tl-row:last-child { border-bottom: 0; }
/* Leading anchor dot — accent on the entry, muted on the nested follow-ups. */
.tl-mark { width: 5px; height: 5px; border-radius: 50%; background: var(--fg-deco); opacity: .55; flex-shrink: 0; }
.tl-entry .tl-mark { width: 7px; height: 7px; background: var(--accent); opacity: 1; }
.tl-row > .pill, .tl-row > .icon { flex-shrink: 0; }
.tl-time { color: var(--fg-3); font-size: var(--t-xs); white-space: nowrap; flex-shrink: 0; }
.tl-detail { color: var(--fg-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; min-width: 0; }
/* The note takes only the room the step's own figures leave (a zero basis): with flex 1 1 auto its
   far wider text kept most of the row and cut the detail to "Oct 30 20…". The detail still
   ellipsises rather than overflowing where the row is short of room (a fixed one pushed the edit
   button off the row at 901 px); an opened position's steps go onto a second line instead (below).
   Phones wrap both (the max-width 900px rules later in this file). */
.tl-note { color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1 1 0; min-width: 40px; }
/* Per-interaction override affordance — appears on row hover (always on touch). */
.tl-edit { flex-shrink: 0; background: none; border: 0; color: var(--fg-3); cursor: pointer; padding: 2px 5px; border-radius: 4px; display: inline-flex; align-items: center; opacity: 0; transition: opacity 120ms, color 120ms, background 120ms; }
.tl-row:hover .tl-edit { opacity: 1; }
.tl-edit:hover { color: var(--accent); background: var(--bg-2); }
@media (hover: none) { .tl-edit { opacity: 1; } }
/* An opened position on a computer keeps to the part of the feed in view (the 901px+ rules further
   down), so in a narrow window a step can be wider than its row: it ellipsised its own figures
   ("mkt 111…", "avg 164…") at 901 to 1024px. There a step goes onto a second line instead of cutting
   them: the figures move under the time when they don't fit beside it, and the note, when fewer than
   8rem are left for it beside them, takes a line of its own under them (a 40px note showed "Portf…").
   A step that fits keeps to one line, as before. The edit button travels with the note, never onto a
   line of its own (hidden until hover, it left a blank line): the note keeps the button's room, the
   row's 10px gap and the 23px button, as a right margin, and the button sits in it. */
@media (min-width: 901px) {
  .position-detail .tl-row { flex-wrap: wrap; row-gap: 3px; }
  .position-detail .tl-note { min-width: 8rem; }
  .position-detail .tl-note:empty { min-width: 0; }
  .position-detail .tl-row:has(> .tl-edit) > .tl-note { margin-right: 33px; }
  .position-detail .tl-row:has(> .tl-edit) > .tl-edit { margin-left: -33px; }
}
.recap .trade-timeline { max-height: 188px; overflow-y: auto; margin: 2px 0; }
.recap .tl-row .tl-note { display: none; }

/* ── Mobile Open Positions accordion (rendered only < 900px by MobilePositionList).
   Collapsed = ticker · instrument · analyst · entry→now; tap to reveal details. */
.mpos-list { display: flex; flex-direction: column; gap: 10px; }
/* A tablet held upright (641-900px) keeps the phone's cards, two to a row: one card stretched to
   790px put the P&L 650px from its ticker and two cards on the first screen. An opened card takes
   the whole row (beside an open card its neighbour sat next to a 4,600px hole). Open Positions
   only; Events and the Watchlist keep one column. The cards' insides are the phone's. */
@media (min-width: 641px) and (max-width: 900px) {
  .mpos-list.pos-cards { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 12px; align-items: start; }
  .mpos-list.pos-cards > .mpos.open { grid-column: 1 / -1; }
  /* The placeholder rows while the list loads, two to a row too, so the first paint does not
     reflow from one column to two; the last pair fades together. */
  .feed-skel.pos-skel { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 12px; }
  .feed-skel.pos-skel .feed-skel-row:nth-last-child(-n+2) { opacity: .45; }
  .feed-skel.pos-skel .feed-skel-row:nth-last-child(3),
  .feed-skel.pos-skel .feed-skel-row:nth-last-child(4) { opacity: .7; }
}
.mpos { background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius); box-shadow: var(--shadow-1); overflow: hidden; }
.mpos.open { border-color: var(--accent-dim); }
.mpos-head { display: block; width: 100%; text-align: left; padding: 14px 15px; background: none; cursor: pointer; }
.mpos-head:active { background: var(--bg-2); }
/* Top row — identity left, status + chevron right. */
.mpos-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.mpos-id { display: inline-flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; row-gap: 6px; }
.mpos-id .ticker { font-size: 1rem; }
.mpos-top-r { display: inline-flex; align-items: center; gap: 9px; flex-shrink: 0; }
.mpos-chev { color: var(--fg-deco); display: inline-flex; }
/* A position card's last action and when (positions.jsx): the shared action pill, a step tighter,
   and the time of the analyst's last update. The right side never shrinks; the identity on the
   left wraps instead. The time is data, so fg-2; brighter within 2 hours of the update (where the
   NEW tag was), amber once it is 2 days old (the desktop's stale update). */
.mpos-top-r .pill { padding: 1px 6px; }
.mpos-ago { font-size: var(--t-xs); color: var(--fg-2); white-space: nowrap; font-variant-numeric: tabular-nums; }
.mpos-ago.is-new { color: var(--fg-0); }
.mpos-ago.is-stale { color: var(--c-trim); }
/* Contract on its own line under the identity — strike · expiry, unpinched. */
.mpos-cx { margin-top: 7px; font-size: var(--t-sm); line-height: 1.3; color: var(--fg-2); letter-spacing: 0.01em; }
.mpos-cx .ic-strike { color: var(--fg-1); font-weight: 600; }
.mpos-cx .ic-sep { color: var(--fg-deco); }
/* Body — analyst left (grows/wraps freely), numbers right-aligned in a stack so a two-line
   analyst name can never shove them around. A grid, so the numbers keep their full width first
   and the name wraps into what is left; only when even the name's longest word has no more room
   (a 320px phone at the largest text size, a price line with its "(mkt …)") does the price line
   wrap, at its breaks (.mpos-flow). The numbers are never cut at the card's edge. */
/* The name's column keeps at least 120 px: a long price line ("188.90 (mkt 188.83) → 186.96") left
   the name 58 px and five lines; now the price line wraps at its breaks first. */
.mpos-bd { display: grid; grid-template-columns: minmax(120px, 1fr) auto; align-items: start; column-gap: 14px; margin-top: 12px; }
.mpos-bd > .analyst { align-items: flex-start; }
.mpos-nums { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; text-align: right; }
/* Always the right-hand column, chip or not (a drilled list shows no chip). */
.mpos-bd > .mpos-nums { grid-column: 2; }
.mpos-pct { font-family: var(--f-mono); font-size: 1rem; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
.mpos-pct.up { color: var(--c-up); }
.mpos-pct.down { color: var(--c-down); }
.mpos-flow { font-size: 0.78125rem; color: var(--fg-2); font-variant-numeric: tabular-nums; }   /* off the scale: a step up moved "→ 61.87" to a line of its own */
.mpos-flow .alt-basis-p, .mpos-flow .mpos-to { white-space: nowrap; }
.mpos-flow .mpos-arrow { color: var(--fg-deco); margin: 0 2px; }
.mpos-entry-lbl { color: var(--fg-3); font-size: var(--t-xs); letter-spacing: .04em; text-transform: uppercase; margin-left: 4px; }
.mpos-metaline { font-size: var(--t-xs); color: var(--fg-3); white-space: nowrap; display: inline-flex; align-items: center; }
.mpos-metaline .mark-age { margin: 0; }
.mpos-nums .mpos-spark { margin-top: 2px; }
.mpos-body { padding: 2px 14px 14px; border-top: 1px solid var(--border-1); }
.mpos-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 11px 16px; margin: 13px 0 4px; }
.mpos-grid > div { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.mpos-grid dt { font-family: var(--f-sans); font-weight: 500; font-size: 0.5625rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--fg-3); }
.mpos-grid dd { margin: 0; font-size: 0.84375rem; color: var(--fg-1); font-family: var(--f-mono); }
.mpos-dn { color: var(--c-down); }
.mpos-sec { margin-top: 14px; }
.mpos-lbl { display: block; font-family: var(--f-sans); font-weight: 500; font-size: 0.5625rem; letter-spacing: 0.05em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 6px; }
/* pre-wrap: an analyst's paragraphs and one-per-line lists keep their line breaks,
   exactly as the desktop note (.wl-raw) shows them. */
.mpos-note { margin: 0; font-size: var(--t-md); line-height: 1.5; color: var(--fg-1); white-space: pre-wrap; word-break: break-word; }
/* Wraps: the owner's three (Manage, Calculator, Chart) ran up to 55px past an opened card at 360 px
   with Larger text, and the card's overflow hid Chart's end. Where they fit they share the row, each at
   least as wide as its label; one that does not fit moves to a second row. Delete never grows: alone on
   a row it keeps its own width at the end, never a full-width red bar. Also the Events and Watchlist
   cards (Override, Parser, Delete). */
.mpos-btns { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.mpos-btns .btn { flex: 1 1 0; min-width: max-content; justify-content: center; height: 2.375rem; }
.mpos-btns .btn.danger { flex-grow: 0; margin-left: auto; }
.mpos-body .parser-detail { margin: 14px -14px 0; border-radius: 0 0 var(--radius) var(--radius); }
.mpos-body .position-detail { display: none; }
/* Shared by the Events / Watchlist accordions (selection, day groups, etc.). Both cards
   use the Positions frame above (.mpos-top / .mpos-id / .mpos-top-r) for their first
   line; .mpos-r2 is their second: analyst on the left, action or category + time on
   the right. node tests/css-classes.test.js lists any class a card uses with no rule. */
.mpos-r2 { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; }
.mpos-r2 > .analyst { flex: 1 1 auto; min-width: 0; }
.mpos-r2r { display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; }
.mpos-meta { font-family: var(--f-mono); font-size: var(--t-sm); color: var(--fg-2); display: inline-flex; align-items: center; white-space: nowrap; }
.mpos-now { color: var(--fg-2); }
.mpos-cb { width: 16px; height: 16px; flex-shrink: 0; margin: 0 2px 0 0; }
.mpos.msel { border-color: var(--accent-dim); background: var(--accent-glow); }
.mpos-day { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; padding: 16px 2px 4px; }
.mpos-day-t { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.mpos-day-m { font-size: var(--t-xs); color: var(--fg-3); }
/* Watchlist card: the post's summary under its tickers, two lines at most until the
   card is opened, and the category as quiet text at the top right (where Positions
   shows its status), not as a pill. */
.mpos-sum {
  margin: 8px 0 0; font-size: var(--t-md); line-height: 1.45; color: var(--fg-1);
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.mpos.open .mpos-sum { display: block; -webkit-line-clamp: unset; overflow: visible; }
.mpos-kind { font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg-2); white-space: nowrap; }
.mpos-kind.watchlist { color: var(--c-watch); }

/* ── Analyst-first Open Positions overview (per-server `positions_by_analyst`).
   An editorial ROSTER, not a card grid: one full-bleed league-table row per
   analyst on hairline rules, ranked by open count. Fills the width by
   construction (a 3-analyst book never reads as a sparse grid) and reflows to a
   clean single-column stack on mobile. Boutique, asymmetric, restrained — no
   rounded-card grid. */
.an-roster { padding: 0; }
/* Head + row share ONE identical grid template — keep them in step (no subgrid
   available) or the columns drift out of alignment. */
.an-roster-head,
.an-roster-row {
  display: grid;
  /* Fixed flags column (not auto) so a row WITH an expiring pill keeps the exact
     same tracks as one without — otherwise the flexible columns absorb the pill's
     width and rows drift out of vertical alignment with each other + the header. */
  /* The name column never goes below 190px: it holds the name and, first on the line under it,
     when they last updated ("updated yesterday" at the largest text size), which the holdings
     peek gives way to on a narrow screen. */
  grid-template-columns: 44px minmax(190px, 1.25fr) 132px 84px minmax(0, 1.6fr) 72px 22px;
  align-items: center;
  column-gap: 18px;
  padding-left: 20px;
  padding-right: 18px;
}
/* A narrow table (a tablet, a small window): tighter gaps leave the holdings some room. */
@media (max-width: 1100px) {
  .an-roster-head, .an-roster-row { column-gap: 14px; }
}
/* 901-1010px beside the open sidebar (a narrow window, a large phone held sideways): the table had no
   room left for the holdings peek (0px at 901, still only 13-52px up to 1000, a lone "M…" under a
   Holdings label that ran over the exp column) and its rows ran up to 29px past the panel, cutting the
   chevron. The peek and its gap go, and Open narrows to its 108px meter. With the sidebar folded to
   its icons the full table fits, so it keeps the peek; from 1011px the peek has room for a ticker. */
@media (min-width: 901px) and (max-width: 1010px) {
  :root:not([data-side="rail"]) .an-roster-head,
  :root:not([data-side="rail"]) .an-roster-row { grid-template-columns: 44px minmax(190px, 1fr) 112px 84px 72px 22px; }
  :root:not([data-side="rail"]) .an-r-watch,
  :root:not([data-side="rail"]) .an-roster-head > :nth-child(5) { display: none; }
}
.an-roster-head {
  position: sticky; top: 0; z-index: 1;
  height: 34px;
  background: var(--bg-1);          /* opaque — rows must not bleed through on scroll */
  border-bottom: 1px solid var(--border-1);
  font: 500 var(--t-2xs)/1 var(--f-sans);   /* column names: the face and base size of the tables' heads (.tbl thead th) */
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--fg-3);
}
.an-roster-head .r { text-align: right; }

.an-roster-row {
  position: relative;
  min-height: 66px;
  padding-top: 13px; padding-bottom: 13px;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border-1);
  cursor: pointer;
  transition: background 130ms ease;
}
/* Colour spine carried over from the old card — reads as an evolution. */
.an-roster-row::before {
  content: "";
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 3px; background: var(--col); opacity: 0.5;
  transition: opacity 130ms ease;
}
.an-roster-row:hover { background: color-mix(in oklab, var(--col) 7%, var(--bg-1)); }
.an-roster-row:hover::before { opacity: 1; }
/* A press on the two things a phone member taps most (a menu item, an analyst row) shows at once,
   in the brand's own tint, instead of the platform's tap flash (a blue on Android). The flash goes
   only together with this pressed state, never alone. */
.nav-item, .an-roster-row { -webkit-tap-highlight-color: transparent; }
.nav-item:active, .an-roster-row:active { background: var(--accent-glow); }
/* A tab picked from the menu that is still drawing (app.jsx tab-pending): the old tab dims after a
   moment, so a slow phone never shows the new title over the old tab at full strength. A quick
   switch is done before the dim starts. */
.content.tab-pending > * { opacity: 0.5; transition: opacity 160ms ease 150ms; }

/* The row's place in the list, which is ordered by open positions (the head's "Open ↓"), not a
   league position: a quiet index, a step smaller than the open count beside it. */
.an-r-rank {
  justify-self: end;
  font: 700 var(--t-lg)/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
  color: color-mix(in oklab, var(--fg-deco) 52%, transparent);
  transition: color 130ms ease;
}
.an-roster-row:hover .an-r-rank { color: color-mix(in oklab, var(--col) 70%, var(--fg-deco)); }
/* Lead row (sparse view): the ghost numeral takes the analyst's own colour. */
.an-roster-row.lead .an-r-rank { color: color-mix(in oklab, var(--col) 55%, transparent); }

.an-r-id { display: flex; align-items: center; gap: 11px; min-width: 0; }
.an-ov-pic {
  box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--col) 65%, transparent);
  background: var(--bg-2);
  flex-shrink: 0;
}
.an-r-id .swatch.lg {
  width: 40px; height: 40px; border-radius: 99px;
  background: color-mix(in oklab, var(--col) 22%, var(--bg-3));
  box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--col) 60%, transparent);
  flex-shrink: 0;
}
.an-r-idtext { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.an-r-eyebrow {
  font: 600 var(--t-2xs)/1 var(--f-sans);
  letter-spacing: 0.08em; text-transform: uppercase;
  color: color-mix(in oklab, var(--col) 55%, var(--fg-2));
}
/* Light mixes toward --fg-1: toward --fg-2, the lighter slots (sky, chartreuse, stone, and the blue on
   Velvet's cream) read 4.1 to 4.5:1 on a hovered row; this is 5.4:1 or better on every brand. Dark
   keeps --fg-2 (4.5:1 or better there). */
[data-theme="light"] .an-r-eyebrow { color: color-mix(in oklab, var(--col) 55%, var(--fg-1)); }
/* The row's title, a name: the sans face (it was the mono, tracked). On a phone its box ends in an
   ellipsis, and Poppins' descenders reach 0.28em under the baseline, past its line box of 1, while its
   brackets rise about 0.1em above it: the padding gives g, p and y their tails and "(" its top inside
   the clip, the negative margins keep the layout. */
.an-r-name {
  font: 600 var(--t-base)/1 var(--f-sans);
  color: var(--fg-0);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding-top: 0.1em; margin-top: -0.1em;
  padding-bottom: 0.15em; margin-bottom: -0.15em;
}
/* In the table (from 761px) a long name takes a second line rather than an ellipsis: a trader's
   two books ("Trader · Name Stocks", "Trader · Name Options") were both cut to "Trader · Name …",
   and a tablet has no hover to tell them apart. Two lines at most; a name that fits stays on one.
   Line height 1.3 and no padding: Poppins' glyphs are taller than a 1.15 line, so a clamped name's
   hidden third line put the tops of its brackets and the dot of an i under the second line ("The Pawn
   (The Market Bishop)" at Larger), and padding there only opened the clip further. At 1.3 the clip
   ends 0.3em under the second line's baseline (its descenders stay inside) and above the third
   line's tallest glyph. */
@media (min-width: 761px) {
  .an-r-name {
    white-space: normal; overflow-wrap: anywhere; line-height: 1.3;
    padding-top: 0; margin-top: 0; padding-bottom: 0; margin-bottom: 0;
    display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  }
}
/* A sub-account book's label under the trader's name (positions.jsx AnalystOverview): neutral,
   never cut. */
.an-r-bookl { font: 500 var(--t-xs)/1.2 var(--f-sans); letter-spacing: 0.02em; color: var(--fg-2); }
/* 0.71875rem, off the type scale: a step up cut the counts at the end of the line ("16 ▼"). */
.an-r-sub {
  font-size: 0.71875rem; color: var(--fg-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* When they last updated a position: data, so a step brighter than the counts after it. */
.an-r-upd { color: var(--fg-2); }
.an-r-sub .up { color: var(--c-up); }
.an-r-sub .down { color: var(--c-down); }
.an-r-sub .muted { color: var(--fg-3); }

.an-r-book { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.an-r-open { display: flex; align-items: baseline; gap: 5px; }
.an-r-open strong {
  font: 700 var(--t-xl)/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
  color: var(--fg-0);
}
.an-r-open-l {
  font: 500 var(--t-2xs)/1 var(--f-sans);   /* the word beside the count (the count stays mono), the size of the avg's label */
  letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--fg-3);
}

/* Signature motif: the segmented book-health meter — up / down / still-unmarked,
   proportioned by count. Semantic colours only; unmarked is neutral grey. */
.bmeter {
  display: flex; height: 5px; width: 108px; gap: 2px;
  border-radius: 3px; overflow: hidden;
  background: var(--bg-3);
}
.bmeter i { display: block; min-width: 3px; }   /* keep a lopsided sliver visible */
.bm-up { background: var(--c-up); }
.bm-down { background: var(--c-down); }
.bm-un { background: color-mix(in oklab, var(--fg-deco) 45%, var(--bg-3)); }

.an-r-pnl { justify-self: end; text-align: right; font-variant-numeric: tabular-nums; }
.an-r-pnl .fig { font: 700 1.1875rem/1 var(--f-mono); }
.an-r-pnl .fig.up { color: var(--c-up); }
.an-r-pnl .fig.down { color: var(--c-down); }
.an-r-pnl .fig.none { color: var(--fg-3); }
.an-r-pnl .lbl { display: block; font-size: var(--t-2xs); letter-spacing: 0.08em; color: var(--fg-3); margin-top: 3px; }

.an-r-watch {
  min-width: 0; overflow: hidden;
  white-space: nowrap; text-overflow: ellipsis;
  font: var(--t-sm)/1.3 var(--f-mono); letter-spacing: 0.02em;
  color: var(--fg-2);
}
.an-r-watch .dot { color: var(--fg-deco); margin: 0 6px; }
.an-r-watch .more { color: var(--fg-3); }

.an-r-flags { justify-self: end; }
/* Expiring pill — amber, matching the app's DTE / "expiring" convention
   (--c-trim), never the analyst identity accent. The size of every other pill. */
.an-r-exp {
  font: 600 var(--t-2xs)/1 var(--f-mono);
  letter-spacing: 0.04em;
  color: var(--c-trim);
  padding: 3px 7px; border-radius: 5px; white-space: nowrap;
  border: 1px solid color-mix(in oklab, var(--c-trim) 40%, transparent);
  background: color-mix(in oklab, var(--c-trim) 12%, transparent);
}
[data-theme="light"] .an-r-exp { background: color-mix(in oklab, var(--c-trim) 10%, var(--bg-1)); }   /* opaque, like .pill */
.an-r-chev { color: var(--fg-deco); display: inline-flex; transition: color 130ms ease, transform 130ms ease; }
.an-roster-row:hover .an-r-chev { color: var(--col); transform: translateX(2px); }

/* The book in a sentence ("2 analysts · 72 open · avg +4.1% · 5 expiring by Fri"): words, so the sans
   face, as the status line under the title; the average is P&L, so it keeps the mono. */
.an-roster-foot {
  display: flex; flex-wrap: wrap; gap: 14px;
  padding: 12px 20px;
  font: var(--t-sm)/1 var(--f-sans);
  color: var(--fg-2);
}
.an-roster-foot .up, .an-roster-foot .down { font-family: var(--f-mono); }
.an-roster-foot .up { color: var(--c-up); }
.an-roster-foot .down { color: var(--c-down); }

/* Low count: spend the extra height on a bigger masthead figure + the best/worst
   subline (earned by data), never on padding. The table layout only: gated at
   min-width 761px so these high-specificity boosts don't leak into the phone stack. */
@media (min-width: 761px) {
  .an-roster.sparse .an-roster-row { min-height: 88px; padding-top: 20px; padding-bottom: 20px; }
  .an-roster.sparse .an-r-rank { font-size: var(--t-xl); }
  .an-roster.sparse .an-r-name { font-size: 0.9375rem; }
  .an-roster.sparse .an-r-open strong { font-size: 1.5625rem; }
}
/* The column head's order cue: the roster is ranked by open positions. */
.an-roster-head .an-sort-cue { margin-left: 4px; letter-spacing: 0; }
/* 761-900px (a tablet held upright, a narrow window): the desktop league table, one row per
   analyst under its column head. The window scrolls here, not the list, so the head is not
   pinned (a sticky head would slide under the top bar). */
@media (min-width: 761px) and (max-width: 900px) {
  .an-roster-head { position: static; }
}

/* Phones: drop the table columns for the known-good vertical stack, now also carrying the
   meter, avg and best/worst. Pure CSS reflow; the JS never branches. From 761px the desktop table
   fits (above), so the stack starts at 760px, not at the app's 900px phone boundary.
   Four columns, so the name has the whole first line: the avg P&L's column used to squeeze it to
   "Fra…" at 320px with the largest text. The expiring count ("5 exp") ends the holdings line,
   under the avg, rather than being hidden; the meter drops under the open count when a narrow
   phone has no room for both, and keeps its width. */
@media (max-width: 760px) {
  .an-roster-head { display: none; }
  .an-roster-row,
  .an-roster.sparse .an-roster-row {
    grid-template-columns: 34px minmax(0, 1fr) auto auto;
    grid-template-areas:
      "rank id    id    chev"
      ".    book  pnl   pnl"
      ".    watch watch watch";
    column-gap: 12px; row-gap: 10px;
    min-height: 0;
    padding: 16px 16px 16px 18px;
  }
  .an-roster-row.has-exp,
  .an-roster.sparse .an-roster-row.has-exp {
    grid-template-areas:
      "rank id    id    chev"
      ".    book  pnl   pnl"
      ".    watch flags flags";
  }
  .an-r-rank { grid-area: rank; justify-self: start; align-self: start; font-size: 0.9375rem; }
  .an-r-id { grid-area: id; }
  .an-r-chev { grid-area: chev; align-self: start; justify-self: end; }
  .an-r-book { grid-area: book; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 7px 12px; }
  .an-r-pnl { grid-area: pnl; }
  .an-r-watch { grid-area: watch; }
  .an-r-flags { grid-area: flags; align-self: center; }
  .an-r-flags:empty { display: none; }
  .bmeter { width: 88px; flex: none; height: 4px; }
}

/* Drill-in header: back to the analyst grid + whose positions you're viewing. Never wider than the
   title it sits in. The title has a row of its own whenever the controls do not fit beside it (always
   on a phone; on a computer, see the 901px rules for .pos-hdr-tools), so it never shrinks for them
   and the whole name shows; only a name longer than the header's whole width ends in an ellipsis
   instead of running off the panel, cut mid-letter. */
.pos-drill-hdr { display: inline-flex; align-items: center; gap: 12px; min-width: 0; max-width: 100%; }
.panel-title.pos-title-drill { min-width: 0; }
.pos-back {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: none; cursor: pointer;
  font-size: var(--t-md); color: var(--fg-2);
  padding: 4px 7px; margin-left: -7px; border-radius: 6px;
  transition: color 120ms ease, background 120ms ease;
}
.pos-back:hover { color: var(--fg-0); background: var(--bg-2); }
.pos-back-arrow { font-size: 0.9375rem; line-height: 1; }
.pos-drill-name {
  font-family: var(--f-mono);
  font-weight: 600;
  letter-spacing: 0.05em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
  /* A button (it opens the analyst's profile) that reads as the title it is. */
  font-size: inherit; line-height: inherit; color: inherit; text-align: left; border-radius: 4px;
}
.pos-drill-name:hover, .pos-drill-name:focus-visible { text-decoration: underline; text-underline-offset: 3px; outline: none; }
.pos-drill-name:focus-visible { box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 50%, transparent); }
.pos-drill-name::before {
  content: ""; display: inline-block;
  width: 1px; height: 12px;
  background: var(--border-2);
  margin-right: 12px;
  vertical-align: -1px;
}

/* Contextual empty state for the quick-filter segments — so All+Today over a
   quiet weekend reads as "nothing today", never a broken/blank page. */
.pos-empty {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  padding: 60px 24px;
  text-align: center;
}
.pos-empty p { margin: 0; color: var(--fg-2); font-size: var(--t-base); max-width: 34ch; }

/* ── Sources tab */
.source-card {
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-1);
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 14px;
  cursor: pointer;
  transition: background 120ms;
}
.source-card:hover { background: var(--bg-2); }
.source-card .check {
  width: 18px; height: 18px;
  border-radius: 4px;
  border: 1.5px solid var(--border-3);
  display: grid;
  place-items: center;
  background: var(--bg-2);
  flex-shrink: 0;
}
.source-card .check.on {
  background: var(--accent);
  border-color: var(--accent);
}
.source-card .check.on::after {
  content: ""; width: 9px; height: 6px;
  border-left: 1.5px solid var(--bg-0);
  border-bottom: 1.5px solid var(--bg-0);
  transform: rotate(-45deg) translate(1px, -1px);
}
.source-card .count {
  font-family: var(--f-mono);
  font-size: var(--t-sm);
  color: var(--fg-2);
}
.source-card .eye {
  width: 16px; height: 16px;
  color: var(--fg-3);
}
.source-card .eye:hover { color: var(--fg-0); }

.ticker-row {
  padding: 10px 18px;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 10px;
  border-bottom: 1px solid var(--border-1);
  cursor: pointer;
  transition: background 120ms;
}
.ticker-row:hover { background: var(--bg-2); }
.ticker-row.active { background: var(--bg-3); }
.ticker-row .name { font-family: var(--f-mono); font-weight: 600; font-size: var(--t-md); color: var(--c-ticker); }
.ticker-row .meta { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-2); }

/* ── Chart */
.chart-wrap {
  position: relative;
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 18px;
  min-height: 460px;
}
.chart-hdr {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
  gap: 16px;
}
.chart-hdr .ticker-big {
  font-family: var(--f-mono);
  font-weight: 600;
  font-size: var(--t-xl);
  color: var(--c-ticker);
}
.chart-hdr .sub {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.10em;
  text-transform: uppercase;
  color: var(--fg-2);
  margin-left: 14px;
}
/* Right column of the chart header: timeframe switcher above the source line */
.chart-hdr-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; flex-shrink: 0; }
.chart-int .seg-btn { padding: 5px 9px; font-size: var(--t-xs); line-height: 1.32; }   /* the height it had at this size */

.chart-hdr .source-attr {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.10em;
  color: var(--fg-2);
}

.chart-legend {
  display: flex; flex-wrap: wrap; gap: 7px 14px;
  margin: 2px 0 10px;
}
.chart-legend .leg-item {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-1);
}
.chart-legend .leg-dot {
  width: 10px; height: 10px; border-radius: 2px;
  border: 1.5px solid rgba(0, 0, 0, 0.85);
  box-shadow: 0 0 0 1px var(--border-2);
  flex-shrink: 0;
}
.chart-svg { width: 100%; height: 460px; display: block; }
.chart-tooltip {
  position: absolute;
  z-index: 5;
  background: var(--bg-0);
  border: 1px solid var(--border-2);
  border-radius: var(--radius);
  padding: 12px 14px;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--fg-1);
  pointer-events: none;
  max-width: 280px;
  box-shadow: var(--shadow-2);
}
.chart-tooltip .t-hdr {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 6px;
  font-weight: 600;
  color: var(--fg-0);
}
.chart-tooltip .t-note { color: var(--fg-1); white-space: normal; line-height: 1.5; }
.chart-tooltip .t-meta { color: var(--fg-2); margin-top: 6px; }

/* ── Signal flyout */
.flyout-back {
  position: fixed; inset: 0;
  background: rgba(2, 4, 8, 0.55);
  backdrop-filter: blur(2px);
  z-index: 50;
  animation: fadeIn 180ms ease-out;
}
.flyout {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: 440px;
  background: var(--bg-1);
  border-left: 1px solid var(--border-2);
  box-shadow: var(--shadow-2);
  z-index: 51;
  display: flex;
  flex-direction: column;
  animation: slideRight 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
.flyout-hdr {
  padding: 18px 22px;
  border-bottom: 1px solid var(--border-1);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.flyout-body { padding: 18px 22px; overflow-y: auto; flex: 1; }
.flyout-body .field { margin-bottom: 18px; }
.flyout-body .field h5 {
  margin: 0 0 9px;
  font-family: var(--f-mono);
  font-size: var(--t-2xs);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-2);
  font-weight: 500;
}

/* ── Watchlist (column table, grouped by day) */
/* Day separator row inside the table body */
.tbl tbody tr.day-sep > td {
  background: var(--bg-2);
  border-bottom: 1px solid var(--border-2);
  padding: 8px var(--pad-x);
  position: sticky; top: 0; z-index: 1;
}
.tbl tbody tr.day-sep:hover > td { background: var(--bg-2); }
.day-sep-title {
  font-size: var(--t-sm); font-weight: 600; color: var(--fg-0);
  letter-spacing: .01em;
}
.day-sep-meta { font-size: var(--t-xs); color: var(--fg-3); margin-left: 10px; }

/* Category pill (analysis / watchlist) */
.wl-kind {
  display: inline-block;
  font-family: var(--f-mono); font-size: 0.59375rem; letter-spacing: 0.08em;
  text-transform: uppercase; padding: 2px 7px; border-radius: 99px;
  border: 1px solid currentColor;
  background: color-mix(in oklab, currentColor 12%, transparent);
  color: var(--c-watch);
}
/* ANALYSIS is the default kind of post: a quiet neutral tag, never a colour. In the
   open green it read as an OPEN pill. WATCHLIST keeps the watch purple. */
.wl-kind.analysis { color: var(--fg-2); border-color: var(--border-2); background: transparent; }
/* The Watchlist table's time ("Thu 11:22 AM", one line) and Category pill (WATCHLIST) columns
   (watchlist.jsx). Up to 1950px, where every table's cells are dense, they are in rem, like the
   Events columns, so both stay whole at every text size (as percentages they cut the stamp at
   Larger text on a 1280px laptop, and the pill from the default size); a wider screen's roomier
   cells keep their percentages. */
.wl-tbl col.wlc-time { width: 11%; }
.wl-tbl col.wlc-kind { width: 9%; }
@media (max-width: 1950px) and (min-width: 901px) {
  .wl-tbl col.wlc-time { width: 6.1rem; }
  .wl-tbl col.wlc-kind { width: 5.6rem; }
}
/* Its Analyst column is 13%, and never narrower than 7.1rem, which holds a book's label whole beside
   its dot at every text size (CHARAN's "20k Challenge", the longest, needs 113px at Small with the
   cell's padding; at 13% it read "20k Challen…" on a 1280px laptop at Larger text, and at 1024px
   every size cut it). A <col> takes no max(13%, 7.1rem) (the table treats it as an auto column), so
   7.1rem applies wherever 13% of the table is less, with the sidebar open: below 1166px at Small,
   1276px at the default size and 1385px at Larger. Wider, 13% is more than 7.1rem and is kept, so a
   long name keeps the room it has on a big screen (a plain 7.1rem cut 19 more names to "…" there).
   In a narrow window it also stops names breaking mid-word (at 901 to 1100px, 13% broke up to 232 of
   the owner's 290). A Note cell narrower than its padding shows nothing rather than a sliver of its
   first letter (the text is its own box). */
.wl-tbl col.wlc-an { width: 13%; }
@media (min-width: 901px) and (max-width: 1165px) { .wl-tbl col.wlc-an { width: 7.1rem; } }
@media (min-width: 901px) and (max-width: 1275px) { :root:not([data-fontscale="sm"]) .wl-tbl col.wlc-an { width: 7.1rem; } }
@media (min-width: 901px) and (max-width: 1384px) { :root[data-fontscale="lg"] .wl-tbl col.wlc-an { width: 7.1rem; } }
@media (min-width: 901px) { .wl-tbl td.cell-note .cell-text { display: block; overflow: hidden; text-overflow: ellipsis; } }
/* The select box and the row buttons' columns are px, like the Events ones: neither the 13px box nor
   a 30px button grows with the text (as 3% of a narrow table the box was cut, 18px wide at 901px).
   Each is there only when the member has it (watchlist.jsx wl-sel, wl-act); 36px for a touch
   screen's 22px box (the pointer: coarse block at the end), and two 36px buttons still fit 100px. */
.wl-tbl { --chk-w: 0px; --act-w: 0px; }
.wl-tbl.wl-sel { --chk-w: 34px; }
.wl-tbl.wl-act { --act-w: 100px; }
.wl-tbl col.col-check { width: var(--chk-w); }
.wl-tbl col.col-actions { width: var(--act-w); }
/* The narrowest the table gets. In a narrow window the Note, the flex column, gives up its room first
   (a row opens to the whole note); past that, a table-layout: fixed table squeezes its percentage
   columns, and with the 7.1rem floor above Tickers (16%) shrank under its own chips: at 901px with
   Larger text a chip was cut on 216 of the owner's 300 rows ("$EOS.AX" by 24px; 284 rows on a touch
   screen, 9 for a member).
   So the table stops narrowing where the Note reaches nothing, the rem and px columns plus Tickers'
   16% and Channel's 15% of it (hence 0.69: change it with them), and the panel scrolls sideways
   instead (.feed-scroll), as Events and Positions do; an opened post and each day's date keep to the
   part in view. Measured 2026-10-16 with the app's 10px scrollbars over the latest 300 posts: for the
   owner it fits from 922px wide at Small, 977px at the default size and 1031px at Larger (925, 980
   and 1034px on a touch screen; at 901px it scrolls 21, 76 and 130px), an analyst (no select box)
   from 927px at the default size and 982px at Larger, and a member with neither box nor buttons
   never scrolls; no ticker, book label, time or pill is cut at any width. Wider, nothing changes. */
@media (min-width: 901px) and (max-width: 1950px) {
  .wl-tbl { min-width: calc((18.8rem + var(--chk-w) + var(--act-w)) / 0.69); }
}

/* Ticker chips inside the cell — wrap within the column. A neutral chip with an amber
   symbol: tickers are --c-ticker everywhere, never the brand accent. */
.wl-tickcell { display: flex; flex-wrap: wrap; gap: 5px; }
/* 0.71875rem, off the type scale on purpose: the Watchlist table's fit (922 / 977 / 1031px for the
   owner) and its Tickers column were measured with chips this size. */
.wl-tick {
  font-family: var(--f-mono); font-weight: 600; font-size: 0.71875rem;
  color: var(--c-ticker);
  padding: 1px 7px; border-radius: 99px;
  border: 1px solid var(--border-2);
  background: var(--bg-2);
  white-space: nowrap;
}

/* Expanded raw-note row */
.tbl tbody tr.wl-raw-row > td {
  background: var(--bg-2);
  border-bottom: 1px solid var(--border-2);
  padding: 12px var(--pad-x) 14px;
}
.tbl tbody tr.wl-raw-row:hover > td { background: var(--bg-2); }
.wl-raw-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); margin: 12px 0 6px; }
.wl-raw {
  font-size: var(--t-md); color: var(--fg-1); line-height: 1.6;
  white-space: pre-wrap; word-break: break-word;
}
/* Discord's small print (a "-# " line) inside an analyst's note (shared.jsx DiscordText). */
.dc-sub { font-size: 0.85em; color: var(--fg-3); }

/* ── Recap card */
/* ── Recaps overview: prominent win/loss/avg summary + win-loss chart */
.recap-overview { padding: 18px 20px; margin-bottom: 14px; display: flex; flex-direction: column; gap: 16px; }
.recap-summary { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
/* Shrinkable, never wider than the panel: at max-content the caption ran past the panel
   edge on every phone and was cut off mid-word ("14 awaiti"). It now wraps, at the dot. */
.recap-hero { display: flex; align-items: baseline; gap: 12px; flex: 0 1 auto; min-width: 0; max-width: 100%; }
/* A figure, so the mono face at 600 like the stats beside it (.rs-val; the mono is loaded at 400 to 600 only).
   The display face is the wordmark's alone. */
.recap-winrate { font-family: var(--f-mono); font-size: 2.625rem; font-weight: 600; line-height: 1; letter-spacing: -0.01em; }
.recap-hero-lbl { display: flex; flex-direction: column; min-width: 0; font-size: var(--t-sm); color: var(--fg-1); text-transform: uppercase; letter-spacing: .08em; }
/* fg-2, not fg-3: these words say what the win rate counts, so they must be legible. Words, so the
   sans face, as every sentence around the figures. */
.recap-hero-lbl span { font-size: var(--t-2xs); color: var(--fg-2); text-transform: none; letter-spacing: 0; margin-top: 6px; }
/* Beside the win rate only where all its figures fit there on one row (an auto basis: the row's own
   width), else under it at the full width. With a 0 basis it sat beside the win rate wherever 240px
   were left and squeezed the six figures into two or three rows there (three at 1024px on the default
   text size), with the space under the win rate left empty. */
.recap-mid { display: flex; flex-direction: column; gap: 12px; flex: 1 1 auto; min-width: 240px; }
.recap-wlbar { display: flex; height: 9px; border-radius: 99px; overflow: hidden; background: var(--bg-3); }
.recap-wlbar .wl-win { background: var(--c-up); }
.recap-wlbar .wl-loss { background: var(--c-down); }
.recap-statset { display: flex; gap: 26px; flex-wrap: wrap; }
.recap-stat { display: flex; flex-direction: column; gap: 4px; }
.recap-stat .rs-val { font-family: var(--f-mono); font-size: 1.1875rem; font-weight: 600; color: var(--fg-0); }
.recap-stat .rs-val.up { color: var(--c-up); }
.recap-stat .rs-val.down { color: var(--c-down); }
.recap-stat .rs-lbl { font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); }
/* How many results an average is taken over: an average never stands without its count. */
.recap-stat .rs-n { font-family: var(--f-mono); font-size: var(--t-2xs); color: var(--fg-2); }
.recap-hero-lbl .rh-nw { white-space: nowrap; margin-top: 0; }
/* A short measure: long counts wrap under the win rate, so it stays narrow and the six figures fit
   beside it more often (.recap-mid). */
.recap-hero-lbl > span { max-width: 18rem; }
.recap-chartwrap { border-top: 1px solid var(--border-1); padding-top: 14px; }
.recap-chart-svg { width: 100%; height: 168px; display: block; }
/* A sentence (the hint, or the hovered bar's day in words), so the sans face; its average is P&L, so the
   mono, as every result. */
.recap-chart-meta { padding: 8px 2px 0; font-size: var(--t-xs); color: var(--fg-2); min-height: 22px; }
.recap-chart-meta strong { font-weight: 600; font-family: var(--f-mono); }

/* Performance-by-period bars. DOM/flex rather than SVG so every bar can carry a
   real, legible % label — the previous cumulative line pushed all the values onto
   a cramped axis where they couldn't be read. */
.recap-chart-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; }
.rc-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); display: flex; flex-direction: column; gap: 5px; }
.rc-title .rc-sub { font-size: var(--t-xs); font-weight: 400; color: var(--fg-2); }
/* pan-y: a vertical swipe still scrolls the page, a sideways drag reads the bars, and a
   pinch still zooms (it reaches the bars as pointercancel, which opens nothing). The left
   padding (the scale's gutter) is set inline, from the same constant the JS uses. */
.rc-bars { position: relative; display: flex; align-items: stretch; gap: 4px; padding: 0 2px; touch-action: pan-y pinch-zoom; }
/* The scale's reference lines: zero solid, +100% / +1,000% dashed. Tokens only, so every
   brand's theme carries them. */
.rc-tick { position: absolute; right: 0; height: 0; border-top: 1px dashed var(--border-2); pointer-events: none; }
.rc-tick.zero { border-top-style: solid; }
.rc-tick-lbl {
  position: absolute; left: 0; transform: translateY(-50%); text-align: right;
  font-family: var(--f-mono); font-size: 0.5625rem; color: var(--fg-2);
  white-space: nowrap; pointer-events: none;
}
.rc-bar {
  position: relative; flex: 1 1 0; min-width: 0; overflow: visible;
  background: none; border: 0; padding: 0; cursor: pointer;
  border-radius: var(--radius);
  transition: background 120ms;
}
.rc-bar:focus-visible { outline-offset: 0; }
.rc-bar:hover, .rc-bar.on { background: color-mix(in oklab, var(--fg-0) 5%, transparent); }
.rc-bar-fill {
  position: absolute; left: 18%; right: 18%;
  border-radius: 2px;
  background: var(--c-up);
  transition: opacity 120ms;
  opacity: .82;
}
.rc-bar.down .rc-bar-fill { background: var(--c-down); }
.rc-bar:hover .rc-bar-fill, .rc-bar.on .rc-bar-fill { opacity: 1; }
.rc-bar.peak .rc-bar-fill { box-shadow: 0 0 0 1px color-mix(in oklab, var(--fg-0) 22%, transparent); }
/* Printed only where a bar is wide enough for it (the JS measures the room); the readout
   carries the exact figure for every bar. Its 0.65625rem is off the type scale because recaps.jsx
   works out that room from this very size (keep the two in step). */
.rc-bar-val {
  position: absolute; left: 0; right: 0; text-align: center;
  font-family: var(--f-mono); font-size: 0.65625rem; font-weight: 600;
  color: var(--c-up); white-space: nowrap; pointer-events: none;
}
.rc-bar.down .rc-bar-val { color: var(--c-down); }
/* A date under every few bars, centred on its bar and free to be wider than it (the JS
   spaces them so none collide); the newest pins to the right edge so it never spills out. */
.rc-bar-x {
  position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  font-family: var(--f-mono); font-size: 0.59375rem; color: var(--fg-2);
  white-space: nowrap; pointer-events: none;
}
.rc-bar-x.end { left: auto; right: 0; transform: none; }
/* Earlier / Later through a history wider than the chart, with the dates in view. */
.rc-nav { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 4px; font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-2); }
.rc-nav-btn {
  display: inline-flex; align-items: center; gap: 4px; min-height: 2rem; padding: 0 6px;
  background: none; border: 0; border-radius: var(--radius-sm); font: inherit; font-family: var(--f-sans); font-weight: 500; color: var(--fg-1); cursor: pointer;
}
.rc-nav-btn:hover:not(:disabled) { color: var(--fg-0); background: var(--bg-2); }
/* Off at either end of the history: the shared disabled look (.btn:disabled). It used to lean on
   --fg-3 being faint, which it no longer is (in light it is the range's own --fg-2 beside it). */
.rc-nav-btn:disabled { opacity: .5; cursor: not-allowed; }
.rc-nav-btn .icon { width: 12px; height: 12px; }
.rc-nav-range { text-align: center; white-space: nowrap; }
.recap-chart-meta .up { color: var(--c-up); }
.recap-chart-meta .down { color: var(--c-down); }
.recap-daypanel { margin-top: 12px; border: 1px solid var(--border-2); border-radius: var(--radius); background: var(--bg-1); }
.recap-daypanel-hd {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 12px 0; font-size: var(--t-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--fg-2);
}
.recap-daypanel .recap-period { border: none; margin: 0; }

/* Trims taken — partial exits listed as part of the period's recap. */
.recap-trims { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--border-1); }
.recap-trims-hd { display: flex; align-items: center; gap: 8px; font-size: var(--t-sm); letter-spacing: .08em; text-transform: uppercase; color: var(--fg-2); margin-bottom: 8px; }
.recap-trims-hd .rt-note { text-transform: none; letter-spacing: 0; font-size: var(--t-xs); color: var(--fg-3); }
.rl-sq.trim { background: var(--c-trim); }
.recap-trim-line { cursor: default; }

/* ── Period recaps (Daily / Weekly / Monthly / Quarterly / Half-yearly / Yearly) */
.recap-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
/* The period names print as written ("Half-yearly"): the Access filter's global capitalize
   on .seg-btn would make it "Half-Yearly". */
.recap-period-seg .seg-btn { text-transform: none; }
/* The Recaps count keeps one line beside its controls, which wrap instead; on a phone, where
   it has a row of its own, it may break at the dot (the 900px block). */
.recap-count, .recap-count > span { white-space: nowrap; }
.recap-periods { display: flex; flex-direction: column; gap: 14px; margin-top: 4px; }
.recap-period {
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  overflow: hidden;
}
.recap-period-hdr {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 12px 18px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--border-1);
  flex-wrap: wrap;
}
.rp-label { font-weight: 600; font-size: var(--t-base); color: var(--fg-0); letter-spacing: -0.01em; display: flex; align-items: center; gap: 10px; }
/* Copy / Image: ALWAYS visible, never hover-revealed. A hover-only control simply
   cannot be reached on a phone, and `@media (hover: none)` is not a dependable
   proxy for touch — hybrid laptops and several Android browsers report hover, so
   the fallback missed exactly the devices it was meant to catch. Kept quiet with a
   ghost style instead, so the header still reads as a label and not a toolbar. */
.rp-copy {
  min-height: 30px;                /* comfortable tap target */
  color: var(--fg-2);
  border-color: var(--border-1);
  background: var(--bg-2);
}
.rp-copy:hover { color: var(--fg-0); border-color: var(--border-2); }
.rp-stats { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; font-size: var(--t-sm); color: var(--fg-2); }
.rp-stat b { font-family: var(--f-mono); font-weight: 600; }
.rp-stat b.up { color: var(--c-up); }
.rp-stat b.down { color: var(--c-down); }
/* What the header counted ("12 closes + 15 trims", "+2 awaiting price"): legible, fg-2. */
.rp-stat.rp-n { color: var(--fg-2); }
.recap-period-body { padding: 6px 10px 12px; display: flex; flex-direction: column; gap: 2px; }
.recap-an-group { padding: 4px 0; }
.recap-an-hd { display: flex; align-items: center; gap: 8px; padding: 12px 8px 6px; }
.recap-an-hd .an-name, .recap-an-hd .analyst-name { font-size: var(--t-md); letter-spacing: .06em; }
.recap-an-n { font-family: var(--f-mono); font-size: var(--t-sm); color: var(--fg-3); }

/* Closed trades read as a TABLE: one template drives the header row and every
   data row, declared once on the shared parent so the two can never drift apart.
   Capped width on purpose — as `1fr` the contract column stretched to ~1700px on a
   wide monitor and marooned the prices at the far edge.
   The figures' columns and the cap are in rem, so they grow with the figures in them at every text
   size and browser font size: in px, a larger size pushed the peak pill over the P&L beside it and a
   four-digit P&L over the exit. The widest in the book (October 2026): a round-trip pill, about 7rem
   with its padding at Small, in 7.25rem; "+3,570.8%", 4.9rem, in 5.375rem. The body is a container,
   so one too narrow for these columns takes the phone's two-line row (the @container block below). */
.recap-period-body { --rl-cols: 10px minmax(0, 1fr) 4.875rem 4.875rem 5.375rem 7.25rem 3.25rem; container-type: inline-size; container-name: recap-body; }
.recap-cols, .recap-line {
  display: grid;
  grid-template-columns: var(--rl-cols);
  /* 20px, not 14: the peak PILL carries a border and background, so at a 14px gap
     it crowded the coloured P&L figure it is meant to be read against. */
  align-items: center; gap: 20px;
  max-width: 60rem;
}
/* Column names: the face, size and tracking of the tables' heads (.tbl thead th), and the face of the
   leaderboard's above. */
.recap-cols {
  padding: 10px 10px 6px;
  font-family: var(--f-sans); font-weight: 500; font-size: var(--t-2xs); letter-spacing: .08em;
  text-transform: uppercase; color: var(--fg-3);
  border-bottom: 1px solid var(--border-1); margin-bottom: 2px;
}
/* Everything from Avg entry rightwards is numeric — right-align header AND cell.
   Scoped away from trim rows, which are a DIFFERENT 5-cell shape (see below). */
.recap-cols > :nth-child(n+3),
.recap-line:not(.recap-trim-line) > :nth-child(n+3) { text-align: right; justify-self: end; }
/* Trims are partial exits listed under the closed table and carry no exit/peak/W-L,
   so they keep their own 5-column shape rather than being forced into the table's
   seven. They inherit --rl-cols from the same parent, hence the explicit override. */
.recap-trim-line {
  grid-template-columns: 10px minmax(72px, max-content) minmax(0, 1fr) max-content minmax(74px, max-content);
  gap: 18px;
}
.recap-line {
  padding: 9px 10px; border-radius: 8px; cursor: pointer;
}
/* Ticker over contract: one identity cell, so the numeric columns start together. */
.rl-name { display: flex; flex-direction: column; gap: 0; min-width: 0; line-height: 1.25; }
/* The contract is the SECOND line of the identity — quieter than the ticker so the
   eye lands on the symbol first and the row stays compact. */
.rl-name .rl-con { font-size: var(--t-xs); color: var(--fg-3); }
.rl-name .rl-tkr { line-height: 1.2; }
/* The ticker plus, for a contract sold to open, its SOLD tag: one unbreakable unit
   kept OUTSIDE the contract, so on a phone the contract ellipsises, never the tag.
   It never shrinks: in the phone row (.rl-name is a flex row there) a shrinkable unit
   squeezed below its text and drew the tag over the contract. */
.rl-tkr-line { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; flex-shrink: 0; }
.rl-tkr-line .inst-tag.sold { margin-right: 0; }
.recap .contract .rc-sold { margin: 0 0 0 8px; vertical-align: 1px; }
.rl-num { font-family: var(--f-mono); font-size: var(--t-md); color: var(--fg-1); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rl-num.muted { color: var(--fg-3); }
.rl-peakcell { display: flex; justify-content: flex-end; }
.rl-dash { color: var(--fg-3); font-family: var(--f-mono); font-size: var(--t-md); }
.rl-wl { font-family: var(--f-mono); font-size: var(--t-2xs); font-weight: 700; letter-spacing: .08em; }
.rl-wl.win { color: var(--c-up); }
.rl-wl.loss { color: var(--c-down); }
.rl-wl.wait { color: var(--fg-3); }
/* BE, a 0.0% break-even: the neutral tone, never green and never yellow (yellow is a TRIM).
   Text in fg-2, marks (the dot, a bar) in fg-3. WAIT, which is not scored at all, is fg-3 text:
   since v2.9.0 that is the same ink as fg-2 in light and close to it in dark, so the word, not a
   colour step, tells the two apart. */
.rl-wl.even, .rl-pct.even, .recap .pnl.even, .recap-stat .rs-val.even, .rb-avg.even,
.recap-chart-meta .even, .rp-rec .even, .ri-dow-v.even, .rc-bar.even .rc-bar-val { color: var(--fg-2); }
.rl-sq.even, .recap-wlbar .wl-even, .rc-bar.even .rc-bar-fill, .ri-bar.even { background: var(--fg-3); }
.rp-rec .up { color: var(--c-up); }
.rp-rec .down { color: var(--c-down); }
.recap-line + .recap-line { border-top: 1px solid var(--border-1); border-radius: 0; }
.recap-line:hover, .recap-line.open { background: var(--bg-2); border-radius: 8px; }
.rl-sq { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.rl-sq.win { background: var(--c-up); }
.rl-sq.loss { background: var(--c-down); }
.rl-tkr { font-family: var(--f-mono); font-weight: 600; color: var(--c-ticker); font-size: 0.90625rem; letter-spacing: -0.01em; }
/* The contract identifies the trade — it was dimmer than the numbers beside it. */
.rl-con { font-size: var(--t-md); color: var(--fg-1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rl-mv { font-size: 0.84375rem; color: var(--fg-1); white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
.rl-arr { color: var(--fg-deco); padding: 0 2px; }
/* Result cell: the P&L, then the peak pill after it. One grid child (the row is a
   fixed 5-column grid, so neither may be a grid child of its own). */
.rl-res { display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
.rl-res .rl-pct { min-width: 0; }
.peak-pill {
  font-family: var(--f-mono); font-size: 0.65625rem; font-weight: 600;   /* off the scale: a step up filled the Peak column to the pixel at Small (the widest pill, "↩ peak +195.9%") */
  letter-spacing: .03em; white-space: nowrap;
  padding: 2px 7px; border-radius: 999px;
  background: var(--bg-2); border: 1px solid var(--border-1); color: var(--fg-2);
}
/* A round trip carries no action hue (the watch purple it wore read as a WATCH tag):
   the neutral recipe above, a step brighter, with the ↩ glyph as its marker. */
.peak-pill.rt { color: var(--fg-1); }
/* A body too narrow for the table takes the phone's two-line row, at any window size (a laptop at
   Larger text, or a large font size set in the browser). Rem, so a larger text size moves the break
   with it. At the break the name column is 4rem at Small, 5.04rem at Default and 5.9rem at Larger,
   while the widest ticker line with its SOLD tag is about 5.7rem ("$CRWD SOLD"): just above the
   break (about 937-963px windows at Small, 1018-1021px at Default) a SOLD row runs up to 22px into
   the gutter, never onto the Avg figure (29px of air at the closest). Breaking on the real need,
   calc(31.375rem + 150px), would send a 1024px window at Default to two lines for that sliver. The same rules as the rows' in the 900px block, which keeps them for a browser
   without container queries; change both together. (That block's `.rl-num:first-of-type` indent never
   applies, since the marker is the row's first span, so it is not repeated here.) */
@container recap-body (max-width: 39rem) {
  .recap-cols { display: none; }
  .recap-line, .recap-line.recap-trim-line {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 3px 7px; max-width: none; padding: 9px 6px;
  }
  .recap-line::after { content: ""; flex-basis: 100%; height: 0; order: 5; }
  .recap-line > .rl-sq { order: 1; align-self: center; flex: 0 0 auto; }
  .recap-line > .rl-name {
    order: 2; flex: 1 1 0; min-width: 0;
    flex-direction: row; align-items: baseline; gap: 6px;
    overflow: hidden;
  }
  .recap-line > .rl-name .rl-con { font-size: var(--t-sm); }
  .recap-line > .rl-pct { order: 3; margin-left: auto; white-space: nowrap; }
  .recap-line > .rl-wl  { order: 4; flex: 0 0 auto; }
  .recap-line > .rl-num { order: 6; font-size: var(--t-md); color: var(--fg-2); }
  .recap-line > .rl-num + .rl-num { order: 7; }
  .recap-line > .rl-num + .rl-num::before { content: "→"; color: var(--fg-deco); margin-right: 5px; }
  .recap-line > .rl-peakcell { order: 8; margin-left: auto; justify-content: flex-end; }
  .recap-line.recap-trim-line > .rl-tkr-line { order: 2; flex: 0 0 auto; }
  .recap-line.recap-trim-line > .rl-con { order: 3; flex: 1 1 0; min-width: 0; font-size: var(--t-sm); }
  .recap-line.recap-trim-line > .rl-pct { order: 4; margin-left: auto; }
  .recap-line.recap-trim-line > .rl-mv  { order: 6; margin-left: 16px; text-align: left; font-size: var(--t-md); color: var(--fg-2); }
}

/* ── Headlines inside an expanded position ─────────────────────────────────── */
.pos-news { margin-top: 14px; }
.pos-news-item {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 8px 0; text-decoration: none; color: inherit;
  border-top: 1px solid var(--border-1);
}
.pos-news-item:hover .pos-news-hl { color: var(--accent); }
.pos-news-img { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; flex: none; background: var(--bg-2); }
.pos-news-txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.pos-news-hl { font-size: var(--t-md); line-height: 1.4; color: var(--fg-1); transition: color .12s ease; }
.pos-news-meta { font-size: var(--t-xs); color: var(--fg-3); text-transform: uppercase; letter-spacing: 0.04em; }

/* ── Add-to-home-screen nudge ───────────────────────────────────────────────
   A bottom sheet on mobile (where installing actually happens) and a corner card
   on desktop. Deliberately not a modal — it must never block reading the tape. */
.install-nudge {
  /* z-index 15, the rating card's layer: under the top bar's (20), so the phone's bell sheet (inside
     the top bar) paints over it, and under the drawer, menus, flyouts, the first-run alert modal and
     the tour (an install nudge must never sit on top of a dialog awaiting an answer); still over
     the page. It also steps out of sight while one of them is open (below). */
  position: fixed; z-index: 15;
  left: 12px; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  background: var(--bg-1, var(--bg-2)); border: 1px solid var(--border-2, var(--border-1));
  border-radius: 14px; box-shadow: var(--shadow-2);
  padding: 14px 14px 12px;
  animation: install-in .22s cubic-bezier(.22,1,.36,1);
}
@keyframes install-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .install-nudge { animation: none; } }
/* The corner card from 720px (a computer, a tablet, a phone held sideways, where the home bar sits
   at the bottom too). */
@media (min-width: 720px) { .install-nudge { left: auto; right: calc(18px + env(safe-area-inset-right, 0px)); bottom: calc(18px + env(safe-area-inset-bottom, 0px)); width: 380px; } }
/* Like the rating card, it waits out of sight, where it was, while a dialog, a flyout (an event, a
   profile), the bell, the phone's filter sheet or menu (html.scroll-locked), the tour or the Ask AI
   panel is open: on a phone the bell's list and the menu's Feedback and Sign out were under it. Its
   show was counted when it appeared, so hiding costs none; toasts drop back down meanwhile
   (useCardLift reads its height as 0). */
body:has(.modal-back, .flyout, .notif-panel, .fsheet, .asst-panel, .tour-root, .announce-back) .install-nudge,
html.scroll-locked .install-nudge { display: none; }
.install-x { position: absolute; top: 8px; right: 8px; }
.install-body { display: flex; gap: 12px; align-items: flex-start; padding-right: 22px; }
.install-icon {
  width: 34px; height: 34px; flex: none; border-radius: 10px;
  display: grid; place-items: center; background: var(--bg-3, var(--bg-2)); color: var(--accent);
}
.install-title { font-size: var(--t-base); font-weight: 650; color: var(--fg-0); margin-bottom: 4px; }
.install-sub { font-size: var(--t-sm); color: var(--fg-2); line-height: 1.5; }
.install-sub strong { color: var(--fg-1); font-weight: 600; }
.install-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }

/* ── First-run alert setup ──────────────────────────────────────────────────
   One screen, both channels pre-selected. Shown once per account. */
.notif-setup { max-width: 420px; padding: 22px 22px 18px; }
.notif-setup-hd { text-align: center; margin-bottom: 18px; }
.notif-setup-icon {
  display: inline-grid; place-items: center; width: 42px; height: 42px;
  border-radius: 12px; background: var(--bg-3, var(--bg-2)); color: var(--accent); margin-bottom: 10px;
}
.notif-setup-hd h3 { margin: 0 0 6px; font-size: var(--t-lg); font-weight: 650; color: var(--fg-0); }
.notif-setup-hd p { margin: 0; font-size: var(--t-md); color: var(--fg-2); line-height: 1.5; }
.notif-setup-opts { display: flex; flex-direction: column; gap: 8px; }
.notif-setup-opt {
  display: flex; align-items: flex-start; gap: 11px; cursor: pointer;
  padding: 12px 13px; border: 1px solid var(--border-1); border-radius: 10px;
  background: var(--bg-2); transition: border-color .12s ease, background .12s ease;
}
.notif-setup-opt:hover { border-color: var(--border-2, var(--border-1)); }
.notif-setup-opt.on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-2)); }
/* An option this browser can't take: its box and title look off, but the line under them says why
   and what to do (add to Home Screen), so it keeps its full colour. The whole option at .6 put that
   line at 2.3 to 2.7:1 on some brands. */
.notif-setup-opt.disabled { cursor: not-allowed; }
.notif-setup-opt.disabled input, .notif-setup-opt.disabled .notif-setup-opt-txt strong { opacity: .6; }
.notif-setup-opt input { margin-top: 2px; accent-color: var(--accent); width: 16px; height: 16px; flex: none; }
.notif-setup-opt-txt { display: flex; flex-direction: column; gap: 3px; }
.notif-setup-opt-txt strong { font-size: 0.84375rem; font-weight: 600; color: var(--fg-0); }
.notif-setup-opt-txt span { font-size: var(--t-sm); color: var(--fg-2); line-height: 1.45; }
/* On a ticked option's tint the quieter tier fell to 4.1 to 4.5:1 on six brand themes; the next tier
   up reads 7.2:1 or better on all of them. */
.notif-setup-opt.on .notif-setup-opt-txt span { color: var(--fg-1); }
.notif-setup-actions { display: flex; gap: 8px; justify-content: flex-end; }
/* "Tick at least one, or choose Not now.": over the buttons it explains, after the choices. */
.notif-setup-why { margin: 0 0 10px; text-align: right; }
/* The buttons and the line under them, pinned to the bottom of the card while it scrolls (a phone
   held sideways: the card is taller than the screen). bottom: -18px cancels the card's 18px bottom
   padding, so the bar meets the card's edge, and the bar's own 18px keeps the line clear of it; the
   -18px margin gives that padding back at the end. The 18px above the buttons is the margin plus
   the padding. Where nothing scrolls (an upright phone, a computer) it sits exactly where it was.
   Opaque, the modal's own surface. */
.notif-setup-end {
  position: sticky; bottom: -18px; z-index: 1;
  background: var(--bg-1);
  margin: 6px 0 -18px; padding: 12px 0 18px;
}
/* Sideways, the card gives its height to the choices: no icon above the title. */
@media (max-height: 500px) and (orientation: landscape) {
  .notif-setup-icon { display: none; }
  .notif-setup-hd { margin-bottom: 12px; }
}
/* --fg-2: --fg-3 read 2.8:1 here, on the card every new member sees, around its own link. */
.notif-setup-foot { margin-top: 12px; text-align: center; font-size: var(--t-xs); color: var(--fg-2); }
/* Its "Settings → Preferences" opens that page (notifications.jsx openPrefs). */
.notif-setup-link {
  padding: 0; border: 0; background: none; font: inherit; color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.notif-setup-link:hover { color: var(--fg-0); }
.notif-setup-link:disabled { color: inherit; text-decoration: none; cursor: default; }
/* Its answer, said on the card itself (notifications.jsx): a row per part the member ticked, on,
   or not and why. A part that didn't turn on is marked in --c-warn (needs attention, not a
   result), never the loss red; the rows that are on stay neutral, so it stands out on every brand. */
.notif-setup-icon.warn { color: var(--c-warn); }
.notif-setup-res {
  display: flex; align-items: flex-start; gap: 11px;
  padding: 12px 13px; border: 1px solid var(--border-1); border-radius: 10px; background: var(--bg-2);
}
.notif-setup-res-icon { display: grid; place-items: center; flex: none; margin-top: 1px; }
.notif-setup-res.ok .notif-setup-res-icon { color: var(--accent); }
.notif-setup-res.bad { border-color: color-mix(in srgb, var(--c-warn) 55%, var(--border-1)); }
.notif-setup-res.bad .notif-setup-res-icon { color: var(--c-warn); }
/* The account didn't take the card's answer (it holds on this device): one quiet line under the rows. */
.notif-setup-note { margin: 2px 0 0; font-size: var(--t-sm); line-height: 1.45; color: var(--fg-2); }

/* "Alerts only show here" cue at the top of the bell list — the state used to be
   completely invisible. */
.notif-cta {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; margin: 0 0 4px;
  background: var(--bg-2); border-bottom: 1px solid var(--border-1);
  font-size: var(--t-sm); color: var(--fg-2);
}
.notif-cta span { flex: 1; line-height: 1.4; }

/* ── The installed app's push offer ─────────────────────────────────────────
   One quiet card at the top of Open Positions, once per device, after the first-run
   prompts. In the flow (never over the book), so nothing it does can cover a row. */
.push-offer {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; border: 1px solid var(--border-1); border-radius: var(--radius);
  background: var(--bg-1); box-shadow: inset 3px 0 0 var(--accent);
}
.push-offer-icon {
  flex: none; width: 32px; height: 32px; border-radius: 9px;
  display: grid; place-items: center; background: var(--bg-3, var(--bg-2)); color: var(--accent);
}
.push-offer-txt { flex: 1; min-width: 0; }
.push-offer-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); line-height: 1.4; }
.push-offer-sub { font-size: var(--t-sm); color: var(--fg-2); line-height: 1.45; margin-top: 2px; }
.push-offer-acts { flex: none; display: flex; gap: 8px; }
@media (max-width: 560px) {
  .push-offer { flex-wrap: wrap; align-items: flex-start; }
  .push-offer-txt { flex: 1 1 calc(100% - 44px); }
  .push-offer-acts { width: 100%; justify-content: flex-end; }
}
/* The settings body rendered inline in Settings → Preferences rather than in the
   bell popover. Needs the extra specificity — the base `.notif-settings` rule is
   defined later in this file and would otherwise win on source order. */
.notif-settings-inline > .notif-settings { max-height: none; overflow: visible; padding: 0; }

/* ── Personal watchlist ─────────────────────────────────────────────────────
   A member's own tracked symbols — deliberately a plain list, not a data grid. */
.mywl-add { display: flex; gap: 8px; align-items: center; }
.mywl-add .input { width: 150px; text-transform: uppercase; }
.mywl-rows { display: flex; flex-direction: column; }
.mywl-row {
  display: grid; grid-template-columns: 1fr auto auto 32px;
  align-items: center; gap: 14px;
  padding: 10px 18px; border-bottom: 1px solid var(--border-1);
}
.mywl-row:last-child { border-bottom: 0; }
.mywl-row:hover { background: var(--bg-2); }
.mywl-sym { font-family: var(--f-mono); font-weight: 600; color: var(--c-ticker); font-size: 0.90625rem; }
.mywl-px { color: var(--fg-0); font-variant-numeric: tabular-nums; }
.mywl-chg { font-size: var(--t-md); min-width: 66px; text-align: right; font-variant-numeric: tabular-nums; }
.mywl-chg.up { color: var(--c-up); } .mywl-chg.down { color: var(--c-down); }
.mywl-row .icon-btn { opacity: 0; transition: opacity .12s ease; }
.mywl-row:hover .icon-btn, .mywl-row .icon-btn:focus-visible { opacity: 1; }
@media (hover: none) { .mywl-row .icon-btn { opacity: 1; } }

/* ── Market session bar ─────────────────────────────────────────────────────
   A quiet status line above the summary, not a banner competing with the data. */
.mkt-session {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--t-sm); color: var(--fg-2);
  padding: 7px 12px; margin-bottom: 10px;
  background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 8px;
}
.mkt-session b { color: var(--fg-0); font-weight: 600; }
.mkt-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--fg-deco); flex: none; }
.mkt-session.is-open .mkt-dot { background: var(--c-up); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-up) 22%, transparent); }
.mkt-session.is-closed .mkt-dot { background: var(--c-watch); }
.mkt-et { margin-left: auto; color: var(--fg-3); font-size: var(--t-xs); }

/* ── Per-contract greeks ─────────────────────────────────────────────────── */
.greeks-strip { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 4px; }
.gk {
  display: inline-flex; align-items: baseline; gap: 5px;
  background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 6px;
  padding: 3px 8px; font-size: var(--t-sm);
}
.gk-k { color: var(--fg-3); font-weight: 600; }
.gk-v { color: var(--fg-1); font-variant-numeric: tabular-nums; }

/* ── Sector mix ──────────────────────────────────────────────────────────── */
.sector-conc { margin: 0 0 14px; }
.sc-toggle {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  color: var(--fg-1); font-size: var(--t-md); font-weight: 600; text-align: left;
}
.sc-toggle:hover { color: var(--fg-0); }
.sc-hint { font-weight: 400; color: var(--fg-3); font-size: var(--t-sm); }
.sc-body { margin-top: 10px; display: flex; flex-direction: column; gap: 6px; }
.sc-row { display: grid; grid-template-columns: minmax(96px, 132px) 1fr 40px; align-items: center; gap: 10px; font-size: var(--t-sm); }
.sc-name { color: var(--fg-1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sc-bar-wrap { background: var(--bg-3, var(--bg-2)); border-radius: 3px; height: 7px; overflow: hidden; }
.sc-bar { display: block; height: 100%; background: var(--accent); border-radius: 3px; }
.sc-pct { color: var(--fg-2); text-align: right; font-variant-numeric: tabular-nums; }
.sc-foot { font-size: var(--t-xs); color: var(--fg-3); margin-top: 4px; }
/* Unclassified (tickers with no sector), always last: a muted bar, never the accent, so the honest
   bucket stays visible without reading as the biggest sector. */
.sc-row.sc-other .sc-name { color: var(--fg-2); }
.sc-row.sc-other .sc-bar { background: var(--fg-3); }
/* The sectors could not load: the row keeps its place and says so, without a toggle. */
.sc-toggle.sc-off { cursor: default; }
.sc-toggle.sc-off:hover { color: var(--fg-1); }
/* One line, always: the row sits right above the list on a phone, and a second line there pushes the
   first position down. The second sector gives way on a phone; the hint ends in "…" only as a last
   resort (a very long sector name at the largest text size). */
.sc-toggle { white-space: nowrap; }
.sc-hint { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 600px) { .sc-hint-more { display: none; } }

/* ── Recap insights (streaks / payoff / giveback / weekday) ─────────────────
   Collapsed by default: the headline numbers already sit above it, and this is
   the "why" a reader opens deliberately rather than a wall of extra figures. */
.recap-insights { border-top: 1px solid var(--border-1); margin-top: 14px; padding-top: 12px; }
.ri-toggle {
  display: flex; align-items: center; gap: 8px; width: 100%;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  color: var(--fg-1); font-size: var(--t-md); font-weight: 600; text-align: left;
}
.ri-toggle:hover { color: var(--fg-0); }
.ri-hint { font-weight: 400; color: var(--fg-3); font-size: var(--t-sm); }
.ri-body { margin-top: 12px; display: flex; flex-direction: column; gap: 16px; }
.ri-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 10px; }
.ri-tile { background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 8px; padding: 10px 12px; }
.ri-k { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-3); }
.ri-v { font-family: var(--f-mono); font-size: 1.125rem; font-weight: 600; margin-top: 4px; font-variant-numeric: tabular-nums; }
.ri-v.up { color: var(--c-up); } .ri-v.down { color: var(--c-down); } .ri-v.watch { color: var(--c-watch); }
.ri-s { font-size: var(--t-xs); color: var(--fg-3); margin-top: 2px; }
.ri-dow-row { display: flex; align-items: flex-end; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.ri-dow-cell { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 44px; }
.ri-bar-wrap { height: 36px; display: flex; align-items: flex-end; }
.ri-bar { width: 22px; border-radius: 2px; }
.ri-bar.up { background: var(--c-up); } .ri-bar.down { background: var(--c-down); }
.ri-dow-v { font-family: var(--f-mono); font-size: var(--t-xs); font-variant-numeric: tabular-nums; }
.ri-dow-v.up { color: var(--c-up); } .ri-dow-v.down { color: var(--c-down); }
.ri-dow-l { font-size: var(--t-xs); color: var(--fg-3); }
.rl-pct { font-family: var(--f-mono); font-weight: 600; font-size: 0.90625rem; text-align: right; min-width: 74px; font-variant-numeric: tabular-nums; }
.rl-pct.up { color: var(--c-up); }
.rl-pct.down { color: var(--c-down); }
/* ── Recaps: best/worst callouts + analyst leaderboard ───────────────────── */
/* The best / worst callouts sit beside the leaderboard only where both get their room (a
   wide screen); anywhere narrower the board wraps under them and takes the full width, and
   the two callouts, now a row of their own, sit side by side. A callout is never narrower
   than 22rem on a computer, the room a name needs beside its label, ticker and figure: two
   share a row only when each gets that, and the column beside the board is at least that
   wide (any narrower cut the name to its first few letters). Bases in rem, so a larger
   text size moves the break with it. */
.recap-highlights { display: flex; flex-wrap: wrap; gap: 14px; }
.recap-bestworst { display: flex; flex-wrap: wrap; align-content: flex-start; gap: 8px; flex: 1 1 22rem; min-width: 0; }
.rbw { flex: 1 1 22rem; min-width: 0; display: flex; align-items: center; gap: 10px; padding: 9px 12px; background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius); }
.rbw-lbl { font-family: var(--f-sans); font-weight: 500; font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: .06em; min-width: 56px; flex-shrink: 0; }   /* a label: the leaderboard heading's face */
.rbw-lbl.up { color: var(--c-up); }
.rbw-lbl.down { color: var(--c-down); }
.rbw .ticker { font-size: var(--t-base); flex-shrink: 0; }
/* The figure never gives way: it is the point of the callout. */
.rbw .rl-pct { margin-left: auto; flex-shrink: 0; }
/* On a computer the name gives way instead, to an ellipsis on one line (the whole name is
   in its title, and one click away on the profile). A phone wraps it; see the 900px block. */
@media (min-width: 901px) {
  .rbw .analyst { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .rbw .analyst > .swatch, .rbw .analyst > .analyst-pic { display: inline-block; vertical-align: middle; margin: -2px 7px 0 0; }
}
/* A real table: five figures per analyst need column heads, and a basis wide enough that
   the longest names keep one line beside them. Narrower than that (beside the callouts),
   it wraps under them and takes the full width. position: relative keeps the screen-reader
   words below (absolutely positioned) inside the board and its scroller: without it they
   belonged to the page itself, which grew taller than the window and let a mouse wheel
   scroll the whole app off screen. The container lets the board pick its layout by its own
   width (the @container block below). */
.recap-board { flex: 1 1 44rem; min-width: 0; position: relative; container-type: inline-size; container-name: board; background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius); padding: 11px 14px; }
.recap-board-hd { font-family: var(--f-sans); font-weight: 500; font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: .06em; color: var(--fg-1); margin-bottom: 9px; }
/* The ranking rule is information, not decoration: legible on every theme. */
.recap-board-hd .rb-hd-rule { color: var(--fg-2); }
/* Rem widths, so the columns grow with the reader's text size and stay aligned row to row. */
.recap-board-cols, .recap-board-row {
  display: grid; grid-template-columns: minmax(0, 1fr) 3.5rem 3rem 4.75rem 4.75rem 5rem;
  gap: 12px; align-items: center;
}
/* While trims count, the first figure is the whole sample, "8 + 10" under "Closes + trims". */
.recap-board.with-trims .recap-board-cols, .recap-board.with-trims .recap-board-row { grid-template-columns: minmax(0, 1fr) 5.5rem 3rem 4.75rem 4.75rem 5rem; }
.recap-board-cols { font-family: var(--f-sans); font-weight: 500; font-size: 0.5625rem; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-2); padding-bottom: 6px; }
.recap-board-cols > span + span { text-align: right; }
.recap-board-row { padding: 7px 0; font-size: var(--t-sm); border-top: 1px solid var(--border-1); }
/* The two pairs dissolve into the table's cells here; a phone gives them a line of their own. */
.recap-board-row .rb-meta, .recap-board-row .rb-wl { display: contents; }
.rb-n, .rb-wr, .rb-aw, .rb-al, .rb-avg { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.rb-n, .rb-wr { color: var(--fg-1); }
.rb-aw.up, .rb-avg.up { color: var(--c-up); }
.rb-al.down, .rb-avg.down { color: var(--c-down); }
.rb-avg { font-weight: 600; }
.recap-board-legend { display: none; }
/* The words beside each figure: off screen but still read aloud, since a screen reader hears
   no column heads (rb-k shows on a phone, where there are none on screen either). */
.rb-k, .rb-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* "Fewer than 10 closes · not ranked": a label, in the face and size of the column names above. */
.recap-board-sep {
  font-family: var(--f-sans); font-weight: 500; font-size: 0.5625rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--fg-2); padding: 14px 0 6px; border-top: 1px solid var(--border-2);
}
/* A best or worst result that was a trim says so: it is not a closed trade. */
.rbw-trim { font-family: var(--f-mono); font-size: 0.5625rem; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-2); flex-shrink: 0; }
/* A board too narrow for the table takes the phone's two-line card, at any window size: below
   40.5rem the name column beside the five figures drops under about 14rem and names start to
   wrap (a laptop, or half of a large monitor). The same rules as the board's in the 900px
   block, which keeps them for a browser without container queries; change both together. */
@container board (max-width: 40.5rem) {
  .recap-board-cols { display: none; }
  .recap-board-legend { display: block; font-size: var(--t-xs); color: var(--fg-2); margin: -3px 0 6px; }
  .recap-board-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px; }
  .recap-board-row::after { content: ""; flex-basis: 100%; height: 0; order: 3; }
  .recap-board-row > .analyst { order: 1; flex: 1 1 0; min-width: 0; }
  .recap-board-row > .rb-avg { order: 2; margin-left: auto; }
  .recap-board-row .rb-meta, .recap-board-row .rb-wl { display: flex; font-size: 0.65625rem; color: var(--fg-2); }   /* off the scale: a step up took a third line */
  .recap-board-row .rb-meta { order: 4; gap: 0 10px; padding-left: 16px; }
  .recap-board-row:has(.analyst-pic) .rb-meta { padding-left: 31px; }
  .recap-board-row .rb-wl { order: 5; gap: 0 5px; margin-left: auto; }
  .recap-board-row .rb-n, .recap-board-row .rb-wr { color: var(--fg-2); }
  .rb-wl .rb-al::before { content: "/"; color: var(--fg-deco); margin-right: 5px; }
  .rb-k { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
}
.recap-line-detail { padding: 6px 4px 12px; }
.recap-line-detail .recap { background: var(--bg-2); }

/* Published-recap overlay (shown for servers with show_recap on, Daily view) */
.pub-recap { margin: 4px 10px 8px; border: 1px solid var(--border-2); border-radius: 10px; background: var(--bg-2); overflow: hidden; }
.pub-recap-hdr { display: flex; align-items: center; gap: 10px; padding: 9px 12px; cursor: pointer; flex-wrap: wrap; }
.pub-tag { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-xs); font-weight: 600; letter-spacing: .03em; text-transform: uppercase; color: var(--c-up); }
.pub-stats { font-size: var(--t-sm); color: var(--fg-2); }
.pub-stats .pub-stat { white-space: nowrap; }
.pub-badge { font-size: var(--t-xs); font-weight: 600; padding: 2px 8px; border-radius: 999px; margin-left: auto; }
.pub-badge.ok { color: var(--c-up); background: color-mix(in srgb, var(--c-up) 14%, transparent); }
.pub-badge.warn { color: var(--c-watch); background: color-mix(in srgb, var(--c-watch) 16%, transparent); }
.pub-caret { color: var(--fg-deco); font-size: var(--t-xs); }
.pub-recap-body { padding: 4px 12px 12px; border-top: 1px solid var(--border-1); display: flex; flex-direction: column; gap: 3px; }
.pub-line { display: flex; align-items: center; gap: 9px; padding: 5px 2px; font-size: var(--t-sm); flex-wrap: wrap; }
/* Says which record is which, for every member: quiet, but legible. */
.pub-note { padding: 0 12px 10px; margin-top: -2px; font-size: var(--t-xs); line-height: 1.45; color: var(--fg-2); }
.pub-an { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-2); min-width: 60px; }
.pub-flag { font-size: var(--t-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--c-watch); background: color-mix(in srgb, var(--c-watch) 15%, transparent); padding: 1px 6px; border-radius: 5px; }
.pub-fix { font-size: var(--t-2xs); font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--c-up); background: color-mix(in srgb, var(--c-up) 14%, transparent); padding: 1px 6px; border-radius: 5px; }
/* Light: opaque, like .pill (see there). */
[data-theme="light"] .pub-badge.ok, [data-theme="light"] .pub-fix { background: color-mix(in oklab, var(--c-up) 10%, var(--bg-1)); }
[data-theme="light"] .pub-badge.warn, [data-theme="light"] .pub-flag { background: color-mix(in oklab, var(--c-watch) 10%, var(--bg-1)); }
.pub-pod { margin-top: 6px; font-size: var(--t-sm); color: var(--fg-1); }

/* Owner-only reconciliation flags */
.recap-flags { margin-bottom: 14px; padding: 0; overflow: hidden; }
.recap-flags-hdr { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; cursor: pointer; font-size: var(--t-md); color: var(--fg-1); }
.recap-flags-hdr b { color: var(--c-watch); }
.recap-flags-body { border-top: 1px solid var(--border-1); display: flex; flex-direction: column; }
.recap-flag { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 16px; border-bottom: 1px solid var(--border-1); flex-wrap: wrap; }
.recap-flag:last-child { border-bottom: 0; }
.rf-main { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.rf-kind { font-size: var(--t-xs); font-weight: 600; text-transform: uppercase; letter-spacing: .03em; color: var(--c-watch); }
.rf-tkr { font-family: var(--f-mono); font-size: var(--t-md); color: var(--fg-0); }
.rf-detail { font-size: var(--t-xs); color: var(--fg-3); }
.rf-actions { display: flex; gap: 8px; }
.rf-actions .btn { padding: 6px 12px; font-size: var(--t-sm); }

.recap-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
  margin-top: 16px;
}
.recap {
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.recap .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
.recap .ticker-big {
  font-family: var(--f-mono);
  font-weight: 600;
  font-size: var(--t-xl);
  color: var(--c-ticker);
  letter-spacing: -0.01em;
}
.recap .contract { font-size: var(--t-sm); color: var(--fg-2); margin-top: 2px; }
.recap .pnl {
  font-family: var(--f-mono);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.recap .pnl.win { color: var(--c-up); }
.recap .pnl.loss { color: var(--c-down); }
.recap .pnl-sub { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-2); }
.recap .timeline {
  display: grid;
  grid-template-columns: repeat(var(--steps, 3), 1fr);
  gap: 1px;
  background: var(--border-1);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.recap .timeline .step {
  background: var(--bg-2);
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
}
.recap .timeline .step .when { color: var(--fg-2); font-size: var(--t-2xs); }
.recap .footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--fg-2);
  padding-top: 6px;
  border-top: 1px solid var(--border-1);
}
/* The pills, and under them, in words, what a ROUND TRIP was. */
.recap .recap-foot-r { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 5px; text-align: right; }
.recap-rt-note { font-size: var(--t-xs); color: var(--fg-2); }
/* "No exit price yet..." under the dash of an awaiting-price card. */
.recap .recap-card-note { max-width: 22rem; margin-left: auto; line-height: 1.45; white-space: normal; }

/* The way from the Recaps numbers to their definitions in the Guide: quiet, at the end. Words, so
   the sans face, as the list's "Show 50 more" below. */
.recap-help { display: flex; justify-content: flex-end; margin-top: -8px; }
.recap-help-btn {
  min-height: 2rem; padding: 0 2px; background: none; border: 0; cursor: pointer;
  font-family: var(--f-sans); font-size: var(--t-xs); color: var(--fg-2);
  text-decoration: underline; text-decoration-color: var(--border-3); text-underline-offset: 3px;
}
.recap-help-btn:hover { color: var(--fg-0); text-decoration-color: currentColor; }

/* Under a long list drawn a page at a time (shared.jsx ListMore): the next page as plain
   underlined text, and a muted line saying how many are shown and how to find another. Left on
   the list's own edge, never a centred call to action. */
/* Positioned, so its hidden live region (.sr-only, absolute) is placed and clipped inside the list's
   scroller: placed against the page instead, it hung below a long list and let the desktop page,
   which never scrolls, scroll by its height. */
.list-more { position: relative; display: flex; align-items: center; flex-wrap: wrap; gap: 4px 14px; padding: 10px 14px 12px; }
.list-more-btn {
  min-height: 2rem; padding: 0 2px; background: none; border: 0; cursor: pointer;
  font-family: var(--f-sans); font-size: var(--t-sm); color: var(--fg-1);
  text-decoration: underline; text-decoration-color: var(--border-3); text-underline-offset: 3px;
}
.list-more-btn:hover { color: var(--fg-0); text-decoration-color: currentColor; }
.list-more-note { font-size: 0.71875rem; color: var(--fg-2); }   /* off the scale: a step up put it on two lines on a phone */
@media (pointer: coarse) { .list-more-btn { min-height: 44px; } }
/* Recaps' day blocks and trade cards sit outside a panel: no inner padding to line up with. */
.recap-periods + .list-more, .recap-grid + .list-more { padding-left: 2px; padding-right: 2px; }

/* ── Forms (add channel / analyst) */
.form-card {
  background: var(--bg-2);
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 18px;
}
.form-card-hdr {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 14px;
}
.form-card-hdr h3 { margin: 0; font-size: var(--t-base); font-weight: 600; }
.form-card-hdr .server-tag { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-3); }
.form-grid { display: grid; gap: 12px; }
.field { display: flex; flex-direction: column; min-width: 0; }
.field-label {
  font-size: var(--t-xs); color: var(--fg-2); font-weight: 500;
  margin-bottom: 6px; letter-spacing: 0.01em;
}
.field-hint { font-size: var(--t-sm); color: var(--fg-3); }
/* An inline text action inside a hint ("paste an ID instead"). Needs a real
   affordance — it is a button rendered as text, so without this it reads as
   ordinary prose and nobody discovers it. */
.lnk {
  color: var(--accent); cursor: pointer; text-decoration: underline;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
}
.lnk:hover { color: var(--accent-dim); }
.lnk:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }
/* Live "id → Discord name" resolver shown under an id input in the add forms. */
.id-resolve { font-size: var(--t-xs); margin-top: 5px; min-height: 14px; display: flex; align-items: center; gap: 4px; }
.id-resolve-wait { color: var(--fg-3); }
.id-resolve-ok { color: var(--c-open); display: inline-flex; align-items: center; gap: 4px; }
.id-resolve-err { color: var(--c-trim); }
/* Analyst linked-accounts (bot/alt merged into one analyst) */
.linked-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; min-height: 20px; align-items: center; }
.linked-chip { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-sm); padding: 3px 6px 3px 10px; border-radius: 99px; background: var(--bg-2); border: 1px solid var(--border-2); color: var(--fg-1); }
.linked-chip-x { display: grid; place-items: center; width: 16px; height: 16px; border-radius: 99px; border: none; background: transparent; color: var(--fg-3); cursor: pointer; }
.linked-chip-x:hover { color: var(--c-down); background: color-mix(in oklab, var(--c-down) 12%, transparent); }
.linked-add { display: flex; gap: 8px; align-items: center; }
.linked-add .input { flex: 1; min-width: 0; }
.form-actions {
  display: flex; align-items: center; justify-content: space-between;
  gap: 14px; margin-top: 16px; flex-wrap: wrap;
}
/* The server editor's Cancel and Save, with "Name can't be empty." just before them: one group at the
   right, away from Reset data and Remove server on the left, and still at the right when the line runs
   out and it wraps under them. Inside, the reason takes a line of its own over the buttons when they
   don't fit beside it. (As two items each pushed right, the reason floated midway along the row.) */
.srv-edit-btns { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; margin-left: auto; }
.srv-name-why { margin-right: 6px; }
.form-err {
  margin-top: 12px; padding: 8px 11px;
  background: color-mix(in oklab, var(--c-down) 12%, transparent);
  border: 1px solid color-mix(in oklab, var(--c-down) 35%, transparent);
  border-radius: var(--radius-sm);
  font-family: var(--f-mono); font-size: var(--t-sm); color: var(--c-down);
  display: flex; align-items: center; gap: 6px;
}
.form-scope {
  display: flex; gap: 18px; align-items: center; flex-wrap: wrap;
  margin-top: 14px; font-size: var(--t-md); color: var(--fg-1);
}
.form-scope { align-items: flex-start; }
.form-scope label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
.form-scope .scope-title { font-size: var(--t-xs); color: var(--fg-2); font-weight: 500; padding-top: 8px; }

/* Server scope as selectable chips (nicer than raw checkboxes) */
.scope-chips { display: flex; flex-wrap: wrap; gap: 8px; }

/* ── Evidence ledger (Positions): provenance + urgency cues (LEDGER_UI) ──
   Every price on this page is a mark an analyst stated in Discord, so age and
   staleness are shown rather than implied. Stale = amber, never red. */
.mark-age { margin-left: 6px; font-size: 0.65625rem; color: var(--fg-3); letter-spacing: .02em; }
.mark-age.aging { color: var(--fg-2); }
.mark-age.stale { color: var(--c-trim); }
.mark-age.none  { color: var(--fg-3); font-style: italic; }
.upd-stale { color: var(--c-trim) !important; }
/* "5d ago" under a stale stamp, on a row with no age chip beside its price (a live mark, or none):
   the amber alone was its only cue. A line of its own, so the stamp's column (sized for the stamp,
   positions.jsx colgroup) never cuts it; the row is two lines tall anyway. */
.upd-quiet { display: block; margin-top: 3px; font-size: var(--t-xs); }

/* Days-to-expiry — computed from the contract spec (no market feed). */
/* --fg-1 on the chip's --bg-3: --fg-2 measured 4.0 to 4.1:1 there on a brand's light field. */
.dte-chip {
  margin-left: 6px; padding: 1px 5px; border-radius: 3px;
  font-family: var(--f-mono); font-size: 0.59375rem; letter-spacing: .04em;
  color: var(--fg-1); background: var(--bg-3); border: 1px solid var(--border-1);
  white-space: nowrap; vertical-align: middle;
}
.dte-chip.soon    { color: var(--c-trim); border-color: color-mix(in oklab, var(--c-trim) 45%, transparent); background: color-mix(in oklab, var(--c-trim) 10%, transparent); }
.dte-chip.today   { color: var(--c-down); border-color: color-mix(in oklab, var(--c-down) 50%, transparent); background: color-mix(in oklab, var(--c-down) 12%, transparent); }
/* Light: opaque, like .pill (see there). */
[data-theme="light"] .dte-chip.soon  { background: color-mix(in oklab, var(--c-trim) 10%, var(--bg-1)); }
[data-theme="light"] .dte-chip.today { background: color-mix(in oklab, var(--c-down) 10%, var(--bg-1)); }
/* Expired: a hollow chip, struck through, a step quieter than a live one. Its --fg-3 sits on the
   row, not on --bg-3, where it measured 4.0:1. */
.dte-chip.expired { color: var(--fg-3); background: transparent; text-decoration: line-through; }
/* "date unclear": an open option whose posted expiry is earlier than the trade (shared.jsx
   expiryLooksMisread). The neutral chip, never struck through (nothing has expired): words to read. */
.dte-chip.unclear { color: var(--fg-1); }
/* A position with no entry price whose exit stated its own result ("+220.0% trim", positions.jsx
   statedResultOf): the analyst's figure for that one exit, in the sign's colour but a step
   quieter than a P&L, with no live arrow. It never counts as the position's P&L. */
.pnl-stated { display: block; font-family: var(--f-mono); font-size: var(--t-md); font-weight: 600; }
.pnl-stated.up, .mpos-stated.up { color: var(--c-up); }
.pnl-stated.down, .mpos-stated.down { color: var(--c-down); }
.pnl-stated-act { margin-left: 5px; font-weight: 400; color: var(--fg-2); }
.mpos-stated { font-family: var(--f-mono); font-size: var(--t-base); font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Under the list: how many open positions are held back for having no entry price. Set like the
   paging line (.list-more) it follows. */
.pos-hidden-note { padding: 10px 14px 12px; font-size: var(--t-sm); color: var(--fg-2); }
.list-more + .pos-hidden-note { padding-top: 0; }

/* Analyst colour edge — categorical reinforcement; the chip stays the signal. */
.pos-row-led > td:first-child { box-shadow: inset 2px 0 0 var(--an-col, var(--accent)); }
/* Expansion scent: hints there's a timeline inside without opening the row. */
/* Its own line under the ticker, never beside it: beside it, a narrow column broke it in
   pieces ("$NBIS 1" / "event") on some rows and not others. */
.pos-scent { display: block; margin-top: 2px; font-family: var(--f-mono); font-size: var(--t-2xs); color: var(--fg-3); white-space: nowrap; }
/* Where this row's prices came from (positions.jsx provenanceLine): data, so fg-2. */
.pd-provenance { margin: 2px 0 10px; font-size: var(--t-sm); color: var(--fg-2); font-style: italic; }
/* "· market" / "· market at close" beside a mark the market set (a stated one gets its age). */
.mark-src { margin-left: 6px; font-size: 0.65625rem; color: var(--fg-2); letter-spacing: .02em; white-space: nowrap; }
.statgrid .val.val-warn { color: var(--c-trim); }
/* Numerals line up column-to-column even at different digit counts. */
.tbl td.num, .tbl td.mono, .pnl-sub, .pnl-big, .mpos-pct, .mpos-flow { font-variant-numeric: tabular-nums; }

/* Roles & capabilities matrix (Guide → Access & roles) */
.guide-matrix { overflow-x: auto; margin-top: 10px; border: 1px solid var(--border-1); border-radius: var(--radius); }
.guide-matrix table { border-collapse: collapse; width: 100%; min-width: 640px; font-size: var(--t-sm); }
.guide-matrix th, .guide-matrix td { padding: 7px 10px; border-bottom: 1px solid var(--border-1); text-align: center; white-space: nowrap; }
.guide-matrix th:first-child, .guide-matrix td:first-child { text-align: left; white-space: normal; min-width: 220px; color: var(--fg-1); }
.guide-matrix thead th { font-family: var(--f-sans); font-weight: 500; font-size: var(--t-2xs); letter-spacing: .05em; text-transform: uppercase; color: var(--fg-2); background: var(--bg-2); }
.guide-matrix tr:last-child td { border-bottom: none; }
.guide-matrix td.mx { font-family: var(--f-mono); }
.guide-matrix td.mx-yes { color: var(--c-up); }
.guide-matrix td.mx-no { color: var(--fg-3); }
.mx-own, .mx-tog { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-1); }

/* Per-server role assignment (Access editor): one row per server, its role
   select appearing when the server is toggled on. */
.scope-role-list { display: flex; flex-direction: column; gap: 8px; }
.scope-role-row { display: flex; align-items: center; gap: 10px; }
.scope-role-row .scope-chip { min-width: 180px; justify-content: flex-start; }
.scope-role-row .select { width: auto; min-width: 150px; padding-top: 6px; padding-bottom: 6px; }
@media (max-width: 760px) {
  .scope-role-row { flex-wrap: wrap; }
  .scope-role-row .scope-chip { min-width: 0; flex: 1 1 100%; }
}
.scope-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px 6px 8px; border-radius: 99px;
  border: 1px solid var(--border-2); background: var(--bg-2);
  color: var(--fg-1); font-size: var(--t-md); line-height: 1; cursor: pointer;
  transition: background 120ms, border-color 120ms, color 120ms;
}
.scope-chip:hover { border-color: var(--border-3); color: var(--fg-0); }
.scope-chip.on { background: var(--accent-glow); border-color: var(--accent-dim); color: var(--accent); }
.scope-chip-box {
  width: 16px; height: 16px; border-radius: 5px; flex-shrink: 0;
  display: grid; place-items: center;
  border: 1.5px solid var(--border-3); color: var(--bg-0); background: transparent;
}
.scope-chip.on .scope-chip-box { background: var(--accent); border-color: var(--accent); }

/* Analyst editor toggles — name + description with a switch on the right */
.toggle-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 14px; }
.toggle-card {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 11px 13px; border: 1px solid var(--border-1); border-radius: var(--radius);
  background: var(--bg-2); cursor: pointer; transition: border-color 120ms;
}
.toggle-card:hover { border-color: var(--border-3); }
.toggle-card .switch { flex-shrink: 0; }
.toggle-meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.toggle-name { font-size: var(--t-md); font-weight: 500; color: var(--fg-0); }
.toggle-desc { font-size: var(--t-xs); color: var(--fg-3); line-height: 1.3; }

/* Access toolbar: role filter on the left, Add-user on the right.
   When the add form opens it breaks to its own full-width line below. */
.access-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px 16px; flex-wrap: wrap; margin-bottom: 16px; padding-right: 8px; }
.access-toolbar .seg-filter { margin: 0; }
.access-toolbar .form-card { flex-basis: 100%; margin-right: -8px; }

/* Right-aligned "Add X" bar (servers, channels, analysts, roles). The add form
   breaks to its own full-width line below when opened. */
.add-bar { display: flex; justify-content: flex-end; flex-wrap: wrap; margin-bottom: 16px; padding-right: 8px; }
.add-bar .form-card { flex-basis: 100%; margin-right: -8px; }

/* Size the primary "Add" buttons to cap the row's action column, centered. */
.add-bar > .btn.primary,
.access-toolbar > .btn.primary { min-width: 118px; justify-content: center; }

/* Segmented role filter (Access): the frame. Its buttons are the shared .seg-btn (above). */
.seg-filter {
  display: inline-flex; gap: 3px; margin: 2px 0 16px;
  background: var(--bg-2); border: 1px solid var(--border-1);
  border-radius: var(--radius); padding: 3px;
}

/* Bounded, scrollable list region — Access users + role grants each ~half */
.access-list { max-height: 38vh; overflow-y: auto; }

/* Toggle switch — the ONE on/off control across the app: on is filled green with a white thumb at
   the right, off is a neutral track with its thumb at the left. Off used to read red, the loss
   colour, so a page of safe defaults read like a page of errors; the thumb's place carries the state,
   and its --fg-2 clears 3:1 on the off track on every brand (4.0 to 4.7:1). Never --fg-3 or fainter
   there. `.rg` is kept as a no-op alias for existing markup. */
.switch { display: inline-flex; align-items: center; cursor: pointer; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-track {
  position: relative; width: 38px; height: 21px; border-radius: 99px;
  background: var(--bg-3);
  border: 1px solid var(--border-3);
  transition: background 140ms, border-color 140ms;
}
.switch-thumb {
  position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
  border-radius: 50%; background: var(--fg-2);
  transition: transform 140ms, background 140ms;
}
.switch input:checked + .switch-track { background: var(--c-up); border-color: var(--c-up); }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(17px); background: #fff; }
.switch input:focus-visible + .switch-track { box-shadow: 0 0 0 2px color-mix(in oklab, var(--c-up) 30%, transparent); }
/* Label sat beside the switch (the "enabled / disabled" style rows). */
.switch.toggle-row { gap: 10px; align-items: center; }
.switch .toggle-txt { font-size: var(--t-md); color: var(--fg-1); }
.switch input:not(:checked) ~ .toggle-txt { color: var(--fg-2); }

/* ── Audit log */
.audit-row {
  display: grid;
  grid-template-columns: 150px 140px auto minmax(0, 1fr);
  align-items: center; gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border-1);
  font-size: 0.78125rem;   /* off the type scale: a step up cut an actor's name in its 140px column */
}
.audit-row:hover { background: var(--bg-2); }
.audit-row:last-child { border-bottom: 0; }
.audit-row .audit-time { color: var(--fg-3); font-size: var(--t-xs); white-space: nowrap; }
.audit-row .audit-actor { color: var(--fg-1); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.audit-row .audit-target { color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── Settings */
/* On a computer the tabs that do not fit wrap onto a second row (the owner's eight in a narrow
   window, or with a larger browser font): on one row they ran past the panel, whose edge hid the
   last ones (Preferences, where Text size is), and Tab to one of them scrolled the whole panel
   sideways. 12px a side keeps the eight on one row in a 1280px window at the default text size
   (they need 976px of its 988) and at 1366px at the largest. A phone scrolls the strip instead
   (the 900px block). */
.settings-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 0 10px;
  border-bottom: 1px solid var(--border-1);
}
.settings-tab {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 13px 12px;
  color: var(--fg-2);
  font-size: 0.84375rem;
  font-weight: 500;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 120ms;
}
.settings-tab:hover { color: var(--fg-0); }
.settings-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.settings-content { padding: 24px 28px; }
.settings-content h2 { font-size: 1.125rem; margin: 0 0 7px; }
.settings-content p.sub { color: var(--fg-2); margin: 0 0 18px; font-size: var(--t-md); }

/* ── Settings → Preferences (personal, every-role view settings) */
.prefpane { display: flex; flex-direction: column; gap: 18px; max-width: 640px; }
.prefpane-sec { background: var(--bg-1); border: 1px solid var(--border-1); border-radius: var(--radius-lg); padding: 18px 20px; }
.prefpane-hd { margin-bottom: 14px; }
.prefpane-hd h3 { margin: 0; font-size: 0.9375rem; font-weight: 600; color: var(--fg-0); }
.prefpane-hd p { margin: 6px 0 0; font-size: var(--t-md); line-height: 1.5; color: var(--fg-2); }
.prefpane-hd p em { color: var(--fg-1); font-style: italic; }
.prefpane-radios { display: flex; flex-direction: column; gap: 8px; }
.prefpane-radio {
  display: flex; align-items: flex-start; gap: 11px; width: 100%; text-align: left;
  padding: 12px 14px; border: 1px solid var(--border-1); border-radius: var(--radius);
  background: var(--bg-2); color: inherit; cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.prefpane-radio:hover { border-color: var(--border-2); }
.prefpane-radio.on { border-color: var(--accent); background: var(--accent-glow); }
.prefpane-radio-dot {
  flex-shrink: 0; width: 15px; height: 15px; margin-top: 2px; border-radius: 50%;
  border: 2px solid var(--border-3); background: transparent; transition: border-color 120ms ease;
}
.prefpane-radio.on .prefpane-radio-dot { border-color: var(--accent); box-shadow: inset 0 0 0 3px var(--accent); }
.prefpane-radio-txt { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.prefpane-radio-txt strong { font-size: 0.84375rem; font-weight: 600; color: var(--fg-0); }
.prefpane-radio-txt span { font-size: var(--t-sm); line-height: 1.45; color: var(--fg-2); }
.prefpane-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--border-1); }
.prefpane-row:first-of-type { padding-top: 0; }
.prefpane-row:last-of-type { border-bottom: none; padding-bottom: 0; }
.prefpane-row.col { flex-direction: column; align-items: stretch; gap: 9px; }
.prefpane-row.col .seg { display: flex; width: 100%; }
.prefpane-row.col .seg-btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; min-width: 0; }
.prefpane-lbl { font-size: var(--t-md); font-weight: 500; color: var(--fg-1); }
/* --fg-2, not --fg-3: at this size --fg-3 measured 2.8:1. */
.prefpane-desc { font-size: var(--t-sm); color: var(--fg-2); line-height: 1.45; }
.prefpane-note { margin-top: 12px; font-size: var(--t-xs); color: var(--fg-2); line-height: 1.45; }
/* A switch that can't be changed right now (saving, or Settings → Platform before its table
   exists) looks it. */
.prefpane .switch:has(input:disabled) { cursor: not-allowed; }
.prefpane .switch input:disabled + .switch-track { opacity: .5; }
/* The time zone: its label keeps one line (the clock beside the word), and on a narrow screen the
   picker takes the row's full width under it. Its longest zone name made the select 330 px wide,
   and it ran 77 px off a 390 px phone with its arrow cut. */
.prefpane-row.tz .prefpane-lbl { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
@media (max-width: 600px) {
  .prefpane-row.tz { flex-direction: column; align-items: stretch; gap: 9px; }
  .prefpane-row.tz .tz-select { width: 100%; min-width: 0; }
}

.channel-row {
  display: grid;
  grid-template-columns: 20px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 14px;
  padding: 10px 8px 10px 14px;
  border-bottom: 1px solid var(--border-1);
}
.row-actions { display: flex; align-items: center; gap: 2px; }
.row-editor {
  padding: 16px 18px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--border-1);
  border-top: 1px dashed var(--border-2);
}
.row-editor:last-child { border-bottom: 0; }
.toggle-line {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--t-md); color: var(--fg-1); cursor: pointer;
  min-height: 38px;
}
.channel-row:hover { background: var(--bg-2); }
.channel-row:last-child { border-bottom: 0; }
.channel-row .hash { color: var(--fg-3); font-family: var(--f-mono); font-size: 0.9375rem; }
.channel-row .name { font-family: var(--f-sans); font-size: var(--t-md); font-weight: 500; color: var(--fg-0); }
.channel-row .id { font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-3); margin-top: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Category pills — used in the channel row AND the add-channel form, so these
   base styles are NOT scoped to .channel-row (the row-specific responsive
   overrides below still are).
   The five wrap onto another line where they don't fit, never clipped: on one line they need 352px,
   and the Add channel form's column (161px at 1366, 102px at 1024) and a phone's card (298px at 390)
   used to hide what didn't fit (recap, chat and ignore). The dividers are the 1px gaps over the box's
   own border colour, so they run between lines too, and one line looks as it always has. */
.cat-select {
  display: flex; flex-wrap: wrap; gap: 1px;
  background: var(--border-1);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-sm);
  overflow: hidden;
}
.cat-select button {
  flex: 1 0 auto;
  padding: 5px 11px;
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  color: var(--fg-2);
  background: var(--bg-1);
  letter-spacing: 0.04em;
  transition: all 120ms;
}
.cat-select button:hover { color: var(--fg-0); background: var(--bg-2); }
/* A category in use carries the segments' 2px underline (.seg-btn.on), which takes no room: by its
   tint and colour alone it measured 1.1 to 1.9:1 against the ones beside it (grey on grey on WarRoom).
   Its tint sits on --bg-1, as the others do, never on the box's border colour behind the gaps. */
.cat-select button.on {
  background: linear-gradient(var(--accent-glow), var(--accent-glow)), var(--bg-1);
  color: var(--accent);
  box-shadow: inset 0 -2px 0 currentColor;
}
/* Multi-author picker (channel add form + row editor) */
.owner-multi { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.owner-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.owner-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-family: var(--f-mono); font-size: var(--t-xs);
  color: var(--fg-1); background: var(--bg-2);
  border: 1px solid var(--border-1); border-radius: var(--radius-sm);
  padding: 2px 3px 2px 8px;
}
.owner-chip button {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border-radius: 4px;
  color: var(--fg-3); font-size: var(--t-base); line-height: 1;
}
.owner-chip button:hover { color: var(--c-down); background: var(--bg-3); }
.channel-row .live-dot {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--f-mono); font-size: var(--t-xs);
}
.channel-row .live-dot .d { width: 6px; height: 6px; border-radius: 99px; background: currentColor; }

/* Category group header in the channels screen */
.cat-group { margin-bottom: 8px; }
.cat-group:last-child { margin-bottom: 0; }
.cat-group-hdr {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 4px;
  margin-top: 18px;
}
.cat-group-hdr .cat-name {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--t-md); font-weight: 600; color: var(--fg-0);
}
.cat-group-hdr .cat-count {
  font-family: var(--f-mono); font-size: var(--t-xs); color: var(--fg-3);
}
.cat-group-hdr .cat-desc { font-size: var(--t-sm); color: var(--fg-2); max-width: 380px; text-align: right; }
.cat-group .rows {
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-1);
}

/* ── Whitelist */
.user-row {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto auto auto;
  align-items: center;
  gap: 14px;
  padding: 12px 8px 12px 16px;
  border-bottom: 1px solid var(--border-1);
}
.user-row:hover { background: var(--bg-2); }
.user-row:last-child { border-bottom: 0; }
.user-row .avatar {
  width: 30px; height: 30px;
  border-radius: 99px;
  background: var(--bg-3);
  display: grid; place-items: center;
  font-family: var(--f-mono);
  font-size: var(--t-md);
  font-weight: 600;
}
.user-row .uname { font-family: var(--f-sans); font-size: var(--t-md); font-weight: 500; color: var(--fg-0); }
.user-row .priority {
  font-family: var(--f-mono); font-size: var(--t-xs);
  padding: 3px 8px;
  border-radius: 99px;
  border: 1px solid var(--border-2);
  color: var(--fg-1);
}
.user-row .priority.high { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-glow); }
.user-row .priority.muted { color: var(--fg-3); }
.tier-legend { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; flex-wrap: wrap; }

/* Analyst reordering.
   .user-row is a FIXED five-column grid, so the drag handle needs its own column —
   adding it as an extra child shifts every cell one to the right (the avatar lands in
   the 1fr column, leaving a huge gap) and wraps the trailing actions onto a second row.
   The modifier is only applied where reordering is offered, so every other .user-row
   (Access, Users) is untouched. */
.user-row.reorderable { grid-template-columns: 14px 34px minmax(0, 1fr) auto auto auto; }
.reorder {
  display: flex; flex-direction: column; align-items: center;
  gap: 3px;
  /* Resting opacity lifted from .3: at that level the arrows read as disabled rather than
     idle, which is the same complaint the row-action icons drew. Still recessive enough that
     a list you are only reading does not look like a list of controls. */
  opacity: .5;
  transition: opacity .12s ease;
}
/* The reveal has to name every row type that carries a .reorder block. The server list uses
   .channel-row, so without it those arrows would sit at the resting opacity forever and never
   brighten — visible, but never confirming they are live. */
.user-row:hover .reorder,
.channel-row:hover .reorder,
.reorder:focus-within { opacity: 1; }
.reorder button {
  appearance: none; background: none; border: 0; padding: 0;
  line-height: 1; cursor: pointer;
  font-size: 0.5rem;
  color: var(--fg-3);
}
.reorder button:hover:not(:disabled) { color: var(--accent); }
.reorder button:disabled { opacity: .25; cursor: default; }

/* Server group header in the analyst list — disambiguates the same analyst
   registered across multiple servers. */
.setting-grp { display: flex; align-items: center; gap: 8px; padding: 13px 16px 9px; border-bottom: 1px solid var(--border-1); }
.setting-grp svg { color: var(--fg-deco); flex-shrink: 0; }
.setting-grp-name { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.setting-grp-n { margin-left: auto; font-size: var(--t-xs); color: var(--fg-3); }

/* ── Misc */
.empty {
  padding: 60px 20px;
  text-align: center;
  color: var(--fg-2);
  font-size: var(--t-md);
}

/* ── Feed states (shared.jsx FeedSkeleton / FeedFailed / FeedStale) ─────────────
   What a list shows before its data has answered, when it could not, and when it stopped
   updating. Placeholders use --boot-fill (the boot skeleton's look), never a semantic colour,
   and are sized like the real rows so the data lands without moving the page. */
.feed-skel { padding: 4px 0; }
.feed-skel-row {
  display: flex; align-items: center; gap: 14px;
  padding: 0 16px; border-bottom: 1px solid var(--border-1);
}
.feed-skel-row i { display: block; height: 11px; border-radius: var(--radius-sm); background: var(--boot-fill); }
.feed-skel-row .fs-a { width: 18%; max-width: 140px; }
.feed-skel-row .fs-b { width: 12%; max-width: 90px; }
.feed-skel-row .fs-c { flex: 1; max-width: 420px; }
.feed-skel-row:nth-child(3n+2) .fs-c { max-width: 300px; }
.feed-skel-row:nth-child(3n) .fs-c { max-width: 360px; }
.feed-skel-row:nth-last-child(2) { opacity: .7; }
.feed-skel-row:last-child { opacity: .45; }
.feed-msg {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 48px 24px; text-align: center;
}
.feed-msg p { margin: 0; color: var(--fg-1); font-size: var(--t-base); max-width: 42ch; line-height: 1.5; }
/* A side panel's failure: one muted line, the main panel beside it carries Try again. */
.feed-msg-quiet { padding: 24px 16px; }
.feed-msg-quiet p { color: var(--fg-2); font-size: var(--t-md); }
.feed-msg-inline { flex-direction: row; justify-content: space-between; text-align: left; padding: 12px 14px;
  border: 1px solid var(--border-1); border-left: 3px solid var(--c-warn); border-radius: var(--radius); background: var(--bg-1); margin-bottom: 12px; }
.feed-stale {
  padding: 8px 16px; font-size: var(--t-sm); color: var(--fg-2);
  border-bottom: 1px solid var(--border-1); background: var(--bg-2);
}
.feed-stale::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 8px; border-radius: 99px;
  box-shadow: inset 0 0 0 1.5px var(--c-warn); vertical-align: 0; }
.stat-loading .val, .stat-loading .delta { color: var(--fg-3); }
/* The footnote under each summary figure, while the book loads, prices or waits: two lines
   when the row needed two the last time on this device (.is-2l, positions.jsx), else one, so
   the row keeps the height the numbers will need. A phone shows no footnotes under the figures
   (the ledger row below), only the one line under the row. */
.pos-summary.is-2l .stat .delta { min-height: calc(2 * 1.45em); }
/* The short labels and the line under the row are the phone ledger's (below). */
.lbl-short, .pos-sum-note { display: none; }
/* Phones: the four figures are one ledger row, not a 2x2 grid of tiles that took 176 to 210 px of
   the first screen before a single trade showed. Hairline dividers, no boxes, short labels, tabular
   figures. The footnotes under each figure give way to one line under the row (.pos-sum-note: the
   up/down split and the not-updated count), so its height never changes when the numbers land.
   Scoped to .pos-summary: Recaps, Usage and Settings share .statgrid and .stat. */
@media (max-width: 900px) {
  .statgrid.pos-summary {
    grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0;
    border-top: 1px solid var(--border-1); border-bottom: 1px solid var(--border-1);
  }
  .pos-summary .stat {
    background: none; border: 0; border-radius: 0; box-shadow: none;
    padding: 8px 6px 9px 10px; gap: 4px; min-width: 0;
    justify-content: space-between;
  }
  .pos-summary .stat:first-child { padding-left: 2px; }
  .pos-summary .stat + .stat { border-left: 1px solid var(--border-1); }
  /* A label that does not fit (a 320 px phone, the largest text size) takes a second line rather
     than being cut; the figures stay level along the bottom of the row. */
  .pos-summary .stat .lbl { gap: 0; font-size: var(--t-xs); line-height: 1.25; display: block; }
  .pos-summary .stat .lbl .icon { display: none; }
  .pos-summary .stat .val { font-size: var(--t-lg); font-variant-numeric: tabular-nums; white-space: nowrap; }
  .pos-summary .stat .delta { display: none; }
  .pos-summary .lbl-full { display: none; }
  .pos-summary .lbl-short { display: inline; }
  .pos-sum-note {
    display: block; margin-top: -6px;
    font-family: var(--f-mono); font-size: var(--t-xs); line-height: 1.45; color: var(--fg-2);
  }
  .pos-sum-note .up { color: var(--c-up); }
  .pos-sum-note .down { color: var(--c-down); }
}
/* A narrow phone: a quarter of 320 px is 73 px, so the cells give up some padding and the figures a
   step of size, and a figure like +64.5% stays inside its own cell. */
@media (max-width: 360px) {
  .pos-summary .stat { padding-left: 7px; padding-right: 4px; }
  .pos-summary .stat .val { font-size: 0.9375rem; }
  /* The ticker field keeps room for its placeholder beside the window buttons and, once a window
     is set, the Clear icon too: at 320 px it had shrunk to 85 px and read "Tick" (70 px and "T" at
     the largest text size). Tighter gaps, buttons, Clear and the field's own padding give it back. */
  .filterbar.pos-filterbar { column-gap: 6px; }
  .pos-filterbar .pos-range .seg-btn { padding: 0 4px; }
  .pos-filterbar .pos-reset { width: 2rem; }
  .pos-filterbar .input.search { padding-left: 28px; background-position: 8px 50%; }
  .pos-filterbar .search-field:not(.has-value) > .input.search { padding-right: 6px; }
}
/* The same room for the field a little wider where the text is bigger: with a window set, "Ticker"
   read "Tick" from 361 to 364 px at the default size and "Tic" to "Ticke" from 361 to 389 px at Larger
   text (the compaction above stopped at a cliff at 360). */
@media (min-width: 361px) and (max-width: 364px) {
  :root:not([data-fontscale="sm"]) .filterbar.pos-filterbar { column-gap: 6px; }
  :root:not([data-fontscale="sm"]) .pos-filterbar .pos-range .seg-btn { padding: 0 4px; }
  :root:not([data-fontscale="sm"]) .pos-filterbar .pos-reset { width: 2rem; }
  :root:not([data-fontscale="sm"]) .pos-filterbar .input.search { padding-left: 28px; background-position: 8px 50%; }
  :root:not([data-fontscale="sm"]) .pos-filterbar .search-field:not(.has-value) > .input.search { padding-right: 6px; }
}
@media (min-width: 365px) and (max-width: 389px) {
  :root[data-fontscale="lg"] .filterbar.pos-filterbar { column-gap: 6px; }
  :root[data-fontscale="lg"] .pos-filterbar .pos-range .seg-btn { padding: 0 4px; }
  :root[data-fontscale="lg"] .pos-filterbar .pos-reset { width: 2rem; }
  :root[data-fontscale="lg"] .pos-filterbar .input.search { padding-left: 28px; background-position: 8px 50%; }
  :root[data-fontscale="lg"] .pos-filterbar .search-field:not(.has-value) > .input.search { padding-right: 6px; }
}
/* The Sector mix toggle's own height (its text line plus 2 px above and below). */
.sector-conc-skel { height: calc(var(--t-md) * 1.45 + 4px); display: flex; align-items: center; }
/* Room kept for something that has not arrived (a count, a meta line): laid out, not seen. */
.is-hold { visibility: hidden; }
.seg-count { font-variant-numeric: tabular-nums; }
/* Winners / Losers / Today: wide enough for the label plus a two-digit count whether or not
   a count is showing (13px padding x2, the 7px gap, the chip's 10px padding, its two mono digits
   at 0.625rem, 1.74ch of the button's face, and the button's 0.01em letter-spacing). The label is
   in the sans face, where a word is never wider than its letter count in ch ("Winners" 47.9px
   against 51.5, "Today" 36.7 against 36.8 at Small), so this still holds them. On a phone the
   segments already share the row equally. */
@media (min-width: 901px) {
  .pos-seg .seg-btn.has-count {
    justify-content: center;
    min-width: calc(43px + (var(--n) + 1.74) * 1ch + var(--n) * 0.01em);
  }
  /* Open Positions' controls fill the header and sit at its right edge (the same place
     space-between put them). When the layout settles from the list to the analyst roster, the
     controls that remain stay put instead of the whole group sliding 550 px across the
     header: on a member's first visit that slide, times everything under it, was most of
     the page's layout shift. */
  .pos-hdr-tools { flex: 1 1 auto; justify-content: flex-end; }
}
/* Where they do not all fit (a narrow computer, a tablet in landscape, a laptop with Log position in
   the header), the controls wrap onto a second row at the header's right edge instead of running
   past the panel or squeezing: at 1024-1340px the instrument switch and the count were cut off, a
   member's drill-in header included, and at 1366px the owner's read "Shar" with Log position, the
   count and the panel's own title on two lines each. The title keeps its one line. Where they fit,
   nothing changes. */
@media (min-width: 901px) {
  .panel-hdr-tools.pos-hdr-tools { flex-wrap: wrap; row-gap: 8px; min-width: 0; }
  .panel-title.pos-title-plain { white-space: nowrap; }
  /* An analyst's drill-in header is two blocks, the name and the controls. The controls stay beside
     the name while the widest of them (the quick filter, at any text size and count) fits there at
     least 24px from the name, and otherwise move under it, still at the right edge. Squeezed in beside
     a long name, they used to shrink below that widest control, which then ran out over the name (a
     long name read only its first words on an iPad in landscape; every server's longest name somewhere
     between 901 and 1175px), and where they did fit, Log position or All could end up touching it. */
  .panel-hdr:has(> .panel-title.pos-title-drill) { flex-wrap: wrap; gap: 8px 24px; }
  .panel-hdr:has(> .panel-title.pos-title-drill) > .panel-hdr-tools.pos-hdr-tools { flex: 1 1 0; min-width: min-content; }
}
.sector-conc-skel i { display: block; width: 180px; height: 11px; border-radius: var(--radius-sm); background: var(--boot-fill); }
/* Text for assistive tech only. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* A slim notice in the flow under the top bar (a session that ended, part of the app that did
   not load): it never covers the sticky phone top bar or the first row. */
.app-banner {
  display: flex; align-items: center; gap: 10px; flex-shrink: 0;
  padding: 9px 28px; font-size: var(--t-md); color: var(--fg-1);
  background: var(--bg-2); border-bottom: 1px solid var(--border-1);
  box-shadow: inset 3px 0 0 var(--c-warn);
}
.app-banner svg { color: var(--c-warn); flex: none; }
.app-banner a { color: var(--accent); font-weight: 600; }
.app-banner .btn { margin-left: auto; }
@media (max-width: 900px) { .app-banner { padding: 9px 16px; } }
/* The server picker's per-row "only". */
.srv-only {
  flex: none; padding: 6px 8px; min-height: 28px; border-radius: var(--radius-sm);
  font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-3);
}
.srv-pick-row:hover .srv-only { color: var(--fg-2); }
.srv-only:hover, .srv-only:focus-visible { color: var(--accent); background: var(--bg-2); }
.scroll-y { overflow-y: auto; }
.flex { display: flex; }
.gap-6 { gap: 6px; }
.gap-8 { gap: 8px; }
.gap-12 { gap: 12px; }

/* ── Focus ring */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
/* A dialog that takes focus itself as it opens (shared.jsx useDialog), so a screen reader announces
   it by name: the container is not a control, so it draws no ring and keeps its own corners. The
   controls inside it still show theirs. Most dialogs' corners come from a rule later in this file
   (.modal, .dtp-pop, .asst-panel); the ones defined earlier, or with corners of their own, are put
   back here. */
:is([role="dialog"], [role="alertdialog"])[tabindex="-1"]:focus-visible { outline: none; }
.notif-panel:focus-visible, .notif-setup:focus-visible { border-radius: var(--radius-lg); }
.fsheet:focus-visible { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
.flyout:focus-visible, .modal-back:focus-visible { border-radius: 0; }

/* ── Toasts
   Over everything that waits for an answer (z 300: a dialog's own error must show), clear of the
   iPhone's home bar in the installed app, and above the bottom card of the moment (the rating
   card, the install nudge: --card-lift) so a toast never lands on that card's buttons. */
.toast-host {
  position: fixed;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px) + var(--card-lift, 0px));
  right: calc(20px + env(safe-area-inset-right, 0px));
  z-index: 300;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-end;
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: 10px;
  min-width: 230px; max-width: 380px;
  padding: 11px 14px;
  background: var(--bg-1);
  border: 1px solid var(--border-2);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow-2);
  font-size: var(--t-md); color: var(--fg-0);
  cursor: pointer;
  animation: toastIn 220ms cubic-bezier(.22, 1, .36, 1);
}
.toast .toast-icon { display: grid; place-items: center; color: var(--accent); flex-shrink: 0; }
.toast .toast-msg { line-height: 1.4; }
/* A message, then (optionally) what went wrong in a quieter line, then (optionally) one button. */
.toast .toast-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 auto; }
.toast .toast-detail { font-size: var(--t-sm); line-height: 1.4; color: var(--fg-2); }
.toast.has-act { align-items: center; }
.toast .toast-act {
  flex: none; margin-left: 4px; padding: 6px 10px; min-height: 30px; border-radius: var(--radius-sm);
  font-size: var(--t-md); font-weight: 600; color: var(--accent);
  border: 1px solid var(--border-2); background: var(--bg-2); cursor: pointer;
}
.toast .toast-act:hover { background: var(--bg-3); }
/* Dismiss, on a toast with no timer only (toast.jsx): the rest leave on their own. */
.toast .toast-x { margin-right: -6px; color: var(--fg-2); }
.toast.success { border-left-color: var(--c-up); }
.toast.success .toast-icon { color: var(--c-up); }
.toast.error { border-left-color: var(--c-down); }
.toast.error .toast-icon { color: var(--c-down); }
@keyframes toastIn { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }

/* ── Confirmation modal */
.modal-back {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(2, 4, 8, 0.55);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  display: grid; place-items: center;
  padding:
    calc(16px + env(safe-area-inset-top, 0px))
    calc(16px + env(safe-area-inset-right, 0px))
    calc(16px + env(safe-area-inset-bottom, 0px))
    calc(16px + env(safe-area-inset-left, 0px));
  animation: fadeIn 150ms ease-out;
}
/* A question asked over a dialog (confirm.jsx) is always drawn over it, Recaps' Add update included
   (drawn at the end of <body>, after the app). Over the phone's filter sheet and the assistant too;
   messages (300) stay above it. */
.modal-back.confirm-back { z-index: 270; }
.modal {
  width: 100%; max-width: 420px;
  background: var(--bg-1);
  border: 1px solid var(--border-2);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
  /* Was overflow:hidden with no height cap — a dialog taller than the screen simply
     had its lower half, buttons included, cut off with no way to reach them. */
  overflow: hidden auto;
  max-height: calc(100vh - 48px);
  max-height: calc(100dvh - 32px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  animation: modalIn 180ms cubic-bezier(.22, 1, .36, 1);
}
.modal-icon {
  width: 40px; height: 40px; border-radius: 10px;
  margin: 20px 20px 0;
  display: grid; place-items: center;
  background: var(--accent-glow); color: var(--accent);
}
.modal-icon.danger { background: color-mix(in oklab, var(--c-down) 14%, transparent); color: var(--c-down); }
.modal-title { padding: 14px 20px 0; font-size: 1rem; font-weight: 600; color: var(--fg-0); }
.modal-body { padding: 8px 20px 20px; color: var(--fg-1); font-size: 0.84375rem; line-height: 1.55; white-space: pre-line; }
.modal-actions {
  display: flex; justify-content: flex-end; gap: 8px;
  padding: 14px 20px; border-top: 1px solid var(--border-1);
  background: var(--bg-2);
}
@keyframes modalIn { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }

/* User-access editor modal — reuses the calc modal frame, a touch wider for the
   two-column form + per-server role controls. */
.user-modal { max-width: 560px; width: 100%; max-height: calc(100vh - 48px); display: flex; flex-direction: column; }

/* ── Position cost calculator */
.calc-modal { max-width: 480px; width: 100%; max-height: calc(100vh - 48px); display: flex; flex-direction: column; }
.calc-hdr { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px 14px; border-bottom: 1px solid var(--border-1); flex-shrink: 0; }
.calc-title { font-size: 1rem; font-weight: 600; color: var(--fg-0); display: flex; align-items: center; gap: 8px; }
.calc-contract { font-size: var(--t-sm); color: var(--fg-2); }
.calc-sub { font-size: var(--t-xs); color: var(--fg-3); margin-top: 6px; letter-spacing: 0.06em; text-transform: uppercase; }
.calc-body { padding: 16px 20px 20px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; }
.calc-field { display: flex; flex-direction: column; gap: 6px; }
.calc-field > span { font-size: var(--t-xs); color: var(--fg-2); letter-spacing: 0.04em; }
.calc-input { display: flex; align-items: center; gap: 6px; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius); padding: 0 10px; }
.calc-input:focus-within { border-color: var(--accent-dim); }
.calc-input input { flex: 1; min-width: 0; background: transparent; border: 0; outline: none; color: var(--fg-0); font-family: var(--f-mono); font-size: 0.9375rem; padding: 9px 0; }
.calc-pre, .calc-suf { color: var(--fg-3); font-family: var(--f-mono); font-size: var(--t-md); }
/* "Posted at" — two on-brand fields (date + ET time) replacing the native
   datetime-local picker, which ignored every design token. */
.postedat { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.postedat .postedat-date { flex: 1 1 120px; min-width: 108px; }
.postedat .postedat-time { flex: 1 1 100px; min-width: 92px; }
.postedat .postedat-date input, .postedat .postedat-time input { font-size: var(--t-base); }
.postedat .btn { flex: 0 0 auto; }
/* Hand-built date + time picker (EtDateTimeField): a trigger button + a portaled
   popover with presets, a typeable date field, an inline calendar and a typeable time
   field — all in the app's tokens. No native chrome, no red weekends, no scroll-spinner. */
.dtp { display: flex; }
.dtp-trigger {
  display: inline-flex; align-items: center; gap: 8px; width: 100%;
  background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius);
  color: var(--fg-0); font-family: var(--f-mono); font-size: var(--t-base); padding: 9px 11px;
  cursor: pointer; text-align: left; transition: border-color .12s;
}
.dtp-trigger:hover { border-color: var(--accent-dim); }
.dtp-trigger.open { border-color: var(--accent-dim); }
.dtp-trigger > svg { color: var(--fg-deco); flex: 0 0 auto; }
.dtp-trigger.empty .dtp-trigger-val { color: var(--fg-3); }
.dtp-trigger-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dtp-x { display: inline-flex; color: var(--fg-3); border-radius: 4px; padding: 1px; flex: 0 0 auto; }
.dtp-x:hover { color: var(--c-down, #ef4444); background: var(--bg-3); }

.dtp-pop {
  position: fixed; z-index: 400; width: min(352px, calc(100vw - 20px));
  /* Never let a tall month get clipped off-screen on a short viewport. */
  max-height: calc(100vh - 20px); overflow-y: auto;
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: 12px;
  box-shadow: var(--shadow-2, 0 20px 50px -12px rgba(0,0,0,.6));
  padding: 10px; display: flex; flex-direction: column; gap: 9px; font-family: var(--f-mono);
}
.dtp-presets { display: flex; flex-wrap: wrap; gap: 6px; }
.dtp-chip {
  font-size: var(--t-xs); color: var(--fg-1); background: var(--bg-2);
  border: 1px solid var(--border-1); border-radius: 999px; padding: 4px 9px; cursor: pointer;
}
.dtp-chip:hover { border-color: var(--accent-dim); color: var(--accent); }
.dtp-in {
  width: 100%; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius);
  color: var(--fg-0); font-family: var(--f-mono); font-size: var(--t-md); padding: 8px 10px;
}
.dtp-in::placeholder { color: var(--fg-3); }
.dtp-in:focus { outline: none; border-color: var(--accent-dim); }

.dtp-cal-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.dtp-cal-title { font-size: var(--t-md); color: var(--fg-0); font-weight: 600; letter-spacing: .02em; }
.dtp-nav {
  width: 26px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--border-1); border-radius: 7px;
  color: var(--fg-1); font-size: 1rem; line-height: 1; cursor: pointer;
}
.dtp-nav:hover:not(:disabled) { border-color: var(--accent-dim); color: var(--accent); }
.dtp-nav:disabled { opacity: .3; cursor: default; }
.dtp-dow { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 3px; }
.dtp-dow span { text-align: center; font-size: 0.5625rem; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-3); }
.dtp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dtp-day {
  /* `aspect-ratio` on a grid item gives it an automatic MINIMUM size, and `1fr`
     is `minmax(auto, 1fr)` — so the auto floor inflated every column to 120px and
     the calendar burst out of its 296px popover at every text size. An explicit
     height plus `min-width: 0` lets the columns actually divide the row. */
  min-width: 0; height: 36px;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; border-radius: 7px;
  color: var(--fg-1); font-family: var(--f-mono); font-size: var(--t-md); cursor: pointer;
}
.dtp-dow span { min-width: 0; }
/* The leading blanks before the 1st of the month. NOT called `empty` — that is the
   app-wide empty-state class (`.empty { padding: 60px 20px }`), and a blank cell
   picking it up floored the first row at 120px tall. Which is why every month
   EXCEPT one starting on a Monday (no blanks at all) looked broken. */
.dtp-day.dtp-blank { visibility: hidden; padding: 0; }
.dtp-day:hover:not(:disabled):not(.sel) { background: var(--bg-3); color: var(--fg-0); }
.dtp-day.today:not(.sel) { box-shadow: inset 0 0 0 1px var(--accent-dim); color: var(--accent); }
.dtp-day.sel { background: var(--accent); color: #04140e; font-weight: 700; }
.dtp-day:disabled { color: var(--fg-deco); opacity: .35; cursor: default; }

.dtp-time { display: flex; align-items: center; gap: 6px; }
.dtp-timein { flex: 1; min-width: 0; }
.dtp-mer { display: inline-flex; border: 1px solid var(--border-2); border-radius: var(--radius); overflow: hidden; flex: 0 0 auto; }
.dtp-mer-b { background: var(--bg-2); border: 0; color: var(--fg-2); font-family: var(--f-mono); font-size: var(--t-xs); padding: 7px 9px; cursor: pointer; }
.dtp-mer-b + .dtp-mer-b { border-left: 1px solid var(--border-2); }
.dtp-mer-b.on { background: var(--accent-glow); color: var(--accent); box-shadow: inset 0 -2px 0 currentColor; }   /* the segments' underline (.seg-btn.on) */
.dtp-step { display: inline-flex; gap: 4px; flex: 0 0 auto; }
.dtp-step button { width: 28px; height: 32px; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius); color: var(--fg-1); font-size: 1rem; line-height: 1; cursor: pointer; }
.dtp-step button:hover { border-color: var(--accent-dim); color: var(--accent); }

.dtp-foot { display: flex; justify-content: space-between; gap: 8px; border-top: 1px solid var(--border-1); padding-top: 8px; }
.dtp-foot-b { font-size: var(--t-sm); color: var(--fg-2); background: transparent; border: 0; cursor: pointer; padding: 4px 8px; }
.dtp-foot-b:hover { color: var(--fg-0); }
.dtp-foot-b.accent { color: var(--accent); font-weight: 600; }
.calc-note { font-size: var(--t-sm); color: var(--fg-1); margin-top: -4px; }
.calc-note strong { color: var(--fg-0); }
/* $ vs % input-mode toggle (manual update modal) */
.calc-field-hdr { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; }
.mode-toggle { display: inline-flex; border: 1px solid var(--border-2); border-radius: var(--radius-sm); overflow: hidden; flex-shrink: 0; }
.mode-toggle button { font-family: var(--f-mono); font-size: var(--t-xs); line-height: 1; padding: 3px 9px; color: var(--fg-2); background: var(--bg-1); white-space: nowrap; transition: all 120ms; }
.mode-toggle button:not(:last-child) { border-right: 1px solid var(--border-2); }
/* The choice in use carries the segments' 2px underline (.seg-btn.on), never the tint alone. */
.mode-toggle button.on { background: var(--accent-glow); color: var(--accent); box-shadow: inset 0 -2px 0 currentColor; }
/* On a touch screen each side is a target of its own size: they were 18px tall (WCAG 2.5.8 asks
   24). Only the height grows, so the label beside the switch never wraps; a computer is unchanged. */
@media (pointer: coarse) { .mode-toggle button { min-height: 32px; } }

/* ── Recap-publish preview (Settings → Servers) — a Discord-embed-style card */
.recap-preview { margin-top: 10px; background: var(--bg-1); border: 1px solid var(--border-1); border-left: 3px solid var(--accent); border-radius: var(--radius); padding: 12px 14px; font-size: var(--t-md); }
.rcp-hd { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.rcp-logo { width: 22px; height: 22px; border-radius: 5px; object-fit: cover; flex-shrink: 0; }
.rcp-title { font-family: var(--f-display); font-weight: 700; letter-spacing: .02em; }
.rcp-stats { color: var(--fg-1); }
.rcp-play { margin-top: 3px; color: var(--fg-1); }
.rcp-block { margin-top: 9px; }
.rcp-sec { font-family: var(--f-mono); font-size: var(--t-xs); letter-spacing: .12em; text-transform: uppercase; color: var(--fg-3); margin-bottom: 3px; }
.rcp-an { font-weight: 600; color: var(--fg-0); margin-top: 4px; }
.rcp-row { display: flex; align-items: center; gap: 7px; padding: 1px 0 1px 8px; }
.rcp-sq { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
.rcp-sq.up { background: var(--c-up); } .rcp-sq.down { background: var(--c-down); } .rcp-sq.trim { background: var(--c-trim); }
.rcp-mv { font-size: var(--t-sm); color: var(--fg-1); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rcp-pct { font-family: var(--f-mono); font-weight: 700; margin-left: auto; flex-shrink: 0; }
.rcp-pct.up { color: var(--c-up); } .rcp-pct.down { color: var(--c-down); }
.rcp-link { margin-top: 9px; color: var(--accent); font-weight: 600; word-break: break-word; }
.rcp-note { margin-top: 9px; color: var(--fg-2); white-space: pre-wrap; font-size: var(--t-sm); line-height: 1.5; }
.rcp-footer { display: flex; align-items: center; gap: 6px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-1); color: var(--fg-3); font-size: var(--t-xs); }
.rcp-flogo { width: 15px; height: 15px; border-radius: 4px; }
.rcp-empty { color: var(--fg-2); font-style: italic; }
.calc-warn { font-size: var(--t-sm); color: var(--c-trim); }
.calc-split { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.calc-card { min-width: 0; background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius); padding: 12px; display: flex; flex-direction: column; gap: 10px; }
.calc-card .calc-input { background: var(--bg-0); }
.calc-card-hdr { font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--fg-2); font-family: var(--f-mono); }
.calc-out { display: flex; flex-direction: column; gap: 2px; min-height: 42px; }
.calc-out > span { font-size: var(--t-sm); color: var(--fg-2); }
.calc-out strong { font-size: 1.125rem; color: var(--accent); font-family: var(--f-mono); }
.calc-left { font-size: var(--t-xs); color: var(--fg-3); }
@media (max-width: 480px) { .calc-split { grid-template-columns: 1fr; } }

/* ── Manage position (admin manual update) */
/* 560, not 480: at 480 the two-column field grid squeezed each column to ~215px,
   which truncated the date+time trigger and made the whole dialog feel cramped. */
.manage-modal {
  max-width: 560px; width: 100%;
  /* vh first as the fallback, then dvh: on a phone 100vh is the browser's LARGEST
     viewport, so a "100vh - 48px" dialog still ran under the URL bar and its footer
     buttons could not be reached. dvh tracks the visible height. */
  max-height: calc(100vh - 48px);
  max-height: calc(100dvh - 32px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  display: flex; flex-direction: column;
}
/* The date+time field carries two values — give it the whole row in any field grid. */
.manage-fields > .calc-field-wide { grid-column: 1 / -1; }
.manage-when { margin-top: 2px; }
/* padding-bottom 18px: .manage-foot's `bottom: -18px` (below) must stay equal to it.
   scroll-padding-bottom: a field brought into view (focus, a tab) stops above the pinned foot. */
.manage-body { padding: 16px 20px 18px; display: flex; flex-direction: column; gap: 14px; overflow-y: auto; scroll-padding-bottom: 76px; }
/* Nothing in the form gives up its height: when the form is taller than the window, the body scrolls.
   A row of choices (.seg clips its corners, overflow hidden) has no minimum height of its own, so it
   took the whole shortfall and shrank to a 2px line: Manage's Add / Trim / Close / Cut / Update on a
   short laptop window or at Larger text, Feedback's Bug / Idea / Other on a small phone. */
.manage-body > * { flex-shrink: 0; }
.manage-ctx { display: flex; gap: 16px; flex-wrap: wrap; font-family: var(--f-mono); font-size: var(--t-sm); color: var(--fg-2); background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius); padding: 9px 12px; }
.manage-ctx strong { color: var(--fg-0); margin-left: 4px; }
.manage-actions.seg { display: flex; flex-wrap: wrap; }
.manage-actions.seg .seg-btn { flex: 1 1 0; min-width: 56px; display: flex; align-items: center; justify-content: center; text-align: center; }
.manage-hint { font-size: var(--t-sm); color: var(--fg-2); margin-top: -6px; }
/* Numeric fields: at most two per row, with labels that reserve a consistent
   height so the inputs always line up (no ragged "up and down" boxes). */
.manage-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px 14px; align-items: end; }
.manage-fields .calc-field > span { min-height: 1.75rem; display: flex; align-items: flex-end; line-height: 1.25; }
/* column-gap: a flex item drops the spaces at its edges, so "Posted at GMT+8" read "Posted atGMT+8".
   Not the price label with its $ / % (or Fill / New avg) toggle, .calc-field-hdr: the toggle stays
   beside the label with that rule's own 8px gap. Wrapped, it dropped under the label and moved every
   field below it (Manage's Trim, Close and Add). */
.manage-fields .calc-field > span:not(.calc-field-hdr) { flex-wrap: wrap; column-gap: 0.3em; }
.manage-fields .calc-field .calc-input,
.manage-fields .calc-field .select { height: 2.375rem; }
.manage-twoseg { display: flex; gap: 10px; flex-wrap: wrap; }
/* Never narrower than both its labels: at a fixed 130px floor "Shares" was cut by up to 26px on a
   phone (360px, Larger text). A pair that doesn't fit beside the other takes a row of its own. */
.manage-twoseg .seg { flex: 1 1 0; min-width: max-content; display: flex; }
.manage-twoseg .seg .seg-btn { flex: 1 1 0; justify-content: center; text-align: center; }
.manage-note { width: 100%; resize: vertical; background: var(--bg-2); border: 1px solid var(--border-2); border-radius: var(--radius); padding: 9px 10px; color: var(--fg-0); font-family: inherit; font-size: var(--t-md); line-height: 1.5; outline: none; }
.manage-note:focus { border-color: var(--accent-dim); }
/* The form's buttons (Log position, Manage, Override, Feedback) stay in view while a long form
   scrolls: pinned to the bottom of the scrolling body, on its own surface under a hairline, at
   every width (a phone and a short laptop window both opened Log position with Open position below
   the fold). bottom: -18px cancels the body's 18px bottom padding (keep the two equal), or a strip
   of the form showed under the bar. No negative bottom margin: the disclaimer follows it. Wraps on
   a narrow phone (Override's Delete, Cancel and Save). The modal backdrop already clears the home bar. */
.manage-foot {
  display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap;
  position: sticky; bottom: -18px; z-index: 1;
  margin: 2px -20px 0; padding: 12px 20px;
  background: var(--bg-1); border-top: 1px solid var(--border-1);
}
.manage-foot .btn { min-width: 96px; justify-content: center; }
.manage-disclaimer { font-size: var(--t-xs); color: var(--fg-3); line-height: 1.5; }
.manual-tag { display: inline-block; margin-left: 6px; padding: 0 5px; border-radius: 4px; font-family: var(--f-mono); font-size: 0.59375rem; letter-spacing: .06em; text-transform: uppercase; color: var(--fg-2); background: var(--bg-2); border: 1px solid var(--border-2); vertical-align: middle; }
/* A timeline's price tags are flex items of their own beside the action pill (positions.jsx):
   the row's gap spaces them, and they never shrink into the detail's ellipsis. */
.tl-row > .manual-tag { flex-shrink: 0; margin-left: 0; white-space: nowrap; }
/* The Events feed: under the price on a computer (its own line, like the stated mark), after it
   on a phone card. */
.ev-tags { display: block; margin-top: 3px; line-height: 1.2; white-space: normal; }
.ev-tags .manual-tag { margin: 0 0 0 4px; }
.mpos-grid .ev-tags { display: inline; margin: 0; }

/* Per-admin "can manage positions" toggle (in the user editor) + row chip */
.perm-list { display: flex; flex-direction: column; gap: 0; margin-top: 14px; }
.perm-list .perm-row { margin-top: 8px; }
.perm-row { display: flex; align-items: center; gap: 12px; margin-top: 12px; padding: 12px 14px; background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius); }
.perm-txt { flex: 1; min-width: 0; }
.perm-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.perm-sub { font-size: var(--t-sm); color: var(--fg-2); margin-top: 4px; }
.perm-chip { margin-left: 8px; padding: 0 6px; border-radius: 4px; font-family: var(--f-mono); font-size: 0.59375rem; letter-spacing: .06em; text-transform: uppercase; color: var(--accent); background: var(--accent-glow); border: 1px solid var(--accent-dim); vertical-align: middle; }

/* ── Pre-auth screens: sign in / loading / access denied ──────────────────────
   The first thing anyone sees of a client's desk, so the MARK is the hero rather
   than the 34px chip the sidebar uses. Deliberately not a centred rounded card:
   the column sits LEFT of a centred field, and the open space to its right is the
   composition. Every colour is a token, so all eight brands (and light mode)
   inherit this for free — the only fixed colour is Discord's own blurple, which
   must not be re-tinted. */
.login-gate {
  position: relative; overflow: hidden;
  min-height: 100vh; min-height: 100dvh;
  display: grid; align-content: center;
  padding: clamp(28px, 7vw, 76px);
}
/* Accent bloom behind the mark + the app's own faint grid, both masked so they
   fade into the field instead of ending on a hard edge. */
.lg-field {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(620px 420px at 24% 40%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 70%);
}
.lg-field::after {
  content: ""; position: absolute; inset: 0;
  background-image:
    linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
    linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
  background-size: 68px 68px;
  -webkit-mask-image: radial-gradient(72% 62% at 28% 46%, #000 0%, transparent 76%);
          mask-image: radial-gradient(72% 62% at 28% 46%, #000 0%, transparent 76%);
}
:root[data-theme="light"] .lg-field { opacity: .6; }

.lg-wrap { position: relative; width: min(940px, 100%); margin: 0 auto; }
.lg-col  { width: min(400px, 100%); }

/* The mark, at the size a logo deserves on a sign-in screen. */
.lg-mark {
  width: clamp(80px, 15vw, 108px); aspect-ratio: 1 / 1;
  border-radius: 24px; display: grid; place-items: center;
  background: var(--accent); color: var(--bg-0);
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(2.4rem, 6.5vw, 3.25rem); line-height: 1;
  box-shadow: 0 0 0 1px var(--border-2),
              0 26px 64px -20px color-mix(in srgb, var(--accent) 60%, transparent);
}
.lg-mark.is-img { background: var(--bg-1); overflow: hidden; }
.lg-mark img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: inherit; }

.lg-name {
  margin: 24px 0 0;
  font-family: var(--f-display); font-weight: 700;
  font-size: clamp(1.45rem, 3.6vw, 1.95rem); line-height: 1.14;
  letter-spacing: .035em; text-transform: uppercase; color: var(--fg-0);
  text-wrap: balance;
}
/* "SIGNALS" as a tracked second line, so a long client name reads as a two-line
   lockup rather than an accidental wrap. */
.lg-name-sub {
  display: block; margin-top: 7px;
  font-size: .52em; font-weight: 500; letter-spacing: .44em;
  color: var(--accent);
}
.lg-rule { width: 52px; height: 2px; margin: 22px 0 18px; background: var(--accent); opacity: .9; }
.lg-lede { color: var(--fg-2); font-size: 0.9375rem; line-height: 1.62; margin: 0 0 24px; max-width: 36ch; }
.lg-fine { color: var(--fg-2); font-size: var(--t-sm); line-height: 1.5; margin: 14px 0 0; }
.lg-fine-top { margin: 0 0 22px; }

.login-gate .discord-btn {
  width: 100%; justify-content: center; text-decoration: none;
  background: #5865F2; border-color: #5865F2; color: #fff;
  padding: 13px 16px; font-size: var(--t-base); font-weight: 500;
}
.login-gate .discord-btn:hover { background: #4752c4; border-color: #4752c4; }

/* Live session state — the same marketSession the Positions tab reads. A real
   fact about the desk, which is why it earns a place on an otherwise quiet page.
   A status line in words, so the sans face at 500, as the one under every page title in the app. */
.lg-session {
  display: flex; align-items: center; gap: 9px; margin-top: 26px;
  padding-top: 18px; border-top: 1px solid var(--border-1);
  font-size: var(--t-xs); font-weight: 500; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-2);
}
.lg-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--fg-deco); flex-shrink: 0; }
.lg-dot.is-open {
  background: var(--c-up);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-up) 22%, transparent);
}
/* Always Sinux emerald, never the brand accent — it names Sinux Signals. */
.lg-powered { margin-top: 12px; font-size: var(--t-xs); color: var(--fg-2); }
.lg-powered b { color: var(--sinux-emerald); font-weight: 600; }

.lg-denied {
  display: inline-flex; align-items: center; gap: 8px; margin: 22px 0 14px;
  font-family: var(--f-display); font-size: 0.9375rem; font-weight: 600;
  letter-spacing: .02em; color: var(--fg-0);
}
.lg-denied-badge {
  width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center;
  color: var(--c-down);
  background: color-mix(in srgb, var(--c-down) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--c-down) 35%, transparent);
}
.login-gate .lg-denied ~ .discord-btn,
.login-gate button.discord-btn {
  background: transparent; border-color: var(--border-2); color: var(--fg-1);
}
.login-gate button.discord-btn:hover { background: var(--bg-2); border-color: var(--border-1); color: var(--fg-0); }
/* A sign-in that did not finish is not a refusal: a grey lock, not the red one. */
.lg-denied.is-soft .lg-denied-badge { color: var(--fg-2); background: var(--bg-2); border-color: var(--border-2); }
/* "Sign in again" (Discord blue) above "Try a different account". */
.lg-actions { display: flex; flex-direction: column; gap: 10px; }

/* One quiet settle on the column as a whole. Deliberately NOT a per-child stagger:
   `animation: both` holds every delayed child at opacity 0 until its turn, so any
   hitch leaves the page looking empty — and a cascade of little entrances is the
   scattered-effects look this product avoids. */
.lg-col { animation: lgRise .42s cubic-bezier(.2,.7,.3,1) both; }
@keyframes lgRise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .lg-col { animation: none; } }

@media (max-width: 560px) {
  .login-gate { padding: 26px 22px; align-content: center; }
  .lg-col { width: 100%; }
  .lg-name { font-size: 1.4rem; }
  .lg-session { letter-spacing: .06em; }
}
/* A phone held sideways (a short, wide screen): the lockup stands LEFT of the column instead of
   above it, so Sign in with Discord (and the loading and access screens' own lines, which share
   LoginShell) are on the first screen. Stacked, the button sat below a 390px-tall screen with
   nothing showing that it scrolled. */
@media (max-height: 500px) and (orientation: landscape) {
  .login-gate { padding: 20px 28px; }
  .lg-col {
    width: min(760px, 100%);
    display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
    column-gap: 36px; align-items: start;
  }
  .lg-lockup { grid-column: 1; grid-row: 1 / span 8; align-self: center; }
  .lg-col > :not(.lg-lockup) { grid-column: 2; }
  .lg-rule { margin-top: 0; }
}

/* ── Sidebar user chip */
.user-chip {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 8px;
  border: 1px solid var(--border-1);
  border-radius: var(--radius);
  background: var(--bg-2);
}
.user-chip .user-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.user-chip .user-name {
  font-size: var(--t-md); font-weight: 500; color: var(--fg-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A word, not a figure: the sans face, like the menu and status line around it (2.10.0). */
.user-chip .user-role {
  font-family: var(--f-sans); font-weight: 500; font-size: 0.59375rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--accent);
}

/* Charts: the two mini-tables sit side by side on desktop. */
.charts-subgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; min-height: 0; }

/* ── Guide (in-app tutorial) — TOC rail + content filling the full width */
.guide {
  display: grid; grid-template-columns: 230px minmax(0, 1fr); gap: 26px; align-items: start;
  width: 100%;
}
.guide-toc {
  position: sticky; top: 0;
  display: flex; flex-direction: column; gap: 2px;
  background: var(--bg-1); border: 1px solid var(--border-1); border-radius: var(--radius-lg);
  padding: 12px 10px;
}
.guide-toc-hdr { font-size: var(--t-2xs); font-weight: 500; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); padding: 2px 8px 8px; }   /* a label: the sans face, as the tables' heads */
/* 0.78125rem, off the type scale: a step up took "Signal Events" and "Managing users" to two lines. */
.guide-toc-item {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 6px 8px; border-radius: var(--radius-sm);
  font-size: 0.78125rem; color: var(--fg-2); text-align: left;
}
.guide-toc-item:hover { background: var(--bg-2); color: var(--fg-0); }
.guide-toc-foot { font-size: var(--t-2xs); color: var(--fg-3); padding: 10px 8px 2px; border-top: 1px solid var(--border-1); margin-top: 8px; }
.guide-toc-foot strong { color: var(--accent); text-transform: uppercase; }
.guide-body { min-width: 0; display: flex; flex-direction: column; gap: 26px; padding-bottom: 30px; }
.guide-sec { background: var(--bg-1); border: 1px solid var(--border-1); border-radius: var(--radius-lg); padding: 20px 22px; }
.guide-sec-title { margin: 0 0 12px; font-size: 1rem; font-weight: 600; color: var(--fg-0); display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Why a position update can't be saved yet. Sits just above the buttons so the
   reason is read before the disabled control is blamed. Plain --fg-1 like every such reason
   (.btn-why): it was the Watchlist purple, an action colour with no action behind it.
   It is a status line that stays in the form (so a reason that appears is announced); empty, it
   takes no room: its margin also gives back the column's 14px gap, so the buttons sit exactly where
   they did without it. Feedback's minimum (.fb-why) sits under its box, 8px from it. */
.manage-blocked { margin: 10px 0 0; font-size: var(--t-md); color: var(--fg-1); }
.manage-body > .manage-blocked:empty { margin: -14px 0 0; }
.manage-blocked.fb-why { margin-top: -6px; font-size: var(--t-sm); line-height: 1.4; }
.manage-body > .manage-blocked.fb-why:empty { margin-top: -14px; }
/* Owner-only per-section PNG export. Sits at the far end of the title row and stays
   quiet until the section is hovered: it is a publishing tool, not part of reading.
   While it saves (disabled) it stays hidden in a section nobody points at, where the shared
   .btn:disabled would show it at .5, and is dimmed wherever it shows. */
.guide-sec-png { margin-left: auto; opacity: 0; transition: opacity .14s ease; }
.guide-sec-png:disabled { opacity: 0; }
.guide-sec:hover .guide-sec-png, .guide-sec-png:focus-visible { opacity: 1; }
.guide-sec:hover .guide-sec-png:disabled { opacity: .5; }
@media (hover: none) { .guide-sec-png { opacity: 1; } .guide-sec-png:disabled { opacity: .5; } }
.guide-sec p { margin: 0 0 12px; font-size: 0.84375rem; line-height: 1.65; color: var(--fg-1); }
.guide-sec p:last-child { margin-bottom: 0; }
.guide-note { font-size: var(--t-md); color: var(--fg-2); border-left: 2px solid var(--accent-dim); padding-left: 12px; }
.guide-list { margin: 0 0 12px; padding-left: 18px; display: flex; flex-direction: column; gap: 7px; font-size: 0.84375rem; line-height: 1.6; color: var(--fg-1); }
.guide-list:last-child { margin-bottom: 0; }
.guide-list strong { color: var(--fg-0); }
.guide-role {
  font-family: var(--f-mono); font-size: 0.59375rem; letter-spacing: .08em; text-transform: uppercase;
  padding: 1px 7px; border-radius: 99px; border: 1px solid var(--border-2); color: var(--fg-2); background: var(--bg-2);
}
.guide-role.r-analyst { color: var(--c-open); border-color: color-mix(in oklab, var(--c-open) 35%, transparent); background: color-mix(in oklab, var(--c-open) 9%, transparent); }
.guide-role.r-editor { color: var(--c-watch); border-color: color-mix(in oklab, var(--c-watch) 35%, transparent); background: color-mix(in oklab, var(--c-watch) 9%, transparent); }
.guide-role.r-admin { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-glow); }
.guide-role.r-server_owner { color: var(--a-1); border-color: color-mix(in oklab, var(--a-1) 35%, transparent); background: color-mix(in oklab, var(--a-1) 9%, transparent); }
.guide-role.r-owner { color: var(--c-close); border-color: color-mix(in oklab, var(--c-close) 35%, transparent); background: color-mix(in oklab, var(--c-close) 9%, transparent); }
/* "Snapshot" frame around live mock components */
.guide-shot { margin: 14px 0; border: 1px solid var(--border-2); border-radius: var(--radius); overflow: hidden; background: var(--bg-0); }
.guide-shot-bar { display: flex; gap: 5px; padding: 8px 10px; border-bottom: 1px solid var(--border-1); background: var(--bg-2); }
.guide-shot-bar span { width: 8px; height: 8px; border-radius: 50%; background: var(--border-3); }
.guide-shot-body { padding: 14px 16px; }
.guide-shot-cap { padding: 8px 16px; border-top: 1px solid var(--border-1); font-size: var(--t-xs); color: var(--fg-3); }   /* a sentence: the sans face */
.guide-rows { display: flex; flex-direction: column; gap: 9px; }
.guide-row { display: flex; align-items: center; gap: 12px; }
.guide-row .pill, .guide-row .inst-tag { flex-shrink: 0; }
.guide-row-txt { font-size: var(--t-md); color: var(--fg-1); }
.guide-dim { color: var(--fg-2); font-size: var(--t-sm); }
.guide-up { color: var(--c-up); }
.guide-posrow { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.guide-posrow-l, .guide-posrow-r { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.guide-flow { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.guide-discord { display: flex; flex-direction: column; gap: 3px; background: #2b2d31; border-radius: 8px; padding: 10px 14px; min-width: 200px; }
.guide-discord-author { color: #949cf7; font-weight: 600; font-size: var(--t-sm); }
.guide-discord-msg { color: #dbdee1; font-size: var(--t-md); }
.guide-flow-arrow { color: var(--fg-3); font-size: var(--t-xs); }
.guide-parsed { display: flex; flex-direction: column; gap: 4px; font-size: var(--t-sm); color: var(--fg-2); background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius); padding: 10px 14px; }
.guide-parsed strong { color: var(--accent); }
.guide-foot { font-size: var(--t-xs); color: var(--fg-3); text-align: center; padding: 4px 0 10px; letter-spacing: .06em; }

/* ── Usage (owner-only consumption dashboard) */
.usage { display: flex; flex-direction: column; gap: 16px; }
.usage-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.usage-head .sub { flex: 1 1 320px; }
.usage-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.usage-srv { padding: 8px 12px; font-size: 0.78125rem; cursor: pointer; max-width: 220px; }   /* off the scale: the select keeps its height */
/* "platform-wide" scope caveat on the user-activity panels — amber note tint */
.usage-pw { color: var(--c-watch); }
.usage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 14px; }
.usage-panel { min-width: 0; }
.usage-chart { padding: 14px 16px 10px; }
.usage-svg { width: 100%; height: 150px; display: block; }
.usage-chart-meta { padding: 6px 2px 4px; font-size: var(--t-xs); color: var(--fg-2); min-height: 22px; }
.usage-chart-meta strong { color: var(--fg-0); }
.usage-dim { color: var(--fg-3); }
.usage-empty { padding: 26px 18px; font-size: var(--t-md); color: var(--fg-3); text-align: center; }
.usage-rank { display: flex; flex-direction: column; gap: 9px; padding: 14px 16px; }
.usage-rank-row { display: grid; grid-template-columns: minmax(90px, 170px) 1fr auto; align-items: center; gap: 10px; }
.usage-rank-name { font-size: var(--t-sm); color: var(--fg-1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.usage-rank-track { height: 8px; border-radius: 99px; background: var(--bg-2); overflow: hidden; }
.usage-rank-fill { display: block; height: 100%; border-radius: 99px; }
.usage-rank-val { font-size: var(--t-xs); color: var(--fg-2); white-space: nowrap; }
.usage-tbl th, .usage-tbl td { padding: 9px 16px; }
.usage-user { display: inline-flex; align-items: center; gap: 8px; }
.usage-role { font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: .08em; color: var(--accent); }

/* ── Report a bug (sidebar entry, every user) */
.bug-report-btn {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 11px;
  border: 1px dashed var(--border-2);
  border-radius: var(--radius);
  color: var(--fg-2);
  font-size: var(--t-sm);
  transition: color 120ms, border-color 120ms, background 120ms;
}
.bug-report-btn:hover { color: var(--c-trim); border-color: color-mix(in oklab, var(--c-trim) 45%, transparent); background: color-mix(in oklab, var(--c-trim) 7%, transparent); }

/* Sidebar footer "Display" preferences — theme, text size, and Open Positions
   layout, consolidated behind one button that opens a popover. Reachable on every
   role (viewers included) without the admin Settings tab. */
.prefs-wrap { position: relative; }
.prefs-btn {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 9px 11px; border: 1px solid var(--border-1); border-radius: var(--radius);
  background: var(--bg-1); color: var(--fg-2);
  font-size: var(--t-md); font-weight: 500;
  transition: color 120ms, border-color 120ms, background 120ms;
}
.prefs-btn:hover { color: var(--fg-0); background: var(--bg-2); }
.prefs-btn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-glow); }
/* Panel + backdrop are PORTALED to <body>, so they're free of the sidebar's
   sticky/transform context. Desktop: fixed, anchored above the trigger via inline
   left/bottom. Mobile: a full-width bottom sheet (see responsive). */
.prefs-backdrop { display: none; position: fixed; inset: 0; z-index: 159; }
.prefs-panel {
  position: fixed; z-index: 160;
  width: 328px; max-width: calc(100vw - 28px); max-height: calc(100vh - 24px);
  display: flex; flex-direction: column;
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2); overflow: hidden;
  animation: modalIn 150ms cubic-bezier(.22,1,.36,1);
}
.prefs-hdr { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; padding: 11px 16px; border-bottom: 1px solid var(--border-1); }
.prefs-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
/* overflow-x:hidden guarantees the panel never scrolls sideways; the body only
   ever scrolls vertically, and only on a short viewport. */
.prefs-body { padding: 6px 16px 12px; display: flex; flex-direction: column; overflow-x: hidden; overflow-y: auto; }
.prefs-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-1); min-width: 0; }
.prefs-row:last-child { border-bottom: none; }
.prefs-row.col { flex-direction: column; align-items: stretch; gap: 9px; }
.prefs-lbl { font-size: var(--t-md); font-weight: 500; color: var(--fg-1); }
.prefs-desc { font-size: var(--t-sm); color: var(--fg-3); line-height: 1.45; }
.prefs-foot { flex-shrink: 0; padding: 10px 16px; border-top: 1px solid var(--border-1); background: var(--bg-2); font-size: var(--t-xs); color: var(--fg-3); line-height: 1.45; }
/* Open Positions layout: the segmented control spans the full width as three even
   thirds, so Auto / Analysts / Flat all breathe instead of overflowing the panel. */
.prefs-row.col .seg { display: flex; width: 100%; }
.prefs-row.col .seg-btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; min-width: 0; padding: 8px 8px; }

/* ── Bugs tab (owner triage) */
.bugs { display: flex; flex-direction: column; gap: 12px; }
.bug-card { padding: 0; }
.bug-card-hdr {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 12px 16px 0;
}
.bug-status { font-size: 0.59375rem; letter-spacing: .08em; text-transform: uppercase; border: 1px solid; border-radius: 99px; padding: 1px 8px; }
.bug-cat { font-size: var(--t-2xs); color: var(--fg-3); text-transform: uppercase; letter-spacing: .08em; }
.bug-time { margin-left: auto; font-size: var(--t-xs); color: var(--fg-3); }
.bug-actions { display: flex; gap: 2px; }
.bug-msg { padding: 10px 16px 4px; font-size: 0.84375rem; line-height: 1.6; color: var(--fg-0); white-space: pre-wrap; overflow-wrap: anywhere; }
.bug-ctx { padding: 0 16px 12px; font-size: 0.65625rem; color: var(--fg-3); letter-spacing: .04em; }   /* off the scale: a step up wrapped it */
.bug-msg:last-child { padding-bottom: 14px; }

/* ── Ratings: star meter + picker (gold is a semantic colour, never rebranded) */
.star-meter { position: relative; display: inline-block; line-height: 0; vertical-align: middle; }
.sm-track { display: flex; }
.sm-row { display: flex; gap: 2px; flex-wrap: nowrap; }
.sm-star { flex: none; line-height: 0; }
.sm-empty { color: var(--border-3); }
.sm-full { position: absolute; inset: 0; width: 0; overflow: hidden; color: #F6C244; }
.star-picker { display: flex; gap: 6px; justify-content: center; padding: 2px 0; }
.star-btn { background: none; border: none; padding: 2px; cursor: pointer; color: var(--border-3); line-height: 0; transition: transform 80ms ease, color 120ms ease; }
.star-btn:hover { transform: scale(1.12); }
.star-btn.on { color: #F6C244; }

/* ── The bottom-corner rating prompt
   z-index 15: under the top bar's layer (20), so the phone's bell sheet (inside the top bar) paints
   over it, and under the drawer, menus, flyouts and dialogs; still over the page (sticky heads are
   1 to 5). It clears the iPhone's home bar in the installed app (env() is 0 everywhere else), and
   toasts and the Ask AI button stand above it while it shows (--card-lift, shared.jsx useCardLift). */
.rating-prompt {
  position: fixed; z-index: 15;
  right: calc(20px + env(safe-area-inset-right, 0px)); bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  width: 322px; max-width: calc(100vw - 24px);
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2); padding: 14px 15px;
  display: flex; flex-direction: column; gap: 10px;
  animation: rpIn 220ms cubic-bezier(.22, 1, .36, 1);
}
/* One ask at a time: the card waits, out of sight, while a dialog, a flyout (an event, a profile),
   the bell, the phone's filter sheet or menu (html.scroll-locked), the tour, the install nudge or
   the Ask AI panel is open. display:none keeps a half-typed note. The install nudge has the same
   layer and the same rule (its own section). */
body:has(.modal-back, .flyout, .notif-panel, .fsheet, .install-nudge, .asst-panel, .tour-root, .announce-back) .rating-prompt,
html.scroll-locked .rating-prompt { display: none; }
/* A failed Submit says why inside the card, under its buttons (Try again is the Submit button). */
.rp-err { font-size: var(--t-sm); line-height: 1.45; color: var(--fg-1); }
.rp-err strong { color: var(--c-down); font-weight: 600; }
@keyframes rpIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.rp-hdr { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.rp-title { font-size: 0.84375rem; font-weight: 600; color: var(--fg-0); line-height: 1.4; text-wrap: balance; }
.rp-sub { font-size: var(--t-xs); color: var(--fg-3); }
.rp-note { width: 100%; resize: vertical; border: 1px solid var(--border-1); border-radius: 8px; background: var(--bg-2); color: var(--fg-0); padding: 8px 10px; font: inherit; font-size: var(--t-md); }
.rp-foot { display: flex; justify-content: flex-end; gap: 8px; }
.rp-done { display: flex; align-items: center; gap: 12px; padding: 6px 4px; }
.rp-done-star { color: #F6C244; line-height: 0; }

/* ── Ratings tab */
.ratings { display: flex; flex-direction: column; gap: 14px; }
.rating-controls { display: flex; align-items: center; gap: 8px; }
.rating-select { background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 8px; color: var(--fg-0); font: inherit; font-size: var(--t-sm); padding: 5px 8px; }
.rating-overview { display: grid; grid-template-columns: 200px 1fr 1.2fr; gap: 12px; }
.rating-avg { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 18px; text-align: center; }
.ra-num { font-size: 2.875rem; font-weight: 700; line-height: 1; color: var(--fg-0); font-family: var(--f-mono); }
.ra-den { font-size: 1.125rem; color: var(--fg-3); font-weight: 500; }
.ra-meta { font-size: var(--t-xs); color: var(--fg-3); }
.rating-dist, .rating-trend-panel { padding: 16px 18px; }
.rd-title { font-size: var(--t-xs); text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); margin-bottom: 10px; }
.rating-dist-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; }
.rdr-label { display: inline-flex; align-items: center; gap: 2px; width: 30px; color: var(--fg-2); font-size: var(--t-xs); }
.rdr-track { flex: 1; height: 8px; background: var(--bg-3); border-radius: 99px; overflow: hidden; }
.rdr-fill { height: 100%; border-radius: 99px; transition: width 400ms cubic-bezier(.22, 1, .36, 1); }
.rdr-count { width: 30px; text-align: right; color: var(--fg-2); font-size: var(--t-xs); }
.rating-trend { display: flex; align-items: flex-end; gap: 3px; height: 92px; padding-top: 6px; }
.rt-col { flex: 1; height: 100%; display: flex; align-items: flex-end; min-width: 2px; }
.rt-bar { width: 100%; border-radius: 3px 3px 0 0; transition: height 400ms ease; }
.rt-axis { display: flex; justify-content: space-between; margin-top: 6px; font-size: var(--t-2xs); color: var(--fg-3); }
.rating-list-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 4px; }
.rlh-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.rating-card { padding: 0; }
.rating-card-hdr { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px 16px 10px; }
.rating-srv { font-size: 0.65625rem; color: var(--fg-3); border: 1px solid var(--border-1); border-radius: 99px; padding: 1px 8px; }   /* off the scale: a step up took a rating's header to a third line on a phone */
.rating-comment { padding: 0 16px 14px; font-size: var(--t-md); line-height: 1.6; color: var(--fg-1); white-space: pre-wrap; overflow-wrap: anywhere; }

/* ── Ratings owner config */
.rating-config { padding: 0; overflow: hidden; }
.rc-summary { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; background: none; border: none; color: var(--fg-0); padding: 14px 16px; cursor: pointer; text-align: left; font: inherit; }
.rc-sum-l { display: flex; align-items: center; gap: 10px; font-size: var(--t-md); }
.rc-dim { color: var(--fg-3); font-size: var(--t-xs); }
.rc-body { padding: 6px 16px 16px; display: flex; flex-direction: column; gap: 14px; border-top: 1px solid var(--border-1); }
.rc-toggle { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.rc-toggle > span:not(.switch-track) { display: flex; flex-direction: column; }
.rc-hint { font-size: var(--t-xs); color: var(--fg-3); font-weight: 400; }
.rc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.rc-field { display: flex; flex-direction: column; gap: 5px; }
.rc-field > span:first-child { font-size: var(--t-xs); font-weight: 600; color: var(--fg-2); }
.rc-field input[type="number"], .rc-field input[type="text"] { background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 8px; color: var(--fg-0); font: inherit; font-size: var(--t-md); padding: 7px 9px; }
.rc-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.rc-chip { background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 99px; color: var(--fg-2); font: inherit; font-size: var(--t-sm); padding: 4px 11px; cursor: pointer; }
.rc-chip.on { background: var(--accent-glow); border-color: var(--accent-dim); color: var(--accent); }
.rc-grant-group { display: flex; flex-direction: column; gap: 5px; margin-bottom: 8px; }
.rc-grant-srv { font-size: var(--t-2xs); color: var(--fg-3); text-transform: uppercase; letter-spacing: .06em; }
.rc-foot { display: flex; justify-content: flex-end; }
.rc-err { color: var(--c-down); font-size: var(--t-xs); }

@media (max-width: 760px) {
  .rating-overview { grid-template-columns: 1fr; }
  .rc-grid { grid-template-columns: 1fr; }
  .rating-prompt {
    right: calc(12px + env(safe-area-inset-right, 0px)); left: calc(12px + env(safe-area-inset-left, 0px));
    bottom: calc(12px + env(safe-area-inset-bottom, 0px)); width: auto;
  }
}
/* A phone: the period drops under the server picker when the two do not fit on one row. The
   phone field floor (the end of this file) made the picker about 50 px wider, and it pushed 180d
   to the screen's edge and 1Y off it, where nothing scrolls sideways to reach it. */
@media (max-width: 900px) {
  .rating-controls { flex-wrap: wrap; }
}

/* ── Guide: tour CTA banner + numbered steps */
.guide-tour-cta {
  display: flex; align-items: center; gap: 16px; justify-content: space-between;
  padding: 16px 18px; border-radius: var(--radius-lg);
  border: 1px solid var(--accent-dim); background: var(--accent-glow);
}
.guide-tour-cta .gtc-txt { min-width: 0; }
.gtc-title { display: inline-flex; align-items: center; gap: 7px; font-size: var(--t-base); font-weight: 600; color: var(--fg-0); }
/* --fg-1: on the banner's accent tint --fg-2 measured 4.1 to 4.4:1 on every branded light theme. */
.gtc-sub { font-size: var(--t-md); color: var(--fg-1); margin-top: 3px; }
.guide-tour-cta .btn { flex-shrink: 0; }
.guide-list.guide-steps { list-style: decimal; }
.guide-list.guide-steps li { padding-left: 2px; }

/* ── Onboarding tour (spotlight + tooltip) */
.tour-catch { position: fixed; inset: 0; z-index: 300; }
/* NO-SPOTLIGHT dim. A step whose target isn't on screen (the sidebar nav is
   off-canvas on a phone, so most steps land here at ~360px) used to dim 64% AND
   blur — obscuring the very tab the step had just navigated to and was narrating.
   Reported as "darkens and blurs the entire background so it's hard to see what
   it's talking about". The blur is gone and the dim is light: with nothing to
   point at, the content behind IS the subject, so it has to stay readable. */
.tour-catch.dim { background: rgba(4, 6, 12, 0.34); }
.tour-highlight {
  position: fixed; z-index: 301; border-radius: 12px; pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(4, 6, 12, 0.64), 0 0 0 2px var(--accent), 0 0 22px 2px var(--accent-glow);
  transition: top .28s cubic-bezier(.22,1,.36,1), left .28s cubic-bezier(.22,1,.36,1), width .28s cubic-bezier(.22,1,.36,1), height .28s cubic-bezier(.22,1,.36,1);
}
.tour-pop {
  position: fixed; z-index: 302; max-width: calc(100vw - 24px);
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2); padding: 18px 20px 16px;
  display: flex; flex-direction: column; gap: 11px;
  animation: rpIn 200ms cubic-bezier(.22, 1, .36, 1);
}
.tour-pop-hd { display: flex; align-items: center; justify-content: space-between; }
.tour-step-n { font-size: var(--t-xs); color: var(--fg-3); letter-spacing: .06em; }
.tour-pop-title { font-size: var(--t-lg); font-weight: 600; color: var(--fg-0); line-height: 1.3; }
.tour-pop-body { font-size: var(--t-base); line-height: 1.6; color: var(--fg-1); }
.tour-dots { display: flex; align-items: center; gap: 5px; padding: 2px 0; }
.tour-dot { width: 6px; height: 6px; border-radius: 99px; background: var(--border-3); transition: all .2s ease; }
.tour-dot.on { width: 17px; background: var(--accent); }
.tour-pop-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 2px; }
.tour-nav { display: flex; gap: 8px; }
.tour-skip { color: var(--fg-3); }

@media (max-width: 640px) {
  .guide-tour-cta { flex-direction: column; align-items: stretch; }
  .guide-tour-cta .btn { width: 100%; justify-content: center; }
  /* Centered cards sit near the bottom on mobile so tab content stays visible above. */
  .tour-pop[data-place="center"] { left: 12px !important; right: 12px; top: auto !important; bottom: 16px; width: auto !important; transform: none !important; }
}

/* ── Notifications (bell + dropdown) */
.notif-backdrop { display: none; }            /* only shown on mobile (see responsive) */
.notif-wrap { position: relative; flex-shrink: 0; }
.notif-bell { position: relative; }
.notif-bell.has-unread { color: var(--fg-0); }
.notif-dot {
  position: absolute; top: -3px; right: -3px;
  min-width: 16px; height: 16px; padding: 0 4px;
  display: grid; place-items: center;
  border-radius: 99px;
  /* The numeral reads on the semantic red in both themes (the red itself is --c-down and never
     changes): near-black on the dark theme's #F2585B is 5.9:1 (white was 3.3:1), white on the
     light theme's deeper #C21E37 is 5.9:1 (near-black would be 3.3:1). */
  background: var(--c-down); color: #0A0D0C;
  font-family: var(--f-mono); font-size: 0.59375rem; font-weight: 700; line-height: 1;
  border: 2px solid var(--bg-0);
}
:where([data-theme="light"]) .notif-dot { color: #fff; }
.notif-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 80;
  width: 348px; max-width: calc(100vw - 24px);
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2); overflow: hidden;
  animation: modalIn 150ms cubic-bezier(.22,1,.36,1);
}
.notif-hdr { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--border-1); }
.notif-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.notif-list { max-height: min(420px, 60vh); overflow-y: auto; }
.notif-empty { padding: 28px 20px; text-align: center; font-size: var(--t-md); color: var(--fg-3); line-height: 1.5; }
.notif-item {
  display: flex; align-items: flex-start; gap: 10px;
  width: 100%; text-align: left;
  padding: 11px 14px; border-bottom: 1px solid var(--border-1);
  transition: background 110ms;
}
.notif-item:hover { background: var(--bg-2); }
.notif-item.fresh { background: color-mix(in oklab, var(--accent) 7%, transparent); }
.notif-item.fresh:hover { background: color-mix(in oklab, var(--accent) 11%, transparent); }
.notif-item-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
.notif-item-main { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.notif-item-r1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.notif-item-r1 .ticker { font-size: var(--t-md); }
.notif-item-ct { font-size: var(--t-xs); color: var(--fg-2); }
.notif-item-kind { font-size: var(--t-md); font-weight: 600; color: var(--fg-1); }
.notif-item-more { font-size: var(--t-xs); color: var(--fg-3); }
.notif-item-recap { display: inline-flex; align-items: center; gap: 6px; font-size: var(--t-md); font-weight: 600; color: var(--accent); }
.notif-item-r2 { font-size: var(--t-xs); color: var(--fg-3); }
/* Settings view */
.notif-settings { padding: 4px 0 8px; max-height: min(440px, 64vh); overflow-y: auto; }
.notif-set-sec { padding: 12px 14px; border-bottom: 1px solid var(--border-1); }
.notif-set-sec:last-of-type { border-bottom: 0; }
.notif-set-row { display: flex; align-items: center; gap: 12px; }
.notif-set-txt { flex: 1; min-width: 0; }
.notif-set-name { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.notif-set-sub { font-size: var(--t-xs); color: var(--fg-2); margin-top: 2px; }
.notif-set-warn { font-size: var(--t-xs); color: var(--c-warn); margin: 8px 0 12px; line-height: 1.4; }
/* The account wants push, this device has none: say so beside the one tap that sets it up. */
.notif-set-here {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 8px 0 12px; font-size: var(--t-xs); color: var(--fg-2); line-height: 1.4;
}
.notif-set-here .btn { flex: none; }
/* The section's label, each choice's line and the foot are --fg-2, not --fg-3: they open
   Preferences now (Notifications first), and --fg-3 read 2.6 to 3.1:1 at these sizes. */
.notif-set-lbl { font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: .08em; color: var(--fg-2); margin-bottom: 8px; }
.notif-trig { display: flex; align-items: center; gap: 11px; padding: 6px 0; cursor: pointer; }
.notif-trig-txt { display: flex; flex-direction: column; }
.notif-trig-txt strong { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.notif-trig-txt span { font-size: var(--t-xs); color: var(--fg-2); }
.notif-set-foot { padding: 12px 14px 4px; font-size: var(--t-xs); color: var(--fg-2); line-height: 1.5; }

/* ════════════════════════════════════════════════════════════════════════
   RESPONSIVE — kept at the very end so these overrides win the cascade.
   ════════════════════════════════════════════════════════════════════════ */

/* ── Medium screens (laptops, scaled-up 1080p): the zone between the phone
   layout (≤900px) and full desktop, where fixed side rails starve the data
   tables and percent-width columns crush their content. Two principles:
   1. A data table never shrinks below readable — the panel scrolls sideways
      instead of clipping values.
   2. Side rails stack below/above their main panel once they'd steal too much
      width. */
/* Dense tables (positions 12-col, events 14-col) don't truncate on smaller
   screens — instead the type + padding shrink so every column FITS, the events
   rail slims, and it only drops below the table once even that isn't enough.
   Goal: "Jun 11, 12:30" stays readable instead of becoming "Jun 11, …".
   The dense sizes (heads 0.5625rem, cells 0.71875rem; 0.53125 and 0.65625rem below 1180px) stay
   literals off the type scale: the Events and Positions columns are measured against exactly them. */
@media (max-width: 1950px) and (min-width: 901px) {
  .split.cols { grid-template-columns: minmax(0, 1fr) 380px; }   /* slimmer events rail */
  .feed-scroll { overflow-x: auto; }
  .tbl-fixed thead th { font-size: 0.5625rem; letter-spacing: .05em; padding-left: 7px; padding-right: 7px; }
  .tbl-fixed tbody td { font-size: 0.71875rem; padding-left: 7px; padding-right: 7px; }
  .tbl-fixed .ticker { font-size: var(--t-sm); }
}
/* Events and Positions keep those dense cells on a wider screen too: their rem columns are sized
   against exactly them (.ev-tbl / .pos-tbl above), and the roomier cells a wide screen gives the
   other tables (16px padding, larger type) cut the very values they were sized for: an expiry, a
   time, a pill, a stamp, the third row button. */
@media (min-width: 1951px) {
  .ev-tbl thead th, .pos-tbl thead th { font-size: 0.5625rem; letter-spacing: .05em; padding-left: 7px; padding-right: 7px; }
  .ev-tbl tbody td, .pos-tbl tbody td { font-size: 0.71875rem; padding-left: 7px; padding-right: 7px; }
  .ev-tbl .ticker, .pos-tbl .ticker { font-size: var(--t-sm); }
}
@media (max-width: 1620px) and (min-width: 901px) {
  /* Not enough room for table + rail side-by-side even scaled → stack the rail
     below; the table takes the full content width. */
  .split.cols, .split.solo { grid-template-columns: 1fr; grid-template-rows: none; flex: none; }
  .split.cols > .panel:last-child .scroll-y { max-height: 380px; }
}
/* Open Positions' event rail (the owner's) stays beside the table only while the table keeps the
   width it needs (.pos-tbl's min-width, 977px at the default size and 1074px at Larger, 26px more
   with a touch screen's row buttons, plus the 380px rail and the list's 10px scrollbar): below that
   it goes under the table, as it does below 1620px, rather than the table scrolling sideways.
   Small needs nothing more. */
@media (max-width: 1670px) and (min-width: 1621px), (pointer: coarse) and (max-width: 1690px) and (min-width: 1621px) {
  :root:not([data-fontscale="sm"]) .split.cols.pos-split { grid-template-columns: 1fr; grid-template-rows: none; flex: none; }
  :root:not([data-fontscale="sm"]) .split.cols.pos-split > .panel:last-child .scroll-y { max-height: 380px; }
}
@media (max-width: 1770px) and (min-width: 1671px), (pointer: coarse) and (max-width: 1790px) and (min-width: 1691px) {
  :root[data-fontscale="lg"] .split.cols.pos-split { grid-template-columns: 1fr; grid-template-rows: none; flex: none; }
  :root[data-fontscale="lg"] .split.cols.pos-split > .panel:last-child .scroll-y { max-height: 380px; }
}
@media (max-width: 1180px) and (min-width: 901px) {
  /* Narrow desktops / split windows: squeeze a little more so 12–14 columns
     still fit the full-width table. */
  .tbl-fixed thead th { font-size: 0.53125rem; letter-spacing: .02em; padding-left: 5px; padding-right: 5px; }
  .tbl-fixed tbody td { font-size: 0.65625rem; padding-left: 5px; padding-right: 5px; }
}
@media (max-width: 1200px) and (min-width: 901px) {
  /* Charts: the sources column stacks above the chart instead of pinching it. */
  .split.cols-charts { grid-template-columns: 1fr; grid-template-rows: none; flex: none; }
  .charts-sources { max-height: 220px; }
}
@media (max-width: 1080px) and (min-width: 901px) {
  /* Guide: the side TOC would pinch the prose — switch to the chip nav early. */
  .guide { grid-template-columns: 1fr; gap: 16px; }
  .guide-toc { position: static; flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .guide-toc-hdr, .guide-toc-foot { flex-basis: 100%; border: 0; padding: 2px 4px; }
  .guide-toc-item { border: 1px solid var(--border-1); border-radius: 99px; padding: 5px 11px; font-size: 0.71875rem; }   /* off the type scale, as the contents list (.guide-toc-item) is */
  .guide-toc-item .guide-role { display: none; }
}

@media (max-width: 900px) {
  /* Phones: drop the viewport lock — the page scrolls naturally instead. */
  .shell { grid-template-columns: 1fr; height: auto; overflow: visible; }
  .main { overflow: visible; min-height: 0; }
  .content { overflow: visible; }
  .split.cols, .split.solo { flex: none; min-height: 0; grid-template-rows: none; }

  /* Reading-progress rail: thin accent line pinned to the top edge of the
     viewport; its fill tracks how far down the page you've scrolled, so it's
     obvious there's more below. Fades out on pages that don't overflow. */
  /* Under the installed iPhone app's status bar, never through it (env() is 0 everywhere else). */
  .scroll-rail {
    display: block; position: fixed; top: env(safe-area-inset-top, 0px); left: 0; right: 0;
    height: 3px; z-index: 90; pointer-events: none;
    background: var(--border-1);
    opacity: 0; transition: opacity 220ms ease;
  }
  .scroll-rail.on { opacity: 1; }
  .scroll-rail-fill {
    display: block; height: 100%; width: 100%;
    transform-origin: left center; transform: scaleX(0);
    background: var(--accent);
    box-shadow: 0 0 8px color-mix(in oklab, var(--accent) 55%, transparent);
  }
  /* The same scaleX as app.jsx would write, from the document's scroll position. The
     timeline comes after the shorthand, which would reset it. */
  @supports (animation-timeline: scroll()) {
    .scroll-rail-fill { animation: scrollRailFill auto linear both; animation-timeline: scroll(root block); }
  }
  .split.cols .scroll-y, .split.solo .scroll-y { flex: none; }

  /* Sidebar → slide-in drawer. Closed, it is also hidden, not only slid out of sight: its links
     leave the Tab order and a screen reader's reading order (they were the first nine stops on every
     page). The hiding waits for the slide out to finish; opening shows it at once. */
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0;
    width: max(252px, 12.6rem); max-width: 84vw;   /* 252px with the default browser font (see .shell) */
    z-index: 120;
    transform: translateX(-100%);
    visibility: hidden;
    transition: transform 220ms cubic-bezier(.22, 1, .36, 1), visibility 0s linear 220ms;
    box-shadow: var(--shadow-2);
    /* clear the status bar (top) and home indicator (bottom) inside the drawer */
    padding-top: calc(18px + env(safe-area-inset-top, 0px));
    padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px));
    padding-left: calc(14px + env(safe-area-inset-left, 0px));
  }
  .sidebar.open { transform: translateX(0); visibility: visible; transition-delay: 0s; }
  .nav-backdrop {
    display: block; position: fixed; inset: 0;
    background: rgba(2, 4, 8, 0.5);
    backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);
    z-index: 110; animation: fadeIn 160ms ease-out;
  }
  .nav-toggle {
    display: inline-grid; place-items: center;
    width: 40px; height: 40px; border-radius: var(--radius);
    color: var(--fg-1); background: var(--bg-1);
    border: 1px solid var(--border-1); flex-shrink: 0;
  }
  .nav-toggle:hover { background: var(--bg-2); }

  /* Header: ONE pinned row, the menu, the page title and the bell (app.jsx renders only these
     inside .topbar on a phone). It was two to three rows, 151 px (198 in the installed iPhone app)
     pinned over every screen. Its background, blur and border are the desktop's, so every brand's
     tokens still apply. */
  .topbar { padding: calc(12px + env(safe-area-inset-top, 0px)) calc(16px + env(safe-area-inset-right, 0px)) 12px calc(16px + env(safe-area-inset-left, 0px)); gap: 10px 12px; flex-wrap: nowrap; }
  .topbar-left { flex: 1 1 auto; min-width: 0; }
  .topbar-title { min-width: 0; flex: 1; }
  .topbar-title h1 { font-size: 1.125rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .notif-wrap { flex-shrink: 0; }
  .notif-bell { width: 38px; height: 38px; }
  /* Under it, as page content that scrolls away: the status line and, for someone with several
     servers, the server picker. */
  .topbar-meta {
    display: flex; flex-direction: column; gap: 10px;
    padding: 10px calc(16px + env(safe-area-inset-right, 0px)) 0 calc(16px + env(safe-area-inset-left, 0px));
  }
  .topbar-sub { font-size: var(--t-xs); gap: 6px; row-gap: 2px; }
  /* One line in every state, so nothing under it moves when a fact arrives or when the connection
     drops and comes back: the state and the last alert keep their width (a phone reads "last 4:51
     AM", without "alert" and the channel count), and the server's name comes last and is the part
     cut short. For a member of one server it is the header's only mention of their community, so it
     keeps at least six letters and an ellipsis whenever they fit (the 9ch basis counts the separator,
     its margin and the ellipsis too); with less room beside a long state ("reconnecting · updated
     9:05 PM") it is left off whole until the line has room again, as a computer
     leaves off a fact that does not fit, never cut to a letter or a stray dot. A second line for it
     moved the whole page down 20 px on every drop. Held at the status pill's height (the taller
     line, 0.75rem) from the first paint. */
  .topbar-meta .topbar-sub { flex-wrap: nowrap; align-items: baseline; min-height: calc(1.45 * var(--t-sm)); }
  /* The state never runs past the screen: wider than the line ("offline · updated Oct 14, 9:05 PM"
     at 320 px with Larger text), its time takes the pill's second line. */
  .topbar-meta .topbar-sub > .status { flex: none; max-width: 100%; flex-wrap: wrap; row-gap: 2px; }
  /* The facts wrap inside a box one line tall, as on a computer, and what wraps is clipped: the
     name, when it has no room for six letters and an ellipsis. */
  .topbar-meta .topbar-facts {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; flex: 1 1 auto; min-width: 0;
    height: calc(1.45 * var(--t-xs)); overflow: hidden;
  }
  /* A blank first item, so the name is never the first on its line: a flex line keeps its first
     item however little room is left, and squeezed the name to a dot beside "offline" or
     "reconnecting" (no last alert then). A zero-width space, not "", so the first line keeps a
     text line's height and baseline when the name wraps (an empty box has neither, and the facts
     dropped below the state); its empty alternative keeps it out of what a screen reader reads.
     Its negative margin cancels the gap after it. */
  .topbar-meta .topbar-facts::before { content: "\200B"; content: "\200B" / ""; margin-right: -6px; }
  .topbar-meta .topbar-fact { display: block; flex: none; white-space: nowrap; }
  .topbar-meta .topbar-fact .sep { margin-right: 6px; }
  .topbar-meta .topbar-fact-srv { order: 9; flex: 1 1 9ch; max-width: max-content; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .topbar-meta .topbar-fact-ch, .topbar-meta .topbar-alert-w { display: none; }
  .server-multi { min-width: 0; }
  .server-multi-btn { width: 100%; }
  .server-multi-btn .icon:last-child { margin-left: auto; }
  /* Keyboard focus, a ticker field's bar (shared.jsx revealFieldOnKeyboard), the News headlines and
     a tour's target stop clear of the pinned bar instead of under it (WCAG 2.4.11). app.jsx writes
     --topbar-h from the bar itself. A margin on what is scrolled to, never html's
     scroll-padding-top: with the page's scrollport padded, the menu and the bell inside the pinned
     bar never count as in view, and focusing one (Escape out of the bell gives it focus back)
     scrolled the page to the top. Lists that scroll inside the page keep their own edges. */
  :is(.content, .topbar-meta, .app-banner) :is(a[href], button, input, select, textarea, summary, [tabindex], [data-tour], .filterbar, .news-drill):not(:is(.af-menu, .charts-fill-cap, .trade-timeline) *) {
    scroll-margin-top: calc(var(--topbar-h, 65px) + 8px);
  }
  .content { padding: 16px calc(14px + env(safe-area-inset-right, 0px)) calc(72px + env(safe-area-inset-bottom, 0px)) calc(14px + env(safe-area-inset-left, 0px)); gap: 14px; }

  /* Notifications open as a clean top sheet over a backdrop — never the broken
     off-screen strip the anchored desktop dropdown produced on mobile. */
  .notif-backdrop { display: block; position: fixed; inset: 0; z-index: 124; background: rgba(2, 4, 8, 0.55); animation: fadeIn 150ms ease-out; }
  .notif-panel {
    position: fixed; top: calc(10px + env(safe-area-inset-top, 0px)); left: 10px; right: 10px; z-index: 125;
    width: auto; max-width: none; max-height: 86vh;
    display: flex; flex-direction: column;
  }
  .notif-list, .notif-settings { max-height: none; flex: 1; }

  /* Display prefs: a clean full-width bottom sheet over a backdrop. Portaled to
     <body>, so (unlike an in-sidebar popover) it fills the viewport, not the drawer. */
  .prefs-backdrop { display: block; background: rgba(2, 4, 8, 0.55); animation: fadeIn 150ms ease-out; }
  .prefs-panel {
    top: auto; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); left: 10px; right: 10px;
    width: auto; max-width: none; max-height: 86vh;
  }

  /* Was a column of three spans, the first of which wrapped — 4+ lines of chrome
     under every screen on a phone. Now one wrapping row that settles at two lines,
     with the wordier halves ("All rights reserved.", "Developed by") dropped. */
  .app-footer {
    flex-direction: row; flex-wrap: wrap; justify-content: center;
    gap: 2px 8px; text-align: center; padding: 10px 14px;
    font-size: var(--t-xs); line-height: 1.5;
  }
  .app-footer-mid { flex: 0 0 auto; }
  .foot-long { display: none; }
  .app-footer-mid { flex: none; }

  /* Stack every desktop split */
  .split.cols, .split.cols-narrow, .split.cols-charts { grid-template-columns: 1fr; }
  .charts-subgrid { grid-template-columns: 1fr; }
  /* Charts: drop the viewport-fill machinery so the page scrolls naturally */
  .split.cols-charts { flex: none; min-height: 0; grid-template-rows: none; }
  .charts-right { grid-template-rows: none; }
  /* Current Positions + Chart Events flow with the page — no nested scrollbars */
  .charts-fill { flex: none; min-height: 0; max-height: none; overflow: visible; }
  /* Tickers list stays a contained scroll (it can be long) */
  .charts-fill-cap { flex: none; min-height: 0; max-height: 44vh; }

  .statgrid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
  .stat { padding: 12px 14px; gap: 5px; }
  .stat .val { font-size: var(--t-xl); }
  .stat .lbl { font-size: var(--t-sm); }
  /* Usage filter controls go full-width and the server picker leads */
  .usage-controls { width: 100%; gap: 8px; }
  .usage-srv { flex: 1 1 140px; max-width: none; }
  .usage-controls .seg { flex: 1 1 auto; display: flex; }
  .usage-controls .seg-btn { flex: 1; text-align: center; justify-content: center; }
  .recap-grid { grid-template-columns: 1fr; }
  /* Phone: a 7-column table does not fit a 360px screen, so the row becomes a
     two-line card. This was a GRID with explicit columns but no rows, which let
     auto-placement drop every cell onto a line of its own — six ragged lines per
     trade, each value floating with nothing to align to. Flex-wrap can put several
     values on one line, which grid cannot without one cell per item, so:

        ● $APP  8/7 380P                          +9.5%  WIN
          19.00 → 20.80                        peak +36.8%

     The line break is a zero-height full-width ::after sitting at order 5 — items
     ordered after it are forced onto the second line. A computer takes the same row
     whenever the body is too narrow for the table (the @container recap-body block
     after the table's own rules); change both together. */
  .recap-cols { display: none; }
  .recap-line, .recap-line.recap-trim-line {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 3px 7px; max-width: none; padding: 9px 6px;
  }
  .recap-line::after { content: ""; flex-basis: 100%; height: 0; order: 5; }
  .recap-line > .rl-sq { order: 1; align-self: center; flex: 0 0 auto; }
  .recap-line > .rl-name {
    order: 2; flex: 1 1 0; min-width: 0;
    flex-direction: row; align-items: baseline; gap: 6px;
    overflow: hidden;
  }
  .recap-line > .rl-name .rl-con { font-size: var(--t-sm); }
  .recap-line > .rl-pct { order: 3; margin-left: auto; white-space: nowrap; }
  .recap-line > .rl-wl  { order: 4; flex: 0 0 auto; }
  /* second line: the move on the left, the peak on the right */
  .recap-line > .rl-num { order: 6; font-size: var(--t-md); color: var(--fg-2); }
  .recap-line > .rl-num + .rl-num { order: 7; }
  .recap-line > .rl-num + .rl-num::before { content: "→"; color: var(--fg-deco); margin-right: 5px; }
  .recap-line > .rl-peakcell { order: 8; margin-left: auto; justify-content: flex-end; }
  /* First line needs a left indent matching the marker so the two lines align. */
  .recap-line > .rl-num:first-of-type { margin-left: 16px; }

  /* Trims carry 5 cells (no exit column, no W/L): identity + result, move beneath. */
  .recap-line.recap-trim-line > .rl-tkr-line { order: 2; flex: 0 0 auto; }
  .recap-line.recap-trim-line > .rl-con { order: 3; flex: 1 1 0; min-width: 0; font-size: var(--t-sm); }
  .recap-line.recap-trim-line > .rl-pct { order: 4; margin-left: auto; }
  .recap-line.recap-trim-line > .rl-mv  { order: 6; margin-left: 16px; text-align: left; font-size: var(--t-md); color: var(--fg-2); }
  /* The analyst's published lines take the same two-line shape: what and how much on the
     first line, who and the move on the second. Every rule is scoped to .pub-line, so the
     closed-trade and trim rows that share the rl-* classes are untouched. */
  .pub-line { align-items: baseline; gap: 3px 7px; }
  .pub-line::after { content: ""; flex-basis: 100%; height: 0; order: 5; }
  .pub-line > .rl-sq { order: 1; align-self: center; flex: 0 0 auto; }
  .pub-line > .rl-tkr { order: 2; flex: 0 0 auto; }
  .pub-line > .rl-con { order: 2; flex: 1 1 0; min-width: 0; font-size: var(--t-sm); }
  .pub-line > .rl-pct { order: 3; margin-left: auto; white-space: nowrap; }
  .pub-line > .pub-an { order: 6; margin-left: 16px; min-width: 0; color: var(--fg-2); }
  .pub-line > .rl-mv { order: 7; font-size: var(--t-md); color: var(--fg-2); }
  .pub-line > .pub-flag, .pub-line > .pub-fix { order: 8; }
  /* The caret stays beside "Published recap" instead of dropping to a line of its own
     under the wrapped figures. */
  .pub-recap-hdr > .pub-caret { order: 1; }
  .pub-recap-hdr > .pub-stats { order: 2; }
  .pub-recap-hdr > .pub-badge { order: 3; }
  .recap-period-hdr { gap: 8px; }
  .rp-stats { gap: 10px; }
  /* Room for three lines, so a finger dragging across the chart never moves the page. */
  .recap-chart-meta { min-height: 4.4em; }
  /* Six hero figures sit three to a row down to a 360px phone. */
  .recap-statset { gap: 12px 18px; }

  /* Best / worst: the result on the first line, who called it on the second, under the
     ticker and whole (a long name wraps rather than being cut, or pushing the figure out).
     The label takes a rem width here so the indent below matches it at every text size.
     With the name on a line of its own, two callouts may share a row from 16rem each (a
     computer, where the name shares the line, needs 22rem). */
  .rbw { flex-basis: 16rem; flex-wrap: wrap; row-gap: 4px; }
  .rbw .rbw-lbl { min-width: 3.25rem; }
  .rbw > .rl-pct { order: 2; }
  .rbw > .analyst { order: 3; flex: 1 1 100%; min-width: 0; padding-left: calc(3.25rem + 10px); }

  /* Leaderboard: a two-line card per analyst, never an ellipsis. The name keeps the
     whole first line (a very long handle may take two) with the average per trade
     beside it; the closes (and trims) and win rate sit under the name, the average win
     and loss under the average. The column heads become one legend line. A computer
     shows the same card whenever the board itself is narrow (the @container board block
     after the board's own rules); change both together. */
  .recap-board-cols { display: none; }
  .recap-board-legend { display: block; font-size: var(--t-xs); color: var(--fg-2); margin: -3px 0 6px; }
  /* A flex card, not a two-column grid: in a grid the second line's pair set the right
     column's width and squeezed the name above it. Here the name shares its line only with
     the average; the second line's pair drops to a third line when a phone is that narrow
     (at 390px, any row whose count reads "26 closes + 42 trims"). */
  .recap-board-row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px; }
  .recap-board-row::after { content: ""; flex-basis: 100%; height: 0; order: 3; }
  .recap-board-row > .analyst { order: 1; flex: 1 1 0; min-width: 0; }
  .recap-board-row > .rb-avg { order: 2; margin-left: auto; }
  .recap-board-row .rb-meta, .recap-board-row .rb-wl { display: flex; font-size: 0.65625rem; color: var(--fg-2); }   /* off the scale: a step up took a third line */
  .recap-board-row .rb-meta { order: 4; gap: 0 10px; padding-left: 16px; }
  .recap-board-row:has(.analyst-pic) .rb-meta { padding-left: 31px; }
  .recap-board-row .rb-wl { order: 5; gap: 0 5px; margin-left: auto; }
  .recap-board-row .rb-n, .recap-board-row .rb-wr { color: var(--fg-2); }
  .rb-wl .rb-al::before { content: "/"; color: var(--fg-deco); margin-right: 5px; }
  .rb-k { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }

  /* Main feed panels grow naturally so the PAGE scrolls — no nested scrollbars. */
  .feed-scroll { max-height: none !important; min-height: 0 !important; overflow: visible !important; }

  /* Guide: TOC becomes a wrap-chip block above the content. */
  .guide { grid-template-columns: 1fr; gap: 16px; }
  .guide-toc { position: static; flex-direction: row; flex-wrap: wrap; gap: 6px; }
  .guide-toc-hdr, .guide-toc-foot { flex-basis: 100%; border: 0; padding: 2px 4px; }
  .guide-toc-item { border: 1px solid var(--border-1); border-radius: 99px; padding: 5px 11px; font-size: 0.71875rem; }   /* off the type scale, as the contents list (.guide-toc-item) is */
  .guide-toc-item .guide-role { display: none; }
  .guide-sec { padding: 16px 14px; }
  .guide-flow-arrow { flex-basis: 100%; text-align: center; }

  /* Panel header: a header that carries tools (e.g. Open Positions' instrument
     filter) drops them onto their own full-width row below the title. */
  .panel-hdr { flex-wrap: wrap; gap: 8px 10px; padding: 14px 14px; }
  .panel-hdr-tools { flex: 1 1 100%; gap: 8px 10px; flex-wrap: wrap; }
  /* Three rows, so nothing crams: (1) the action button, (2) the analyst filter
     + the count, (3) the instrument seg full-width (All / Options / Shares get
     even, readable space instead of being squished against the count). */
  .panel-hdr-tools > .btn { order: 1; flex: 1 1 100%; justify-content: center; }
  .panel-hdr-tools .analyst-filter { order: 2; }
  .panel-hdr-tools .panel-meta { order: 3; margin-left: auto; white-space: nowrap; flex-shrink: 0; }
  .panel-hdr-tools .seg { order: 4; flex: 1 1 100%; display: flex; min-width: 0; }
  .panel-hdr-tools .seg-btn { flex: 1 1 0; min-width: 0; display: inline-flex; align-items: center; justify-content: center; text-align: center; padding-left: 8px; padding-right: 8px; }
  /* Analyst dropdown opens from the LEFT on mobile (the trigger sits left), so
     it never spills off the left edge and clips the names. */
  .af-menu { left: 0; right: auto; min-width: 240px; max-width: calc(100vw - 28px); }

  .chart-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; padding: 14px; }

  /* ── Tables → stacked cards ──────────────────────────────────────────────
     Dense tables become one card per row: each cell shows its column name as a
     label and its value beside it, so nothing is truncated or off-screen. */
  .tbl-cards { min-width: 0 !important; }
  .tbl-cards, .tbl-cards tbody { display: block; width: 100%; }
  .tbl-cards colgroup, .tbl-cards thead { display: none; }
  .tbl-cards tr { display: block; }

  .tbl-cards tbody tr:not(.day-sep):not(.wl-raw-row):not(.expand-row):not(.is-empty) {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 14px;
    align-items: start;
    background: var(--bg-1);
    border: 1px solid var(--border-2);
    border-radius: var(--radius);
    padding: 11px 14px 12px;
    margin-bottom: 14px;
    box-shadow: var(--shadow-1);
  }
  .tbl-cards tbody tr:hover { background: var(--bg-1); }
  .tbl-cards tbody tr.row-selected,
  .tbl-cards tbody tr.row-selected:hover {
    background: var(--accent-glow); border-color: var(--accent-dim);
  }
  .tbl-cards tbody tr.expanded { background: var(--bg-1); }

  /* Each cell is a plain block: a tiny label on its own line, then the value
     flowing normally below it (so inline pieces like "0.49 → 0.35 (-28%)" stay
     on one wrapping line, NOT stacked). Two cells per row via the grid above. */
  /* `.tbl tbody td` sets height:var(--row-h) at specificity (0,1,2); this reset was
     `.tbl-cards td` at (0,1,1) and therefore LOST. Every card cell stayed pinned to the
     desktop row height while its content needed half again as much, so the text simply
     ran over the cell beneath it — the stacked cards looked like overlapping garbage on
     every phone. Matching (0,1,2) via `tbody` lets the reset win. */
  .tbl-cards tbody td,
  .tbl-cards td {
    display: block;
    height: auto; min-height: 0; min-width: 0;
    padding: 6px 0; border: none;
    overflow: visible; white-space: normal; text-overflow: clip;
    font-size: 0.84375rem; text-align: left; line-height: 1.4;
  }
  .tbl-cards td::before {
    content: attr(data-label); display: block; margin-bottom: 3px;
    font-family: var(--f-sans); font-weight: 500; font-size: 0.5625rem; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--fg-3); line-height: 1.2;
  }
  .tbl-cards td:not([data-label])::before { content: none; }
  /* long values (P&L, timestamps) wrap instead of overflowing their half-cell */
  .tbl-cards td.num, .tbl-cards td.mono { white-space: normal !important; }
  .tbl-cards td .inst-detail { white-space: normal; }

  /* Checkbox row */
  .tbl-cards td.cell-check { grid-column: 1 / -1; display: flex; flex-direction: row; align-items: center; gap: 8px; }
  .tbl-cards td.cell-check::before { content: "Select"; display: inline; order: 2; margin: 0; }
  .tbl-cards td.cell-check input { order: 1; }

  /* Ticker chips → label left, chips wrap on the right (aligned with the other
     values for a consistent left-label / right-value rhythm). */
  .tbl-cards td.cell-chips {
    grid-column: 1 / -1; display: block;
    white-space: normal !important; overflow: visible !important;
  }
  .tbl-cards td.cell-chips .wl-tickcell { justify-content: flex-start; flex-wrap: wrap; gap: 6px; min-width: 0; }

  /* Long note → label on its own line (left), text wraps below, clamped to 3
     lines. Overrides the base card cell's right-align + the desktop inline
     ellipsis styles so the label sits left and the text never clips mid-word. */
  .tbl-cards td.cell-note {
    grid-column: 1 / -1;
    display: block; padding: 8px 0 4px; text-align: left;
    white-space: normal !important; overflow: visible !important; text-overflow: clip !important;
  }
  .tbl-cards td.cell-note::before {
    display: block; margin: 0 0 5px; margin-right: 0;
  }
  .tbl-cards td.cell-note .cell-text {
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden; white-space: normal; word-break: break-word;
    color: var(--fg-1); font-size: var(--t-md); line-height: 1.5;
  }

  /* Action buttons → a prominent split footer: equal-width buttons with icon +
     label spanning the bottom of the card (e.g. left = Parser, right = Delete). */
  .tbl-cards td.cell-actions {
    grid-column: 1 / -1;
    display: flex; gap: 8px; padding: 10px 0 2px; margin-top: 4px;
    border-top: 1px solid var(--border-1);
  }
  .tbl-cards td.cell-actions::before { content: none; }
  .tbl-cards td.cell-actions:empty { display: none; }
  .tbl-cards td.cell-actions .icon-btn {
    flex: 1; width: auto; height: 34px;
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    border: 1px solid var(--border-1); border-radius: var(--radius-sm);
    background: var(--bg-2); color: var(--fg-1);
    font-size: var(--t-sm); font-weight: 500;
  }
  .tbl-cards td.cell-actions .icon-btn svg { width: 14px; height: 14px; }
  .tbl-cards td.cell-actions .icon-btn:active { background: var(--bg-3); }
  .tbl-cards td.cell-actions .icon-btn .btn-label { display: inline; }
  .tbl-cards td.cell-actions .icon-btn.danger {
    color: var(--c-down);
    border-color: color-mix(in oklab, var(--c-down) 32%, transparent);
    background: color-mix(in oklab, var(--c-down) 9%, transparent);
  }

  /* Day separator → plain section heading, aligned with the card content */
  .tbl-cards tr.day-sep > td {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
    position: static; padding: 18px 14px 8px;
    background: transparent; border: none;
  }
  .tbl-cards tr.day-sep > td::before { content: none; }
  .tbl-cards tr.day-sep .day-sep-meta { margin-left: 0; }

  /* Expanded note + empty state → plain full-width block */
  .tbl-cards tr.wl-raw-row > td,
  .tbl-cards tr.expand-row > td,
  .tbl-cards tr.is-empty > td {
    display: block; padding: 4px 0 12px; border: none;
  }
  .tbl-cards tr.wl-raw-row > td::before,
  .tbl-cards tr.expand-row > td::before,
  .tbl-cards tr.is-empty > td::before { content: none; }
  /* Parser/timeline detail can be wide — let it scroll inside the card */
  .tbl-cards tr.expand-row > td { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  /* The owner's parser panel in an opened phone position card (positions.jsx MobilePositionCard;
     a phone never draws the table, so a .tbl-cards rule never reached it): one column, the message
     and reasoning over the JSON, instead of two of about 190 and 125px that broke every JSON key
     from its value. minmax(0, …) so a long line wraps inside it rather than widening the card. */
  .mpos-body .parser-detail { grid-template-columns: minmax(0, 1fr); }

  /* Expansion (interaction timeline + parser JSON) → readable on a phone:
     tighter padding, rows wrap instead of truncating, JSON wraps (no h-scroll). */
  .position-detail, .parser-detail { padding: 12px 14px; gap: 14px; }
  .position-detail { border-top: none; }
  .tl-row { flex-wrap: wrap; gap: 3px 8px; padding: 9px 0 9px calc(var(--tl-level, 0) * 14px); }
  .tl-detail, .tl-note {
    white-space: normal; overflow: visible; text-overflow: clip;
    flex: 1 1 100%; min-width: 0;
  }
  .parser-detail pre { font-size: var(--t-xs); white-space: pre-wrap; word-break: break-word; }
  .parser-detail .raw-msg { font-size: var(--t-md); }
  /* Pull the expansion up so it reads as part of the card above it */
  .tbl-cards tbody tr.expanded { margin-bottom: 0; box-shadow: none; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
  .tbl-cards tr.wl-raw-row, .tbl-cards tr.expand-row {
    background: var(--bg-1); border: 1px solid var(--border-2); border-top: none;
    border-radius: 0 0 var(--radius) var(--radius);
    padding: 0 14px 6px; margin-bottom: 16px; box-shadow: var(--shadow-1);
  }

  /* Filter bars: every control gets its own full-width row, segmented toggles
     spread their buttons evenly so all options are visible and tappable. */
  .filterbar { display: flex; flex-wrap: wrap; gap: 8px; }
  .filterbar > * { flex: 1 1 100%; min-width: 0; }
  .filterbar .seg, .filterbar .select { width: 100%; }
  .filterbar .seg { display: flex; }
  /* Timeframe / type segments get a bigger, comfortably-tappable size on mobile. */
  .filterbar .seg-btn { flex: 1; text-align: center; justify-content: center; padding: 10px 8px; font-size: var(--t-md); }
  /* The six recap periods, three to a row: their names no longer fit one row. Scoped to
     this one control; every other filter bar keeps its single row. */
  .filterbar .recap-period-seg { flex-wrap: wrap; height: auto; }
  /* A step smaller and tighter than the other segs, so "Half-yearly" keeps one line in a
     third of a 360px phone even at the largest text size. */
  .filterbar .recap-period-seg .seg-btn { flex: 1 1 30%; min-height: 2.375rem; font-size: var(--t-sm); letter-spacing: 0; padding-left: 4px; padding-right: 4px; }
  .filterbar .recap-period-seg .seg-btn:nth-child(3n) { border-right: 0; }
  .filterbar .recap-period-seg .seg-btn:nth-child(n+4) { border-top: 1px solid var(--border-1); }
  /* The Recaps count has this row to itself: at a large text size it breaks at the dot
     rather than running past the screen edge. */
  .filterbar .recap-count { white-space: normal; }

  /* Mobile select-all bar */
  .cards-selectall {
    display: flex; align-items: center; gap: 10px;
    padding: 11px 14px; border-bottom: 1px solid var(--border-1);
    font-size: var(--t-md); color: var(--fg-1); cursor: pointer;
  }
  .cards-selectall input { width: 16px; height: 16px; }
  .cards-selectall-n { margin-left: auto; font-size: var(--t-xs); color: var(--accent); }

  /* Forms → one column, full-width actions */
  .form-grid { grid-template-columns: 1fr !important; }
  .form-card { padding: 14px; }
  .form-actions { flex-direction: column-reverse; align-items: stretch; gap: 10px; }
  .form-actions .btn { width: 100%; justify-content: center; }
  .form-actions .field-hint { text-align: center; }
  /* Why Save is off ("Name can't be empty."): over the buttons, not under Cancel. */
  .form-actions > .btn-why { order: 3; text-align: center; margin-left: 0; }
  /* The server editor's: a line of its own at the top of its group; Cancel and Save share the next. */
  .form-actions > .srv-edit-btns { margin-left: 0; }
  .srv-edit-btns > .srv-name-why { flex: 1 1 100%; text-align: center; margin-right: 0; }
  .srv-edit-btns > .btn { flex: 1 1 0; width: auto; }

  /* ── Settings (mobile) ─────────────────────────────────────────────────── */
  .settings-content { padding: 18px 16px; }
  .toggle-grid { grid-template-columns: 1fr; }

  /* Sub-nav: pan horizontally without the ugly native scrollbar; momentum +
     snap make it obvious the strip scrolls and keep every tab tappable.
     Lock the vertical axis explicitly — otherwise `overflow-x: auto` makes the
     browser compute `overflow-y: auto` too, and iOS lets the strip rubber-band
     up/down inside its frame. overflow-y: hidden keeps vertical drags scrolling
     the page instead. */
  .settings-tabs {
    overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap; gap: 0;
    -webkit-overflow-scrolling: touch; scroll-snap-type: x proximity;
    scrollbar-width: none; touch-action: pan-x;
  }
  .settings-tabs::-webkit-scrollbar { display: none; }
  .settings-tab { white-space: nowrap; flex: 0 0 auto; padding: 12px 14px; font-size: var(--t-md); scroll-snap-align: start; }

  /* Config rows (channels / analysts / servers / access / role grants) become
     standalone cards — the icon + name block (.row-main) fills the first line and
     pushes the controls to the right; bulky controls wrap below. */
  .channel-row, .user-row {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 8px 12px; grid-template-columns: none;
    padding: 12px 14px;
    background: var(--bg-1); border: 1px solid var(--border-2);
    border-radius: var(--radius); box-shadow: var(--shadow-1);
    margin-bottom: 10px;
  }
  .channel-row:hover, .user-row:hover { background: var(--bg-1); }
  .channel-row .name, .user-row .uname { font-size: var(--t-md); }
  .channel-row .row-main, .user-row .row-main { flex: 1 1 auto; min-width: 0; }
  .row-actions { margin-left: 0; }
  /* strip the wrapping container so each row stands alone as a card */
  .cat-group .rows, .access-list, .setting-list, .servers .rows {
    border: none; border-radius: 0; overflow: visible; background: none;
  }
  /* the inline editor reads as a connected continuation of the card it edits */
  .channel-row:has(+ .row-editor), .user-row:has(+ .row-editor) {
    margin-bottom: 0; border-bottom-left-radius: 0; border-bottom-right-radius: 0;
  }
  .row-editor {
    border: 1px solid var(--border-2); border-top: none;
    border-radius: 0 0 var(--radius) var(--radius); margin-bottom: 10px;
  }

  /* Channels: category picker spans its own row, each option evenly tappable. Five don't fit one line
     of a phone's card, so the last ones take a second line (each grows from its own width, so a line's
     options share it). */
  .channel-row .cat-select { flex: 1 1 100%; order: 5; }
  .channel-row .cat-select button { flex: 1 1 auto; text-align: center; }
  .channel-row .live-dot { order: 4; }
  /* Category-group header → name on top, description full-width below (was squished right) */
  .cat-group-hdr { flex-wrap: wrap; gap: 4px 10px; margin-top: 16px; padding: 6px 2px; }
  .cat-group-hdr .cat-desc { flex: 1 1 100%; text-align: left; max-width: none; font-size: var(--t-sm); }

  /* Analysts: legend pills wrap; per-row tier buttons become a full-width
     segmented control on their own line so you can actually pick a tier. */
  .tier-legend .pill { font-size: var(--t-2xs); }
  .user-row .avatar { order: 1; }
  .user-row .row-main { order: 2; }
  .user-row .mono { order: 3; }
  .user-row .row-actions { order: 4; }
  .user-row .tier-btns { order: 5; flex: 1 1 100%; }
  .user-row .tier-btns .btn { flex: 1; justify-content: center; }
  /* server group header reads as a plain section label between the cards */
  .setting-grp { border: none; padding: 16px 2px 4px; }

  /* Access toolbar: role filter spreads full-width (so the roles are togglable),
     Add-user button stacks full-width beneath it. */
  .access-toolbar { flex-direction: column; align-items: stretch; gap: 10px; padding-right: 0; }
  /* Role filter: with 6 roles, scroll horizontally instead of squishing each
     chip until "Server Owner" is unreadable. */
  .access-toolbar .seg-filter { width: 100%; margin: 0; display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
  .access-toolbar .seg-filter::-webkit-scrollbar { display: none; }
  .access-toolbar .seg-filter .seg-btn { flex: 0 0 auto; justify-content: center; padding: 8px 12px; white-space: nowrap; }
  .access-toolbar > .btn.primary { width: 100%; min-width: 0; }
  .add-bar { padding-right: 0; }
  .add-bar > .btn.primary { width: 100%; min-width: 0; }
  /* open Add-forms span full width without the desktop -8px pull (avoids overflow) */
  .access-toolbar .form-card, .add-bar .form-card { margin-right: 0; }

  /* Access + role-grant rows: name fills line 1 with the toggle + actions; the
     role-grant level <select> drops to its own full-width line. Release the
     bounded inner scroll so the whole page scrolls as one. */
  .access-list { max-height: none; overflow: visible; }
  .access-list .channel-row > .select { flex: 1 1 100%; order: 5; }

  /* Audit log → clean stacked card: time + action pill on line 1, actor and
     target on their own lines; long IDs wrap instead of clipping. */
  .audit-row {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: 3px 10px; padding: 11px 14px 12px;
  }
  .audit-row .audit-time { order: 1; }
  .audit-row .pill { order: 2; margin-left: auto; }
  .audit-row .audit-actor { order: 3; flex-basis: 100%; white-space: normal; overflow: visible; }
  .audit-row .audit-target { order: 4; flex-basis: 100%; white-space: normal; overflow: visible; word-break: break-word; }

  /* Integrations: pipeline cards 2×2 (no more vertical word-piles); run-log
     rows stack the time over the message and break the long stored-as ID. */
  .pipeline-stats { grid-template-columns: 1fr 1fr !important; }
  .runlog-row { grid-template-columns: 1fr !important; gap: 3px !important; overflow-wrap: anywhere; }

  .flyout { width: 100%; }
  /* Full-height sheet on mobile: the header (with its close ✕) must clear the
     status bar / notch, or it lands under it and can't be tapped. Pad past the
     safe-area inset, with a hard minimum for devices that report a 0 inset but
     still draw the page under the status bar. */
  .flyout-hdr { padding-top: max(calc(16px + env(safe-area-inset-top, 0px)), 44px); }
  .flyout-body { padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px)); }
  .toast-host { left: calc(12px + env(safe-area-inset-left, 0px)); right: calc(12px + env(safe-area-inset-right, 0px)); align-items: stretch; }
  .toast { max-width: none; min-width: 0; }

  /* Recap card: keep ticker (left) and P&L (right) on one row — it fits at
     full width and reads far cleaner than stacking them. */
  .recap { padding: 16px; }
  .recap .top { gap: 12px; }
  .recap .top > div:first-child { min-width: 0; }
  .recap .ticker-big { font-size: 1.25rem; }
  .recap .pnl { font-size: var(--t-xl); }
}

/* A tablet held upright: the tiles four to a row from 760px, as on a computer (the owner's Usage
   tiles; Open Positions' summary is its own one-row ledger at every phone width). */
@media (min-width: 760px) and (max-width: 900px) {
  .statgrid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 460px) {
  .statgrid { grid-template-columns: 1fr 1fr; }
  .settings-content h2 { font-size: 1rem; }
  /* Compact stat tiles so the four KPIs don't dominate the phone screen. */
  .stat { padding: 10px 12px; gap: 3px; }
  .stat .val { font-size: 1.1875rem; }
  .stat .lbl { font-size: var(--t-xs); gap: 6px; }
  .stat .delta { font-size: var(--t-2xs); }
}
/* A narrow phone: the status line under the title is two lines from the first paint, the state and
   the last alert, then the server's name on its own line in full. On one line the name was cut to
   "The …" at 320 px with Larger text, or squeezed out entirely beside a dated alert, leaving a
   stray dot. Two lines held from the start, so nothing moves when the facts arrive. */
@media (max-width: 360px) {
  .topbar-meta .topbar-sub {
    flex-wrap: wrap; align-content: flex-start;
    min-height: calc(1.45 * var(--t-sm) + 2px + 1.45 * var(--t-xs));
  }
  .topbar-meta .topbar-facts { display: contents; }
  .topbar-meta .topbar-facts::before { display: none; }
  .topbar-meta .topbar-fact-srv { flex: 1 1 100%; max-width: 100%; }
  .topbar-meta .topbar-fact-srv .sep { display: none; }
}

/* The originating post on a parse-quality to-do — enough to recognise the trade
   without leaving the tab. Clamped: some forwards are very long. */
.todo-src {
  display: flex; gap: 8px; align-items: baseline;
  margin-top: 6px; padding: 6px 8px;
  background: var(--bg-0); border: 1px solid var(--border-1);
  border-radius: calc(var(--radius) - 2px);
}
.todo-src-ch { font-size: 0.65625rem; color: var(--fg-3); flex-shrink: 0; }   /* off the scale: a step up showed less of the post beside it */
.todo-src-txt {
  font-size: var(--t-sm); color: var(--fg-2); line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden; word-break: break-word;
}

/* Recap approval — the recap's own numbers as evidence, then the choice. Laid
   out as a block so it never squeezes the row's other actions. */
.todo-approve { display: flex; flex-direction: column; gap: 6px; align-items: flex-end; }
.todo-approve-ev { display: flex; gap: 10px; font-size: var(--t-xs); color: var(--fg-3); }
.todo-approve-px {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--t-sm); color: var(--fg-2);
}
.todo-approve-acts { display: flex; gap: 6px; }
@media (max-width: 640px) {
  .todo-approve { align-items: stretch; }
  .todo-approve-acts { justify-content: flex-end; }
}

/* Position-level edit affordance above a timeline. The warning only appears when
   the events genuinely disagree about the contract — i.e. the aggregator is
   already treating them as more than one position. */
.tl-poshead {
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  padding: 0 0 6px; margin-bottom: 4px; border-bottom: 1px solid var(--border-1);
}
.tl-warn {
  font-family: var(--f-mono); font-size: var(--t-xs); letter-spacing: .03em;
  color: var(--c-down); border: 1px solid currentColor;
  padding: 1px 6px; border-radius: 999px;
}
.tl-posedit { font-size: var(--t-sm); padding: 3px 8px; }
.manage-acts { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }

/* Subsection heading inside a period block ("Positions closed" / "Trims taken").
   Both lists now carry one, so the day reads as labelled sections rather than a
   run of rows whose grouping you have to infer. */
.recap-sub-hd {
  display: flex; align-items: baseline; gap: 8px;
  font-size: var(--t-sm); letter-spacing: .08em; text-transform: uppercase;
  color: var(--fg-2); margin: 14px 0 2px; padding: 0 8px 6px;
  border-bottom: 1px solid var(--border-1);
}
.recap-sub-hd:first-child { margin-top: 4px; }
.recap-sub-hd .rt-note { text-transform: none; letter-spacing: 0; font-size: var(--t-sm); color: var(--fg-3); }
/* The trims block keeps its own top rule only when it follows closed trades. */
.recap-trims { margin-top: 0; padding-top: 0; border-top: 0; }

/* Inline qualifier inside a field label ("market time (ET)"). */
.calc-hint { color: var(--fg-3); font-weight: 400; text-transform: none; letter-spacing: 0; }

/* "Trims in stats" — a real toggle switch (reuses the shared .switch), so its
   on/off state is unmistakable at a glance rather than a mystery checkbox. */
.recap-trim-toggle { gap: 8px; font-size: var(--t-sm); color: var(--fg-2); user-select: none; }
.recap-trim-toggle .recap-trim-lbl { white-space: nowrap; }
.recap-trim-toggle input:checked ~ .recap-trim-lbl { color: var(--fg-1); }

/* ── Live market data — flash, chips, sparkline ─────────────────────────────
   Terminal idiom (TWS/Bloomberg): flash the changed NUMBER's text colour, never
   a background wash — reads on both themes, survives white-label --bg overrides,
   and keeps WCAG 2.3.1 headroom. Attack ~150ms hold, decay to steady over
   ~800ms (IBM Carbon productive easing); 950ms total sits under the 1Hz paint
   gate so flashes never overlap. Tick colours derive from the semantic pair via
   color-mix, so brands + the light theme inherit them for free.
   prefers-reduced-motion KEEPS the colour flash (colour change is not vestibular
   motion, and without a transient, silent swaps go unseen) and kills only the
   decorative pulses. */
:root {
  --tick-up:   color-mix(in oklab, var(--c-up) 72%, #fff);
  --tick-down: color-mix(in oklab, var(--c-down) 72%, #fff);
}
:root[data-theme="light"] {
  --tick-up:   color-mix(in oklab, var(--c-up) 60%, #000);
  --tick-down: color-mix(in oklab, var(--c-down) 60%, #000);
}
.lv-flash { display: inline; }
.lv-flash.lv-up   { animation: lvTickUp   950ms cubic-bezier(0.2, 0, 0.38, 0.9) 1; }
.lv-flash.lv-down { animation: lvTickDown 950ms cubic-bezier(0.2, 0, 0.38, 0.9) 1; }
@keyframes lvTickUp   { 0%, 16% { color: var(--tick-up); }   100% { color: currentColor; } }
@keyframes lvTickDown { 0%, 16% { color: var(--tick-down); } 100% { color: currentColor; } }
/* Persistent direction glyph beside the P&L — steady SIGN colour (position vs
   entry), never the transient tick colour. Direction is never colour-only. */
.lv-arrow { font-size: 0.5625rem; margin-right: 3px; vertical-align: 1px; }
.lv-arrow.up { color: var(--c-up); } .lv-arrow.down { color: var(--c-down); }
/* Data-state chip: amber for SIM / DELAYED so neither is mistaken for real-time;
   green for a genuine LIVE feed. */
.lv-chip {
  font-family: var(--f-mono); font-size: 0.5625rem; letter-spacing: .08em; font-weight: 600;
  color: var(--c-watch); border: 1px solid currentColor; border-radius: 999px;
  padding: 0px 5px; margin-left: 6px; vertical-align: 1px;
  animation: lvPulse 2.4s ease-in-out infinite;
}
.lv-chip-live { color: var(--c-up); }
/* The underlying's live quote riding along on an option row (context only). fg-2: it is
   data, and fg-3 measured 2.6:1. Its arrow keeps the sign colour. */
.lv-underlying { color: var(--fg-2); }
.lv-underlying .lv-u-arrow { font-size: 0.5rem; margin-left: 2px; }
.lv-underlying .lv-u-arrow.up { color: var(--c-up); }
.lv-underlying .lv-u-arrow.down { color: var(--c-down); }
@keyframes lvPulse { 0%,100% { opacity: 1; } 50% { opacity: .55; } }
/* Stale = FEED problem while the session is open (grey + hollow dot). A closed
   market is NOT stale: marks hold full-strength colour with an "At close" chip. */
.lv-stale { color: var(--fg-3) !important; }
.lv-stale-dot { color: var(--fg-deco); margin-right: 3px; font-size: 0.5625rem; }
.lv-spark { display: block; margin-top: 3px; opacity: .8; margin-left: auto; }
@media (prefers-reduced-motion: reduce) {
  /* Colour flashes STAY (no transform in the keyframes). Only decoration stops. */
  .lv-chip { animation: none; }
}

/* ── "What's new" announcement modal + CSS fireworks ────────────────────────
   A one-time celebration for a shipped feature. Fireworks are pure CSS box-
   shadow bursts (no libraries); disabled under prefers-reduced-motion. */
.announce-back {
  position: fixed; inset: 0; z-index: 320;
  background: rgba(2, 4, 8, 0.62);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  animation: rpIn 200ms ease;
}
.announce {
  position: relative; overflow: hidden;
  width: min(460px, 100%);
  background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 10%, var(--bg-1)), var(--bg-1));
  border: 1px solid color-mix(in oklab, var(--accent) 34%, var(--border-2));
  border-radius: var(--radius-lg); box-shadow: var(--shadow-2);
}
.announce-body { position: relative; z-index: 2; padding: 26px 24px 22px; text-align: center; }
.announce-badge {
  display: inline-block; font-family: var(--f-mono); font-size: var(--t-xs); font-weight: 700;
  letter-spacing: .14em; color: var(--bg-0); background: var(--accent);
  padding: 3px 9px; border-radius: 999px; margin-bottom: 12px;
}
.announce-title { font-size: var(--t-xl); font-weight: 700; color: var(--fg-0); margin: 0 0 8px; letter-spacing: -0.01em; }
.announce-copy { font-size: 0.84375rem; line-height: 1.55; color: var(--fg-1); margin: 0 auto 18px; max-width: 380px; }
.announce-acts { display: flex; gap: 10px; justify-content: center; }
.announce-acts .btn { padding: 9px 18px; font-size: var(--t-md); }

/* Fireworks: each <i> launches (translateY) then bursts via a spray of
   box-shadow points that expand outward and fade. */
.announce-fw { position: absolute; inset: 0; z-index: 1; pointer-events: none; }
.announce-fw i {
  position: absolute; top: 58%; width: 4px; height: 4px; border-radius: 50%;
  opacity: 0; transform: translateY(0);
}
.announce-fw i:nth-child(1) { left: 22%; --fw: var(--c-up);    animation: fwLaunch 2.6s ease-out 0.2s infinite, fwBurst 2.6s ease-out 0.2s infinite; }
.announce-fw i:nth-child(2) { left: 50%; --fw: var(--c-trim);  animation: fwLaunch 2.6s ease-out 0.9s infinite, fwBurst 2.6s ease-out 0.9s infinite; }
.announce-fw i:nth-child(3) { left: 74%; --fw: var(--c-add);   animation: fwLaunch 2.6s ease-out 1.5s infinite, fwBurst 2.6s ease-out 1.5s infinite; }
.announce-fw i:nth-child(4) { left: 38%; --fw: var(--c-watch); animation: fwLaunch 2.6s ease-out 2.0s infinite, fwBurst 2.6s ease-out 2.0s infinite; }
@keyframes fwLaunch {
  0%   { opacity: 0; transform: translateY(40px); }
  18%  { opacity: 1; transform: translateY(-30px); }
  22%  { opacity: 1; transform: translateY(-34px); }
  100% { opacity: 0; transform: translateY(-34px); }
}
@keyframes fwBurst {
  0%, 20% { box-shadow: 0 0 0 0 var(--fw); }
  40% {
    box-shadow:
      0 -34px 0 0 var(--fw), 24px -24px 0 0 var(--fw), 34px 0 0 0 var(--fw),
      24px 24px 0 0 var(--fw), 0 34px 0 0 var(--fw), -24px 24px 0 0 var(--fw),
      -34px 0 0 0 var(--fw), -24px -24px 0 0 var(--fw);
  }
  75% {
    box-shadow:
      0 -60px 0 0 transparent, 42px -42px 0 0 transparent, 60px 0 0 0 transparent,
      42px 42px 0 0 transparent, 0 60px 0 0 transparent, -42px 42px 0 0 transparent,
      -60px 0 0 0 transparent, -42px -42px 0 0 transparent;
  }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .announce-fw { display: none; }
  .announce-back { animation: none; }
}

/* Live sparkline on a mobile position row — its own line under the price. */
.mpos-spark { display: block; margin-top: 4px; }
.mpos-spark .lv-spark { margin-left: 0; margin-top: 0; }

/* ── Owner-only assistant ──────────────────────────────────────────────────── */
/* The launcher and the panel clear the iPhone's home bar in the installed app; the launcher also
   stands above the bottom card of the moment (the rating card, the install nudge: --card-lift). */
.asst-fab {
  position: fixed; z-index: 250;
  right: calc(20px + env(safe-area-inset-right, 0px));
  bottom: calc(20px + env(safe-area-inset-bottom, 0px) + var(--card-lift, 0px));
  width: 48px; height: 48px; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  background: var(--accent); color: #fff;
  border: 1px solid color-mix(in oklab, var(--accent) 60%, #000);
  box-shadow: 0 6px 20px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.04) inset;
  transition: transform 120ms ease, background 120ms ease;
}
.asst-fab:hover { transform: translateY(-1px); }
.asst-fab.open { background: var(--bg-3); color: var(--fg-1); border-color: var(--border-2); }

.asst-panel {
  position: fixed; z-index: 251;
  right: calc(20px + env(safe-area-inset-right, 0px)); bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  width: 380px; max-width: calc(100vw - 24px);
  height: 560px; max-height: calc(100vh - 40px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  display: flex; flex-direction: column;
  background: var(--bg-1); border: 1px solid var(--border-2);
  border-radius: var(--radius-lg); overflow: hidden;
  box-shadow: 0 18px 50px rgba(0,0,0,0.45);
}
.asst-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 11px 14px; border-bottom: 1px solid var(--border-2);
  background: var(--bg-2);
}
.asst-title { display: flex; align-items: center; gap: 8px; font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.asst-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--sinux-emerald); box-shadow: 0 0 8px var(--sinux-emerald); }
.asst-close { background: none; border: none; color: var(--fg-2); cursor: pointer; font-size: var(--t-base); padding: 4px; line-height: 1; }
.asst-close:hover { color: var(--fg-0); }

.asst-msgs { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
.asst-empty { color: var(--fg-2); font-size: var(--t-md); line-height: 1.5; }
.asst-empty p { margin: 0 0 12px; }
.asst-hints { display: flex; flex-direction: column; gap: 7px; }
.asst-hint {
  text-align: left; background: var(--bg-2); border: 1px solid var(--border-2);
  color: var(--fg-1); border-radius: var(--radius); padding: 8px 11px; font-size: var(--t-md); cursor: pointer;
}
.asst-hint:hover { border-color: var(--accent); color: var(--fg-0); }

.asst-msg { display: flex; flex-direction: column; gap: 8px; max-width: 100%; }
.asst-msg.user { align-items: flex-end; }
.asst-bubble {
  padding: 9px 12px; border-radius: 12px; font-size: var(--t-md); line-height: 1.5;
  white-space: pre-wrap; word-break: break-word; max-width: 90%;
}
.asst-msg.user .asst-bubble { background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
.asst-msg.bot .asst-bubble { background: var(--bg-2); color: var(--fg-0); border: 1px solid var(--border-2); border-bottom-left-radius: 4px; }

.asst-typing { display: inline-flex; gap: 4px; }
.asst-typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--fg-deco); animation: asstBlink 1.2s infinite ease-in-out; }
.asst-typing span:nth-child(2) { animation-delay: 0.2s; }
.asst-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes asstBlink { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } }

.asst-prop {
  background: var(--bg-2); border: 1px solid var(--border-2); border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: 10px 12px; max-width: 100%; font-size: var(--t-md);
}
.asst-prop.is-applied { border-left-color: var(--c-up); }
.asst-prop.is-failed { border-left-color: var(--c-down); }
.asst-prop.is-cancelled { opacity: 0.6; }
.asst-prop-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.asst-prop-kind { font-size: var(--t-xs); font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent); }
.asst-prop-label { font-weight: 600; color: var(--fg-0); font-size: var(--t-md); }
.asst-prop-reason { color: var(--fg-2); margin-bottom: 8px; line-height: 1.45; }
.asst-prop-diff { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
.asst-diff-row { display: flex; align-items: center; gap: 8px; font-variant-numeric: tabular-nums; }
.asst-diff-k { color: var(--fg-2); min-width: 82px; font-size: var(--t-sm); }
.asst-diff-before { color: var(--fg-2); text-decoration: line-through; }
.asst-diff-arrow { color: var(--fg-deco); }
.asst-diff-after { color: var(--c-up); font-weight: 600; }
.asst-prop-actions { display: flex; gap: 8px; }
.asst-prop-note { font-size: var(--t-sm); color: var(--fg-2); }
.asst-prop-note.ok { color: var(--c-up); }
.asst-prop-note.bad { color: var(--c-down); }

.asst-input-row { display: flex; align-items: flex-end; gap: 8px; padding: 10px; border-top: 1px solid var(--border-2); background: var(--bg-2); }
.asst-input {
  flex: 1; resize: none; max-height: 120px; min-height: 20px;
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius);
  color: var(--fg-0); font: inherit; font-size: var(--t-md); padding: 8px 10px; line-height: 1.4;
}
.asst-input:focus { outline: none; border-color: var(--accent); }
.asst-send {
  width: 34px; height: 34px; flex-shrink: 0; border-radius: var(--radius); cursor: pointer;
  background: var(--accent); color: #fff; border: none; font-size: 1rem; line-height: 1;
}
.asst-send:disabled { opacity: 0.4; cursor: default; }

@media (max-width: 560px) {
  .asst-panel {
    right: calc(8px + env(safe-area-inset-right, 0px)); left: calc(8px + env(safe-area-inset-left, 0px));
    bottom: calc(8px + env(safe-area-inset-bottom, 0px)); width: auto; height: 72vh;
  }
  .asst-fab { right: calc(14px + env(safe-area-inset-right, 0px)); bottom: calc(14px + env(safe-area-inset-bottom, 0px) + var(--card-lift, 0px)); }
}
/* Nothing in the corner covers the launcher either: while it is there, toasts stand above it (its
   48px and a 12px gap); with the panel open they stand beside the panel where a readable column is
   left beside it (from 800px; below that, beside the 380px panel, a toast was squeezed to 137-276px)
   and above its input row on a narrower screen. The install nudge waits out of sight under an open
   panel (its own rule, with the other overlays). */
body:has(.asst-fab) .toast-host { bottom: calc(80px + env(safe-area-inset-bottom, 0px) + var(--card-lift, 0px)); }
@media (min-width: 800px) {
  body:has(.asst-panel) .toast-host { bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: calc(412px + env(safe-area-inset-right, 0px)); }
}
.mpos-metaline .lv-underlying { white-space: nowrap; }
.recap .pnl + .pnl-sub { margin-top: 6px; }
.recap .pnl-sub + .pnl-sub { margin-top: 3px; }

/* ── Analyst profile ──────────────────────────────────────────────────────── */
/* A tapped name reads as interactive without shouting — the name underlines on
   hover/focus; the whole chip stays inline. */
.analyst.analyst-tap { cursor: pointer; border-radius: 5px; }
.analyst.analyst-tap:hover, .analyst.analyst-tap:focus-visible { text-decoration: underline; text-underline-offset: 2px; outline: none; }
.analyst.analyst-tap:focus-visible { box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 50%, transparent); }

.swatch.lg { width: 40px; height: 40px; border-radius: 10px; }

/* The card rides the shared .flyout drawer (right on desktop, full sheet on
   mobile). A thin accent spine in the analyst's own colour is the signature. */
.an-flyout { border-left: 3px solid var(--col, var(--accent)); }
.an-hdr { align-items: flex-start; }
.an-id { display: flex; gap: 12px; align-items: center; min-width: 0; }
.an-pic { border-radius: 10px; }
.an-id-txt { min-width: 0; }
.an-name { font-size: var(--t-lg); font-weight: 700; color: var(--fg-0); letter-spacing: 0.01em; }
.an-tag {
  display: inline-block; margin-top: 6px; font-size: var(--t-sm); font-weight: 600;
  letter-spacing: 0.03em; text-transform: uppercase;
  color: color-mix(in oklab, var(--col, var(--accent)) 78%, var(--fg-1));
}
.an-hdr-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
/* The profile's close is its only way out on a phone (a full-screen sheet with no backdrop): it
   reads clearly (5.4:1 dark, 5.3:1 light) instead of the icon buttons' faint --fg-3 (2.6-2.8:1). */
.an-hdr .icon-btn { color: var(--fg-2); }
.an-hdr .icon-btn:hover, .an-hdr .icon-btn:focus-visible { color: var(--fg-0); }

.an-body { display: flex; flex-direction: column; gap: 16px; }
/* Restrained inline stats — a single line, NOT a grid of tiles. */
.an-stats { display: flex; flex-wrap: wrap; gap: 12px 22px; padding: 2px 0 2px; border-bottom: 1px solid var(--border-2); padding-bottom: 14px; }
.an-stat { display: flex; flex-direction: column; gap: 3px; }
.an-stat-v { font-size: 1rem; font-weight: 700; color: var(--fg-0); font-variant-numeric: tabular-nums; }
.an-stat-v.up, .an-stat-v .up { color: var(--c-up); } .an-stat-v.down, .an-stat-v .down { color: var(--c-down); }
.an-stat-v.even, .an-stat-v .even { color: var(--fg-2); }
.an-stat-l { font-size: var(--t-xs); letter-spacing: 0.04em; text-transform: uppercase; color: var(--fg-3); }
/* What the figures count (closes, or closes and trims; always all time). */
.an-stats-cap { flex-basis: 100%; font-size: var(--t-xs); color: var(--fg-2); }

/* Risk note as a pull-quote — the motif that makes the card feel authored. */
.an-risk {
  margin: 0; padding: 2px 0 2px 14px; border-left: 3px solid color-mix(in oklab, var(--col, var(--accent)) 60%, var(--border-2));
  font-size: 0.9375rem; line-height: 1.5; color: var(--fg-0); font-style: italic;
}
.an-note { margin: 0; font-size: 0.84375rem; line-height: 1.6; color: var(--fg-1); }
.an-empty { color: var(--fg-2); font-size: var(--t-md); line-height: 1.5; }

/* Inline editor (self-service + owner). */
.an-edit { display: flex; flex-direction: column; gap: 14px; }
.an-field { display: flex; flex-direction: column; gap: 5px; }
.an-field-l { font-size: var(--t-sm); color: var(--fg-1); font-weight: 600; }
.an-field-l em { font-style: normal; font-weight: 400; color: var(--fg-3); }
.an-input {
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius);
  color: var(--fg-0); font: inherit; font-size: 0.84375rem; padding: 8px 10px; resize: vertical;
}
.an-input:focus { outline: none; border-color: var(--accent); }
.an-err { font-size: var(--t-md); color: var(--c-down); }
.an-edit-actions { display: flex; gap: 8px; }
/* The profile's small buttons (Edit, Save, Cancel, Write profile) and the assistant's Confirm and
   Cancel are 30px for a mouse; a finger gets 40px, as the app's other phone controls do (the
   assistant's were full size until 2.9.0 gave them the small size, and the owner confirms its
   corrections on a phone). */
@media (pointer: coarse) { .an-flyout .btn-sm, .asst-prop-actions .btn-sm { min-height: 40px; } }

/* The style caption under a name in Event / Position detail headers — the fix for
   acting on a signal without knowing the analyst's style. */
.an-caption { margin-top: 6px; font-size: var(--t-sm); line-height: 1.4; color: var(--fg-2); }
.an-caption .an-tag { display: inline; margin: 0; font-size: var(--t-xs); }
.an-caption-dot { margin: 0 6px; color: var(--fg-deco); }
.an-caption-risk { color: var(--fg-2); }

/* Contract detail (strike · expiry) as a quiet sub-line under the instrument pill
   in the ledger positions table — brings back strike/expiry without a new column. */
.inst-contract { margin-top: 6px; font-size: var(--t-xs); line-height: 1.3; color: var(--fg-2); letter-spacing: 0.01em; white-space: nowrap; }
.inst-contract .ic-strike { color: var(--fg-1); font-weight: 600; }
.inst-contract .ic-sep { color: var(--fg-deco); }


/* ═══ Mobile ergonomics ═══════════════════════════════════════════════════════
   These live at the END on purpose: they are single-class selectors competing with
   the base rules (.icon-btn 30px, .input 0.8125rem) at IDENTICAL specificity, so
   source order is the only thing that decides. Placed earlier, they silently lost.
   (The touch-target block is further down; the iOS zoom-on-focus floor is the file's last rule.) */

/* ── Market news: a heatmap ───────────────────────────────────────
   A strip (ticker search + lead stories), a two-level treemap, then the selected
   thing's headlines. Tile AREA is story volume; tile COLOUR is the day's move.

   Two colour rules hold this together:
   1. Cells are painted from --c-up / --c-down mixed toward --bg-2, i.e. toward the
      THEME'S OWN surface. That is what lets one declaration read correctly in light
      and dark, and P&L hues are never rebranded per client, so the map reads the same
      in every white-label -- which is the entire point of a signal colour.
   2. The desk signal (a name the followed analysts hold) is a 3px accent spine and
      weight, NEVER a hue. Market Magicians' accent (#C9A84C) is in the same gold
      family as --c-ticker, so a hue-only distinction is invisible for that brand by
      construction -- and on a red/green map a third hue would be unreadable anyway.

   The search tile is the ACCENT and never red/green: it is a control, and a control
   that turned green would be claiming the market moved. */
/* A 13th nav item can push the user chip + Feedback button off a short viewport;
   .sidebar nav had no overflow rule before this tab existed. */
.sidebar nav { overflow-y: auto; min-height: 0; }
/* A phone held sideways, or any window under 500px tall: the whole menu scrolls, and the account
   card follows the tabs. Pinned at the bottom, it left the tab list a 142px box showing 3 of 7 tabs
   (News, Recaps and Settings looked missing): in the phone's drawer, and from 901px (a large phone
   sideways, 932x430) in the computer's sidebar, which showed 2. An upright phone and a computer of
   ordinary height keep the pinned account card. The icon rail hides the scrollbar, as its tab list
   did, so the icons stay centred. */
@media (max-height: 500px) and (orientation: landscape) {
  .sidebar { overflow-y: auto; overscroll-behavior: contain; }
  .sidebar nav { overflow: visible; flex: none; }
  .sidebar-foot { margin-top: 12px; }
}
@media (max-height: 500px) and (orientation: landscape) and (min-width: 901px) {
  :root[data-side="rail"] .sidebar { scrollbar-width: none; }
  :root[data-side="rail"] .sidebar::-webkit-scrollbar { display: none; }
}

.news-paper { padding: 14px 16px 16px; }

/* The strip: auto-fit so it reflows 4-up to 2-up to 1-up instead of hiding a story.
   Nothing here is display:none at any width -- a lead headline that vanishes on a
   phone is the one the reader most needed. */
.news-strip {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(215px, 1fr));
  gap: 10px; margin-bottom: 12px;
}

.news-tile-search {
  display: flex; flex-direction: column; gap: 8px; justify-content: center;
  padding: 11px 13px; border-radius: var(--radius);
  background: color-mix(in oklab, var(--accent) 11%, var(--bg-2));
  border: 1px solid color-mix(in oklab, var(--accent) 32%, var(--border-1));
}
.news-tile-search.on { border-color: var(--accent); }
.news-tile-search-hd {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--t-2xs); letter-spacing: 0.12em; color: var(--accent); font-weight: 600;
}
.news-tile-search-row { display: flex; gap: 6px; }
.news-tile-search-in {
  flex: 1; min-width: 0;
  background: var(--bg-0); color: var(--fg-0);
  border: 1px solid var(--border-1); border-radius: var(--radius);
  padding: 6px 9px; font-family: var(--f-mono); font-size: var(--t-sm);
  letter-spacing: 0.06em; text-transform: uppercase;
}
.news-tile-search-in:focus { outline: none; border-color: var(--accent); }
.news-tile-search-in::placeholder { color: var(--fg-3); text-transform: none; letter-spacing: 0; }

.news-tile-lead {
  display: flex; flex-direction: column; justify-content: space-between; gap: 8px;
  padding: 11px 13px; border-radius: var(--radius);
  background: var(--bg-2); border: 1px solid var(--border-1);
  text-decoration: none; color: inherit;
  box-shadow: inset 3px 0 0 transparent;          /* reserved, so marking one shifts nothing */
  transition: background 120ms, box-shadow 120ms;
}
.news-tile-lead:hover { background: var(--bg-3); }
.news-tile-lead.on-desk { box-shadow: inset 3px 0 0 var(--accent); }
.news-tile-lead-hl {
  font-size: var(--t-md); line-height: 1.4; font-weight: 500; color: var(--fg-0);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.news-tile-lead-meta {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: 0.06em;
}
.news-tile-lead-meta .news-tk { cursor: pointer; }

/* The map. A fixed ASPECT RATIO rather than a fixed height, so the treemap gets a sane
   canvas at every width; it goes PORTRAIT on a phone, because eleven groups inside a
   21:9 letterbox on a 390px screen is confetti. */
.news-map {
  position: relative; width: 100%; aspect-ratio: 21 / 9; min-height: 300px;
  border-radius: var(--radius); overflow: hidden; background: var(--bg-0);
  border: 1px solid var(--border-1);
}
.news-grp { position: absolute; overflow: hidden; }
.news-grp.on { outline: 2px solid var(--accent); outline-offset: -2px; z-index: 3; }
.news-grp-hd {
  display: flex; align-items: center; gap: 6px;
  width: 100%; height: 22px; padding: 0 6px;
  background: var(--bg-3); border: 0; border-bottom: 1px solid var(--bg-0);
  text-align: left; cursor: pointer;
}
/* The desk spine, same motif as .todo-row and .news-row. */
.news-grp.has-desk .news-grp-hd { box-shadow: inset 3px 0 0 var(--accent); padding-left: 9px; }
.news-grp-name {
  flex: 1; min-width: 0;
  font-family: var(--f-mono); font-size: 0.5625rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--fg-1); font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* --fg-1, as the name beside it: on the header's --bg-3 --fg-2 measured 4.0:1 on a branded light field. */
.news-grp-chg { font-size: 0.5625rem; color: var(--fg-1); flex-shrink: 0; }
.news-grp-body { position: relative; height: calc(100% - 22px); }

.news-cell {
  position: absolute; display: flex; flex-direction: column;
  align-items: flex-start; justify-content: center; gap: 1px;
  padding: 2px 5px; border: 0; border-radius: 2px; overflow: hidden;
  cursor: pointer; color: var(--fg-0); text-align: left;
}
.news-cell:hover { outline: 1px solid var(--fg-1); outline-offset: -1px; z-index: 2; }
.news-cell.on { outline: 2px solid var(--accent); outline-offset: -2px; z-index: 2; }
.news-cell.held { box-shadow: inset 3px 0 0 var(--accent); padding-left: 8px; }
.news-cell-sym {
  font-size: var(--t-xs); font-weight: 700; line-height: 1.1;
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.news-cell-chg { font-size: 0.5625rem; line-height: 1.1; opacity: 0.8; }
/* Light: the deeper tile colours leave no room for the fade (it dropped to about 4.1:1). */
[data-theme="light"] .news-cell-chg { opacity: 1; }

.news-legend {
  display: flex; align-items: center; justify-content: center; gap: 10px;
  margin-top: 8px; font-size: 0.5625rem; letter-spacing: 0.1em; color: var(--fg-3);
}
.news-legend-scale { display: inline-flex; gap: 2px; }
.news-legend-scale i { display: block; width: 18px; height: 8px; border-radius: 1px; }

.news-drill { margin-top: 14px; border-top: 1px solid var(--border-2); }
.news-drill-hd {
  position: sticky; top: 0; z-index: 2;
  display: flex; align-items: center; gap: 8px;
  padding: 9px 0; background: var(--bg-1);
  border-bottom: 1px solid var(--border-1);
}
.news-drill-title { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.news-drill-kind {
  font-size: 0.5625rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-3);
}
.news-drill-chg { font-size: var(--t-xs); }
.news-drill-chg.up { color: var(--c-up); }
.news-drill-chg.down { color: var(--c-down); }
.news-drill-n {
  margin-left: auto; font-size: 0.5625rem; letter-spacing: 0.1em; color: var(--fg-3);
}
/* Rows sit flush inside the drill panel rather than inset like the old paper layout. */
.news-drill .news-row { padding-left: 12px; padding-right: 12px; }
/* "Held by CK and Instinct": a ticker's stories name who on the desk holds it now. Quiet data, under
   the drill's head. */
.news-drill-held { padding: 8px 12px; border-bottom: 1px solid var(--border-1); font-size: var(--t-xs); letter-spacing: 0.02em; color: var(--fg-2); }

.news-row {
  display: block; padding: 11px 18px 11px 18px;
  border-bottom: 1px solid var(--border-1);
  text-decoration: none; color: inherit;
  box-shadow: inset 3px 0 0 transparent;         /* the spine, reserved so nothing shifts */
  transition: background 120ms, box-shadow 120ms;
}
.news-row:last-child { border-bottom: 0; }
.news-row:hover { background: var(--bg-2); }
.news-row.on-desk { box-shadow: inset 3px 0 0 var(--accent); }

.news-hl {
  font-size: var(--t-md); line-height: 1.45; color: var(--fg-0);
  /* Headlines run to 461 characters on this wire. Two lines, then clamp — an unclamped
     one turns a hairline row into a paragraph and destroys the rhythm of the page. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.news-row.on-desk .news-hl { color: var(--fg-0); font-weight: 500; }
.news-sum {
  margin-top: 3px;
  font-size: var(--t-sm); line-height: 1.45; color: var(--fg-2);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.news-meta {
  margin-top: 6px;
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  font-family: var(--f-mono); font-size: var(--t-2xs); letter-spacing: 0.06em;
}
.news-tk { color: var(--c-ticker); }
/* Held names are marked by WEIGHT and a plate, not a different hue — see the note above. */
.news-tk.held {
  color: var(--fg-0); font-weight: 600;
  background: var(--accent-glow);
  border: 1px solid var(--accent-dim);
  border-radius: 3px; padding: 1px 5px;
}
.news-more { color: var(--fg-3); }
.news-when { color: var(--fg-3); margin-left: auto; }

@media (max-width: 900px) {
  /* Below 900px the DOCUMENT scrolls, not .content -- a sticky head then pins to the
     viewport, where the topbar has already scrolled away, and sits under the notch
     behind the fixed .scroll-rail. Static is the only correct behaviour here. */
  .news-drill-hd { position: static; }
  .news-paper { padding: 12px 12px 14px; }
  /* The header's count line ("143 STORIES · 11 SECTORS · 99 ON YOUR DESK") wraps inside the panel
     instead of running past its edge (it was clipped at "113 ON YOUR"). The News panel only: the
     same header rule lays out Positions and the other panels. */
  .news-paper .panel-hdr-tools .panel-meta { order: 0; flex: 1 1 0; min-width: 0; margin-left: 0; white-space: normal; }
  /* Portrait: on a phone eleven groups need vertical room far more than width. */
  .news-map { aspect-ratio: 3 / 4; min-height: 420px; }
  .news-row { padding-left: 12px; padding-right: 12px; }
  .news-when { margin-left: 0; }
}

/* Belt-and-braces against sideways scroll. `clip` rather than `hidden` on purpose:
   hidden makes the body a scroll container and breaks position:sticky (the sidebar
   and table headers rely on it); clip does not. */
html, body { overflow-x: clip; max-width: 100%; }

/* ── Touch targets ────────────────────────────────────────────────────────────
   .icon-btn is 30x30, which is fine for a mouse and far too small for a thumb —
   the close X on the analyst profile was effectively unhittable. Apple asks for
   44px, Material for 48. Growing the BOX rather than overlaying an invisible 44px
   hit area on purpose: these buttons sit in rows (Parser / Delete, close / edit),
   and overlapping invisible targets would steal each other's taps. 40px real, with
   the icon still centred, plus a little more room between neighbours. */
@media (max-width: 900px) {
  .icon-btn { width: 40px; height: 40px; }
  .row-actions { gap: 6px; }
  /* Card footers already give their buttons a full-width split, leave those. */
  .tbl-cards td.cell-actions .icon-btn { width: auto; height: 40px; }
  /* Segmented controls and pills people actually press. */
  .seg-btn { min-height: 38px; }
  .modal .icon-btn, .flyout-hdr .icon-btn, .calc-hdr .icon-btn { width: 44px; height: 44px; }
}
/* A touch screen wider than 900px (a tablet in landscape, a large phone sideways, a Windows tablet
   with no mouse) gets the computer's layout with finger-sized controls: sizing used to follow the
   width alone, so an iPad in landscape got 30px icon buttons, 29px segments and 13px checkboxes.
   A separate block on purpose: the 900px block above also carries the phone cards' rules. Row
   actions take 36px: three of them and the cell's padding need 122px, so the Events and Positions
   actions columns widen from 104 to 130px (--act-w; their tables' minimum widths follow it), an
   Events column of two from 74 to 86px, and two fit the Watchlist's 100px. The row checkbox column
   widens to hold its 22px box (.col-check), and the head's select-all box above them grows with
   them. A touch laptop with a mouse reports a fine pointer and keeps the computer's sizes. */
@media (pointer: coarse) and (min-width: 901px) {
  .icon-btn { width: 40px; height: 40px; }
  .tbl .cell-actions .icon-btn { width: 36px; height: 36px; }
  .ev-tbl, .pos-tbl { --act-w: 130px; }
  .ev-tbl.ev-act-2 { --act-w: 86px; }
  .ev-tbl.ev-act-1 { --act-w: 50px; }
  .ev-tbl.ev-sel, .wl-tbl.wl-sel { --chk-w: 36px; }
  .seg-btn { min-height: 38px; }
  .modal .icon-btn, .flyout-hdr .icon-btn, .calc-hdr .icon-btn { width: 44px; height: 44px; }
  .sc-toggle, .ri-toggle { min-height: 40px; }
  .tbl td.cell-check input,
  .tbl thead th input[type="checkbox"] { width: 22px; height: 22px; }
  .tbl col.col-check { width: 36px; }
}

/* ── Overlays never scroll the page behind them ───────────────────────────────
   On a phone the window is the scroller, and a drag inside the bell, the menu drawer or an analyst
   profile used to carry on into the page under it (Positions moved 400 to 1033 behind the bell), so
   closing it left the member somewhere else. Three layers:
   1. a list that scrolls stops at its own end (overscroll-behavior), for every list in an overlay;
   2. a one-finger drag on a dim backdrop does nothing (touch-action), while a pinch still zooms:
      never `none`, which also stops pinch zoom (WCAG 1.4.4). Touch-action adds up from an element
      through its ancestors, and .modal-back is the parent of every dialog, so `none` there turned
      zoom off inside all of them; a dialog's own .modal scrolls, which gives its content its panning
      back. The bug screenshot lightbox (an image straight on the backdrop, nothing that scrolls)
      also pans with one finger, so a zoomed-in screenshot can be read on a phone;
   3. while an overlay is open the page itself is locked (html.scroll-locked, shared.jsx
      useScrollLock: the drawer, the bell, the flyouts and the filter sheet). The position is never
      touched, so the page is exactly where it was when the overlay closes. A dialog (.modal-back)
      locks it by being there, whichever of the app's dialogs it is. Phones only: a computer scrolls
      .content and is left alone. */
.notif-list, .notif-settings, .flyout-body, .sidebar nav, .manage-body, .calc-body, .asst-panel, .asst-msgs,
.modal, .fsheet-body, .af-menu { overscroll-behavior: contain; }
.nav-backdrop, .flyout-back, .notif-backdrop, .modal-back, .fsheet-back { touch-action: pinch-zoom; }
.modal-back:has(> .shot-full) { touch-action: manipulation; }
html.scroll-locked { overflow: hidden; }
@media (max-width: 900px) {
  html:has(.modal-back) { overflow: hidden; }
}

/* Back-to-top: appears once you're a screen or so into a list. Bottom-LEFT on mobile
   so it never sits under the Ask-AI launcher (bottom-right) or the home indicator. */
.scroll-top-btn {
  position: fixed; z-index: 150;
  right: calc(20px + env(safe-area-inset-right, 0px));
  bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center;
  color: var(--fg-1); background: var(--bg-2);
  border: 1px solid var(--border-2); box-shadow: var(--shadow-2);
  cursor: pointer; animation: fadeIn 150ms ease-out;
}
.scroll-top-btn:hover { color: var(--fg-0); background: var(--bg-3); border-color: var(--border-3); }
@media (max-width: 900px) {
  .scroll-top-btn {
    width: 46px; height: 46px;
    left: calc(16px + env(safe-area-inset-left, 0px)); right: auto;
    bottom: calc(74px + env(safe-area-inset-bottom, 0px));
  }
}
/* Never on top of something else in the same corner, or over an open overlay: the install nudge and
   the rating card (full width at the bottom of a phone, the bottom-right corner of a computer), a
   toast (full width on a phone; two at once reach it on a computer), a flyout (a computer's
   right-hand panel), and on a phone anything that locked the page (the menu, the bell, a profile,
   the filter sheet: html.scroll-locked). */
body:has(.install-nudge, .rating-prompt, .flyout, .toast-host) .scroll-top-btn,
html.scroll-locked .scroll-top-btn { display: none; }
/* Secondary line under a Preferences row label. */
.prefpane-sub { display: block; margin-top: 3px; font-size: var(--t-sm); font-weight: 400; color: var(--fg-2); line-height: 1.45; max-width: 42ch; }

/* Analyst signalling examples — a short code block inside a Shot, and an inline aside. */
.guide-code {
  padding: 12px 14px; border-radius: var(--radius-sm);
  background: var(--bg-2); border: 1px solid var(--border-1);
  color: var(--fg-0); font-size: var(--t-md); line-height: 1.75;
  white-space: pre-wrap; word-break: break-word;
}
.guide-note-inline { color: var(--fg-3); }

/* "Send the signal guide" — sits above each server's analyst rows in the registry. */
.send-guide-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 11px 14px; margin: 2px 0 10px;
  background: var(--bg-2); border: 1px solid var(--border-1); border-radius: var(--radius);
}
.send-guide-txt { flex: 1 1 260px; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.send-guide-txt strong { font-size: var(--t-md); font-weight: 600; color: var(--fg-0); }
.send-guide-txt span { font-size: var(--t-sm); line-height: 1.5; color: var(--fg-2); }
@media (max-width: 900px) { .send-guide-row .btn { width: 100%; justify-content: center; } }

/* ── Creator escalation alarm (creator-alert.jsx) ──────────────────────────
   The one screen in the product that is supposed to be alarming. Everything
   here is scoped under .klaxon and shares no selector with anything else, so
   it can sit at the end of the sheet without disturbing the mobile block
   above it. All motion is switched off under prefers-reduced-motion; the
   alarm still reads as an alarm through colour and the hazard borders. */
.klaxon{
  position:fixed; inset:0; z-index:1200;
  display:flex; align-items:center; justify-content:center;
  padding:26px 16px; overflow:hidden;
  background:radial-gradient(120% 82% at 50% 0%, #1B0406 0%, #08070A 56%, #050508 100%);
  --kx-red:#FF2E2E; --kx-amber:#FF8A1F; --kx-ink:#FFE9E4; --kx-dim:#C9A9A4;
}
.klaxon-beacon{
  position:absolute; top:-46vh; width:118vmax; height:118vmax; pointer-events:none;
  background:conic-gradient(from 0deg,
    rgba(255,46,46,.20) 0deg 13deg, transparent 13deg 178deg,
    rgba(255,138,31,.13) 178deg 190deg, transparent 190deg 360deg);
  filter:blur(3px); animation:kx-sweep 4.6s linear infinite;
}
.klaxon-beacon.left{ left:-44vmax; }
.klaxon-beacon.right{ right:-44vmax; animation-duration:5.6s; animation-direction:reverse; }
@keyframes kx-sweep{ to{ transform:rotate(360deg); } }
.klaxon-pulse{
  position:absolute; inset:0; pointer-events:none;
  box-shadow:inset 0 0 190px 44px rgba(255,46,46,.30);
  animation:kx-pulse 1.24s ease-in-out infinite;
}
@keyframes kx-pulse{ 0%,100%{ opacity:.32 } 50%{ opacity:1 } }
.klaxon-scan{
  position:absolute; inset:0; pointer-events:none; opacity:.15;
  background:repeating-linear-gradient(180deg, rgba(255,255,255,.10) 0 1px, transparent 1px 3px);
}
.klaxon-tape{
  position:absolute; left:-40%; width:180%; height:13px; pointer-events:none; opacity:.9;
  background:repeating-linear-gradient(115deg, var(--kx-amber) 0 17px, #0B0A0E 17px 34px);
  animation:kx-march 1.5s linear infinite;
}
.klaxon-tape.top{ top:0 } .klaxon-tape.bottom{ bottom:0; animation-direction:reverse }
@keyframes kx-march{ to{ transform:translateX(34px) } }

.klaxon-panel{
  position:relative; width:min(560px,100%); max-height:calc(100dvh - 64px); overflow:auto;
  padding:18px 20px 20px; color:var(--kx-ink);
  background:linear-gradient(180deg,#100C12 0%,#0A090D 100%);
  border:1px solid rgba(255,46,46,.5);
  box-shadow:0 0 0 1px rgba(0,0,0,.9), 0 30px 90px rgba(0,0,0,.8), 0 0 64px rgba(255,46,46,.16);
}
.klaxon-head{ display:flex; align-items:center; gap:9px; padding-bottom:11px; border-bottom:1px solid rgba(255,46,46,.22); }
.klaxon-siren{ color:var(--kx-red); display:flex; animation:kx-blink 1.24s steps(1,end) infinite; }
@keyframes kx-blink{ 0%,49%{ opacity:1 } 50%,100%{ opacity:.25 } }
.klaxon-title{
  flex:1; font-family:var(--f-mono); font-size:.66rem; letter-spacing:.16em;
  text-transform:uppercase; color:var(--kx-red);
}
.klaxon-mute{
  background:none; border:1px solid rgba(255,233,228,.22); color:var(--kx-dim); cursor:pointer;
  font-family:var(--f-mono); font-size:.56rem; letter-spacing:.12em; padding:3px 7px;
}
.klaxon-mute:hover{ color:var(--kx-ink); border-color:rgba(255,233,228,.45); }

.klaxon-steps{ display:flex; gap:6px; margin:12px 0 14px; flex-wrap:wrap; }
.klaxon-step{
  display:flex; align-items:center; gap:6px; padding:4px 9px; flex:1; min-width:96px;
  font-family:var(--f-mono); font-size:.58rem; letter-spacing:.1em; text-transform:uppercase;
  color:var(--kx-dim); border:1px solid rgba(255,233,228,.14); background:rgba(255,255,255,.02);
}
.klaxon-step b{
  display:grid; place-items:center; width:15px; height:15px; font-size:.56rem;
  background:rgba(255,233,228,.12); color:var(--kx-ink);
}
.klaxon-step.on{ color:var(--kx-ink); border-color:var(--kx-red); background:rgba(255,46,46,.10); }
.klaxon-step.on b{ background:var(--kx-red); color:#0A0507; }
.klaxon-step.done{ color:var(--kx-dim); border-color:rgba(255,138,31,.4); }
.klaxon-step.done b{ background:var(--kx-amber); color:#0A0507; }

.klaxon-target{ padding:10px 12px; border-left:2px solid var(--kx-red); background:rgba(255,46,46,.07); }
.klaxon-target-l{ font-family:var(--f-mono); font-size:.56rem; letter-spacing:.14em; text-transform:uppercase; color:var(--kx-dim); }
.klaxon-target-n{ font-size:1.06rem; font-weight:600; line-height:1.3; margin-top:2px; }
.klaxon-target-id{ font-size:.62rem; color:var(--kx-dim); margin-top:2px; }

.klaxon-lede{ font-size:.86rem; line-height:1.55; color:var(--kx-ink); margin:14px 0 0; }
.klaxon-lede code{ background:rgba(255,46,46,.16); color:#FFD9D2; padding:1px 6px; font-size:.8em; letter-spacing:.06em; }
.klaxon-powers{ list-style:none; margin:12px 0 0; padding:0; display:grid; gap:1px; background:rgba(255,233,228,.10); }
.klaxon-powers li{ padding:8px 11px; background:#0B0A0E; font-size:.78rem; line-height:1.5; color:var(--kx-dim); }
.klaxon-powers li span{
  display:block; font-family:var(--f-mono); font-size:.56rem; letter-spacing:.14em;
  text-transform:uppercase; color:var(--kx-amber); margin-bottom:2px;
}
.klaxon-in{ width:100%; margin-top:12px; background:#08070B; border-color:rgba(255,46,46,.45); color:var(--kx-ink); letter-spacing:.08em; }
.klaxon-in:focus{ border-color:var(--kx-red); box-shadow:0 0 0 2px rgba(255,46,46,.18); }
.klaxon-err{ margin-top:9px; font-size:.74rem; color:#FF8E8E; font-family:var(--f-mono); letter-spacing:.02em; }

.klaxon-actions{ display:flex; gap:9px; justify-content:flex-end; margin-top:16px; flex-wrap:wrap; }
.klaxon-actions .btn{ background:#151217; border-color:rgba(255,233,228,.24); color:var(--kx-ink); }
.klaxon-actions .btn:hover:not(:disabled){ border-color:rgba(255,233,228,.5); }
.klaxon-go{ border-color:var(--kx-red) !important; color:#fff !important; background:rgba(255,46,46,.18) !important; }
.klaxon-go:disabled{ opacity:.45; }
.klaxon-hold{
  position:relative; overflow:hidden; min-width:214px; touch-action:none;
  user-select:none; -webkit-user-select:none;
  background:#1A0A0C !important; border-color:rgba(255,46,46,.55) !important; color:#fff !important;
}
.klaxon-hold.live{ border-color:var(--kx-red) !important; box-shadow:0 0 22px rgba(255,46,46,.3); }
.klaxon-hold-fill{
  position:absolute; inset:0; transform-origin:left center; transform:scaleX(0);
  background:linear-gradient(90deg, var(--kx-red), var(--kx-amber)); opacity:.85;
}
.klaxon-hold.busy .klaxon-hold-fill{ transition:transform .25s linear; }
.klaxon-hold-t{ position:relative; }

@media (max-width: 560px){
  .klaxon{ padding:16px 10px; }
  .klaxon-panel{ padding:14px 14px 16px; }
  .klaxon-step{ min-width:0; flex:1 1 auto; }
  .klaxon-actions .btn{ flex:1 1 auto; }
  .klaxon-hold{ min-width:0; }
}
@media (prefers-reduced-motion: reduce){
  .klaxon-beacon, .klaxon-tape, .klaxon-pulse, .klaxon-siren{ animation:none; }
  .klaxon-pulse{ opacity:.7; }
  .klaxon-siren{ opacity:1; }
}

/* SOLD-to-open marker (shared.jsx InstrumentTag). Deliberately NOT an accent or a
   P&L colour — it states a fact about the contract, and must read identically in
   every brand, exactly like the PUT / CALL tags it sits beside. */
.inst-tag.sold{
  background:rgba(255,255,255,.06); color:var(--fg-1);
  border:1px solid var(--border-3); letter-spacing:.1em; margin-right:3px;
}

/* ── Boot skeleton + view transitions ──────────────────────────────────────────
   WHY THIS IS PLAIN HTML AND NOT A COMPONENT. React does not exist until every
   script app.html names has arrived and run, so a React skeleton could not render
   in that window. In development Babel standalone also compiles each .jsx in the
   BROWSER first; production loads them compiled once at deploy (tools/build-web.js)
   and keeps them between visits. Time until the app shell replaces this skeleton,
   on /app served as production serves it (v2.7.0, owner and a member, median of 4,
   cold = a fresh profile, warm = a reload):

       desktop, no throttle .... 0.3s cold, 0.3s warm    (1.4s / 1.1s compiling in the browser)
       phone at 4x CPU ......... 0.7s cold, 0.35s warm   (6.1s / 4.7s; 22-26s before v2.0.0)

   What is left on a phone is mostly the download: at 4x CPU on a slow mobile link
   (1.6 Mbps, 150 ms) the shell takes 3.8s cold, against 11.1s compiling in the
   browser. This markup lives in app.html, paints with the first frame, and is
   removed by window.__sinuxBootDone() once App mounts.

   It is a SILHOUETTE of the real shell, not a spinner: same sidebar width, same
   topbar height, same panel geometry, so the swap to real content is a fill-in
   rather than a re-layout. The brand mark and name are drawn for real (from a value
   cached on the last visit) because on a white-label domain this screen is the one
   a client's members stare at longest. */

/* One token for every placeholder, so the contrast is tuned in one place: --boot-fill, on
   :root (see the top of this file), shared with the in-app loading rows. Mixed from
   --fg-2 rather than stepping --bg-1/2/3: those steps are only ~15 RGB points apart on
   the dark field, which renders as flat black, and a brand is free to redefine them. */
#boot {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: var(--bg-0);
  background-image: var(--body-bg);
  opacity: 1;
  transition: opacity 260ms ease;
  /* Held back for a beat so a warm load that mounts fast never flashes a skeleton on
     its way to the real thing. backwards = invisible during the delay, and nothing
     persists afterwards. */
  animation: bootIn 180ms ease 140ms backwards;
}
@keyframes bootIn { from { opacity: 0; } to { opacity: 1; } }
#boot.boot-off { opacity: 0; pointer-events: none; animation: none; }

.boot-shell {
  display: grid;
  grid-template-columns: max(224px, 11.2rem) minmax(0, 1fr);   /* the real shell's sidebar (.shell) */
  height: 100%;
  overflow: hidden;
}

/* Sidebar silhouette */
.boot-side {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 18px 14px;
  border-right: 1px solid var(--border-1);
  background: var(--bg-1);
}
.boot-brand { display: flex; align-items: center; gap: 11px; margin-bottom: 18px; }
/* Shown only where the sidebar is not (see the 900px block below). */
.boot-top-brand { display: none; align-items: center; gap: 10px; min-width: 0; }
.boot-mark {
  width: 30px; height: 30px;
  border-radius: 8px;
  background: var(--accent);
  color: var(--bg-0);
  display: grid; place-items: center;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: min(1rem, 16px);   /* capped per step, as the sidebar's mark */
  flex-shrink: 0;
  overflow: hidden;
}
[data-fontscale="md"] .boot-mark { font-size: min(1rem, 18px); }
[data-fontscale="lg"] .boot-mark { font-size: min(1rem, 20px); }
.boot-mark img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: inherit; }
.boot-name {
  font-family: var(--f-display);
  font-weight: 600;
  font-size: min(var(--t-md), 13px);   /* each step's size with the default 16px font, at most: the lockup is capped (.brand-name) */
  color: var(--fg-0);
  letter-spacing: 0.02em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[data-fontscale="md"] .boot-name { font-size: min(var(--t-md), 14.625px); }
[data-fontscale="lg"] .boot-name { font-size: min(var(--t-md), 16.25px); }
.boot-nav { display: flex; flex-direction: column; gap: 6px; }
.boot-nav i { display: block; height: 30px; border-radius: var(--radius-sm); background: var(--boot-fill); }
/* The nav is a known list, so the placeholders are not all one length — a uniform
   stack of identical bars reads as a loading graphic, a varied one reads as words. */
.boot-nav i:nth-child(1) { width: 68%; }
.boot-nav i:nth-child(2) { width: 82%; }
.boot-nav i:nth-child(3) { width: 55%; }
.boot-nav i:nth-child(4) { width: 74%; }
.boot-nav i:nth-child(5) { width: 61%; }
.boot-nav i:nth-child(6) { width: 78%; }
.boot-nav i:nth-child(7) { width: 50%; }
.boot-side-foot { margin-top: auto; }
.boot-wake {
  font-family: var(--f-mono);
  font-size: var(--t-xs);
  letter-spacing: 0.06em;
  color: var(--fg-3);
}

/* Main column */
.boot-main { display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
.boot-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px 28px;
  padding-top: calc(14px + env(safe-area-inset-top, 0px));
  border-bottom: 1px solid var(--border-1);
  background: var(--bg-1);
  flex-shrink: 0;
}
.boot-pills { display: flex; gap: 8px; }
.boot-pills i { display: block; width: 84px; height: 26px; border-radius: 99px; background: var(--boot-fill); }
.boot-bar { display: block; height: 14px; border-radius: var(--radius-sm); background: var(--boot-fill); }
.boot-bar.w-lg { width: 190px; }
.boot-bar.w-md { width: 128px; }

.boot-content { padding: 22px 28px; display: flex; flex-direction: column; gap: 16px; min-height: 0; flex: 1 1 auto; }
.boot-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }
.boot-stats i {
  display: block; height: 74px; border-radius: var(--radius);
  background: var(--bg-1); border: 1px solid var(--border-2);
}
/* Grows to the bottom of the viewport: the real table panel does, and a silhouette
   that stops a third of the way down reads as a broken page rather than a loading one. */
.boot-panel {
  border: 1px solid var(--border-1); border-radius: var(--radius);
  background: var(--bg-1); overflow: hidden; flex: 1 1 auto; min-height: 0;
}
.boot-panel-hdr { padding: 14px 16px; border-bottom: 1px solid var(--border-1); }
.boot-rows { padding: 4px 0; }
.boot-rows i {
  display: block;
  height: 12px;
  margin: 16px;
  border-radius: var(--radius-sm);
  background: var(--boot-fill);
}
/* Rows taper toward the bottom: the eye reads that as a list running out of view
   rather than as a block of identical placeholder bars. */
.boot-rows i:nth-child(1) { width: calc(100% - 32px); }
.boot-rows i:nth-child(2) { width: calc(94% - 32px); }
.boot-rows i:nth-child(3) { width: calc(97% - 32px); }
.boot-rows i:nth-child(4) { width: calc(88% - 32px); }
.boot-rows i:nth-child(5) { width: calc(92% - 32px); }
.boot-rows i:nth-child(6) { width: calc(80% - 32px); }
.boot-rows i:nth-child(7) { width: calc(86% - 32px); opacity: 0.7; }
.boot-rows i:nth-child(8) { width: calc(64% - 32px); opacity: 0.45; }

/* One sweep across the whole skeleton rather than a shimmer per bar: cheaper (a
   single animated layer) and it reads as light moving over one surface instead of
   twenty independent loaders.
   The sweep is a TRANSFORM on a 240%-wide layer, never an animated background-position:
   Chrome cannot run background-position off the main thread, so the old sheen froze for
   the whole JSX compile, the one moving sign of life stopping exactly while a phone
   waited. A transform runs on the compositor and keeps moving through those long tasks.
   -75.83% / 40.83% of the 240% layer are the old background-position 130% / -70%, so
   the speed and the path are unchanged. overflow: hidden keeps the wide layer from
   widening the page. */
.boot-sheen {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}
.boot-sheen::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 240%;
  background: linear-gradient(100deg,
    transparent 30%,
    color-mix(in oklab, var(--fg-0) 8%, transparent) 46%,
    color-mix(in oklab, var(--fg-0) 13%, transparent) 50%,
    color-mix(in oklab, var(--fg-0) 8%, transparent) 54%,
    transparent 70%);
  animation: bootSheen 1900ms linear infinite;
  will-change: transform;
}
@keyframes bootSheen { from { transform: translateX(-75.83%); } to { transform: translateX(40.83%); } }

/* The phone's own stand-ins (app.html): the status line, the market session strip, the two lines
   under the summary and the ticker row. A computer's skeleton has none of them. */
.boot-ph { display: none; }

/* Phones: the real app has no docked sidebar under 900px, so neither does this. */
@media (max-width: 900px) {
  .boot-shell { grid-template-columns: 1fr; }
  .boot-side { display: none; }
  .boot-top, .boot-content { padding-left: 16px; padding-right: 16px; }
  /* The silhouette is the phone's first screen of Open Positions, top to bottom, so the hand-over
     to the real page is a cross-fade between two of the same shape: the pinned bar (65 px), the
     status line, the session strip, the summary as one ledger row (58 px, the 2x2 tiles it was are
     gone from the app), its two lines, the ticker row, then the list. */
  .boot-top { padding-top: calc(17px + env(safe-area-inset-top, 0px)); padding-bottom: 17px; }
  .boot-content { padding-top: 0; gap: 0; }
  .boot-ph { display: block; flex: none; }
  .boot-status { width: 58%; height: 10px; margin: 15px 0 21px; border-radius: var(--radius-sm); background: var(--boot-fill); }
  .boot-strip { height: 35px; margin-bottom: 24px; border: 1px solid var(--border-1); border-radius: var(--radius); }
  .boot-stats {
    grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin-bottom: 12px;
    border-top: 1px solid var(--border-1); border-bottom: 1px solid var(--border-1);
  }
  .boot-stats i { position: relative; height: 56px; border: 0; border-radius: 0; background: none; }
  .boot-stats i + i { border-left: 1px solid var(--border-1); }
  .boot-stats i::before, .boot-stats i::after {
    content: ""; position: absolute; left: 10px; border-radius: var(--radius-sm); background: var(--boot-fill);
  }
  .boot-stats i::before { top: 11px; width: 52%; height: 8px; }
  .boot-stats i::after { bottom: 10px; width: 40%; height: 14px; }
  .boot-stats i:first-child::before, .boot-stats i:first-child::after { left: 2px; }
  .boot-lines { position: relative; height: 45px; margin-bottom: 36px; }
  .boot-lines::before, .boot-lines::after {
    content: ""; position: absolute; left: 0; height: 9px; border-radius: var(--radius-sm); background: var(--boot-fill);
  }
  .boot-lines::before { top: 0; width: 52%; }
  .boot-lines::after { bottom: 0; width: 40%; }
  .boot-filter { display: grid; grid-template-columns: minmax(0, 1fr) 44%; gap: 8px; height: 42px; margin-bottom: 26px; }
  .boot-filter i { display: block; border: 1px solid var(--border-1); border-radius: var(--radius); }
  /* The sidebar held the only brand lockup; move it into the topbar rather than
     letting a white-label desk boot anonymously on a phone. */
  .boot-top-brand { display: flex; }
  .boot-top > .boot-bar.w-lg { display: none; }
  .boot-pills i:nth-child(2) { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .boot-sheen { opacity: 0.5; }
  .boot-sheen::before { animation: none; }
  #boot { transition: none; }
}

/* The boot that did not finish (app.html __sinuxBootFailed): plain words on the boot field,
   shown on a phone too (the skeleton's only words lived in its sidebar, hidden under 900 px). */
.boot-fail {
  position: absolute; inset: 0; margin: auto; height: fit-content;
  width: min(420px, calc(100% - 32px));
  padding: 28px 24px; border-radius: var(--radius-lg);
  background: var(--bg-1); border: 1px solid var(--border-2);
  font-family: var(--f-sans); color: var(--fg-0);
}
.boot-fail-title { margin: 0 0 8px; font-size: var(--t-lg); font-weight: 600; line-height: 1.4; }
.boot-fail-sub { margin: 0 0 20px; font-size: var(--t-base); color: var(--fg-2); line-height: 1.5; }
.boot-fail-btn {
  font: inherit; font-size: var(--t-base); font-weight: 600; cursor: pointer;
  padding: 10px 18px; border-radius: var(--radius);
  color: var(--bg-0); background: var(--accent); border: 1px solid var(--accent);
}

/* ── Tab transition ───────────────────────────────────────────────────────────
   Applied to the CHILDREN of .content, never to .content itself: .content is the
   scroll container and the tables inside it use position:sticky headers, which a
   transform on their scroll container would break. Restarted imperatively from
   app.jsx (remove class, force reflow, re-add) because the tab switch does not
   remount .content — keying it would destroy the scroll container and lose the
   per-tab scroll positions that are deliberately remembered.

   fill-mode is BACKWARDS, not both: backwards holds the from-state during the
   stagger delay (without it a delayed block paints fully opaque, then blinks to
   0 and fades in), while still reverting to the element's own natural style once
   the animation ends — so no transform lingers and no containing block is left
   behind to break the sticky headers. */
@keyframes tabIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
.content.tab-enter > * { animation: tabIn 260ms cubic-bezier(0.22, 0.61, 0.36, 1) backwards; }
/* A short stagger over the first few blocks only — enough to read as the view
   settling, not as a queue of things arriving one at a time. */
.content.tab-enter > *:nth-child(1) { animation-delay: 0ms; }
.content.tab-enter > *:nth-child(2) { animation-delay: 32ms; }
.content.tab-enter > *:nth-child(3) { animation-delay: 64ms; }
.content.tab-enter > *:nth-child(n+4) { animation-delay: 84ms; }

@media (prefers-reduced-motion: reduce) {
  .content.tab-enter > * { animation: none; }
}

/* Usage tab: the one-line explanation under a spend chart. Deliberately quiet — it is a
   footnote to the number above it, not a second headline. */
.usage-note {
  margin: 10px 16px 14px;
  font-size: var(--t-xs);
  line-height: 1.5;
  color: var(--fg-2);
}

/* A multi-leg structure. Violet, like the watch action - it is deliberately NOT the call or
   put green/red, because a spread has no single direction those colours could mean. */
.inst-tag.spread {
  color: var(--c-watch);
  border-color: color-mix(in oklab, var(--c-watch) 40%, transparent);
  background: color-mix(in oklab, var(--c-watch) 12%, transparent);
}

/* ── Drag to reorder ─────────────────────────────────────────────────────────
   The grip lives inside the existing .reorder column, under the arrows, so adding drag
   costs no layout. Visible at the same resting opacity as the arrows and lifting with
   them on row hover. */
.reorder .grip {
  margin-top: 3px;
  line-height: 1;
  font-size: 0.8rem;
  color: var(--fg-3);
  user-select: none;
  /* The grip is now the PRIMARY way to reorder, and on a phone it is grabbed with a
     thumb — a 10px glyph is not a target. Padded out to a real one without widening the
     column, since the padding overlaps the arrows' own spacing. */
  padding: 4px 6px;
  margin-left: -6px;
  margin-right: -6px;
  border-radius: var(--radius-sm);
}
.reorder .grip:hover { color: var(--accent); background: var(--bg-3); }
@media (max-width: 900px) {
  /* Apple asks for 44px; the row is only so tall, so this takes what it can. */
  .reorder .grip { padding: 9px 10px; margin-left: -10px; margin-right: -10px; font-size: 0.9rem; }
}
/* While a drag is running, stop the whole page selecting text and show the grab cursor
   everywhere — the pointer is captured, so it can travel well outside the handle. */
body.dragging-row, body.dragging-row * { user-select: none !important; cursor: grabbing !important; }
/* The row being carried. Lifted rather than ghosted: it is still the row you are holding,
   so it should read as picked up, not as a placeholder left behind. */
.row-drag {
  background: var(--bg-2);
  box-shadow: 0 6px 18px color-mix(in oklab, #000 45%, transparent);
  border-radius: var(--radius);
  position: relative;
  z-index: 2;
}
.row-drag .reorder { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .row-drag { box-shadow: none; outline: 1px solid var(--accent); } }

/* ── Feedback screenshots ────────────────────────────────────────────────────
   Small, quiet, and clearly optional: a report is words first, and the attach row must not
   look like something you have to fill in. */
.shots { margin: 12px 0 4px; }
.shots-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.shots-add { position: relative; overflow: hidden; font-size: var(--t-sm); padding: 6px 11px; display: inline-flex; align-items: center; gap: 6px; }
.shots-add input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.shots-add:has(input:disabled) { opacity: .45; pointer-events: none; }
.shots-hint { font-size: var(--t-xs); color: var(--fg-3); }
.shots-grid { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
.shot {
  position: relative; width: 92px; height: 68px;
  border: 1px solid var(--border-2); border-radius: var(--radius-sm);
  overflow: hidden; background: var(--bg-2); padding: 0;
}
.shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shot-load { display: grid; place-items: center; height: 100%; font-size: var(--t-2xs); color: var(--fg-3); }
.shot-x { position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; background: color-mix(in oklab, #000 55%, transparent); }
.shot-meta {
  position: absolute; left: 0; right: 0; bottom: 0;
  font-size: 0.5625rem; text-align: center; color: var(--fg-1);
  background: color-mix(in oklab, #000 62%, transparent); padding: 1px 0;
}
.bug-shots .shot { cursor: zoom-in; }
/* Full size, but never larger than the viewport — a phone screenshot is taller than it is
   wide and would otherwise run off the bottom. */
.shot-full { max-width: 92vw; max-height: 88vh; border-radius: var(--radius); cursor: zoom-out; }

/* ── Cross-server copies of one alert ("Fix the copy in Saucy Swings too?") */
.twin-modal { max-width: 520px; }
.twin-sub { padding: 6px 20px 12px; color: var(--fg-2); font-size: var(--t-md); line-height: 1.5; }
.twin-list { display: flex; flex-direction: column; gap: 8px; padding: 0 20px 16px; }
.twin-row { border: 1px solid var(--border-1); border-radius: var(--radius); padding: 10px 12px; background: var(--bg-2); display: flex; flex-direction: column; gap: 6px; }
.twin-row.on { border-color: color-mix(in oklab, var(--accent) 45%, var(--border-1)); }
.twin-row.is-applied { border-color: color-mix(in oklab, var(--c-up) 45%, var(--border-1)); }
.twin-head { display: flex; align-items: flex-start; gap: 10px; cursor: pointer; }
.twin-head input { margin-top: 3px; }
.twin-where { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.twin-server { font-weight: 600; color: var(--fg-0); font-size: 0.84375rem; }
.twin-meta { color: var(--fg-3); font-size: var(--t-sm); overflow-wrap: anywhere; }
.twin-raw { color: var(--fg-2); font-size: var(--t-sm); line-height: 1.45; white-space: pre-line; max-height: 4.4em; overflow: hidden; padding-left: 24px; }
.twin-fields { display: flex; flex-direction: column; gap: 3px; padding-left: 24px; font-size: var(--t-md); }
.twin-also { display: flex; align-items: flex-start; gap: 8px; margin-top: 4px; color: var(--fg-2); font-size: var(--t-sm); cursor: pointer; }
.twin-warn { padding-left: 24px; color: var(--c-trim); font-size: var(--t-sm); }
.twin-status { padding-left: 24px; font-size: var(--t-sm); color: var(--fg-2); }
.twin-status.ok { color: var(--c-up); }
.twin-status.bad { color: var(--c-down); }

/* Shift+Tab onto a row button partly under a stuck day heading (or a table's sticky head) brings it
   wholly below it, focus ring included: revealFocused and the browser's own focus scrolling both
   honour this. The day heading is one row tall (--row-h, 44px) at every text size, since only text
   scales (each table's sticky head is shorter), and 6px more clears the 4px ring (2px outline, 2px
   offset). Not in rem: at Small text 2.75rem came to the heading's own 44px and left the ring's top
   under it. */
@media (min-width: 901px) { .feed-scroll { scroll-padding-top: calc(var(--row-h) + 6px); } }

/* ── iOS zoom-on-focus ────────────────────────────────────────────────────────
   Safari zooms the whole page whenever a focused text control is under 16px, and
   it does NOT zoom back out — which is why the app "loads slightly zoomed in" and
   can then be scrolled sideways. Our controls are 0.8125rem: 13px at the small
   text size and 14.6px at the default, so every tap on the ticker filter or a
   price field triggered it. 16px is the floor; `max()` keeps the LARGE text size
   larger still. Buttons don't trigger it, so they keep their own scale.
   !important on purpose: this is a platform floor, not styling. A selector list lost to every
   class rule that sized a control (.rating-select, .news-tile-search-in, .asst-input, .an-input,
   .dtp-in, select.input) and to the inline sizes on the Events action select and the custom-domain
   field, so about a dozen fields still zoomed the page (News search, the time zone picker, the
   profile editor, the date fields). Never fix this with maximum-scale in the viewport meta: that
   stops pinch zoom (WCAG 1.4.4). Keep this block at the end of the file. */
@media (max-width: 900px) {
  input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=button]):not([type=submit]):not([type=reset]):not([type=color]):not([type=hidden]):not([type=image]),
  select, textarea {
    font-size: max(16px, 1rem) !important;
  }
}
