/* LRA TECHNICALS — minimalist warm-serif theme
   White/transparent backgrounds, thin dark hairlines, no fills. */

:root {
  --ink:        #1c1a17;   /* very dark warm gray — primary text */
  --ink-soft:   #6f6a62;   /* muted text */
  --line:       #2a2724;   /* thin "black" hairline */
  --line-soft:  #d8d3c8;   /* faint separators inside tables */
  --paper:      #fcfbf8;   /* warm white background */
  --hover:      #f3f0e9;   /* barely-there hover wash */
  /* Percent-change direction (#movers). Deliberately desaturated so they sit
     in the warm-serif palette rather than shouting like a trading terminal;
     still unambiguously green/red, and distinct at the usual color-vision
     deficiencies because the +/- sign carries the same information. */
  --gain:       #2f6b45;   /* positive change */
  --loss:       #9c3328;   /* negative change */
  --header-h:   64px;      /* kept in sync with header's real height by
                              updateHeaderHeightVar() in app.js — this value
                              is only the pre-JS/no-JS fallback */
  --display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --body:    "EB Garamond", Georgia, "Times New Roman", serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 18px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

/* ── Header (always on top) ───────────────────────────────────────────────── */
header {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px 40px 18px;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
}

.logo {
  font-weight: 600;
  font-size: 25px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  cursor: pointer;
  user-select: none;
}

nav { display: flex; align-items: center; gap: 34px; }
.nav-menu { display: flex; align-items: center; gap: 34px; }
.nav-menu-toggle {
  display: none;   /* shown only in the mobile media query below */
  background: none;
  border: none;
  padding: 4px;
  margin: 0;
  cursor: pointer;
}
.nav-menu-icon { display: block; width: 22px; }
.nav-menu-icon span {
  display: block;
  height: 1px;
  background: var(--ink-soft);
  margin: 6px 0;
  transition: background 0.15s ease;
}
.nav-menu-toggle:hover .nav-menu-icon span { background: var(--ink); }

/* Header buttons: the text *is* the button — no border, no fill. */
nav button {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  font-family: var(--body);
  font-size: 18px;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  transition: color 0.15s ease;
}
nav button:hover { color: var(--ink); }
nav button.active {
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
  padding-bottom: 2px;
}

/* ── Content zone ─────────────────────────────────────────────────────────── */
main {
  max-width: 1080px;
  margin: 0 auto;
  padding: 38px 40px 80px;
  transition: max-width 0.35s ease;
}

/* ── Global fade-in for dynamically-loaded content ────────────────────────────
   ONE rule, applied everywhere content gets built on demand: every full view
   redraw (Leaderboard/Alerts/Pick a Date/Top Charts/Chart/Symbol page — all
   of which replace #content's children wholesale), every #symbols/<SYM>
   section as it moves through pending -> ready/error
   (.symbol-section-body's child), and the standalone #chart/<SYM> view's own
   placeholder/error slot (#chart-frame-slot's child).

   This works with ZERO JavaScript and zero per-call-site wiring because it's
   a CSS *animation* (not a transition): an animation defined via a class/
   selector plays automatically the instant a matching element is created —
   unlike a transition, it needs no later "now show it" toggle. Since every
   one of those call sites only ever builds this markup once its data has
   actually arrived (an `await fetch(...)` already resolved, a task already
   reached "ready"/"error", etc.), the animation's start time is pinned to
   "this exact element just became ready to show", never to whenever the
   page itself happened to load — which is exactly why a slow section fades
   in on its own schedule instead of waiting for (or racing) a fast one.

   Chart iframes are excluded (:not(.chart-fade-in)) because they already
   have their OWN reveal — see .chart-fade-in below — deliberately delayed
   past "ready" until the chart library finishes drawing, not something this
   generic rule should also apply to. */
@keyframes contentFadeIn { from { opacity: 0; } to { opacity: 1; } }
#content > *:not(.chart-fade-in),
.symbol-section-body > *:not(.chart-fade-in),
#chart-frame-slot > *:not(.chart-fade-in),
#market-frame-slot > *:not(.chart-fade-in) {
  animation: contentFadeIn 0.5s ease;
}

.view-title {
  font-size: 30px;
  font-weight: 600;
  letter-spacing: 0.03em;
  margin: 0 0 4px;
}
.view-sub {
  color: var(--ink-soft);
  margin: 0 0 26px;
  font-size: 16px;
  letter-spacing: 0.02em;
}

/* ── Controls (filter / order) ────────────────────────────────────────────── */
.controls {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between; /* one line: filter left, order right; wrapped: each line left */
  gap: 26px 34px;
  margin-bottom: 22px;
}
/* #movers puts Filter, Order and Rank on three separate lines, all flush
   left — unlike the leaderboard/alerts .controls above, which spreads its two
   groups to opposite ends of one line. Column-direction + flex-start does
   both jobs (one group per line, each left-aligned) without touching the
   shared .control-group / .chip styling. */
