:root {
  color-scheme: light dark;
  --surface-1:      #ffffff;
  --surface-page:   #ffffff;
  --text-primary:   #0b0b0b;
  --text-secondary: #52514e;
  --text-muted:     #898781;
  --gridline:       #f2f2f2;
  --border:         rgba(11,11,11,0.12);
  /* Deepened from the original #2a78d6 "default SaaS blue" -- same hue
     family (still reads calm/trustworthy for a financial tool), just
     less saturated/generic. #ccd8f0 vs white (link/text use): 7.2:1. */
  --seq-blue:       #3452a8;
  --seq-blue-track: #ccd8f0;
  /* Deepened from #0ca30c/#d03b3b -- same red=bearish/green=bullish
     convention (that's load-bearing, not up for change), just less
     "default Bootstrap." #1a7a3c vs white: 5.4:1, #b8342f vs white: 5.9:1. */
  --status-good:      #1a7a3c;
  --status-warning:   #fab219;
  --status-serious:   #ec835a;
  --status-critical:  #b8342f;
  /* Vltx (volatility) badge ramp -- Low/Normal/High/Super High/Extreme is
     one ORDERED magnitude scale, not five independent judgments, so it
     gets one hue (amber -> deep red-orange) ramped light->dark instead of
     borrowing status-good/warning/serious/critical the way it used to
     (Low and Normal were literally the same green as "Bullish", and the
     four status colors had no shared logic tying them together as a
     scale). Distinct from both --status-good and --status-critical at
     every step so it never collides with bullish/bearish meaning.
     Text flips dark->white partway down the ramp to stay readable as the
     fill darkens (Super High/Extreme here; same idea as .badge.warning's
     existing dark-text exception, just going the other direction).
     Every step checked: light 4.46-10.29:1, dark 4.86-9.86:1. */
  --vltx-low:          #eecaa0; --vltx-low-fg:     #2b2100;
  --vltx-normal:       #e9ad67; --vltx-normal-fg:  #2b2100;
  --vltx-high:         #e8902c; --vltx-high-fg:    #2b2100;
  --vltx-super:        #a76111; --vltx-super-fg:   #fff;
  --vltx-extreme:      #8e510b; --vltx-extreme-fg: #fff;
  /* Categorical slots 1-3 (dataviz skill's validated default order) --
     identity, not severity. Used for alert TYPE (CHoCH/Pullback/
     Speculative); status colors stay reserved for actual state. Pullback
     (confirmed continuation) gets the green/aqua slot, Speculative
     (unconfirmed) gets orange -- green reading as "confirmed good" for
     the most tentative alert type would send the wrong signal. */
  --cat-1: #2a78d6;
  --cat-2: #1baf7a;
  --cat-3: #eb6834;
  /* RR badge/accent-stripe sequential ramp -- one hue (green), light->dark
     mapped to magnitude (dataviz skill: sequential = one hue, not a
     rainbow). Backgrounds are color-mix()ed straight from --status-good
     over --surface-1 -- percent-of-brand-green as the single source of
     truth instead of 3 hand-picked hex values per theme, and it tracks
     --status-good/--surface-1 automatically if either ever changes. (Not
     the CSS `opacity` property -- that would also fade the number text
     sitting inside the badge, not just the fill.) Percentages differ
     light vs dark (see the dark override below) because the same % reads
     very differently against a white vs near-black surface -- each was
     checked against --rr-text for >=4.5:1 contrast, not eyeballed.
     tier-1 (RR>4) is the strongest tint, tier-3 (RR 1.5-3) the faintest;
     a single --rr-text works for every tier since --rr-tier1-bg is
     deliberately the hardest-to-read case and still passes with margin.
     Stripe colors stay solid (not mixed) since a 3px line needs to read
     at a glance, unlike a pill with text sitting inside it -- stripe-2/3
     are proportionally re-derived from the new --status-good (same
     white-mix fraction as before, ~27%/~55%) rather than left as the old
     hardcoded hex, so the ramp stays one coherent hue-family, not tier-1
     alone shifting while 2/3 stay on the previous green. tier-1 now
     carries white text (.rr-badge.tier-1, per feedback) instead of
     --rr-text like tier-2/3 -- at 62% that failed badly (2.62:1), so
     tier1-bg went to 92% instead (near-solid fill, 4.6:1 with white),
     which reads as intentional extra emphasis for the top tier rather
     than an inconsistency. */
  --rr-tier1-bg: color-mix(in srgb, var(--status-good) 92%, var(--surface-1));
  --rr-tier2-bg: color-mix(in srgb, var(--status-good) 38%, var(--surface-1));
  --rr-tier3-bg: color-mix(in srgb, var(--status-good) 10%, var(--surface-1));
  --rr-text:     #073d09;
  --rr-stripe-1: var(--status-good);
  --rr-stripe-2: #589e71;
  --rr-stripe-3: #97c3a7;
}
@media (prefers-color-scheme: dark) {
  :root {
    --surface-1:      #1a1a19;
    --surface-page:   #121211;
    --text-primary:   #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted:     #8f8d84;
    --gridline:       #2c2c2a;
    --border:         rgba(255,255,255,0.12);
    /* Brightened for the dark surface, same as before -- #7186c2 vs dark
       surface (link/text use): 4.9:1. The .apply-btn's white-on-this-bg
       use is 3.56:1, slightly under 4.5 -- same tradeoff the previous
       dark value (#3987e5) already had at 3.64:1, not a new regression,
       just not perfect; button text is bold/large enough that it stays
       legible in practice. */
    --seq-blue:       #7186c2;
    --seq-blue-track: #2e3a52;
    /* --status-good previously had NO dark-mode override at all (a real
       gap -- every other accent color here does) -- it silently reused
       the light value #0ca30c, which happened to still clear contrast
       against the dark surface (5.19:1) so it went unnoticed. Brightened
       properly now, same as the rest: #489563 vs dark surface: 4.8:1. */
    --status-good:      #489563;
    --status-critical:  #ca6763;
    --vltx-low:          #e8bb87; --vltx-low-fg:     #2b2100;
    --vltx-normal:       #e29e50; --vltx-normal-fg:  #2b2100;
    --vltx-high:         #e3861c; --vltx-high-fg:    #2b2100;
    --vltx-super:        #a56112; --vltx-super-fg:   #fff;
    --vltx-extreme:      #97560c; --vltx-extreme-fg: #fff;
    --cat-1: #3987e5;
    --cat-2: #199e70;
    --cat-3: #d95926;
    /* Lower percentages than the light-mode formula above -- --status-good
       is brighter in dark mode (#489563 vs #1a7a3c), so the same ratio
       against a near-black surface reads stronger sooner. Re-verified at
       the existing 55/30/8 split against the new base: 6.1-12.9:1, no
       percentage change needed here, only the base color did. tier-1's
       white text (see .rr-badge.tier-1) already clears 7.45:1 here at
       55% -- unlike light mode, no percentage bump was needed for that. */
    --rr-tier1-bg: color-mix(in srgb, var(--status-good) 55%, var(--surface-1));
    --rr-tier2-bg: color-mix(in srgb, var(--status-good) 30%, var(--surface-1));
    --rr-tier3-bg: color-mix(in srgb, var(--status-good) 8%, var(--surface-1));
    --rr-text:     #c2f5c3;
    --rr-stripe-1: var(--status-good);
    --rr-stripe-2: #90bfa1;
    --rr-stripe-3: #b2d3be;
  }
}

