/* Dark theme matching the desktop (PySide6) app's palette, so the web and
   desktop UIs feel like the same product. */

/* -- responsive breakpoint reference (2026-09-20) ------------------------
   One tier boundary is used everywhere in this file: 1024px.

   Below 1024px (phone AND tablet-portrait): compact tier - results
   table stacks above its chart instead of sitting beside it, the
   toolbar wraps onto multiple lines instead of forcing page-wide
   horizontal overflow, and table font/padding shrink a notch.
   At/above 1024px: desktop tier - unchanged, full side-by-side layout.

   1024px was picked from three real anchor widths, not a round number:
     - iPhone 16 (phone anchor):        393 CSS px portrait
     - iPad 10th-gen (tablet anchor):   820 CSS px portrait / 1180 landscape
     - Common desktop monitor:          1920 CSS px (1920x1080, the single
       largest share of desktop screen resolutions worldwide)
   iPad portrait (820px) is meaningfully wider than a phone but still too
   narrow for the side-by-side split to breathe (a 46/54 split of 820px
   leaves ~370px for the chart, cramped once indicator panels are in
   play) - by explicit decision (2026-09-20), it gets the same compact/
   stacked treatment as phones rather than a dedicated third tier, so the
   breakpoint sits at 1024px: above iPad portrait's 820px, at/below iPad
   landscape's 1180px and the 1920px desktop anchor, both of which get
   the untouched desktop layout. A distinct tablet-specific layout (e.g.
   2-up indicator panels) was considered and deferred as a follow-up
   rather than folded into this pass. */

/* -- global responsive type scale (2026-09-20) ---------------------------
   Explicit decision, replacing ~48 uncoordinated font-size values spread
   across every tab/component in this file (buttons, table, modal,
   watchlist, backtest, portfolio, money-flow, strategy picker, admin
   panel...): exactly TWO sizes at a time - the app's <h1> title, and
   everything else - each tier just a fixed pair, not a proportional
   scale. Scoped to .app-shell (index.html's <body> - see its own comment)
   so login.html/admin.html's separately-designed typography is untouched
   even though they share this stylesheet.

     Phone   (<600px):        title 16px / everything else 11px
     Tablet  (600px-1023px):  title 20px / everything else 14px
     Desktop (>=1024px):      title 18px / everything else 13px

   Desktop's pair is a recommendation, not a measured requirement like
   the phone/tablet pair (those were specified directly, 2026-09-20):
   18px keeps the title clearly the largest thing in the toolbar without
   dwarfing it at 1920px, and 13px sits in the middle of what nearly
   every component already used individually (12-13px labels/buttons,
   12.5px table) so the unification is a small nudge, not a redesign.

   !important is deliberate here, not a shortcut: with ~48 pre-existing
   rules of mixed specificity (#ids, .classes, bare elements) all setting
   their own font-size, matching every one individually is exactly the
   source-order/specificity trap this file already got bitten by once
   (see the .table-wrap/.chart-wrap note further down) - !important makes
   this reset unconditionally win regardless of any other rule's
   specificity or position, which is the actual intent: one deliberate
   override layer, not another rule quietly competing in the cascade.
   The trade-off: any future component-specific font-size in .app-shell
   is dead on arrival unless it's also flagged !important (or the
   element is genuinely meant to be sized independently of this scale,
   in which case exclude it here the same way h1 is excluded below).
   Rules made fully redundant by this reset (the old .split table and
   Watchlist-tab font-size overrides) were removed rather than left as
   confusing dead weight; their padding/layout-only rules stayed. */
.app-shell :not(h1) { font-size: 13px !important; }
.app-shell h1 { font-size: 18px !important; }

@media (max-width: 1023px) {
  .app-shell :not(h1) { font-size: 14px !important; }
  .app-shell h1 { font-size: 20px !important; }
}
@media (max-width: 599px) {
  .app-shell :not(h1) { font-size: 11px !important; }
  .app-shell h1 { font-size: 16px !important; }
}

html, body {
  height: 100%;
  margin: 0;
}

body {
  display: flex;
  flex-direction: column;
  background-color: #121417;
  color: #e6e6e6;
  font-family: "Segoe UI", -apple-system, Arial, sans-serif;
  font-size: 14px;
}

button {
  background-color: #23262c;
  color: #e6e6e6;
  border: 1px solid #3a3f47;
  padding: 6px 14px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 13px;
}
button:hover { background-color: #2f333b; }
button:active { background-color: #1b1e23; }

/* Primary/accent button - reserved for the one action-triggering button per
   tab (currently just "Run Backtest"), so it reads as the obvious next step
   rather than blending in with the default gray buttons. Same blue as
   .progress-bar-fill/.ws-status.connected-adjacent accents elsewhere, for
   visual consistency. */
.btn-primary {
  background-color: #2f6fed;
  color: #ffffff;
  border: 1px solid #4a83f0;
  font-weight: 600;
}
.btn-primary:hover { background-color: #3f7bf5; }
.btn-primary:active { background-color: #2559c4; }
.btn-primary:disabled {
  background-color: #2a3550;
  border-color: #38404f;
  color: #8891a3;
  cursor: default;
}

/* -- toolbar ------------------------------------------------------------ */

.toolbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  background-color: #1b1e23;
  border-bottom: 1px solid #2b2f36;
}
.toolbar h1 {
  font-size: 15px;
  font-weight: 600;
  margin: 0 8px 0 0;
  white-space: nowrap;
}
#universeLabel {
  color: #b7bcc4;
  font-size: 12px;
  white-space: nowrap;
}
.industry-progress {
  color: #b7bcc4;
  font-size: 11px;
  white-space: nowrap;
}
#marketFilterLabel {
  color: #b7bcc4;
  font-size: 12px;
  white-space: nowrap;
}
#marketFilterSelector {
  background-color: #23262c;
  color: #e6e6e6;
  border: 1px solid #3a3f47;
  padding: 5px 8px;
  border-radius: 4px;
  font-size: 13px;
}
/* "View Symbol" toolbar search box (by explicit user request) - a bare
   <input type="text"> doesn't inherit background/border from the dark
   theme the way <button> already does (browsers only auto-inherit color/
   font onto form controls, not background/border), so it needs its own
   dedicated rule - copied from #marketFilterSelector's own dark styling
   for visual consistency with the rest of the toolbar. */