.controls-stacked {
  flex-direction: column;
  /* Explicitly stretch (NOT the base .controls rule's `baseline`, and NOT
     flex-start): in a column-direction flex container, align-items
     controls the CROSS axis (horizontal) sizing of each row. Either of the
     other two would size every .control-group to its own CONTENT width
     (shrink-to-fit — effectively `width: max-content`), which is what let a
     long, unwrapped Rank row grow past `main`'s bounds and drag the whole
     page into horizontal scroll: with no imposed width to shrink FROM,
     .control-group's min-width:0 below never got a chance to do anything.
     stretch instead gives every row a real, bounded width — this
     container's own width — which is what .control-options-viewport further
     down actually needs in order to clip/scroll ITS overflow internally
     instead of pushing the page wider. */
  align-items: stretch;
  justify-content: flex-start;
  gap: 12px;
}
/* Row wraps if needed at THIS level (not the individual chip level) never
   happens: label + fixed-width viewport always fit on one line together
   because the viewport does its own internal clipping instead of pushing
   width. flex-wrap:nowrap here is what actually enforces that. */
.controls-stacked .control-group { display: flex; width: 100%; flex-wrap: nowrap; align-items: baseline; gap: 14px; min-width: 0; }
/* Fixed width (not just min-width) so Filter/Order/Rank's shorter labels
   ("Rank") start their options at the exact same x-position as the longer
   ones ("Filter") — a shared column, not per-row auto-sizing. */
.controls-stacked .control-label {
  flex: 0 0 68px;
  width: 68px;
}
/* .control-options-viewport is the FIXED, regulated-size window: it takes
   whatever width is left after the label (flex:1 1 auto + min-width:0, now
   meaningful because .control-group actually stretches to a bounded width
   — see .controls-stacked above) and clips/scrolls ITS OWN overflow rather
   than growing to fit content. .control-options-track is the thing that
   actually scrolls inside it — sized to its natural content width (however
   wide the buttons need to be), never wrapped, never shrunk. Every chip
   inside the track gets an inline `width` LOCKED to its own FULL/untruncated
   label (set once in JS — see initControlOptionsClipping in app.js) so a
   button's box never resizes as its displayed text changes; only the text
   drawn inside a partially-visible button changes as you scroll (see the
   same function), which is what makes "Weekly Ch..." grow into
   "Weekly Cha..." smoothly as more of a fixed-size box comes into view,
   rather than the box itself jumping around. */