* { box-sizing: border-box; }
body {
  /* SF Pro named explicitly first (used if actually installed, e.g. on
     macOS/iOS or manually installed elsewhere) -- not embedded as a
     webfont, since Apple's license doesn't allow redistributing SF Pro on
     a public site. -apple-system already renders as SF Pro on real Apple
     devices regardless, so this covers the intent without that risk. */
  font-family: "SF Pro Display", "SF Pro Text", "SF Pro", -apple-system, system-ui, "Segoe UI", sans-serif;
  max-width: 1280px; margin: 0 auto 2rem; padding: 0 1.5rem 5rem;
  background: var(--surface-page); color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
}
/* --- top nav --- */
header {
  display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem 1.5rem;
  padding: 1.1rem 0; margin-bottom: 2rem; border-bottom: 1px solid var(--border);
}
.main-nav { display: flex; gap: 1.75rem; flex-wrap: wrap; }
.main-nav .nav-link {
  font-size: 0.9rem; font-weight: 650; color: var(--text-secondary); text-decoration: none;
  padding-bottom: 0.3rem; border-bottom: 2px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.main-nav .nav-link:hover { color: var(--text-primary); }
.main-nav .nav-link.active { color: var(--seq-blue); border-bottom-color: var(--seq-blue); }
.header-utility { display: flex; gap: 1rem; align-items: center; font-size: 0.82rem; }
.header-utility a { color: var(--text-secondary); text-decoration: none; }
.header-utility a:hover { color: var(--seq-blue); }
.section-divider { border: none; border-top: 1px solid var(--border); margin: 0 0 1.5rem; }
@media (min-width: 901px) {
  /* Full viewport width, not just edge-to-edge of body's own 1280px
     centered max-width -- the calc(50% - 50vw) trick is the standard way
     to break an element out to the true browser edges regardless of an
     ancestor's max-width/auto-margin centering, without needing to
     restructure the DOM. Mobile keeps its own narrower full-bleed
     (within body's 1.5rem padding, not the viewport) further down --
     deliberately not touched, this is desktop-only per request. */
  .section-divider {
    width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw); margin-bottom: 3rem;
    box-shadow: 0 4px 6px -2px rgba(0, 0, 0, 0.08);
  }
}
a { color: var(--seq-blue); }
input, select, button { font-size: 1rem; padding: 0.4rem; font-family: inherit; }
.card { font-size: 1rem; border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin-bottom: 1rem; background: var(--surface-1); }
.muted { font-size: inherit; color: var(--text-muted); }

/* --- page layout: filter sidebar + main --- */
.page-layout { display: flex; gap: 2rem; align-items: flex-start; }
.filter-sidebar { width: 250px; flex-shrink: 0; }
.main-content { flex: 1; min-width: 0; }

.page-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1.5rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
h1 { font-size: 1.4rem; font-weight: 650; margin: 0 0 0.2rem; letter-spacing: -0.01em; }
/* "KYX Daily Signal" specifically, not the global h1 default above --
   that default is also used unscoped by admin_users.html/pending.html,
   which weren't part of this request. */
.page-head h1 { font-weight: 700; font-size: 1.3rem; }
.subhead { color: var(--text-muted); font-size: 0.88rem; margin: 0; }

.search-box { position: relative; }
.search-box input {
  width: 220px; font-size: 0.85rem; padding: 0.55rem 0.9rem 0.55rem 2.1rem;
  border-radius: 8px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary);
}
.search-box input::placeholder { color: var(--text-muted); }
.search-box input:focus { outline: 2px solid var(--seq-blue); outline-offset: 1px; }
.search-box::before {
  content: "⌕"; position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%);
  color: var(--text-muted); font-size: 1rem;
}

/* --- filter panel (vertical, collapsible) --- */
.filter-panel { background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; }
.filter-panel summary {
  list-style: none; cursor: pointer; user-select: none;
  display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
  padding: 0.9rem 1.1rem;
}
.filter-panel summary::-webkit-details-marker { display: none; }
.filter-title { font-size: 0.85rem; font-weight: 650; }
.filter-summary-text { font-size: 0.75rem; color: var(--text-muted); line-height: 1.4; }
/* Bold while collapsed -- reads as "more here, tap to expand"; once open
   the content itself is the signal, so the chevron relaxes to a lighter
   weight (still bold when it's the mobile sheet's close affordance --
   see .sheet-panel[open] .chevron below, which wins on specificity). */
.chevron { margin-left: auto; color: var(--text-muted); transition: transform 0.15s ease; font-size: 0.8rem; align-self: center; font-weight: 700; }
.filter-panel[open] summary { border-bottom: 1px solid var(--gridline); }
.filter-panel[open] .chevron { transform: rotate(180deg); font-weight: 400; }
.filter-body { padding: 1.1rem; display: flex; flex-direction: column; gap: 1.25rem; }

/* Breathy panel (Alerts only for now, Macro's own pass is a separate,
   later decision) -- now applies at every width, mobile included. Alerts
   no longer carries .sheet-panel in the template (that's what used to
   turn it into a full-screen bottom sheet on mobile, a separate pattern
   Filters still uses) -- per request, mobile Alerts now matches desktop:
   same borderless/no-background treatment, same scaled-up heading, plain
   inline collapse instead of a fixed-position overlay. The chevron needed
   no change -- it was already a plain rotating glyph, not a boxed badge,
   so it already fit both desktop and mobile. Summary keeps its own
   border-bottom OFF here since .chip-legend below already supplies one
   divider; two back-to-back rules would read as clutter, not structure. */