#viewSymbolInput {
  background-color: #23262c;
  color: #e6e6e6;
  border: 1px solid #3a3f47;
  padding: 5px 8px;
  border-radius: 4px;
  font-size: 13px;
  width: 140px;
}
.toolbar-right {
  margin-left: auto;
}
.ws-status {
  font-size: 12px;
  color: #b7bcc4;
  white-space: nowrap;
}
.ws-status.connected { color: #4caf7d; }
.ws-status.disconnected { color: #e57373; }

/* Step 7: logged-in-user label + sign-out link, next to .ws-status in the
   main toolbar. */
.current-user-label {
  font-size: 12px;
  color: #b7bcc4;
  white-space: nowrap;
}
.logout-link {
  font-size: 12px;
  color: #7aa2f7;
  white-space: nowrap;
  text-decoration: none;
}
.logout-link:hover { text-decoration: underline; }


/* -- tabs ----------------------------------------------------------------*/

.tabs {
  flex: 0 0 auto;
  display: flex;
  background-color: #1b1e23;
  border-bottom: 1px solid #2b2f36;
}
.tab-btn {
  background: #1b1e23;
  color: #b7bcc4;
  border: 1px solid #2b2f36;
  border-bottom: none;
  border-radius: 0;
  padding: 8px 18px;
}
.tab-btn.active {
  background: #23262c;
  color: #ffffff;
}

/* -- main / tab panels ---------------------------------------------------*/

main {
  flex: 1 1 auto;
  min-height: 0;
}

.tab-panel {
  display: none;
  flex-direction: column;
  height: 100%;
  box-sizing: border-box;
  padding: 6px 10px;
}
.tab-panel.active { display: flex; }

/* Money Flow specifically needs its own vertical scroll: its content
   (heatmap + RRG chart + full leaderboard table) can run taller than the
   viewport, especially now that the RRG chart is 2x its original height
   (2026-09-08). .tab-panel itself has no overflow rule and is capped to
   height:100% of #panels, so without this, taller content here would
   silently overflow past the panel's own box (painting over whatever
   sits below it in the page, like .status-bar) instead of scrolling.
   Scoped to just this one panel via its data-tf-panel attribute, rather
   than added to the shared .tab-panel rule above, so tabs with fixed-size
   chart layouts (2-Day/1-Week/etc.) are untouched. */
.tab-panel[data-tf-panel="__money_flow__"] {
  overflow-y: auto;
}

/* Money Flow tab content used to stretch edge-to-edge on wide monitors.
   The tab's whole innerHTML is now wrapped in a single <div
   class="money-flow-wrap"> (app.js), and this rule caps + centers that
   wrapper, matching the narrower centered feel of the original standalone
   prototype. margin:0 auto centers it as a flex item of .tab-panel
   (auto margins absorb the extra cross-axis space in a column flex
   container, so no align-self override is needed). */
.money-flow-wrap {
  width: 100%;
  max-width: 1320px;
  margin: 0 auto;
}

.progress-row { font-size: 12px; color: #e6e6e6; margin-bottom: 2px; }

.progress-bar-track {
  height: 14px;
  background-color: #1b1e23;
  border: 1px solid #2b2f36;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 4px;
}
.progress-bar-fill {
  height: 100%;
  width: 0%;
  background-color: #2f6fed;
  transition: width 0.2s ease;
}

.hint {
  color: #6d7480;
  font-size: 11px;
  margin: 0 0 6px 0;
}

/* Backtest results table win-rate call-outs - a win rate at or below 55%
   isn't a real edge (the threshold is the user's own call, not just "beats
   a coin flip" - originally 60%, revised down to 55% by explicit user
   decision), so those cells fade into the background while anything
   genuinely above 55% is bolded white to jump out immediately. */
.backtest-edge {
  color: #ffffff;
  font-weight: 700;
}
.backtest-no-edge {
  color: #8891a3;
  font-weight: 400;
}

/* Market results sub-tabs (Backtest tab) - one per market ("US"/"Hong
   Kong"), each with its own results table - see app.js's buildLayout()/
   showMarketResultsTab(). Nested one level deeper than the top-level
   .tab-btn/.tab-panel pair above, so given its own class names rather than
   reusing those directly (avoids any risk of showMarketResultsTab()
   colliding with showTab()'s own top-level tab-switching logic), but
   styled to look like a natural continuation of the same tab convention. */
.market-tab-bar {
  display: flex;
  border-bottom: 1px solid #2b2f36;
  margin-bottom: 6px;
}
.market-tab-btn {
  background: #1b1e23;
  color: #b7bcc4;
  border: 1px solid #2b2f36;
  border-bottom: none;
  border-radius: 0;
  padding: 5px 14px;
  font-size: 12px;
}
.market-tab-btn.active {
  background: #23262c;
  color: #ffffff;
}
.market-tab-panel {
  display: none;
}
.market-tab-panel.active {
  display: block;
}

/* Fundamentals tab - a model cell that couldn't compute (see
   fundamentals_valuation.ModelResult's own docstring) - grey text, matching
   ui_app.py's FundamentalsTab._model_item() foreground color (#8891a3).
   The specific reason is available as a native title/tooltip attribute
   (see fundamentalsModelTd() in app.js), not shown inline. */
td.fundamentals-na {
  color: #8891a3;
}

/* Backtest results table wrapper - deliberately its OWN class, not a reuse
   of .table-wrap above, by explicit user request ("just display in full,
   no need for a scroll bar on the right"). .table-wrap's flex: 0 0 46% +
   overflow: auto exist for the live results tables' split view (a table
   sharing height with a chart panel next to it) - reusing it here forced
   the Backtest table into a short, independently-scrolling box even though
   there's no chart panel beside it to share space with. This class carries
   the same border/background styling for visual consistency, but no height
   cap and no overflow: auto - the table renders at its full natural height,
   and the page scrolls normally (via the browser's own document-level
   scrollbar) if that's taller than the viewport. */
.backtest-table-panel {
  border: 1px solid #2b2f36;
  background-color: #1b1e23;
}

/* -- watchlist tab (2026-09-20 UI revision) ------------------------------
   Combined "latest signal per symbol" table + the per-symbol Recent
   History table below it, both reuse .backtest-table-panel's border/
   background (natural height, no forced scroll box - same reasoning as
   that class's own comment) plus overflow-x:auto here so the 5-column
   table doesn't force the whole page wider on a narrow/mobile viewport;
   it scrolls sideways within its own box instead. */
.watchlist-table-panel {
  overflow-x: auto;
}
.watchlist-symbol-cell {
  cursor: pointer;
  color: #6fa8ff;
}
.watchlist-symbol-cell:hover {
  text-decoration: underline;
}
tr.watchlist-row.selected {
  background-color: #23262c;
}
#watchlistAddInput {
  flex: 0 1 160px;
  min-width: 100px;
}

/* Responsive: below 1024px (phones and tablet-portrait, see the shared
   breakpoint reference at the top of this file) the Watchlist tab's add
   row stacks instead of clipping, and its table cells get tighter padding
   so more of each row fits without needing to scroll sideways too (font
   size itself is handled by the global type scale at the top of this
   file, not here). Scoped to the Watchlist panel only, by explicit user
   request - the rest of the app's layout (toolbar, other tabs) is
   unchanged. */
@media (max-width: 1023px) {
  .tab-panel[data-tf-panel="__watchlist__"] .watchlist-add-row {
    align-items: stretch;
  }
  .tab-panel[data-tf-panel="__watchlist__"] #watchlistAddInput {
    flex: 1 1 auto;
    width: 100%;
    box-sizing: border-box;
  }
  .tab-panel[data-tf-panel="__watchlist__"] #watchlistAddBtn {
    width: 100%;
  }
  .tab-panel[data-tf-panel="__watchlist__"] thead th,
  .tab-panel[data-tf-panel="__watchlist__"] td {
    padding: 4px;
  }
}