.controls-stacked .control-options-viewport {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  padding-bottom: 6px;               /* clears the scrollbar so it doesn't sit under the chip text */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.controls-stacked .control-options-track {
  display: flex;
  align-items: baseline;
  gap: 14px;
  width: max-content;
}
.controls-stacked .control-options-track .chip {
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;      /* the box width is locked in JS to the full label; this is just a backstop */
}
/* A chip currently showing a scroll-clipped (partial) label: JS sets
   data-partial="true" and disabled at the same time — disabled is what
   actually blocks the click (a native <button disabled> fires no click
   event at all, keyboard included), this is just the matching visual so it
   doesn't look interactive while it can't be interacted with. */
.controls-stacked .chip[data-partial="true"] {
  cursor: default;
  opacity: 0.55;
}
.controls-stacked .chip[data-partial="true"]:hover { color: var(--ink-soft); border-bottom-color: transparent; }

/* Scoped away from .controls-stacked: re-declaring display:flex here (even
   at the same value already set by .controls above) was, empirically,
   enough to break the stretch-based width containment .controls-stacked
   .control-group depends on for #movers' scrollable rows — with this rule
   also matching, .control-group grew to fit its content instead of being
   held to .controls-stacked's actual width, letting the whole page get
   dragged into horizontal scroll. #leaderboards/#alerts (the only other
   users of plain .control-group) don't use .controls-stacked, so scoping
   this out costs them nothing. */
.controls:not(.controls-stacked) .control-group { display: flex; align-items: baseline; gap: 14px; }
.control-label {
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.chip {
  background: none;
  border: none;
  padding: 0 0 2px;
  cursor: pointer;
  font-family: var(--body);
  font-size: 17px;
  color: var(--ink-soft);
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.chip:hover { color: var(--ink); }
.chip.on { color: var(--ink); border-bottom: 1px solid var(--ink); }

/* ── Table ────────────────────────────────────────────────────────────────── */
table { width: 100%; border-collapse: collapse; }
thead th {
  text-align: left;
  font-family: var(--body);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 0 14px 10px;
  border-bottom: 1px solid var(--line);
}
tbody td {
  padding: 13px 14px;
  border-bottom: 1px solid var(--line-soft);
  font-size: 18px;
  transition: padding-left 0.2s ease, padding-right 0.2s ease;
}
/* Transitions key off style-recalculation boundaries, not paint boundaries —
   so even a brand-new row's FIRST padding value can still be treated as "a
   change" (from the CSS default) if a forced layout read (computeAlignmentBothModes's
   peek) recalculates style before the real value lands. That arms an
   animation that plays the instant the row becomes visible, even though the
   wrong value was never actually painted. finishRenderWithAlignment adds
   this class for exactly that first commit, then removes it — so a LATER,
   genuine change (the Standard/Wide toggle) still transitions normally. */
tbody.no-transition td { transition: none; }
tbody tr { transition: background 0.12s ease; }

/* Staggered reveal for a freshly-rendered table (see finishRenderWithAlignment
   in app.js) — rows are held at visibility:hidden until their column padding
   is correctly computed, then revealed with this. Applied via inline
   `animation` per row (with a per-row delay), not a class, so @keyframes is
   all that's needed here. */
@keyframes rowFadeIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
tbody tr.row-link:hover { background: var(--hover); }
.col-symbol { font-size: 21px; font-weight: 600; letter-spacing: 0.06em; }
.sym-link {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
  cursor: pointer;
  transition: border-color 0.15s ease;
}
/* currentColor: the underline always matches whichever color is active —
   black for a charted symbol, gray for one that isn't rendered yet — without
   needing a separate hover rule per variant. Color itself never changes on
   hover; only the underline appears. */
.sym-link:hover { border-bottom-color: currentColor; }
.sym-link.sym-link-pending { color: var(--ink-soft); }
.col-num { text-align: right; font-variant-numeric: tabular-nums; }
.col-strength { text-align: right; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.tag {
  font-size: 12px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--ink-soft); margin-right: 8px;
}

.expander {
  margin-top: 22px;
  background: none; border: none; padding: 4px 0; cursor: pointer;
  font-family: var(--body); font-size: 16px; color: var(--ink-soft);
  border-bottom: 1px solid transparent; letter-spacing: 0.04em;
}
.expander:hover { color: var(--ink); border-bottom-color: var(--ink); }
.expander-row { display: flex; gap: 28px; }
.expander-row .expander { margin-top: 22px; }

/* ── Card grid (calendar + top charts) ────────────────────────────────────── */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: 16px;
}
.card {
  border: 1px solid var(--line);
  background: transparent;
  padding: 22px 18px;
  cursor: pointer;
  transition: background 0.12s ease;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 96px;
  justify-content: center;
}
.card:hover { background: var(--hover); }
.card-date-day { font-size: 30px; font-weight: 600; line-height: 1; }
.card-date-rest { color: var(--ink-soft); font-size: 15px; letter-spacing: 0.04em; }
.card-symbol { font-size: 30px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; }
.card-score { color: var(--ink-soft); font-size: 15px; font-variant-numeric: tabular-nums; }

/* ── Chart view ───────────────────────────────────────────────────────────── */
.chart-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 14px;
}
.chart-head-left { display: flex; align-items: baseline; gap: 16px; }
.chart-symbol { font-size: 34px; font-weight: 600; letter-spacing: 0.08em; }
.chart-profile-btn {
  background: none; border: none; padding: 0 0 2px; cursor: pointer;
  font-family: var(--body); font-size: 15px; color: var(--ink-soft);
  border-bottom: 1px solid transparent; letter-spacing: 0.04em;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.chart-profile-btn:hover { color: var(--ink); border-bottom-color: var(--ink); }
.chart-pos { color: var(--ink-soft); font-size: 15px; letter-spacing: 0.04em; }
#chart-wrap { position: relative; }
#chart-frame {
  width: 100%;
  height: calc(100vh - 220px);
  min-height: 460px;
  border: none;
  background: none;
}
/* Every chart iframe gets this class at creation and stays invisible until
   app.js's revealChartFrame() adds chart-fade-in-visible — once the load is
   fully settled (see wireMainChartFrame in app.js) plus a short buffer, so
   the messy first paint (chart library drawing, then indicators being
   pruned to the featured set) is never on screen. (Deliberately separate
   from, and excluded by, the generic content-fade-in rule near the top of
   this file — that one fires the instant an element is inserted; this one
   waits past that, until the chart is actually done drawing.) */
.chart-fade-in { opacity: 0; transition: opacity 0.5s ease; }
.chart-fade-in.chart-fade-in-visible { opacity: 1; }
.chart-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  background: none; border: none; cursor: pointer;
  font-size: 46px; color: var(--ink-soft);
  padding: 0 10px; transition: color 0.15s ease; user-select: none;
}
.chart-nav:hover { color: var(--ink); }
.chart-nav[disabled] { opacity: 0.2; cursor: default; }
.chart-nav.prev { left: -34px; }
.chart-nav.next { right: -34px; }
.chart-hint { text-align: center; color: var(--ink-soft); font-size: 14px; letter-spacing: 0.06em; margin-top: 12px; }

/* ── Markets view (3D galaxy/treemap, embedded as an iframe) ──────────────
   Fallback sizing if #market-frame is ever shown without market-fullbleed
   active below (shouldn't normally happen — renderMarket() always sets
   both together — but better than the iframe collapsing to 0 height). */
#market-frame {
  width: 100%;
  min-height: 460px;
  border: none;
  background: none;
}

/* market-fullbleed: toggled on BOTH <html> and <body> by setMarketFullBleed()
   in app.js, only while #market is showing the actual embedded 3D page (not
   its loading/not-available states, which keep the normal padded layout).

   The bug this fixes: every other view's page height is simply however
   tall its content is — header is `position: sticky`, not a true layout
   partner, so it never actually subtracts itself from anything; a page
   just scrolls if its content runs long, same as any normal document. That
   breaks down for THIS view specifically, where we want the iframe to fill
   precisely "the viewport, minus however tall the header actually
   rendered" — and a previous fixed `calc(100vh - 220px)` here was only an
   approximation of the header's real height, which is what produced the
   slight (~5-10%) page overflow/scroll bug.

   The fix: make body a column flex container exactly 100vh tall, let the
   header take whatever its natural height is (flex: 0 0 auto), and let
   main consume EXACTLY whatever's left (flex: 1 1 auto) — computed live by
   the browser's own flex layout, not a guessed pixel constant. That sizing
   then cascades down through the slot div to the iframe via the same
   flex:1/min-height:0 pattern at each level (the standard fix for flex
   children that would otherwise refuse to shrink below their content's
   intrinsic size). */