.breathy-panel { background: none; border: none; border-radius: 0; }
.breathy-panel summary { padding: 0 1.1rem 0.9rem; }
.breathy-panel[open] summary { border-bottom: none; }
.breathy-panel .filter-title { font-size: 1.3rem; font-weight: 700; letter-spacing: -0.01em; }
.breathy-panel .filter-summary-text { font-size: 0.85rem; }

.group-label {
  font-size: 0.68rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--text-muted); margin-bottom: 0.55rem;
}
.option-list { display: flex; flex-direction: column; gap: 0.15rem; }
.option {
  display: flex; align-items: center; gap: 0.55rem; font-size: 0.85rem; color: var(--text-secondary);
  padding: 0.3rem 0.4rem; border-radius: 6px; cursor: pointer;
}
.option:hover { background: var(--gridline); }
.option input { accent-color: var(--seq-blue); margin: 0; }
.option:has(input:checked) { color: var(--text-primary); font-weight: 600; }
.option .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.show-more-toggle {
  display: flex; align-items: center; font-size: 0.78rem; color: var(--seq-blue);
  background: none; border: none; cursor: pointer; padding: 0.3rem 0.4rem; text-align: left;
  font-family: inherit;
}
.show-more-toggle:hover { text-decoration: underline; }

.field-input {
  width: 100%; font-size: 0.85rem; padding: 0.5rem 0.7rem;
  border-radius: 8px; border: 1px solid var(--border); background: var(--surface-1); color: var(--text-primary);
}
.field-input:focus { outline: 2px solid var(--seq-blue); outline-offset: 1px; }

.range-inputs { display: flex; align-items: center; gap: 0.4rem; }
.range-inputs input { flex: 1 1 0; min-width: 0; }

.field-suffix-wrap { position: relative; }
.field-suffix-wrap .field-input { padding-right: 1.9rem; }
.field-suffix-wrap .field-suffix {
  position: absolute; right: 0.7rem; top: 50%; transform: translateY(-50%);
  color: var(--text-muted); font-size: 0.85rem; pointer-events: none;
}
.range-sep { font-size: 1rem; color: var(--text-muted); }

.apply-btn {
  width: 100%; font-size: 0.85rem; font-weight: 600; padding: 0.6rem 0.7rem;
  border-radius: 8px; border: none; background: var(--seq-blue); color: #fff; cursor: pointer;
}
.apply-btn:hover { background: color-mix(in srgb, var(--seq-blue) 85%, black); }

/* --- sort-by row --- */
.sort-row { display: flex; align-items: center; gap: 0.75rem; margin: 1.25rem 0 0.9rem; flex-wrap: wrap; }
.sort-row .group-label { font-size: 0.68rem; margin-bottom: 0; }
.pill-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.chip {
  font-size: 0.78rem; padding: 0.32rem 0.75rem; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--border); color: var(--text-secondary); background: var(--surface-1);
  text-decoration: none; display: inline-block;
}
.chip.active { border-color: var(--seq-blue); color: var(--seq-blue); background: color-mix(in srgb, var(--seq-blue) 10%, var(--surface-1)); }

/* --- table shell --- */
.table-card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
thead th {
  text-align: left; font-size: 0.68rem; font-weight: 650; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--text-muted);
  padding: 0.9rem 1.1rem 0.7rem; border-bottom: 1px solid var(--gridline);
  white-space: nowrap;
}
/* font-size explicitly re-asserted, not just text-align -- the generic
   .num utility class (tbody data cells) sets font-size:1rem, and a bare
   class selector actually outranks thead th's two type-selectors in CSS
   specificity, so without this, any header with class="num" (Price,
   Upside/Target, Risk, RR, Max Buy) silently inherited .num's 1rem
   instead of the 0.68rem every other header gets -- same font-weight,
   but visibly larger reads as "bolder". */
thead th.num { text-align: right; font-size: 0.68rem; }
thead th.sorted { color: var(--seq-blue); }
thead th a { text-decoration: none; color: inherit; }
thead th a:hover { text-decoration: underline; }
thead .arrow { font-size: 0.65rem; margin-left: 0.15rem; }

tbody td { padding: 0.95rem 1.1rem; border-bottom: 1px solid var(--gridline); vertical-align: middle; }
tbody tr.data-row:hover td { background: var(--gridline); }
tbody tr.data-row { cursor: pointer; }
tbody tr:last-child td { border-bottom: none; }

.num { font-size: 1rem; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }

/* stock cell */
.stock-cell { display: flex; align-items: flex-start; gap: 0.55rem; }
.dir-dot { width: 8px; height: 8px; border-radius: 50%; margin-top: 0.4rem; flex-shrink: 0; }
.dir-dot.long { background: var(--status-good); }
.dir-dot.short { background: var(--status-critical); }
.ticker { font-weight: 650; font-size: 1rem; letter-spacing: 0.01em; }
.dir-caption { font-size: 0.8rem; margin-top: 0.1rem; }
.dir-caption.long, .direction-long { color: var(--status-good); }
.dir-caption.short, .direction-short { color: var(--status-critical); }
/* .direction-long/.direction-short are also used bare (no .dir-caption) in
   alerts.html -- .dir-caption already sets its own font-size, this only
   covers the bare case without overriding that. */
.direction-long, .direction-short { font-size: 1rem; }

.price-value { font-size: 1rem; font-weight: 600; }
.sub-caption { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.15rem; }

.state-label { font-size: 0.9rem; font-weight: 550; }
.condition-caption { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.15rem; }

/* RR is the column the table is sorted by, so every row renders a
   .rr-badge -- including the <2/no-signal case (.tier-none), which is
   just the same badge with no fill and muted text, so every RR cell in
   the column shares one font-size/padding/line-height instead of a
   pill next to a differently-sized plain <div> for the untiered rows.
   Three colored tiers (>4 / 3-4 / 1.5-3), one green hue ramped light->dark
   by magnitude (see --rr-tier*-bg/--rr-text in :root). */