/* Portfolio tab - EXE alert flag (see scan_engine.ScanEngine.
   check_portfolio_alerts()'s own docstring for what triggers this: a raw
   bearish EXE bar on a held stock's most recent 2-Day bar). A CSS animation,
   not a JS setInterval, so the flashing keeps running even if the tab is
   backgrounded/inactive - genuinely eye-catching, matching ui_app.py's own
   QTimer-driven blink for the desktop Portfolio tab. */
@keyframes portfolio-alert-blink {
  0%, 49% { background-color: #ff3b30; color: #ffffff; }
  50%, 100% { background-color: #5c1a17; color: #f2c4c1; }
}
td.portfolio-alert-cell {
  animation: portfolio-alert-blink 1s step-end infinite;
  font-weight: 700;
  text-align: center;
}
button.portfolio-ack-btn {
  padding: 3px 10px;
  font-size: 11px;
}

/* Strategy picker (Backtest tab) - a native <details>/<summary> disclosure,
   one checkbox per active strategy, used to scope a backtest run to a
   subset of strategies. See app.js's buildLayout()/selectedBacktestStrategyKeys(). */
.strategy-picker {
  position: relative;
}
.strategy-picker > summary {
  list-style: none;
  cursor: pointer;
  background-color: #23262c;
  color: #e6e6e6;
  border: 1px solid #3a3f47;
  padding: 6px 14px;
  border-radius: 4px;
  font-size: 13px;
}
.strategy-picker > summary:hover { background-color: #2f333b; }
.strategy-picker > summary::-webkit-details-marker { display: none; }
.strategy-picker-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  background-color: #1b1e23;
  border: 1px solid #2b2f36;
  border-radius: 6px;
  padding: 8px 10px;
  min-width: 220px;
  max-height: 320px;
  overflow-y: auto;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
}
.strategy-picker-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 2px;
  font-size: 12px;
  white-space: nowrap;
}
.strategy-picker-actions {
  display: flex;
  gap: 8px;
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid #2b2f36;
}
.strategy-picker-actions button {
  padding: 3px 8px;
  font-size: 11px;
}

.split {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  gap: 8px;
}

/* -- results table ---------------------------------------------------- */

.table-wrap {
  flex: 0 0 46%;
  overflow: auto;
  border: 1px solid #2b2f36;
  background-color: #1b1e23;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
thead th {
  position: sticky;
  top: 0;
  background-color: #23262c;
  color: #b7bcc4;
  padding: 6px;
  border: 1px solid #2b2f36;
  z-index: 1;
}
/* Clickable-header sorting (Symbol/Name/Market/Signal/Last/Industry/Market
   Cap - see state.sort's docstring in app.js). The only non-sortable
   header is Bar Date, left as plain thead th above, no hover/cursor
   change. Bid/Ask/Volume columns were removed entirely by explicit user
   request; Market was added in Volume's place, and Name was later
   inserted between Symbol and Market (also by explicit user request).
   Market Cap was added later still, sourced opportunistically from IBKR's
   reqFundamentalData and also sortable (numeric) from the start. */
thead th.sortable {
  cursor: pointer;
  user-select: none;
}
thead th.sortable:hover { background-color: #2b2f36; }
.sort-indicator {
  color: #5bc0de;
  font-size: 10px;
}
tbody td {
  padding: 5px 6px;
  border: 1px solid #2b2f36;
  text-align: center;
  white-space: nowrap;
}
tbody td.left { text-align: left; }
tbody tr:nth-child(even) { background-color: #202329; }
tbody tr:hover { background-color: #262a31; cursor: pointer; }
/* Backtest-informed PER-STRATEGY coloring for the live per-timeframe
   results tables (2-Day/1-Week/1-Month) - "only bold the strategy that
   meets the win-rate criteria for that timeframe" (explicit user request,
   2026-09-04, replacing an earlier whole-row edge-row/no-edge-row design:
   a row's Signal cell can list 2+ matched strategy names, and bolding the
   WHOLE row just because ONE of them cleared the bar wrongly implied the
   others did too). Each strategy name in the Signal cell is its own
   <span class="match-label" data-strategy-key=... data-market=...> (see
   renderRow() in app.js). .match-edge (white+bold) when THAT span's own
   strategy_key cleared BACKTEST_EDGE_WIN_RATE at any horizon for THIS
   timeframe AND THIS row's own market (state.testedStrategies/
   effectiveStrategies are market-scoped too, since the same date - US and
   HK results are no longer pooled together, matching the Backtest tab's
   own per-market sub-tabs); .match-no-edge (light grey) when tested but
   never cleared it. A span with neither class (never backtested for this
   timeframe/market) stays the plain default text color - unproven-but-
   untested shouldn't look the same as proven-and-failed. See app.js's
   applyRowEffectiveness(). */
td.signal-cell span.match-edge { color: #ffffff; font-weight: 700; }
td.signal-cell span.match-no-edge { color: #8891a3; font-weight: 400; }
/* Row selection (click to view chart) always wins over the backtest-derived
   per-strategy coloring above. Unlike the old whole-row design, a plain
   `tr.selected td { color: ... }` rule alone isn't enough here: a span's
   OWN explicit color (set by the two rules above) beats an inherited value
   from its parent td regardless of the td rule's specificity, so the
   spans need their own explicit override too. */
tbody tr.selected td { color: #ffffff; }
tbody tr.selected td.signal-cell span.match-edge,
tbody tr.selected td.signal-cell span.match-no-edge { color: #ffffff; }
tbody tr.selected { background-color: #2f6fed; color: #ffffff; }
/* "Last" backfilled from the latest stored bar rather than a live IBKR
   snapshot (ScanMatch.quote_is_live == false) - italic so it's visually
   distinct from a real-time quote, independent of whatever color the row's
   backtest-edge status above already set. See app.js's renderRow() (the
   Volume column this comment originally also covered was later removed
   entirely, replaced by a sortable Market column). */
td.stale-quote {
  font-style: italic;
}

/* -- chart panel ---------------------------------------------------------*/

.chart-wrap {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-width: 0;
  /* NEW: contained vertical scroll - .chart-container below is now a fixed
     height (not flex:1 1 auto) and the 6 indicator panels further below
     add a further ~1420px on top of that, so the combined content height
     routinely exceeds this pane's own bounded height (inherited from
     .split above). Scoping the scrollbar to just this pane (rather than
     the whole page) keeps the results table/toolbar always visible
     alongside it - by explicit user request ("If we need to scroll down
     the screen to see the results of the 6 indicators, so be it"). */
  overflow-y: auto;
}
.chart-title-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 1px 2px 4px 2px;
}
.chart-title {
  font-size: 12px;
  font-weight: 600;
  color: #e6e6e6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1 1 auto;
  min-width: 0;
}
.chart-legend {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 10px;
  color: #9fa6b2;
  white-space: nowrap;
}
.legend-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-left: 6px;
}
.legend-dot:first-child { margin-left: 0; }
.legend-dot.sma20 { background-color: #f2c14e; }
.legend-dot.sma50 { background-color: #5bc0de; }
.legend-dot.sma200 { background-color: #c77dff; }
.legend-dot.swinglow { background-color: #4ade80; }
.legend-dot.pastsignal { background-color: #ffff00; }
.chart-container {
  /* CHANGED from flex: 1 1 auto - a fixed height, by explicit user request
     ("Keep the chart size the same size, and add the indicators below").
     Previously this stretched to fill 100% of .chart-wrap's own bounded
     height; now .chart-wrap holds MORE than just this one element (the 6
     indicator panels below it too), so leaving this flexible would let it
     get squeezed smaller every time content is added beneath it - a fixed
     500px keeps it stable and predictable regardless of what's below.
     flex-shrink: 0 (via the 0 in "0 0 500px") is what actually forces
     .chart-wrap's total content past its own bounded height, which is
     what makes the overflow-y: auto scrollbar above actually engage. */
  flex: 0 0 500px;
  min-height: 0;
  /* NEW: anchors .signal-marker-canvas below, which is absolutely
     positioned to fill this same box - "the past signals" chart-marker
     overlay (2026-08-22, see web/static/app.js's renderSignalMarkers())
     is drawn on a plain <canvas> stacked on top of lightweight-charts'
     own DOM, not through its series.setMarkers() API - see that
     function's own docstring for why. */
  position: relative;
}

/* "Past signals" arrow-marker overlay canvas - see .chart-container's own
   comment above and app.js's renderSignalMarkers()/drawSignalMarkers().
   pointer-events: none is required, not decorative - without it this
   transparent, full-covering canvas would sit in front of the chart and
   swallow every mouse event (pan/zoom drag, crosshair hover) meant for
   the lightweight-charts instance underneath it.
   z-index: 5 added 2026-08-22 while chasing the "markers compute fine but
   never appear on screen" bug - console diagnostics confirmed the canvas
   is correctly sized and the draw calls run with valid coordinates every
   time, which pointed away from the JS entirely and at the DOM/paint
   layer instead. Relying on "later in the DOM = painted on top" was the
   bug: this canvas is a SIBLING of whatever wrapper lightweight-charts
   itself builds inside .chart-container (every other overlay in this app -
   SMA lines, swing lines - is drawn INSIDE the chart's own canvas via its
   own series API, so this sibling-canvas arrangement was never exercised
   before this feature). If that internal wrapper establishes its own
   stacking context (a real possibility for a canvas library that layers
   multiple internal canvases - background/series/crosshair - for
   performance), plain DOM order no longer decides paint order and this
   canvas can end up silently buried underneath it despite being appended
   after createChart(). An explicit z-index removes the ambiguity outright
   rather than depending on how lightweight-charts happens to be
   implemented internally. */
.signal-marker-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 5;
}

/* -- indicator panels (Momentum/MACD/Ultimate Oscillator/Slow Stochastic/
   RSI/ROC) ---------------------------------------------------------------
   6 chart-panel indicators added below the main candlestick chart - a pure
   manual-analysis aid, by explicit user request, after a real data-grounded
   investigation into automated divergence-based detection using these same
   6 indicators came back with no usable edge (see web/static/app.js's own
   INDICATOR_PANEL_CONFIG comment). Sized so all 6 combined fit within a
   1440px-tall viewport once scrolled to (6 * 230px panels + 5 * 8px gaps =
   1420px, comfortably under the 1440px budget) - independent of, and NOT
   counted against, .chart-container's own 500px above, since "keep the
   chart size the same size" (the user's own words) meant the MAIN chart,
   not these new panels.

   ATR added 2026-09-01 (below Volume, above Momentum - see app.js's own
   INDICATOR_PANEL_CONFIG comment) makes it 7 panels: 7 * 230px + 6 * 8px
   gaps = 1658px, past the original 1440px budget above - the stack now
   needs a bit more scrolling than it used to, by design (flex column,
   no fixed max-height on .indicator-panels), not a bug. */
.indicator-panels {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
}
.indicator-panel {
  flex: 0 0 230px;
  display: flex;
  flex-direction: column;
  border: 1px solid #2b2f36;
  background-color: #1b1e23;
}
.indicator-panel-title {
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 600;
  color: #9fa6b2;
  padding: 3px 6px;
  border-bottom: 1px solid #2b2f36;
}
.indicator-panel-chart {
  flex: 1 1 auto;
  min-height: 0;
}

/* -- status bar ----------------------------------------------------------*/

.status-bar {
  /* Hidden outright, all breakpoints (explicit user request, 2026-09-20)
     - not just the "Universe loaded..." message appendLog() filters out
     above, the whole footer line. scanner-table.js's showTab() still
     toggles this element's inline style.display between "none" (on the
     Watchlist tab) and "" (every other tab) when switching tabs; "" just
     clears that inline override rather than forcing it visible, so this
     stylesheet rule is what actually keeps it hidden everywhere - no JS
     change needed. Left as display:none (not removed from the DOM/JS)
     so #statusBar can be brought back with a one-line revert here if
     ever wanted, without undoing the appendLog()/event-handlers.js
     cleanup done alongside it. */
  display: none;
  flex: 0 0 auto;
  padding: 4px 14px;
  background-color: #1b1e23;
  border-top: 1px solid #2b2f36;
  font-size: 12px;
  color: #b7bcc4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* -- match detail modal --------------------------------------------------*/

.modal-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  align-items: center;
  justify-content: center;
  z-index: 1000;
}
.modal-backdrop.open { display: flex; }
.modal {
  background-color: #1b1e23;
  border: 1px solid #2b2f36;
  border-radius: 6px;
  padding: 18px 22px;
  max-width: 640px;
  width: 90%;
  max-height: 80vh;
  overflow: auto;
  position: relative;
}
.modal h2 { margin: 0 0 8px 0; font-size: 18px; }
.modal p { margin: 6px 0; line-height: 1.5; font-size: 13px; }
.modal-close {
  position: absolute;
  top: 6px;
  right: 10px;
  background: none;
  border: none;
  color: #b7bcc4;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
}
.modal-close:hover { color: #ffffff; }

.detail-table {
  width: 100%;
  border-collapse: collapse;
  margin: 6px 0 14px 0;
  font-size: 12px;
}
.detail-table th, .detail-table td {
  border: 1px solid #2b2f36;
  padding: 4px 6px;
  text-align: center;
}
.detail-table th { background-color: #23262c; color: #b7bcc4; }
.pass-cell { background-color: #1f6f43; }

/* Money Flow tab - heatmap + RRG chart (see money_flow_module.md's "RRG
   signal construction" section and money_flow_engine.py's _rrg_series()).
   Sector tiles/dots drill into a sector's industries; the breadcrumb above
   the heatmap tracks that drill state. Colors reuse the leaderboard
   table's own score palette (#1a7a3c green / #a52020 red) so the whole tab
   reads as one consistent scale. */
.money-flow-breadcrumb {
  font-size: 12px;
  color: #b7bcc4;
  margin: 2px 0 8px 0;
}
.money-flow-breadcrumb a {
  color: #6ea8fe;
  text-decoration: none;
}
.money-flow-breadcrumb a:hover { text-decoration: underline; }
.money-flow-crumb-current {
  color: #ffffff;
  font-weight: 600;
}

.money-flow-heatmap {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 6px;
  margin-bottom: 10px;
}
.money-flow-tile {
  border: 1px solid #2b2f36;
  border-radius: 4px;
  padding: 8px 8px 6px 8px;
  min-height: 62px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  color: #ffffff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}
.money-flow-tile-clickable {
  cursor: pointer;
}
.money-flow-tile-clickable:hover {
  outline: 1px solid #6ea8fe;
}
.money-flow-tile-label {
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
}
.money-flow-tile-symbol {
  font-size: 10px;
  opacity: 0.85;
}
.money-flow-tile-score {
  font-size: 15px;
  font-weight: 700;
  align-self: flex-end;
}

/* Cross-Asset Capital Flow panel (GET /api/money-flow/cross-asset) - a
   4-row grid (Today / 1wk / 2wk / 4wk ago) sitting above the sector/
   industry breadcrumb+heatmap. Reuses the same green/red heat scale
   (moneyFlowHeatColor() in app.js) so the whole tab still reads as one
   consistent palette, even though this panel's tiles are peers of each
   other (Stocks/Bonds/Cash/Gold/...) rather than sector/industry rows. */
.money-flow-cross-asset {
  display: flex;
  flex-direction: column;
  border: 1px solid #2b2f36;
  border-radius: 4px;
  margin-bottom: 14px;
  /* No `overflow: hidden` here on purpose - this div is a flex ITEM
     inside .tab-panel.active's column flex layout (see styles.css's
     tab-panel rules). Per the flexbox spec, a flex item's automatic
     minimum size is content-based ONLY when overflow is `visible`;
     any other overflow value (hidden/auto/scroll) switches its auto
     min-height to 0, so when .tab-panel's children compete for a
     bounded height, THIS item was the one silently collapsing to a
     hairline while its siblings (which don't set overflow) held
     their content height. Discovered 2026-09-08 via computed-style
     inspection: offsetHeight was 2px despite a real, correctly
     populated 12.5KB innerHTML underneath. Rounded corners on the
     first/last row are lost without clipping, but a visible panel
     matters more than that cosmetic detail. */
}
.money-flow-cross-asset-row {
  display: grid;
  grid-template-columns: 120px 1fr;
  gap: 10px;
  align-items: center;
  padding: 8px 10px;
  border-bottom: 1px solid #2b2f36;
  background-color: #1b1e23;
}
.money-flow-cross-asset-row:last-child {
  border-bottom: none;
}
.money-flow-cross-asset-row-label .lbl {
  font-size: 12px;
  font-weight: 700;
  color: #e6e6e6;
}
.money-flow-cross-asset-row-label .date {
  font-size: 10px;
  color: #6d7480;
  margin-top: 1px;
}
.money-flow-cross-asset-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
  gap: 6px;
}
.money-flow-cross-asset-tile {
  border: 1px solid #2b2f36;
  border-radius: 4px;
  padding: 6px 8px 5px 8px;
  min-height: 56px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  color: #ffffff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}
.money-flow-cross-asset-tile-label {
  font-size: 11px;
  font-weight: 600;
  line-height: 1.2;
}
.money-flow-cross-asset-tile-symbol {
  font-size: 9.5px;
  opacity: 0.8;
}
.money-flow-cross-asset-tile-score {
  font-size: 15px;
  font-weight: 700;
  text-align: right;
}
.money-flow-cross-asset-delta {
  font-size: 10px;
  font-weight: 700;
  text-align: right;
  margin-top: 1px;
}
.money-flow-cross-asset-delta.up { color: #d7ffe4; }
.money-flow-cross-asset-delta.down { color: #ffe0e0; }
.money-flow-cross-asset-delta.flat { color: rgba(255, 255, 255, 0.6); }

.money-flow-rrg-wrap {
  border: 1px solid #2b2f36;
  background-color: #1b1e23;
  border-radius: 4px;
  padding: 8px 10px;
  margin-bottom: 10px;
}
#moneyFlowRrgChart {
  width: 100%;
  height: 840px; /* 2x the original 420px, per Derek's 2026-09-08 request -
     the SVG's viewBox (640x480) and preserveAspectRatio scale up to fill
     this uniformly, so the chart's dots/paths/labels all render larger,
     not just the box around them. */
  display: block;
}

/* RRG chart hover glow-up (2026-09-08): hovering a sector's dot, trail,
   or label highlights that entity's full rotation path and dims every
   other one, so a single sector's movement is easy to trace even when
   several trails overlap. The <g class="rrg-entity"> grouping and
   mouseenter/mouseleave listeners live in renderMoneyFlowRrgChart() in
   app.js; this is purely the visual side of that interaction. */
.rrg-entity {
  transition: opacity 0.15s ease;
}
.rrg-entity.rrg-dimmed {
  opacity: 0.15;
}
.rrg-entity .rrg-dot {
  cursor: pointer;
  transition: r 0.15s ease, filter 0.15s ease;
}
.rrg-entity.rrg-hovered .rrg-dot {
  r: 8;
  /* currentColor here resolves to the entity's own red/green hue, set
     via group.style.color in app.js, so the glow tints to match. */
  filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 8px currentColor) drop-shadow(0 0 14px currentColor);
}
.rrg-entity .rrg-trail {
  transition: opacity 0.15s ease, stroke-width 0.15s ease;
}
.rrg-entity.rrg-hovered .rrg-trail {
  opacity: 0.95;
  stroke-width: 3;
}
.rrg-entity.rrg-hovered .rrg-label {
  font-weight: 700;
  font-size: 12px;
}


/* -- Admin UI (Step 7 v2) -------------------------------------------------
   web/static/admin.html / js/admin.js - allowlist management + the
   operational actions (refresh/scan/backfill/add-stock) that used to live
   on the main Scanner toolbar for every logged-in user, moved here and
   restricted to role=admin. Reuses .detail-table (users table) and plain
   <button> (already styled globally) rather than inventing new component
   styles; only the page-level layout below is new. */
/* Three stacked rows (User access / Scheduled Jobs+Operations /
   Operational Issues) sized to fit one screen by default: the first two
   rows take only the height their content needs, and Operational Issues
   - the panel an admin actually monitors day to day - gets whatever's
   left (roughly the bottom third on a typical monitor) with its own
   internal scroll (.admin-table-scroll). If content ever needs more
   room than a single screen provides, the page scrolls normally rather
   than clipping anything. */
.admin-shell {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px 22px;
  box-sizing: border-box;
  /* No `overflow: hidden` here on purpose - the layout below still packs
     everything into one screen by default (Operational Issues gets an
     internal scrollbar via .admin-table-scroll first), but if the rows
     above it ever need more room than the viewport has, the page itself
     scrolls rather than silently clipping content. */
}
.admin-row {
  flex: 0 0 auto;
  max-width: 1400px;
  width: 100%;
  margin: 0 auto;
}
.admin-row-mid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}
.admin-row-bottom {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}
.admin-row-bottom .admin-panel {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.admin-table-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.admin-panel {
  background-color: #1b1e23;
  border: 1px solid #2b2f36;
  border-radius: 6px;
  padding: 12px 18px;
}
/* Status pills - Scheduled Jobs' own Succeeded/Degraded/Failed badge and
   Operational Issues' severity badge, reusing the same three-color
   convention the old main-page scheduler-status strip established
   (green=ok, amber=degraded/warning, red=failed/error) rather than
   inventing a fourth palette. */
.status-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 10px;
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}
.status-pill.status-succeeded { background-color: #1c3327; color: #4caf7d; }
.status-pill.status-degraded { background-color: #3a2f14; color: #e0a64c; }
.status-pill.status-failed { background-color: #3a1d1d; color: #e57373; }
.status-pill.status-pending { background-color: #23262c; color: #9fa6b2; }
.run-detail {
  margin: 6px 0 0 0;
  padding: 8px;
  background-color: #17191d;
  border: 1px solid #2b2f36;
  border-radius: 4px;
  font-size: 11px;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 220px;
  overflow-y: auto;
}
.admin-panel h2 {
  margin: 0 0 8px 0;
  font-size: 16px;
}
.admin-panel h3 {
  margin: 18px 0 8px 0;
  font-size: 12.5px;
  font-weight: 600;
  color: #9fa6b2;
}
.admin-inline-form {
  display: flex;
  gap: 8px;
  margin: 14px 0;
  flex-wrap: wrap;
}
.admin-inline-form input[type="email"],
.admin-inline-form input[type="text"] {
  flex: 1 1 200px;
  background-color: #23262c;
  color: #e6e6e6;
  border: 1px solid #3a3f47;
  padding: 6px 10px;
  border-radius: 4px;
  font-size: 13px;
}
.admin-inline-form select {
  background-color: #23262c;
  color: #e6e6e6;
  border: 1px solid #3a3f47;
  padding: 6px 8px;
  border-radius: 4px;
  font-size: 13px;
}
#usersTable {
  width: 100%;
}
.admin-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 4px;
}
/* Admin UI's live progress section (admin.js's ensureOpProgressRow) - one
   .op-progress-block per in-flight timeframe/backfill, built from the same
   .progress-row/.progress-bar-track/.progress-bar-fill classes the
   Scanner tab's own per-timeframe progress bars use (see layout.js), so a
   running refresh/scan/backfill looks the same way it always has, just on
   this page instead. Empty (display:none via :empty) when nothing is
   running so it doesn't leave a visible gap between the action buttons
   and the Add-stock form.
*/
.op-progress:empty {
  display: none;
}
.op-progress {
  margin: 10px 0 4px 0;
}
.op-progress-block {
  margin-bottom: 8px;
}
.op-progress-block:last-child {
  margin-bottom: 0;
}
.activity-log {
  /* Shares row 2 with the Scheduled Jobs panel (see .admin-row-mid) - kept
     shorter than its old standalone-page height so that row doesn't grow
     taller than Scheduled Jobs' own table needs to be. */
  height: 130px;
  overflow-y: auto;
  border: 1px solid #2b2f36;
  background-color: #17191d;
  border-radius: 4px;
  padding: 8px 10px;
  font-size: 11.5px;
  color: #b7bcc4;
}
.activity-line {
  padding: 2px 0;
  border-bottom: 1px solid #21242a;
  white-space: pre-wrap;
  word-break: break-word;
}
.activity-line:last-child { border-bottom: none; }


/* Shared brand mark (the small gradient square next to "Jumerius..." in
   every page's toolbar - login.html, index.html, admin.html) - same
   gradient login.html introduced inline for its own topbar, pulled out
   here so every page uses the exact same element/styling instead of each
   page re-inventing it slightly differently. */
.toolbar .brand-mark {
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: linear-gradient(135deg, #2f6fed, #5bc0de);
  flex: 0 0 auto;
  margin-right: 2px;
}

/* NOTE: this mobile .split/.table-wrap/.chart-wrap override block was
   moved to the END of the stylesheet (from right after the Watchlist
   tab's own @media block, where it originally lived) because @media
   doesn't add specificity - a plain, unconditional ".table-wrap {
   flex: 0 0 46%; }" declared later in this file (see the "results
   table" section above) was beating this equally-specific rule for
   the properties they both set (flex), since CSS falls back to
   source order as the tiebreaker. Keep this block LAST if
   .split/.table-wrap/.chart-wrap gain further rules elsewhere. */
/* Responsive: below 1024px (phones and tablet-portrait, see the shared
   breakpoint reference at the top of this file) the 2-Day/1-Week/1-Month/Backtest
   results-table + chart ".split" is a side-by-side flex row sized for a
   desktop viewport (.table-wrap pinned to 46% width, .chart-wrap filling
   the rest) - on a phone screen that squeezes both panes down to roughly
   half the already-narrow viewport width, which is what made the table
   unreadable and the chart mostly clipped off-screen in the reported bug.
   Below this breakpoint, stack them vertically instead: table on top
   (its own bounded height + independent scroll, so the symbol list stays
   reachable without scrolling past the whole chart first), full-width
   chart pane below it. .chart-wrap's own overflow-y:auto (see that rule
   above) is turned off here in favor of a single scroll on .split itself
   - nesting two independent touch-scroll containers (outer .split +
   inner .chart-wrap) makes the chart/indicator-panels stack awkward to
   scroll through on a touchscreen, so one unified scroll region is used
   instead. */
@media (max-width: 1023px) {
  .split {
    flex-direction: column;
    overflow-y: auto;
  }
  .table-wrap {
    flex: 0 0 auto;
    max-height: 42vh;
  }
  .chart-wrap {
    flex: 0 0 auto;
    overflow-y: visible;
  }
}
/* Responsive (continued): the actual root cause behind "the whole app
   isn't mobile responsive" (2026-09-20 bug report, screenshots showed the
   results table/chart pinned to only the left ~1/3 of a widened,
   horizontally-scrolled page) turned out to be .toolbar, not .split -
   .toolbar is a single un-wrapped flex ROW (brand mark, title, universe
   label, market filter, view-symbol input+button, ws-status, user email,
   admin/sign-out links) with several nowrap children and no flex-wrap.
   Its natural content width is ~900px+, far past a phone's ~375-430px
   viewport; with no wrapping and nothing to shrink into, the toolbar
   overflows ITS OWN box, and since body sets no overflow-x, that overflow
   drags the whole page into horizontal scroll - so .split/.table-wrap/
   .chart-wrap below it were actually already rendering correctly at
   100% of the (unscrolled) viewport width the whole time; they just
   looked squeezed into a narrow left column because the page around them
   had been forced ~2-3x wider by the toolbar. Letting the toolbar wrap
   onto multiple lines below this breakpoint keeps every control reachable
   without forcing that page-wide overflow. */
@media (max-width: 1023px) {
  .toolbar {
    flex-wrap: wrap;
    row-gap: 6px;
  }
  .toolbar-right {
    margin-left: 0;
  }
  #viewSymbolInput {
    width: 110px;
  }
}
/* Responsive (continued): the 2-Day/1-Week/1-Month results table inside
   .split still used 5-6px cell padding at any viewport width, which read
   as cramped once the global type scale (top of file) started shrinking
   its font on phones/tablets - tighter padding keeps row density in
   proportion to the smaller text instead of leaving oversized gaps.
   Scoped to .split (not a blanket `table` rule) so this doesn't also
   touch the Backtest tab's own .backtest-table-panel (deliberately full
   desktop size - see that class's own comment) or the Watchlist tab
   (handled separately above). */
@media (max-width: 1023px) {
  .split thead th,
  .split tbody td {
    padding: 4px 5px;
  }
}
/* Responsive (continued): hide the Name/Market/Bar Date columns of the
   Live Strategy Scanner table (2-Day/1-Week/1-Month) on phone widths, by
   explicit user request (2026-09-20). Phone, not phone+tablet: at
   tablet-portrait width (820px, the iPad anchor) the table has enough
   room for all 8 columns to stay legible, so this is scoped to the
   <600px phone tier specifically, narrower than the 1023px tier used
   everywhere else in this file - unlike the earlier stacking/toolbar/
   type-scale fixes, which apply from 1023px down since those problems
   (page-wide overflow, a split that doesn't fit) start well above
   phone width. Reasoning for these three over the other five: Name
   mostly duplicates Symbol (which stays visible) for anyone who already
   knows the tickers they're scanning, Market is usually implied by
   context (the toolbar's own market filter, or the Backtest tab's
   per-market sub-tabs elsewhere in the app), and Bar Date is bookkeeping
   metadata rather than something that informs the trade signal itself -
   whereas Signal/Last/Industry/Market Cap (kept) are what the scan is
   actually for. `display: none` on both the header cell and its column's
   data cells (not just visibility:hidden) so the hidden columns don't
   leave empty gaps in the row - see layout.js/scanner-table.js for the
   col-name/col-market/col-bardate class markers this targets. */
@media (max-width: 599px) {
  .split th.col-name,
  .split td.col-name,
  .split th.col-market,
  .split td.col-market,
  .split th.col-bardate,
  .split td.col-bardate {
    display: none;
  }
}
/* Responsive (continued): the main candlestick chart looked "out of
   shape" on phones (2026-09-20 bug report) - .chart-container is a fixed
   500px tall regardless of viewport (by earlier explicit user request,
   see that rule's own comment), so at a phone's ~374px content width
   the chart rendered in a narrow, unusually tall/portrait-ish box
   instead of the wide, short aspect ratio a price chart actually reads
   well in. Capped to 40% of the viewport height on phones specifically
   (explicit user request, 2026-09-20: "2/5 of the mobile screen") -
   scoped to the same <600px phone tier as the column-hiding rule above,
   not the wider 1023px tier, since tablet-portrait (820px+) already has
   enough width that the existing fixed 500px reads proportionate rather
   than distorted. This only resizes the main chart itself; the 6-7
   indicator panels below it (Volume/ATR/Momentum/MACD/etc.) are
   deliberately left untouched for now - a separate follow-up.
   chart.applyOptions({width: container.clientWidth, height:
   container.clientHeight}) in layout.js already re-sizes the actual
   lightweight-charts instance off this container's real box size via a
   ResizeObserver, so this CSS-only change is enough - no JS change
   needed for the chart itself to pick up the new height. */
@media (max-width: 599px) {
  .chart-container {
    flex: 0 0 40vh;
    max-height: 40vh;
  }
}
/* Responsive (continued): the 6-7 indicator panels below the main chart
   (Volume/ATR/Momentum/MACD/Ultimate Oscillator/Slow Stochastic/RSI/ROC)
   were a fixed 230px tall each at every viewport width (explicit prior
   user request - see .indicator-panel's own base rule/comment), which on
   mobile meant scrolling past most of one panel just to see the next.
   Resized to 30% of the viewport height each on mobile AND tablet-portrait
   (explicit user request, 2026-09-20: "resize... to 30% size each... so
   3 indicators can be seen on one full screen") - three panels then add
   up to ~90vh, comfortably fitting together in one screen with a small
   margin for the gaps between them. Scoped to the same <1024px tier as
   the earlier stacking/toolbar fixes (not the narrower <600px phone-only
   tier used for the chart-height/column-hiding changes) since this was
   asked for "mobile and iPad" together, as one value, not two different
   ones per tier the way the type scale was. Like the main chart above,
   indicator panels already re-size their actual lightweight-charts
   instance off their container's real box height via a ResizeObserver
   (indChart.applyOptions({width, height: indContainer.clientHeight}) in
   layout.js), so this CSS-only change is enough - no JS change needed. */
@media (max-width: 1023px) {
  .indicator-panel {
    flex: 0 0 30vh;
  }
}
/* Responsive (continued): on mobile/tablet, reorder the toolbar's wrapped
   rows so "connected / <email> / Admin / Sign out" sits right below the
   title (row 2), swapping places with the "All Markets" filter/search
   group, which moves down a row - explicit user request (2026-09-20).
   Uses flex `order` rather than physically reordering these elements in
   index.html: .toolbar already wraps at this width (see the earlier
   flex-wrap fix above), and a wrapped flex container lays its rows out
   in effective `order` sequence, not DOM sequence - so this is a pure
   CSS-only reorder. brand-mark/h1 (the title) are left at the default
   order:0, so they stay first regardless. Desktop (>=1024px, a single
   unwrapped row) is unaffected either way, since `order` only changes
   anything once a row actually wraps.
   Caveat worth knowing: `order` changes visual position only, not tab/
   keyboard focus order (DOM order still drives that) - a keyboard user
   will tab through these in their original document sequence even
   though the two groups now appear swapped on screen. Acceptable
   trade-off for a same-page toolbar reorder like this one; flagged here
   rather than silently ignored. */
@media (max-width: 1023px) {
  .ws-status,
  .current-user-label,
  #adminLink,
  #logoutLink {
    order: 1;
  }
  #marketFilterLabel,
  #marketFilterSelector,
  #viewSymbolInput,
  #viewSymbolBtn {
    order: 2;
  }

  /* "Filter:" label hidden - the "All Markets" dropdown reads fine on
     its own once space is tight (explicit user request, 2026-09-20).
     Desktop keeps the label; space isn't a concern there. */
  #marketFilterLabel {
    display: none;
  }
}