html.market-fullbleed { overflow: hidden; }
body.market-fullbleed {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
body.market-fullbleed header { flex: 0 0 auto; }
body.market-fullbleed main {
  flex: 1 1 auto;
  min-height: 0;
  max-width: none;   /* override main's normal 1080px cap */
  margin: 0;
  padding: 0;         /* override main's normal padding — no left/right gap either */
  display: flex;
  flex-direction: column;
}
body.market-fullbleed #market-frame-slot { flex: 1 1 auto; min-height: 0; display: flex; }
body.market-fullbleed #market-frame {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  height: 100%;
  border: none;
  display: block;
}

.placeholder {
  padding: 60px; text-align: center;
  color: var(--ink-soft); font-size: 19px;
}
.empty { color: var(--ink-soft); font-size: 18px; padding: 40px 0; }

@media (max-width: 640px) {
  header { padding: 16px 20px; }
  main { padding: 26px 20px 60px; }
  nav { gap: 16px; }
  .chart-nav.prev { left: 0; } .chart-nav.next { right: 0; }
  nav#main-nav.search-active .symbol-search { width: min(var(--search-expanded-width, 320px), calc(100vw - 220px)); }

  /* Hamburger menu: once there's no longer room for all 6 header buttons
     side by side, they collapse behind one toggle and expand downward,
     full page width, as a vertical list instead. header is already
     `position: sticky` (itself a positioned element), so .nav-menu's
     `position: absolute` below resolves against its box with no extra
     positioning needed on nav — and left:0/right:0 against that same box
     is what makes it span the full page width rather than just the nav
     cluster's own width.

     THE HEADER/CONTENT SEPARATOR LINE ITSELF MOVES, rather than there
     being two separate borders: header's own border-bottom is switched off
     for this breakpoint, and .nav-menu's border-bottom takes over that job
     full-time. A height:0 box with a border-bottom still renders that
     1px line (perfectly normal, reliable CSS — width/color aren't affected
     by how tall the box is) sitting right where header's old border used
     to be, so closed and open look continuous, just two positions of the
     SAME line. Animating .nav-menu's max-height is then enough on its own
     to make that line slide down to reveal the menu, or back up to hide
     it again — no separate border animation needed.

     OPENING is two beats, both driven by nothing but the single
     `menu-open` class toggle in app.js:
       1. .nav-menu's own max-height grows from 0 — the border slides down
          revealing empty space first; the buttons are already sitting
          inside it at opacity:0, so nothing shows yet.
       2. Only once that growth finishes does each button fade in, top to
          bottom, 0.07s after the previous one — their transition-delay
          values below start at 0.22s, exactly when the box's own 0.22s
          max-height transition ends.
     CLOSING skips the stagger entirely (see the un-delayed base rule just
     below) so dismissing the menu feels instant rather than replaying the
     same sequence backwards. This asymmetry falls out for free from how
     CSS transitions work: the duration/delay used for a property change is
     read from the style being transitioned TO, so opening reads its timing
     from the `.menu-open` rules and closing reads it from this base rule. */
  header { border-bottom: none; }
  .nav-menu-toggle { display: inline-flex; align-items: center; padding: 4px; }
  .nav-menu {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    max-height: 0;
    overflow: hidden;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding-bottom: 0;   /* only becomes 14px once open — see below. A fixed
                             padding, just like border-width, isn't shrunk by
                             max-height:0 — left at 14px here it would force
                             extra rendered height even while "closed". */
    z-index: 60;
    transition: max-height 0.22s ease, padding-bottom 0.22s ease;
  }
  nav#main-nav.menu-open .nav-menu {
    max-height: 360px;
    padding-bottom: 14px;   /* extra gap below the last button, on top of
                                that button's own 14px */
  }

  .nav-menu button {
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity 0.12s ease, transform 0.12s ease;
  }
  nav#main-nav.menu-open .nav-menu button {
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.25s ease, transform 0.25s ease;
  }
  nav#main-nav.menu-open .nav-menu button:nth-child(1) { transition-delay: 0.22s; }
  nav#main-nav.menu-open .nav-menu button:nth-child(2) { transition-delay: 0.29s; }
  nav#main-nav.menu-open .nav-menu button:nth-child(3) { transition-delay: 0.36s; }
  nav#main-nav.menu-open .nav-menu button:nth-child(4) { transition-delay: 0.43s; }
  nav#main-nav.menu-open .nav-menu button:nth-child(5) { transition-delay: 0.50s; }
  nav#main-nav.menu-open .nav-menu button:nth-child(6) { transition-delay: 0.57s; }
  nav#main-nav.menu-open .nav-menu button:nth-child(7) { transition-delay: 0.64s; }

  .nav-menu button[data-nav] { width: 100%; padding: 14px 20px; text-align: left; }
  .nav-menu .mode-toggle { margin: 10px 20px; align-self: flex-start; }

  /* Color alone marks the current page in this menu — no underline. */
  .nav-menu button.active { border-bottom: none; padding-bottom: 14px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   Additions: header cluster + Standard/Wide toggle, fixed column ratios,
   inset (mass-centred) alignment, and the wide layout mode.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Header left cluster: [logo · mode toggle] with the date beneath ──────── */
.header-left { display: flex; flex-direction: column; gap: 4px; }
.header-top  { display: flex; align-items: baseline; gap: 16px; }
.header-date {
  font-family: var(--body);
  font-size: 14px;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}

/* Standard / Wide toggle — shares the nav buttons' font/size, bordered pill,
   sits at the left of the nav group (only shown on wide enough viewports). */
.mode-toggle {
  background: none;
  border: 1px solid var(--line-soft);
  border-radius: 999px;
  padding: 4px 14px 5px;
  cursor: pointer;
  font-family: var(--body);
  font-size: 18px;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  line-height: 1;
  transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}
.mode-toggle:hover { color: var(--ink); border-color: var(--ink); }
.mode-toggle[hidden] { display: none !important; }

/* ── Symbol search (header, rightmost) ─────────────────────────────────────
   Idle: just the magnifying-glass icon, to save space. Hovering does
   nothing — clicking it focuses the input directly (native click-to-focus,
   since the icon has pointer-events:none and the input fills the same box
   underneath it), which expands the box and fades the icon from gray to
   black; clicking anywhere else blurs the input, which fades it back and
   collapses the box. app.js drives this single focus/blur-derived state by
   toggling nav#main-nav.search-active, and also measures the placeholder
   text's exact rendered width once (after fonts load) into
   --search-expanded-width, so the expanded box is exactly as wide as
   "What company are you looking for?" — no more, no less. */
.symbol-search {
  position: relative;
  display: flex;
  align-items: center;
  width: 28px;
  height: 28px;
  overflow: hidden;
  transition: width 0.35s ease;
}
.symbol-search-icon {
  position: absolute;
  left: 6px;
  top: 50%;
  width: 16px;
  height: 16px;
  transform: translateY(-50%);
  pointer-events: none;   /* clicks pass through to the input beneath */
}
.symbol-search-icon-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transition: opacity 0.21s ease;
}
.symbol-search-icon-idle   { opacity: 1; }
.symbol-search-icon-active { opacity: 0; }
.symbol-search input {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0 0 0 28px;
  border: none;
  background: none;
  outline: none;
  opacity: 0;
  cursor: pointer;
  font-family: var(--body);
  font-size: 16px;
  letter-spacing: 0.01em;
  color: var(--ink);
  transition: opacity 0.21s ease;
}
.symbol-search input::placeholder { color: var(--ink-soft); }