.rr-badge {
  display: inline-block; font-size: 0.8rem; padding: 0.15rem 0.6rem; border-radius: 999px;
  font-variant-numeric: tabular-nums; color: var(--rr-text);
}
.rr-badge.tier-1 { background: var(--rr-tier1-bg); color: #fff; font-weight: 700; }
.rr-badge.tier-2 { background: var(--rr-tier2-bg); font-weight: 700; }
.rr-badge.tier-3 { background: var(--rr-tier3-bg); font-weight: 700; }
.rr-badge.tier-none { background: transparent; color: var(--text-muted); font-weight: 400; }

/* Left accent stripe, same RR tier as .rr-badge -- lets the whole row read
   at a glance while scanning down the list, not just the RR column. A
   <tr> never paints its own left border in a collapsed table (browsers
   only ever render borders on td/th there), so desktop uses a ::before on
   the first cell instead; the mobile media query below overrides this
   with a real border since tr.data-row becomes a grid "card" there and
   borders work normally again. */
tr.data-row.rr-tier-1 td:first-child,
tr.data-row.rr-tier-2 td:first-child,
tr.data-row.rr-tier-3 td:first-child { position: relative; padding-left: 1.5rem; }
tr.data-row.rr-tier-1 td:first-child::before,
tr.data-row.rr-tier-2 td:first-child::before,
tr.data-row.rr-tier-3 td:first-child::before {
  content: ""; position: absolute; left: 0; top: 0.6rem; bottom: 0.6rem; width: 3px; border-radius: 3px;
}
tr.data-row.rr-tier-1 td:first-child::before { background: var(--rr-stripe-1); }
tr.data-row.rr-tier-2 td:first-child::before { background: var(--rr-stripe-2); }
tr.data-row.rr-tier-3 td:first-child::before { background: var(--rr-stripe-3); }

.lv-cell { display: flex; flex-direction: column; gap: 0.3rem; align-items: flex-end; }
.liq-row { display: flex; align-items: center; gap: 0.45rem; font-size: 0.8rem; }
.liq-track { width: 46px; height: 5px; border-radius: 3px; background: var(--seq-blue-track); overflow: hidden; }
.liq-fill { display: block; height: 100%; background: var(--seq-blue); }
.vol-row { display: flex; align-items: center; gap: 0.4rem; }
.badge, .status-badge {
  font-size: 0.7rem; font-weight: 650; padding: 0.1rem 0.5rem; border-radius: 999px; color: #fff;
  letter-spacing: 0.01em; display: inline-block;
}
.badge.good, .status-good { background: var(--status-good); }
.badge.warning, .status-warning { background: var(--status-warning); color: #2b2100; }
.badge.serious, .status-serious { background: var(--status-serious); font-weight: 500; }
.badge.critical, .status-critical { background: var(--status-critical); }
/* Vltx (volatility) badges -- see the --vltx-* ramp defined in :root. */
.badge.vltx-low { background: var(--vltx-low); color: var(--vltx-low-fg); }
.badge.vltx-normal { background: var(--vltx-normal); color: var(--vltx-normal-fg); }
.badge.vltx-high { background: var(--vltx-high); color: var(--vltx-high-fg); }
.badge.vltx-super { background: var(--vltx-super); color: var(--vltx-super-fg); }
.badge.vltx-extreme { background: var(--vltx-extreme); color: var(--vltx-extreme-fg); }
/* Alert TYPE badges -- categorical (identity), not status (severity).
   All three now read white text (previously cat-2/cat-3 used dark text,
   since white on their raw brand color only cleared 2.8-3.9:1 contrast)
   -- color-mix()ed 25% toward black keeps the same hue per type while
   passing >=4.5:1 with white in both themes (checked: 4.7-6.9:1 across
   all six type x theme combos, cat-1 needed no darkening (already 4.4+)
   but gets the same treatment for a consistent look across all three). */
.badge.type-choch { background: color-mix(in srgb, var(--cat-1) 75%, black); color: #fff; }
.badge.type-continuation { background: color-mix(in srgb, var(--cat-2) 75%, black); color: #fff; }
.badge.type-mid_conviction { background: color-mix(in srgb, var(--cat-3) 75%, black); color: #fff; }
.atr { font-size: 0.75rem; color: var(--text-muted); }

/* --- alerts widget (dashboard) --- */
/* Dense chip grid -- ticker-forward, icon+color carries alert type identity
   (see .chip-glyph). No bias text at a glance; click through for detail. */
.chip-grid { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.mini-chip {
  display: flex; align-items: center; gap: 0.35rem; font-size: 0.78rem; font-weight: 650;
  text-decoration: none; color: var(--text-primary); background: var(--surface-1);
  border-radius: 7px; padding: 0.3rem 0.55rem 0.3rem 0.45rem;
}
.mini-chip .chip-glyph { font-weight: 700; flex-shrink: 0; font-size: 0.85em; }
.mini-chip .chip-tf { font-weight: 500; color: var(--text-muted); font-size: 0.7rem; }
.mini-chip .chip-time { font-weight: 500; color: var(--text-muted); font-size: 0.68rem; margin-left: 0.15rem; }
.chip-legend {
  display: flex; gap: 1rem; flex-wrap: wrap; padding: 0.55rem 1.1rem;
  border-bottom: 1px solid var(--gridline); font-size: 0.72rem; color: var(--text-secondary);
}
.chip-legend span { font-size: inherit; display: inline-flex; align-items: center; gap: 0.35rem; }
.chip-legend .glyph { font-size: 1em; font-weight: 700; }
/* Formerly inline style="padding: ..." on the chip-grid/view-all/empty-
   state wrapper divs -- moved into real classes so the mobile media
   query below can override just padding-left (an inline style attribute
   outranks any external stylesheet rule short of !important, so this
   couldn't be done cleanly while it stayed inline). */
.panel-section-pad { padding: 0.85rem 1.1rem; }
.alerts-empty { text-align: center; padding: 1.75rem 1.1rem; color: var(--text-muted); }

/* --- expanded multi-timeframe panel --- */
/* A sheet overlay rather than an inline expansion below the clicked row
   -- the detail content (6 tf-cards + chart) is too dense to push the
   table flow around, and the chart library's price-scale rendering
   breaks when its container has a <table>/<td> ancestor (a genuine
   ResizeObserver/table-layout interaction bug, confirmed via headless
   inspection), which the old <tr>/<td> version had. This is now a
   plain <div>, swapped into a slot outside the table entirely --
   position:fixed takes it out of the table/page flow either way;
   desktop gets a centered modal, mobile (below) a bottom sheet. */
.expand-row { display: none; }
body:has(.expand-row.open)::after {
  content: ''; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); z-index: 40;
}
.expand-row.open {
  display: block; position: fixed; top: 50%; left: 50%; right: auto; bottom: auto;
  transform: translate(-50%, -50%);
  /* ~950px matches .table-card's actual rendered width: body caps at
     1280px (minus 1.5rem padding each side) minus the 250px filter
     sidebar and its 2rem gap. */
  width: min(950px, 96vw); max-width: 96vw; max-height: 90vh; overflow-y: auto;
  z-index: 50; background: #fff; border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
  /* Pinned to the light theme regardless of OS dark mode -- #fff is a
     fixed light color, so descendants must stay on light-mode text
     colors too. */
  --surface-1: #ffffff; --surface-page: #ffffff;
  --text-primary: #0b0b0b; --text-secondary: #52514e; --text-muted: #898781;
  --gridline: #f2f2f2; --border: rgba(11, 11, 11, 0.12);
  --seq-blue: #3452a8; --seq-blue-track: #ccd8f0;
  --status-good: #1a7a3c; --status-critical: #b8342f;
}
/* Padding lives on this inner wrapper, not .expand-row.open itself --
   .expand-row.open is the sticky positioning ancestor (it's the
   overflow-y:auto scroll container), and a sticky child's "stuck" top
   offset is measured from ITS scroll ancestor's padding edge. Padding
   and scroll-container living on the SAME element made the sticky
   header's negative-margin trick below overlap the content after it
   (the taxonomy row got visually sliced under the header). Splitting
   them back into separate elements restores the original math. */
.expand-row-inner { padding: 1rem 1.1rem 1.25rem; }
.expand-row .detail-header {
  position: sticky; top: 0; background: #fff; margin: -1rem -1.1rem 0rem; padding: 1rem 1.1rem 0.5rem;
}
.detail-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
.panel-close { text-decoration: none; font-size: 1.6rem; font-weight: 700; line-height: 1; color: var(--text-primary); }
.stock-meta {
  display: flex; flex-direction: column; gap: 0.3rem;
  font-size: 0.9rem; color: var(--text-muted);
  padding: 0 0 0.2rem; margin-bottom: 0.85rem;
}
.stock-meta strong { font-size: inherit; color: var(--text-secondary); }
.stock-meta-taxonomy { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 0.9rem; }
.stock-meta-taxonomy span { font-size: inherit; display: flex; gap: 0.3rem; }
.stock-meta-taxonomy .tax-label { font-size: inherit; font-weight: 500; color: var(--text-secondary); }
.stock-meta-stats { display: flex; flex-wrap: wrap; gap: 0.35rem 1.2rem; margin-top: 0.1rem; }
/* border-top spans the full sheet width, not just this grid's own column --
   negative margin cancels .expand-row-inner's 1.1rem side padding to reach
   the sheet's edges, then the same 1.1rem is re-added as this element's own
   padding so the cards themselves still line up with the content above. */
.tf-cards {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem;
  border-top: 1px solid var(--border);
  margin: 0.1rem -1.1rem 0; padding: 0.85rem 1.1rem 0;
}
/* .tf-card is a <details> -- collapsed by default on mobile (JS opens it
   by default on desktop, see the inline script after .tf-cards) so a
   narrow screen only commits to the label+bias line up front, expanding
   to state/fibo/target/stop/RR on tap. summary carries the collapsed
   row's own padding; .tf-card-body carries the rest, since <details>'
   only other layout hook is "everything after <summary>". */
.tf-card {
  min-width: 0;
  border-radius: 10px;
  background: var(--surface-1);
}
.tf-card summary { list-style: none; cursor: pointer; padding: 0.85rem 1rem; }
.tf-card summary::-webkit-details-marker { display: none; }
.tf-card[open] summary { border-bottom: 1px solid var(--border); }
/* Circular gray badge + SVG chevron icon (not a text glyph) -- a rotating
   disclosure hint. Hidden by default: desktop cards can't actually be
   collapsed (see the click-blocking script in _scanner_expand_content.html),
   so a disclosure indicator there would be misleading. Mobile media query
   below re-enables it, since cards genuinely are collapsible there. */
.tf-card .chevron {
  display: none; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; background: var(--gridline);
  color: var(--text-muted); margin-left: auto; flex-shrink: 0;
  transition: transform 0.15s ease;
}
.tf-card .chevron svg { width: 14px; height: 14px; }
.tf-card[open] .chevron { transform: rotate(180deg); }
/* summary already carries its own padding/border for this spacing --
   cancel the general .tf-row rule's margin-bottom so it doesn't double up.
   align-items:center (not .tf-row's baseline) since summary's own direct
   children are now .tf-summary-text + .chevron, not text spans directly. */
.tf-card summary.tf-row { margin-bottom: 0; align-items: center; }
.tf-card-body { padding: 0.7rem 1rem 0.85rem; }
.tf-card .tf-row { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.3rem; padding-bottom:0.5rem; font-size: 0.9rem; font-weight: 600; }
/* Default (desktop-like): label+bias inline, e.g. "D1: Bearish (...)" --
   unchanged from before .tf-summary-text existed. Mobile media query
   below flips this to a stacked label-above-bias layout instead. */
.tf-summary-text { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.3rem; min-width: 0; flex: 1; }
.tf-card .tf-label, .tf-card .tf-bias { font-size: 0.9rem; }
.tf-card .tf-label { text-transform: uppercase; letter-spacing: 0.06em; }
.tf-card .tf-label::after { content: ":"; }
/* This is the tf-state/tf-fibo row specifically (targeted via :has() since
   both tf-row divs in a card share the same class) -- nowrap + ellipsis on
   each span independently instead of wrapping to a second line. */
.tf-card .tf-row:has(.tf-fibo) { flex-wrap: nowrap; min-width: 0; }
.tf-card .tf-state, .tf-card .tf-fibo {
  font-size: 0.8rem; font-weight: 500; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.tf-card .tf-state { flex-shrink: 1; }
.tf-card .tf-fibo { flex-shrink: 2; }
/* State/Fibo: a vertical list of two rows (State, then Fibo), each with
   its own label to the left of its value -- opacity (not just a muted
   color token) so the label recedes relative to the value it's labeling,
   same treatment mobile already used for this. */
.tf-detail-row { flex-direction: column; gap: 0.35rem; }
.tf-detail { display: flex; flex-direction: row; align-items: baseline; gap: 0.4rem; min-width: 0; }
.tf-detail-label {
  display: block; flex-shrink: 0; min-width: 2.6rem;
  font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); opacity: 0.75;
}
.tf-card .tf-stats-row { display: flex; justify-content: space-between; gap: 0.5rem; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border); }
.tf-card .tf-stat { display: flex; flex-direction: column; gap: 0.1rem; }
.tf-card .tf-stat-label { font-size: 0.65rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted);opacity: 0.75; }
.tf-card .tf-stat-value { font-size: 0.75rem; font-weight: 500; color: var(--text-secondary); }

/* --- pivot chart --- */
.chart-section { margin-top: 2rem; }
.chart-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.6rem; }
.chart-title { font-size: 0.9rem; font-weight: 650; color: var(--text-primary); }
.chart-tabs { display: flex; gap: 0.4rem; }
.chart-tab {
  font-size: 0.72rem; font-weight: 650; padding: 0.3rem 0.7rem; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-1); color: var(--text-secondary); cursor: pointer;
}
.chart-tab.active { border-color: var(--seq-blue); color: var(--seq-blue); background: color-mix(in srgb, var(--seq-blue) 10%, var(--surface-1)); }
/* .chart-tab reused on a <select> (investor-type filter) -- native select
   chrome (padding/line-height) differs from a <button>'s, so this closes
   the gap without hiding the dropdown affordance entirely. */
select.chart-tab { line-height: 1.4; }
/* Populated by chart.js only while Broker Flow lines are visible --
   empty/hidden otherwise, so it never sits there blank. */
.broker-flow-legend { display: none; gap: 0.9rem; flex-wrap: wrap; padding: 0 0 0.5rem; font-size: 0.72rem; color: var(--text-secondary); }
.broker-flow-legend .swatch { display: inline-flex; align-items: center; gap: 0.3rem; }
.broker-flow-legend .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex: none; }
#chart-container { position: relative; width: 100%; height: 480px; border: 1px solid var(--border); border-radius: 10px; }
.ob-label { position: absolute; transform: translate(-50%, -50%); font-size: 0.7rem; font-weight: 600; color: rgba(43, 46, 56, 0.7); pointer-events: none; white-space: nowrap; }