nav#main-nav.search-active .nav-menu,
nav#main-nav.search-active .nav-menu-toggle {
  display: none;
}
nav#main-nav.search-active .symbol-search {
  width: min(var(--search-expanded-width, 320px), calc(100vw - 320px));
}
nav#main-nav.search-active .symbol-search-icon-idle   { opacity: 0; }
nav#main-nav.search-active .symbol-search-icon-active { opacity: 1; }
nav#main-nav.search-active .symbol-search input { opacity: 1; cursor: text; }

/* ── Wide mode: drop the max-width wall ───────────────────────────────────────
   100% (not `none`) is deliberate: `none` isn't a length, so it can't be
   transitioned — 100% resolves to the same fully-unconstrained width here
   (main is a block element with width:auto, which already fills its
   container), but as an actual length it animates smoothly. */
body.wide main { max-width: 100%; }

@media (prefers-reduced-motion: reduce) {
  main, .symbol-search, .symbol-search-icon-img, .symbol-search input, tbody td { transition: none; }
}

/* ── Symbol page (#symbols/<SYM>) ─────────────────────────────────────────── */
.symbol-head { margin-bottom: 8px; }

/* flex-wrap + margin-left:auto on the ticker is what gives the "snap to the
   right of the SAME line when there's room, otherwise drop to its own line
   and stay right-aligned" behavior — no JS width-measuring needed: the
   ticker is simply the only thing after the auto margin, so whichever line
   it ends up on, it consumes all of that line's leftover space itself. */
.symbol-head-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 16px;
}
.symbol-head-row .view-title { font-size: 34px; margin: 0; flex: 1 1 auto; min-width: 0; }
.symbol-head-ticker {
  flex: 0 0 auto;
  margin-left: auto;
  font-size: 34px;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--ink-soft);
}
.symbol-subtitle { color: var(--ink-soft); font-size: 16px; margin: 6px 0 30px; }

.symbol-section { border-bottom: 1px solid var(--line-soft); padding: 22px 0; }
.symbol-section:first-of-type { border-top: 1px solid var(--line-soft); }
.symbol-section-toggle {
  display: flex;
  align-items: center;
  gap: 10px;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  width: 100%;
  text-align: left;
  cursor: pointer;
  font-family: var(--body);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.03em;
  color: var(--ink);
  transition: color 0.15s ease, transform 0.08s ease, opacity 0.08s ease;
}
.symbol-section-toggle:hover { color: #000; }
.symbol-section-toggle:active { transform: translateY(2px); opacity: 0.7; }
.symbol-section-body { margin-top: 16px; }
.symbol-section.collapsed .symbol-section-body { display: none; }
.empty-note { color: var(--ink-soft); font-size: 17px; margin: 0; }

#symbol-chart-frame {
  width: 100%;
  height: 480px;
  min-height: 380px;
  border: none;
  background: none;
}
.symbol-loading { display: flex; align-items: center; justify-content: center; gap: 12px; }
.loading-dots {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ink-soft);
  animation: symbol-pulse 1.1s ease-in-out infinite;
}
@keyframes symbol-pulse {
  0%, 100% { opacity: 0.25; transform: scale(0.85); }
  50%      { opacity: 1;    transform: scale(1); }
}

/* Rendered research markdown (Overview/News/Fundamentals/Financials/
   Valuations) — basic readable typography matching the site's serif theme.
   Output comes from marked + DOMPurify (see renderMarkdown() in app.js); this
   only styles plain markdown elements, nothing fancy. */
.markdown-body { font-size: 17px; line-height: 1.65; color: var(--ink); }
.markdown-body :first-child { margin-top: 0; }
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
  font-family: var(--body); font-weight: 600; line-height: 1.3;
  margin: 1.3em 0 0.5em;
}
.markdown-body h1 { font-size: 24px; }
.markdown-body h2 { font-size: 20px; }
.markdown-body h3 { font-size: 18px; }
.markdown-body p, .markdown-body ul, .markdown-body ol, .markdown-body blockquote {
  margin: 0 0 1em;
}
.markdown-body ul, .markdown-body ol { padding-left: 1.4em; }
.markdown-body li { margin-bottom: 0.35em; }
.markdown-body a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--line-soft); }
.markdown-body strong { font-weight: 600; }
.markdown-body blockquote {
  border-left: 2px solid var(--line-soft);
  padding-left: 16px;
  color: var(--ink-soft);
}
.markdown-body code {
  font-family: "Inconsolata", "SF Mono", Consolas, monospace;
  font-size: 0.9em;
  background: var(--hover);
  padding: 0.15em 0.4em;
  border-radius: 4px;
}
.markdown-body pre {
  background: var(--hover);
  padding: 14px 16px;
  border-radius: 6px;
  overflow-x: auto;
}
.markdown-body pre code { background: none; padding: 0; }
.markdown-body table { border-collapse: collapse; width: 100%; margin: 0 0 1em; }
.markdown-body th, .markdown-body td {
  border-bottom: 1px solid var(--line-soft);
  padding: 8px 12px;
  text-align: left;
}
.markdown-body hr { border: none; border-top: 1px solid var(--line-soft); margin: 1.5em 0; }

/* ── Financials section (#symbols/<SYM>) ──────────────────────────────────
   Reuses .chip (Annual/Quarterly toggle), the global table/.ta-left/.ta-right
   rules, and .empty-note (no_data state) — only what's genuinely new for
   this section lives here: per-metric spacing and the bar chart itself. */
.fin-metrics { display: flex; flex-direction: column; gap: 40px; }
.fin-metric-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 14px;
}
.fin-metric-title { font-size: 19px; font-weight: 600; margin: 0; letter-spacing: 0.02em; }
.fin-metric-toggle { display: flex; gap: 18px; flex: 0 0 auto; }