footer.page-footer { margin-top: 1.25rem; font-size: 0.78rem; color: var(--text-muted); }

.mobile-label { display: none; }

/* --- "new alert" snackbar (dashboard only, see alerts_toast.js) --- */
.alert-toast {
  position: fixed; z-index: 60; right: 1.5rem; bottom: 1.5rem; max-width: 360px;
  display: flex; align-items: center; gap: 0.7rem;
  background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15), 0 2px 6px rgba(0, 0, 0, 0.08);
  padding: 0.65rem 0.7rem 0.65rem 0.9rem; font-size: 0.85rem; line-height: 1.4;
  opacity: 0; pointer-events: none; transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.alert-toast[hidden] { display: none; }
.alert-toast.show { opacity: 1; pointer-events: auto; transform: translateY(0); }
.alert-toast-body { flex: 1; min-width: 0; }
.alert-toast-body strong { font-weight: 650; }
.alert-toast-action { font-size: 0.8rem; font-weight: 650; text-decoration: none; white-space: nowrap; color: var(--seq-blue); }
.alert-toast-close { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 1.1rem; line-height: 1; padding: 0 0.1rem; }
.alert-toast-close:hover { color: var(--text-primary); }

@media (max-width: 900px) {
  /* Nav bar: keep utility (Admin/Log out) pinned to the right of the menu
     on one row, instead of wrapping below it once the 4 links + utility
     don't all fit -- .main-nav scrolls horizontally instead (same
     pattern as .tf-scroll elsewhere), min-width:0 is what actually lets
     a flex child shrink below its content size so overflow-x can kick
     in; flex-shrink:0 on .header-utility stops it being squeezed. */
  header { flex-wrap: nowrap; }
  .main-nav {
    flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
    gap: 1.25rem;
  }
  .main-nav::-webkit-scrollbar { display: none; }
  .main-nav .nav-link { white-space: nowrap; }
  .header-utility { flex-shrink: 0; }

  .page-layout { flex-direction: column; }
  /* .page-layout sets align-items:flex-start (needed on desktop so the
     250px filter-sidebar doesn't stretch to the taller main-content's
     height) -- but in column mode that axis flips to width, so without an
     explicit width here .main-content shrinks to fit its own content
     instead of stretching to match .filter-sidebar's forced 100%. */
  .main-content { width: 100%; }
  /* Full viewport-width bleed -- same negative-margin-cancels-padding trick
     as the tf-cards divider, sized to body's own 1.5rem horizontal padding. */
  .section-divider { margin-left: -1.5rem; margin-right: -1.5rem; }

  /* Search box stays on the title's right instead of wrapping to its own
     row -- desktop's flex-wrap:wrap only kicks in here because the title
     block has no width limit, so it (not the fixed 220px search input)
     was what ran out of room first. Shrinking the search box and letting
     the title truncate with an ellipsis keeps both on one line down to
     narrow phone widths. */
  .page-head { flex-wrap: nowrap; align-items: center; gap: 0.75rem; }
  .page-head > div:first-child { min-width: 0; flex: 1 1 auto; }
  .page-head h1, .page-head .subhead { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .page-head .search-box { flex: 0 0 auto; }
  .page-head .search-box input { width: 42vw; min-width: 110px; max-width: 170px; }

  /* Panel headers (Macro/Alerts/Filters all share .filter-title) read too
     small at the desktop 0.85rem size once they're mobile-width headings --
     bump uniformly so Filters' bottom-sheet header matches Alert/Macro. */
  .filter-title { font-size: 1.1rem; }

  /* Collapsed chevron -> right arrow (iOS-style disclosure) on mobile,
     replacing desktop's down-chevron glyph -- same collapse-old/inject-new
     ::after trick already used below for .sheet-panel[open]'s close (X),
     which has higher specificity and still wins whenever a sheet is open,
     so this only ever shows on the collapsed/closed state. Macro (not a
     sheet-panel, stays a plain inline collapsible on mobile) instead
     rotates this same arrow 90deg when open via the second rule below,
     landing it pointing down instead of desktop's 180deg up-flip.
     Scoped to .filter-panel .chevron specifically, not the bare .chevron
     class -- the tf-card detail sheet reuses that same class name for its
     own SVG-icon chevron, which an unscoped rule here would also match,
     injecting a stray "\203A" into that unrelated badge. */
  .filter-panel .chevron { font-size: 0; }
  .filter-panel .chevron::after {
    content: "\203A"; display: inline-block; font-size: 1.1rem; font-weight: 700; color: var(--text-muted);
  }
  .filter-panel[open] .chevron { transform: rotate(90deg); }

  /* Alerts (.breathy-panel), mobile: flush left edge to match the page's
     own heading ("KYX Daily Signal" etc, which has no extra left padding
     of its own beyond body's) -- the panel's internal pieces each carried
     their own 1.1rem left padding (fine when there was a card to inset
     from; there isn't one anymore), so without this Alerts sat visibly
     indented deeper than every other heading on the page. Right padding
     is untouched, this is specifically a left-alignment fix. */
  .breathy-panel summary { padding-left: 0; }
  .breathy-panel .chip-legend,
  .breathy-panel .panel-section-pad,
  .breathy-panel .alerts-empty { padding-left: 0; }
  /* Alerts, mobile: always expanded, no collapse -- forced via JS (see
     the inline <script> in _alerts_compact.html, which sets .open=true
     and blocks the summary click unconditionally on every render/poll,
     not just page load, since this whole file re-renders via htmx and a
     document.readyState check wouldn't refire on those swaps). Chevron
     hidden here rather than left showing-but-inert, same principle as
     the tf-card's desktop chevron: a disclosure indicator for something
     that can't actually be collapsed is misleading, not decorative. */
  .breathy-panel .chevron { display: none; }

  /* Filters (.sheet-panel) becomes a bottom sheet on mobile: collapsed by
     default (an inline <script> right after its </details> in the
     template -- see scanner.js's top comment for why it's not here) and,
     once opened, detached into a fixed-position sheet identical in spirit
     to .expand-row.open. Alerts no longer shares this -- see above. */
  body:has(.sheet-panel[open])::after {
    content: ''; position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); z-index: 40;
  }
  .sheet-panel[open] {
    position: fixed; top: auto; right: 0; bottom: 0; left: 0; z-index: 50;
    max-height: 85vh; overflow-y: auto; border-radius: 16px 16px 0 0;
    box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.3);
    background: #fff;
    --surface-1: #ffffff; --surface-page: #ffffff;
    --text-primary: #0b0b0b; --text-secondary: #52514e; --text-muted: #898781;
    --gridline: #f2f2f2; --border: rgba(11, 11, 11, 0.12);
    --seq-blue: #3452a8; --seq-blue-track: #ccd8f0;
  --status-good: #1a7a3c; --status-critical: #b8342f;
  }
  .sheet-panel[open] summary { position: sticky; top: 0; background: #fff; z-index: 1; }
  /* Chevron -> bold close (X) once a sheet is actually open full-screen --
     "tap to expand" no longer applies, "tap to close" does. font-size:0
     collapses the original glyph without removing it (still the same
     <span>, still toggles the details natively via the summary click).
     Same glyph + sizing as .panel-close (the ticker sheet's own close
     button) for consistency between the two sheet types. */
  .sheet-panel[open] .chevron { font-size: 0; transform: none; }
  .sheet-panel[open] .chevron::after {
    content: "\00D7"; display: inline-block; font-size: 1.6rem; font-weight: 700; line-height: 1; color: var(--text-primary);
  }

  .filter-sidebar { width: 100%; }
  .option-list { flex-direction: row; flex-wrap: wrap; gap: 0.4rem; }
  .option { border: 1px solid var(--border); border-radius: 999px; padding: 0.32rem 0.7rem; }
  .option:has(input:checked) { border-color: var(--seq-blue); background: color-mix(in srgb, var(--seq-blue) 10%, var(--surface-1)); }
  .show-more-toggle { width: 100%; }

  /* 2-column grid, collapsed by default (see .tf-card[open] in the base
     rules + the inline script after .tf-cards in the template that opens
     them on desktop). */
  .tf-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; }
  /* Re-enable the chevron (hidden by default now, desktop-only) -- mobile
     cards genuinely are collapsible, so the disclosure hint still applies. */
  .tf-card .chevron { display: flex; }
  /* Restore the original row spacing here -- the base rule above was
     tightened for desktop specifically, mobile's compact cards already
     had this tuned separately (padding, gaps) and don't need it changed. */
  .tf-card .tf-row { margin-bottom: 0.5rem; }
  /* Label sits to the left like a rank/index column -- same position and
     min-width for every row regardless of direction, so rows line up
     consistently instead of each starting at a different point. Left/right
     padding dropped (see summary/tf-card-body below) to give the now-
     narrower 2-column row back the width label+bias need to fit inline. */
  .tf-summary-text { flex-direction: row; flex-wrap: nowrap; align-items: baseline; gap: 0.4rem; min-width: 0; }
  .tf-card .tf-label {
    flex-shrink: 0; min-width: 1.5rem; color: var(--text-muted); font-size: 0.65rem; font-weight: 700; letter-spacing: 0.04em;
  }
  .tf-card .tf-label::after { content: none; }
  .tf-card .tf-bias { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; max-width: 100%; font-weight:600;}
  .tf-card summary { padding: 0.4rem 0.5rem; }
  .tf-card-body { padding: 0.35rem 0.5rem 0.5rem; }

  /* State/Fibo: same vertical-list-of-rows idea as desktop now uses, but
     label stacks above the value here instead of sitting to its left --
     narrower cards need the vertical room back more than desktop does. */
  .tf-detail-row { flex-direction: column; align-items: stretch; gap: 0.4rem; flex-wrap: nowrap; }
  .tf-detail { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
  /* opacity, not a lighter color token -- .tf-state/.tf-fibo already use
     var(--text-muted) too, so the label needs to recede further still to
     stay visually distinct from the value it's labeling. */
  .tf-detail-label {
    display: block; font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.04em; color: var(--text-muted); opacity: 0.75;
  }
  .tf-card .tf-fibo::before { content: none; }
  .tf-card .tf-state, .tf-card .tf-fibo {
    white-space: normal; overflow: visible; text-overflow: clip; flex-shrink: initial;
  }

  /* Target/Stop/RR: vertical list instead of 3 side-by-side columns --
     each stat becomes its own label-left/value-left row. Label gets a
     fixed min-width (fits "TARGET", the longest of the three) so values
     all start at the same indent regardless of their own label's length,
     instead of sitting right after wherever "RR"/"STOP" happen to end. */
  .tf-card .tf-stats-row { flex-direction: column; gap: 0.45rem; }
  .tf-card .tf-stat { flex-direction: row; justify-content: flex-start; align-items: baseline; gap: 0.1rem; }
  .tf-card .tf-stat-label { flex-shrink: 0; min-width: 3.2rem; }
  .tf-card .tf-stat-value { text-align: left; }

  /* Table -> one grid "card" per stock instead of a wide scrolling table.
     Row 1: ticker+direction (left) | structure (right)
     Row 2: price | target | risk | RR
     Row 3: buying power & volatility, as two separate left-aligned pills
     The .table-card shell itself goes transparent/borderless -- each row
     becomes its own bordered, rounded card with a gap between, rather than
     one long box with hairline separators (which read as "table", not
     "cards", on a narrow screen). */
  .table-card { overflow-x: visible; background: transparent; border: none; border-radius: 0; }
  table { width: 100%; }
  tbody { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; }
  thead { display: none; }
  tr.data-row {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-areas:
      "stock stock structure structure"
      "price target risk rr"
      "buypower buypower buypower buypower";
    column-gap: 0.6rem; row-gap: 1.1rem;
    padding: 1rem 1.1rem 0.5rem;
    background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  }
  tr.data-row td { padding: 0; border-bottom: none; }
  tr.data-row td:nth-child(1) { grid-area: stock; text-align: left; }
  /* matches the visual indent of the ticker text in row 1, which sits past
     the colored direction dot (8px) + its gap (0.55rem ~ 8.8px). */
  tr.data-row td:nth-child(2) { grid-area: price; text-align: left; padding-left: 16.8px; }
  tr.data-row td:nth-child(3) { grid-area: structure; text-align: right; }
  tr.data-row td:nth-child(4) { grid-area: target; text-align: left; }
  tr.data-row td:nth-child(5) { grid-area: risk; text-align: left; }
  tr.data-row td:nth-child(6) { grid-area: rr; text-align: left; }
  /* Separator above this row (sitting just past the RR badge in the row
     above) is full-bleed edge-to-edge across the whole card, not inset
     within the card's own 1.1rem padding like it was -- same negative-
     margin-cancels-padding trick already used for .section-divider and
     .tf-cards elsewhere in this file. Content padding-left is restored
     to the original 16.8px plus the cancelled 1.1rem, so the Rp text
     still lines up with row 2 above it despite the box itself now
     starting further left. */
  tr.data-row td:nth-child(7) {
    grid-area: buypower; display: flex; justify-content: flex-start;
    margin: 0 -1.1rem; padding: 0.5rem 1.1rem 0 calc(1.1rem + 16.8px);
    border-top: 1px solid var(--gridline);
  }
  /* No :not(:last-child) separator needed -- each row is now its own
     bordered card (see tr.data-row above), spaced by tbody's own gap. */

  tr.data-row td:nth-child(2), tr.data-row td:nth-child(4),
  tr.data-row td:nth-child(5), tr.data-row td:nth-child(6) { margin: 0 auto; width: 100%; }

  tr.data-row td:nth-child(7) .lv-cell {
    flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: flex-start; gap: 0.5rem;
  }
  tr.data-row td:nth-child(7) .liq-row {
    border: 1px solid var(--border); border-radius: 999px; background: var(--surface-1);
    padding: 0.3rem 0.7rem 0.3rem 0.5rem; margin: 0;
  }
  tr.data-row td:nth-child(7) .vol-row {
    border: 1px solid var(--border); border-radius: 999px; background: var(--surface-1);
    padding: 0.3rem 0.7rem; margin: 0;
  }

  /* RR accent stripe: tr.data-row is a real grid box here (not a table
     row), so a left border renders normally -- swap the desktop
     ::before-on-first-cell hack for a plain border-left override instead. */
  tr.data-row.rr-tier-1, tr.data-row.rr-tier-2, tr.data-row.rr-tier-3 { border-left-width: 3px; }
  tr.data-row.rr-tier-1 { border-left-color: var(--rr-stripe-1); }
  tr.data-row.rr-tier-2 { border-left-color: var(--rr-stripe-2); }
  tr.data-row.rr-tier-3 { border-left-color: var(--rr-stripe-3); }
  tr.data-row.rr-tier-1 td:first-child, tr.data-row.rr-tier-2 td:first-child,
  tr.data-row.rr-tier-3 td:first-child { padding-left: 0; }
  tr.data-row.rr-tier-1 td:first-child::before, tr.data-row.rr-tier-2 td:first-child::before,
  tr.data-row.rr-tier-3 td:first-child::before { display: none; }

  .mobile-label {
    display: block; font-size: 0.65rem; font-weight: 650; text-transform: uppercase;
    letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.15rem;
  }
  .stat-value { font-weight: 600; font-size: 0.95rem; color: var(--text-primary); }

  /* Bottom sheet instead of the desktop centered modal -- override
     just the position/dimensions, the fixed-overlay mechanics and
     light-theme pinning above already apply. */
  .expand-row.open {
    top: auto; right: 0; bottom: 0; left: 0; transform: none; width: auto; max-width: 100%;
    max-height: 94vh; border-radius: 16px 16px 0 0;
    box-shadow: 0 -6px 24px rgba(0, 0, 0, 0.3);
  }

  /* Shorter chart on mobile -- 480px desktop height leaves barely any
     room for the tf-cards/stock-meta above it in a shorter viewport. */
  #chart-container { height: 300px; }
  /* Breathing room before the EMA/timeframe-tabs row below it, which wraps
     onto its own line under the title at this width (chart-header's own
     flex-wrap). */
  .chart-title { padding-bottom: 0.3rem; }

  /* "New alert" snackbar -- bottom-right (base rule, defined further up
     before this media block so it loses the cascade here) works fine on
     desktop where it never competes with anything, but sits over the
     thumb-reach zone on mobile, so it moves to a top bar instead. */
  .alert-toast {
    left: 0.75rem; right: 0.75rem; top: 0.75rem; bottom: auto; max-width: none;
    transform: translateY(-8px);
  }
  .alert-toast.show { transform: translateY(0); }
}