.fin-chart-scroll {
  overflow-x: auto;
  overflow-y: hidden;
}
.fin-chart {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  padding: 0 2px 8px;
  border-bottom: 1px solid var(--line-soft);
  margin-bottom: 18px;
}
.fin-bar-col {
  flex: 0 0 40px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.fin-bar-track {
  width: 100%;
  height: 140px;
  display: flex;
  align-items: flex-end;
}
.fin-bar {
  width: 100%;
  background: var(--ink);
  opacity: 0.85;
  border-radius: 2px 2px 0 0;
  transition: opacity 0.15s ease;
}
.fin-bar-col:hover .fin-bar { opacity: 1; }
.fin-bar-label {
  margin-top: 6px;
  font-size: 11px;
  color: var(--ink-soft);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.fin-table { margin-bottom: 0; }
.fin-row-extra[hidden] { display: none; }
.fin-table-expand {
  display: block;
  margin: 14px auto 0;
  background: none;
  border: none;
  padding: 0 0 2px;
  cursor: pointer;
  font-family: var(--body);
  font-size: 15px;
  color: var(--ink-soft);
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.fin-table-expand:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* ── Fixed column ratios + inset alignment ────────────────────────────────── */
table { table-layout: fixed; }

/* Column widths, by position (1=Symbol, 2=Price, 3=Indicator/Alert Type,
   4=Details, 5=Strength/Rarity — identical shape on both tables). Previously
   inline `style="width:%"` on each <col>; moved here so the mobile
   breakpoint below can actually override them — an inline style on the
   element itself would otherwise always win over a stylesheet rule. */
.lb-table     colgroup col:nth-child(1),
.lb-table     colgroup col:nth-child(2),
.lb-table     colgroup col:nth-child(3),
.lb-table     colgroup col:nth-child(5) { width: 12.5%; }
.lb-table     colgroup col:nth-child(4) { width: 50%; }

.alerts-table colgroup col:nth-child(1),
.alerts-table colgroup col:nth-child(2),
.alerts-table colgroup col:nth-child(5) { width: 12.5%; }
.alerts-table colgroup col:nth-child(3) { width: 25%; }
.alerts-table colgroup col:nth-child(4) { width: 37.5%; }

/* ── Movers header: sticky title + quicknav ──────────────────────────────
   .movers-sentinel is a zero-height marker placed immediately above the
   sticky bar — see initMoversStickyObserver() in app.js, which watches it
   with an IntersectionObserver to detect the exact moment .movers-sticky
   actually engages (as opposed to merely being present), and toggles
   .pinned accordingly. CSS `position: sticky` alone can't distinguish
   "about to stick" from "stuck", which is why the pin-state styling below
   is driven by a class rather than a plain :sticky selector. */
.movers-sentinel { height: 0; }

.movers-sticky {
  position: sticky;
  top: var(--header-h);
  z-index: 40;
  background: var(--paper);
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;   /* reserved so .pinned's real line doesn't shift layout */
  transition: border-color 0.15s ease;
}
.movers-sticky-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px 28px;
  padding-top: 0;
  padding-bottom: 0;
}
.movers-pin-title {
  font-size: 30px;
  font-weight: 600;
  letter-spacing: 0.03em;
  margin: 0;
  font-family: inherit;             /* same serif body font throughout — only size changes when pinned */
  transition: font-size 0.15s ease;
}
.movers-quicknav { display: flex; align-items: baseline; gap: 26px; }
.movers-quicknav button {
  background: none;
  border: none;
  padding: 0 0 2px;
  cursor: pointer;
  font-family: var(--body);
  font-size: 17px;
  color: var(--ink-soft);
  border-bottom: 1px solid transparent;
  transition: color 0.15s ease, border-color 0.15s ease, font-size 0.15s ease;
}
.movers-quicknav button:hover { color: var(--ink); border-bottom-color: currentColor; }

/* Pinned = the sentinel above has scrolled out of view, i.e. this bar is
   genuinely stuck under the header right now (see initMoversStickyObserver
   in app.js). Same font size as the Filter/Order/Rank chips below
   (.chip is 17px) — same body font family throughout, only the size drops. */
.movers-sticky.pinned {
  border-bottom-color: var(--line);   /* same hairline style as the header's own border-bottom */
  /* .movers-sticky's own padding-bottom:2px (see above) exists for the
     UNPINNED title-to-subtitle gap only — left in place here, it would add
     on top of the row's own pinned padding below and make the bottom 2px
     taller than the top, undoing the exact-match-with-a-table-row this is
     going for. Zeroing it JUST for .pinned keeps the unpinned spacing
     completely untouched. */
  padding-bottom: 0;
}
/* Pinned matches a table row's own font-size and top/bottom padding
   (tbody td: 18px / 13px top+bottom at this width — see styles.css's
   generic tbody td rule) rather than the .chip size — this is the resting/
   unpinned state's counterpart override below in the mobile media query,
   which matches the mobile tbody td row size (14px / 10px) instead. */
.movers-sticky.pinned .movers-sticky-row { padding: 13px 0; }
.movers-sticky.pinned .movers-pin-title { font-size: 18px; }
.movers-sticky.pinned .movers-quicknav button { font-size: 18px; }

/* ── Movers (#movers) ─────────────────────────────────────────────────────
   Three stacked table blocks. Unlike .lb-table / .alerts-table above, NO
   width is declared for any column: with `table-layout: fixed` (set on the
   bare `table` rule above) and no explicit widths, the browser splits the
   table evenly across however many <col>s are present — which is exactly
   the "all columns identical width" requirement, and it keeps holding as
   the Filter control adds and removes horizon columns.

   These tables deliberately do NOT carry the .data-table class. That class
   exists to drive the mobile rule further down that hides its 2nd and 4th
   columns (Price/Details) — positions that mean something completely
   different here, where column 4 is a percentage-change horizon. #movers
   gets its own, narrower mobile treatment instead (bottom of this block). */
.movers-block { margin-bottom: 64px; }
.movers-block:last-child { margin-bottom: 0; }
/* Offsets scrollIntoView() (see the data-mjump handler in app.js) so a
   jumped-to table's own title lands below both the real header AND the
   .movers-sticky bar, rather than underneath them. 80px covers
   .movers-sticky's own height across its pinned/unpinned states; --header-h
   handles the header itself, which is already accounted for by
   .movers-sticky's `top`, so this only needs to add the sticky bar's slice. */
.movers-block { scroll-margin-top: calc(var(--header-h) + 80px); }
.movers-title {
  font-size: 24px;
  font-weight: 600;
  letter-spacing: 0.03em;
  margin: 0 0 4px;
}
.movers-sub { margin-top: 2px; margin-bottom: 18px; }

/* Percentages are tabular so the decimal points line up column-wise, and
   sized to match .col-num rather than the larger .col-symbol. */
.col-pct { text-align: right; font-variant-numeric: tabular-nums; }
.pct-up   { color: var(--gain); }
.pct-down { color: var(--loss); }
/* Exactly 0.00% — no direction to signal, so it stays neutral. */
.pct-flat { color: var(--ink-soft); }
.pct-na   { color: var(--ink-soft); }

/* Inset so a column's content sits away from the edges: rows stay mutually
   left-aligned (same start line) but the visual mass shifts inward. */
th.ta-left,  td.ta-left  { text-align: left;  padding-left: 24px; }
th.ta-right, td.ta-right { text-align: right; padding-right: 24px; }

/* Headers are centered for both tables; body cells keep their column alignment. */
thead th.ta-left, thead th.ta-right {
  text-align: center;
  padding-left: 14px;
  padding-right: 14px;
}

/* Short cells never wrap (ellipsis if ever needed); details may wrap. */
.cell-clip { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* #industries' Industry column: SIC descriptions are free text (sometimes
   40+ chars) and don't have a natural short form the way a ticker does, so
   instead of ellipsis-truncating them, a name too wide for its cell rolls
   into view — see initCellMarquee() in app.js, which measures overflow and
   sets --marquee-shift/--marquee-duration per cell. The cell itself never
   changes size (overflow: hidden, same as .cell-clip); only .marquee-track
   moves, so this never affects the table's own column widths. A name that
   already fits just sits still (no .marquee-active class added). */
.marquee-cell { overflow: hidden; white-space: nowrap; }
.marquee-track { display: inline-block; white-space: nowrap; }
.marquee-cell.marquee-active .marquee-track {
  animation: cellMarqueeScroll var(--marquee-duration, 8s) ease-in-out infinite;
}
@keyframes cellMarqueeScroll {
  0%, 12%  { transform: translateX(0); }
  50%, 62% { transform: translateX(var(--marquee-shift, 0)); }
  100%     { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .marquee-cell.marquee-active .marquee-track { animation: none; }
}

/* #industries/<code>'s Industry Overview — one stat per horizon, reusing
   sic_performance.json's own averaged metrics (see run_movers_ranking.py
   and renderIndustryDetail() in app.js). Wraps naturally on narrow screens
   since it's a flex row, not a table. */
.industry-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 28px 40px;
  margin: 4px 0 30px;
}
.industry-stat {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 22px;
  font-variant-numeric: tabular-nums;
}
.industry-stat-label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.col-ind { font-size: 16px; }
.col-details {
  font-size: 16px;
  color: var(--ink);
  white-space: normal;
  word-break: break-word;
}

@media (max-width: 640px) {
  th.ta-left,  td.ta-left  { padding-left: 14px; }
  th.ta-right, td.ta-right { padding-right: 14px; }

  /* Not enough width for Price or Details either — drop both entirely and
     split the remaining three columns evenly. Both the <col> AND the
     matching th/td need hiding: table-layout:fixed sizes columns from the
     <col> declarations regardless of cell content, so hiding only the
     cells would leave an empty reserved gap where Price/Details used to
     be. Column position (2nd = Price, 4th = Details) is identical on both
     tables, so .data-table covers both with one rule. */
  .data-table colgroup col:nth-child(2),
  .data-table colgroup col:nth-child(4) { display: none; }
  .data-table colgroup col:nth-child(1),
  .data-table colgroup col:nth-child(3),
  .data-table colgroup col:nth-child(5) { width: 33.34%; }
  .data-table th:nth-child(2), .data-table td:nth-child(2),
  .data-table th:nth-child(4), .data-table td:nth-child(4) { display: none; }

  /* #movers keeps every horizon column the user asked for (that's the whole
     point of its Filter) and drops Latest Price instead — the one column
     that's supplementary rather than the subject of the table. Column 2 is
     always Latest Price regardless of how many horizons survive the filter,
     so this single position rule is stable. Both the <col> and the cells
     must go, or table-layout:fixed keeps reserving the empty track.
     Scoped to .has-price (see moverTableHTML) — #industries has no Latest
     Price column at all, so its column 2 is really the first horizon and
     must NOT be hidden here. */
  .movers-table.has-price colgroup col:nth-child(2) { display: none; }
  .movers-table.has-price th:nth-child(2), .movers-table.has-price td:nth-child(2) { display: none; }

  /* Up to five horizon columns still have to fit, so shrink the type and
     insets well past the other tables' mobile treatment. */
  .movers-table th.ta-left,  .movers-table td.ta-left  { padding-left: 8px; }
  .movers-table th.ta-right, .movers-table td.ta-right { padding-right: 8px; }
  .movers-table thead th { font-size: 11px; letter-spacing: 0.06em; padding: 0 5px 8px; }
  .movers-table tbody td { font-size: 14px; padding: 10px 5px; }
  .movers-table .col-symbol { font-size: 15px; letter-spacing: 0.03em; }
  .movers-block { margin-bottom: 44px; }

  /* Sticky bar: unlike the pinned state (which intentionally matches a
     table row's own font-size and top/bottom padding at this width — see
     .movers-table tbody td just above, and the un-media-queried .pinned
     rules for the desktop equivalent), the RESTING title stays 30px at
     every width, same as #leaderboards' .view-title — no mobile-only
     override here. Only the quicknav scrolls sideways instead of wrapping,
     same pattern as .control-options above. */
  .movers-sticky-row { flex-wrap: nowrap; gap: 14px; }
  .movers-sticky.pinned .movers-sticky-row { padding: 10px 0; }
  .movers-sticky.pinned .movers-pin-title { font-size: 14px; }
  .movers-sticky.pinned .movers-quicknav button { font-size: 14px; }
  .movers-quicknav {
    overflow-x: auto;
    flex-shrink: 1;
    min-width: 0;
    gap: 18px;
    -webkit-overflow-scrolling: touch;
  }
  .movers-quicknav button { flex: 0 0 auto; }

  .controls-stacked .control-label { flex-basis: 52px; width: 52px; font-size: 12px; }
}